Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This workflow runs lint, formatting, type checking, and tests with
# coverage on every push and pull request to main.
name: ci

on:
push:
branches: [main]
pull_request:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
- run: uv sync --locked
- run: uv run ruff check src/ tests/
- run: uv run ruff format --check src/ tests/
- run: uv run mypy src/
- run: uv run pytest --cov=snake_eyes --cov-report=term-missing --cov-fail-under=85
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ __marimo__/
.uf/dewey/graph.db-wal
.uf/dewey/*.lock
.uf/dewey/cache/
.uf/dewey/learnings/
.uf/dewey/dewey.log
.uf/replicator/*.db
.uf/replicator/*.db-shm
Expand Down
18 changes: 7 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,16 @@ snake-eyes/
│ ├── __init__.py
│ ├── __main__.py # Entry point (snake-eyes --stdio)
│ ├── server.py # JSON-RPC server (stdin/stdout)
│ ├── protocol.py # Request/response types
│ ├── discovery.py # File discovery (source + tests)
│ ├── analysis/
│ │ ├── __init__.py
│ │ ├── detector.py # Side effect detection
│ │ ├── inference.py # Name/type resolution (astroid)
│ │ ├── effects.py # Effect type definitions
│ │ └── patterns.py # Known I/O and mutation patterns
│ ├── complexity.py # Cyclomatic complexity (radon)
│ └── coverage.py # coverage.py data parsing
│ └── protocol.py # Request/response types
├── tests/
├── .github/workflows/ # CI: ruff, mypy, pytest gates
├── pyproject.toml
└── .gaze.yaml
├── uv.lock
├── README.md
├── LICENSE
└── NOTICE
```
Planned later: `discovery.py`, `analysis/`, `complexity.py`, `coverage.py` (issues #3–#6).

## Shell Commands

Expand Down
6 changes: 6 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
snake-eyes
Copyright 2026 zero-dot-force

This product includes software originally developed in gaze-py

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Present-tense "includes" asserts gaze-py code ships in this change; the lift is a scheduled follow-up (design.md Non-Goal). Council deferred to human: (a) reword to future intent (amend NOTICE + spec + test together), or (b) keep and disclose in 0.1.0 release notes. (Herald/Scribe)

(https://github.com/mpeter/gaze-py) by Matt Peter, licensed under
Apache License 2.0. Copyright headers on lifted files are preserved.
116 changes: 22 additions & 94 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,78 +1,38 @@
# Snake Eyes

**Python analyzer for [Gaze](https://github.com/unbound-force/gaze) -- test quality analysis via side effect detection.**
**Python analyzer backend for [Gaze](https://github.com/unbound-force/gaze).**

Snake Eyes detects observable side effects in Python functions and reports them to Gaze's universal scoring engine via JSON-RPC. Gaze handles classification, CRAP scoring, reporting, and everything else -- Snake Eyes focuses entirely on understanding Python.
Snake Eyes is a Gaze-spawned subprocess. It speaks JSON-RPC 2.0 over stdin/stdout
(protocol v1.1.0). Gaze owns the CLI, scoring, and reports.

## How It Works
## Current status (v0.1.0)

```
gaze analyze --analyzer snake-eyes ./src
```

Gaze spawns Snake Eyes as a subprocess and communicates via JSON-RPC 2.0 over stdin/stdout:

```
┌─────────────────────────────────┐
│ Gaze (Go core) │
│ │
│ CLI, TUI, Reports, AI pipeline │
│ Taxonomy, Classification │
│ CRAP scoring, Quadrants │
└──────────────┬──────────────────┘
│ JSON-RPC stdin/stdout
┌──────────────▼──────────────────┐
│ Snake Eyes (Python) │
│ │
│ Side effect detection │
│ Coverage parsing │
│ Complexity calculation │
│ Test-to-assertion mapping │
└────────────────────────────────┘
```

Snake Eyes implements Gaze's [external analyzer protocol](https://github.com/unbound-force/gaze/issues/95), providing six JSON-RPC methods:

| Method | Purpose |
|--------|---------|
| `initialize` | Handshake and capability negotiation |
| `discover` | Find Python source and test files |
| `analyze` | Detect side effects per function |
| `complexity` | Cyclomatic complexity per function |
| `coverage` | Parse coverage.py data |
| `shutdown` | Clean process exit |

Future methods (`test_mapping`, `classify_signals`) will enable full test quality assessment and contract classification.

## What It Detects

Snake Eyes detects Python-specific side effects and reports them using Gaze's [universal taxonomy](https://github.com/unbound-force/gaze/issues/96):
This release is the project scaffold and protocol lifecycle only:

| Tier | Effects |
|------|---------|
| **P0** Must Detect | Return values, raised exceptions, `self` attribute mutations, mutable argument mutations |
| **P1** High Value | Global/module-level mutations, `print()`/stdout writes, `yield`/`yield from`, container mutations (list/dict/set) |
| **P2** Important | File I/O, database writes, subprocess/thread spawning, logging, decorator mutations, descriptor protocol, context manager effects, monkey-patching, async generator yields |
| Method | Status |
|--------|--------|
| `initialize` | Implemented |
| `shutdown` | Implemented |
| `discover`, `analyze`, `complexity`, `coverage` | Not implemented (`-32601`) |

Detection uses Python's `ast` and `symtable` modules for structural analysis, with [Astroid](https://github.com/pylint-dev/astroid) for name resolution, type inference, and cross-module import resolution.
Capability flags advertised at handshake (`discover`, `test_mapping`,
`classify_signals`, `streaming`) are all `false`. Side-effect detection and
analysis dependencies (`astroid`, `radon`, `coverage.py`) are later issues.

## Installation

```bash
pip install snake-eyes
```

Or with [uv](https://docs.astral.sh/uv/):
From a clone (not published to PyPI):

```bash
uv pip install snake-eyes
uv sync
uv run snake-eyes --stdio
```

Snake Eyes requires Python 3.11+ and a working [Gaze](https://github.com/unbound-force/gaze) installation.
Requires Python 3.11+.

## Configuration

Configure Snake Eyes as an analyzer in your project's `.gaze.yaml`:
Point Gaze at the local entry point in `.gaze.yaml`:

```yaml
analyzers:
Expand All @@ -81,53 +41,21 @@ analyzers:
args: ["--stdio"]
```

## Project Status

Snake Eyes is in early development (v0.x). It is part of the [zero-dot-force](https://github.com/zero-dot-force) labs organization -- the experimental incubator for [Unbound Force](https://github.com/unbound-force).

### Roadmap

- **v0.1** -- P0 side effect detection, CRAP score support (complexity + coverage), basic CLI
- **v0.2** -- P1 effects, test-to-assertion mapping, classification signals
- **v0.3** -- P2 effects, full test quality assessment

### Related Issues on Gaze

- [#95 -- External analyzer protocol](https://github.com/unbound-force/gaze/issues/95)
- [#96 -- Universal taxonomy](https://github.com/unbound-force/gaze/issues/96)

## Architecture
## Project structure

```
snake-eyes/
├── src/snake_eyes/
│ ├── __init__.py
│ ├── __main__.py # Entry point (snake-eyes --stdio)
│ ├── server.py # JSON-RPC server (stdin/stdout)
│ ├── protocol.py # Request/response types
│ ├── discovery.py # File discovery (source + tests)
│ ├── analysis/
│ │ ├── __init__.py
│ │ ├── detector.py # Side effect detection (ast + symtable)
│ │ ├── inference.py # Name/type resolution (astroid)
│ │ ├── effects.py # Effect type definitions
│ │ └── patterns.py # Known I/O and mutation patterns
│ ├── complexity.py # Cyclomatic complexity (radon)
│ └── coverage.py # coverage.py data parsing
│ └── protocol.py # Request/response types
├── tests/
├── pyproject.toml
└── .gaze.yaml
└── NOTICE
```

## Dependencies

| Dependency | Purpose |
|------------|---------|
| [astroid](https://github.com/pylint-dev/astroid) | Name resolution, type inference, cross-module imports |
| [radon](https://github.com/rubik/radon) | Cyclomatic complexity computation |
| [coverage](https://github.com/nedbat/coveragepy) | Coverage data parsing |

Python's `ast` and `symtable` modules (stdlib) provide the primary parsing and scope analysis.
Planned later: file discovery, analysis, complexity, and coverage (issues #3–#6).

## License

Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/scaffold-and-protocol/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-25
55 changes: 55 additions & 0 deletions openspec/changes/scaffold-and-protocol/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
## Context

snake-eyes is the Python backend for Gaze's external analyzer protocol (JSON-RPC 2.0 over stdin/stdout, protocol v1.1.0). The project currently has no Python source, no packaging, no tests, and no CI — only a stale Speckit spec (`specs/001-jsonrpc-prototype/`) that predates protocol v1.1.0 and is explicitly superseded for handshake/lifecycle. This change lays the foundation: package scaffold, protocol types, a working stdio server loop, CI, and tests. Authoritative protocol source: unbound-force/gaze `docs/protocol.md` (v1.1.0) and `internal/protocol/types.go`. Build input: issue zero-dot-force/snake-eyes#2.

Constitution I (Protocol Fidelity) requires every JSON-RPC response to match the v1.1.0 schema exactly. Constitution IV (Testability) requires the coverage strategy to be implemented now, not deferred.

## Goals / Non-Goals

**Goals:**
- A minimal `snake-eyes` Python package (stdlib only at runtime) installable via `uv sync`.
- A JSON-RPC 2.0 stdio server implementing `initialize` and `shutdown` correctly per protocol v1.1.0.
- A CI pipeline (ruff, mypy, pytest with 85% coverage fail-under) on Python 3.11 and 3.12.
- Tests that drive the server through stdin/stdout and assert JSON field values.

**Non-Goals:**
- `analyze`, `complexity`, `coverage`, `discover`, `test_mapping`, `classify_signals`, or `analyze/stream` — these are later issues.
- Any gaze-py source lift (only a `NOTICE` placeholder is added).
- `astroid`, `radon`, `coverage.py` as dependencies.
- Logging library, config-file parsing, or modification of the stale Speckit spec.

## Decisions

1. **Transport is line-delimited JSON, not LSP Content-Length headers.**
One JSON object per line over stdin/stdout. This matches Gaze protocol v1.1.0 and keeps the server trivially streamable. Alternative (LSP framing) rejected: the protocol spec mandates line-delimited JSON.

2. **Stdlib `dataclasses` for protocol types, serialized via a recursive `to_dict` helper + `json.dumps`.**
No pydantic. The envelope is small and fixed; a serialization dependency buys nothing at this scope and violates the "prefer stdlib" default. `to_dict` is equivalent to `dataclasses.asdict` plus a `None`-`data` omission filter (plain `asdict` cannot omit a key). The protocol forbids emitting `null` for omitted fields.

3. **Server reads `sys.stdin`/writes `sys.stdout` and accepts injected streams plus an injectable dispatch table.**
A `Server` object takes `stdin`/`stdout`/`stderr` as constructor args so tests can inject `io.StringIO` and drive the loop deterministically. It also accepts an injectable dispatch table (method name → handler callable), defaulting to the built-in `initialize`/`shutdown` table, so a test can register a raising handler to exercise the `-32603` internal-error path through stdin/stdout. This satisfies the "drive through stdin/stdout, not private helpers" test mandate. The broken-pipe test injects a minimal stub whose `write` raises `BrokenPipeError` (not `io.StringIO`, which cannot raise).

4. **Sequential request processing.**
Read a line, handle it, flush, repeat. No pipelining or concurrency — Gaze issues requests sequentially, and ordering matters for `shutdown`.

5. **Error taxonomy is the standard JSON-RPC 2.0 set.**
`-32700` parse error (id `null`), `-32600` invalid request, `-32601` method not found, `-32602` invalid params (fired when `initialize` `params` is absent, non-object, or lacks a string `root_path`), `-32603` internal error. On handler exception, write `-32603` with the message and log tracebacks only to stderr (stdout is the protocol channel).

6. **`initialize` is idempotent; `shutdown` exits 0 after ack.**
A second `initialize` returns a valid result. `shutdown` writes `{}` then the process exits 0. Empty and whitespace-only lines are ignored; stdin EOF without `shutdown` exits 0 silently. Exit codes are surfaced as `SystemExit` raised in-process (the server loop raises `SystemExit(0)` after `shutdown`/EOF and `main()` propagates it); tests primarily catch `SystemExit` from injected streams, with two subprocess smoke tests pinning the real process-boundary behavior.

7. **`--stdio` is the only CLI flag.**
With `--stdio`, start the server and block. Without it, print `snake-eyes --stdio` to stderr and exit 2. No config files, no subcommands. `main(argv=None, *, stdin=None, stdout=None, stderr=None)` defaults each to `sys.*`, mirroring the `Server` seam so tests drive the entry point in-process.

8. **CI: `astral-sh/setup-uv` + `uv sync --locked`, matrix 3.11/3.12, four gates.**
`ruff check`, `ruff format --check`, `mypy src/` (strict), and `pytest --cov=snake_eyes --cov-fail-under=85`. The 85% gate is a governance value and is not lowered. The `astral-sh/setup-uv` action SHALL be pinned to a full commit SHA (tag recorded as a trailing comment) for supply-chain integrity, and the workflow SHALL declare an explicit `permissions: contents: read` block (least privilege).

9. **Transport hardening bounds: 16 MiB line cap, 64-char method echo, UTF-8 streams.**
Request lines longer than 16 MiB (`MAX_LINE_CHARS`) are rejected `-32600` before parsing; deeply nested JSON (`RecursionError`) and undecodable bytes (`UnicodeDecodeError`) yield `-32700` while the loop stays alive; the `-32601` method echo is truncated to 64 characters (`MAX_METHOD_ECHO`). Real stdio text streams are reconfigured to UTF-8 (stdout with `\n` newlines) so the protocol channel is locale-independent. On an output-pipe failure (`OSError`, e.g. EPIPE), the real process stdout is redirected to `os.devnull` before `SystemExit(0)` so interpreter finalization cannot re-flush the broken pipe — a finalization error can exit 120 on Python 3.12+.

## Risks / Trade-offs

- [Protocol drift vs. Gaze v1.1.0] → Pin `protocol_version` to the literal `"1.1.0"` and assert it in tests; keep the envelope field names verbatim from the issue.
- [stdlib-only serialization omits `data` incorrectly] → Centralize in one `to_dict` helper with a `None`-omission rule and unit-test the exact JSON output.
- [stdout contamination from tracebacks] → Route all diagnostics to stderr; tests assert stdout contains only protocol JSON.
- [Coverage gate vs. small surface area] → `__main__.py`, `protocol.py`, and `server.py` are covered at effectively 100% (statements and branches); the enforced governance gate is the aggregate `--cov-fail-under=85`.
42 changes: 42 additions & 0 deletions openspec/changes/scaffold-and-protocol/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
## Why

snake-eyes is the Python Gaze external analyzer backend, but it has no Python source, no `pyproject.toml`, no tests, and no CI. Gaze (Go) spawns it as a subprocess and speaks JSON-RPC 2.0 over stdin/stdout (protocol v1.1.0), so the project cannot even boot without a scaffold and a working `initialize`/`shutdown` lifecycle. This change establishes that foundation.

## What Changes

- Add a Python package scaffold: `pyproject.toml`, `src/snake_eyes/__init__.py`, `src/snake_eyes/__main__.py`, and a `NOTICE` file for future gaze-py attribution.
- Add `src/snake_eyes/protocol.py` with stdlib `dataclasses` for the JSON-RPC 2.0 envelope and the `initialize`/`shutdown` method contracts, plus standard error codes.
- Add `src/snake_eyes/server.py`: a line-delimited JSON-RPC server loop over stdin/stdout with sequential dispatch and graceful error handling.
- Add `.github/workflows/ci.yml` running ruff, mypy, and pytest-with-coverage on Python 3.11 and 3.12.
- Add `tests/test_protocol.py`, `tests/test_server.py`, and `tests/test_cli.py` driving the server through stdin/stdout.

Out of scope (later issues): analysis, complexity, coverage, discovery, streaming, gaze-py source lift, `astroid`/`radon`/`coverage.py` runtime deps, and any change to the stale `specs/001-jsonrpc-prototype/` Speckit spec. A follow-up will retire or mark the stale spec as superseded; this change leaves it untouched.

## Capabilities

### New Capabilities
- `protocol`: JSON-RPC 2.0 envelope types and the `initialize`/`shutdown` method contracts (request params, result schemas, error codes, serialization).
- `server`: the line-delimited JSON-RPC stdio server loop — request parsing, sequential dispatch, and error handling.
- `cli`: package scaffold and the `snake-eyes --stdio` entry point (usage-on-error behavior).

### Modified Capabilities

None — this is the first implementation; no existing OpenSpec specs exist yet.

### Removed Capabilities

None.

## Impact

- New files under `src/snake_eyes/`, `tests/`, and `.github/workflows/`.
- No changes to existing code (there is none) and no runtime third-party dependencies — stdlib (`json`, `dataclasses`, `sys`) only.
- Dev-only dependencies added: `pytest`, `pytest-cov`, `mypy`, `ruff`.
- Establishes the project CI gate: ruff lint/format, mypy strict, pytest with 85% coverage fail-under.

## Constitution Alignment

- **I. Protocol Fidelity** — Implemented directly: exact v1.1.0 handshake (`protocol_version = "1.1.0"`), exact envelope/result field names, the standard JSON-RPC error taxonomy, and `data`-omission serialization, all pinned by tests.
- **II. Detection Accuracy** — N/A: no analysis/detection code is introduced in this change (scaffold + `initialize`/`shutdown` lifecycle only).
- **III. Python-Native Analysis** — N/A: no analysis/detection code is introduced in this change.
- **IV. Testability** — The coverage strategy is implemented now, not deferred: effectively 100% statement-and-branch coverage of `protocol.py`, `server.py`, and `__main__.py`; the enforced governance gate is the 85% aggregate (`--cov-fail-under=85`, branch coverage enabled).
Loading
Loading