Skip to content

feat: implement analyze, complexity, and coverage analyzer methods - #9

Merged
jflowers merged 3 commits into
mainfrom
opsx/analysis-methods
Aug 29, 2026
Merged

feat: implement analyze, complexity, and coverage analyzer methods#9
jflowers merged 3 commits into
mainfrom
opsx/analysis-methods

Conversation

@jflowers

Copy link
Copy Markdown
Contributor

Summary

Implements the three required Gaze analyzer-protocol v1.1.0 methods for Snake Eyes
(issue #4): analyze (Python side-effect detection), complexity (per-function
McCabe cyclomatic complexity), and coverage (parse coverage.py data without
executing tests). These methods previously returned method-not-found (-32601); they
now return protocol-shaped results. initialize capabilities are unchanged
{discover:true, test_mapping:false, classify_signals:false, streaming:false}.

  • Detector maps the universal taxonomy to Python AST: the 6 P0 types plus 10 new
    Python-specific types (ErrorSignal, GeneratorYield, ContainerMutation,
    StreamOutput, AsyncGeneratorYield, MetaprogrammingMutation, DescriptorEffect,
    ResourceManagement, ImportSideEffect, MonkeyPatch); reports ambiguous constructs
    rather than dropping them; never fabricates effects for the 6 Go-only taxonomy
    types (retained only as vocabulary).
  • complexity.py and detector.py lift gaze-py logic (Apache-2.0; provenance headers
    and §4(b) change notices retained; no radon).
  • coverage is static-only (never runs pytest/coverage run), confines file access to
    the discovered set, and degrades gracefully on missing/malformed data.
  • Adds a single pinned runtime dependency coverage>=7.0,<8.
  • Analysis Safety: os.stat + S_ISREG regular-file guard and MAX_FILE_BYTES /
    MAX_AST_DEPTH bounds on every read path; deterministic byte-identical output.

How to Test

Run the CI-parity gate suite locally:

uv sync --locked
uv run ruff check src/ tests/
uv run ruff format --check src/ tests/
uv run mypy src/
uv run pytest --cov=snake_eyes --cov-report=term-missing --cov-fail-under=85

Expected: all green — 321 tests pass, project coverage 93.82% (per-module:
complexity.py 100%, detector.py 91%, coverage.py 93%). Acceptance scenarios in
openspec/changes/analysis-methods/specs/**/*.md are covered by
tests/test_analysis_methods.py (JSON-RPC e2e), test_detector*.py (all 10 new + 6 P0
types), test_complexity*.py (hand-derived McCabe oracle), and test_coverage*.py
(coverage.json + .coverage lookup, confinement, graceful degradation).

How to Demo

Snake Eyes is spawned by Gaze as a subprocess; drive it via JSON-RPC over stdio:

uv run snake-eyes --stdio

Send initialize (capabilities unchanged), then analyze/complexity/coverage
with {"root_path":"<abs>","patterns":["./..."]} and observe protocol-shaped
functions[] results. Point it at tests/fixtures/effects/ for side-effect detection,
or a project with a coverage.json/.coverage for coverage mapping.

Key Files Changed

  • src/snake_eyes/analysis/detector.py (+1775) — side-effect detector:
    analyze_path/analyze_source, all effect families, 10 new types, ambiguity fallback,
    resource/S_ISREG bounds, gaze-py provenance.
  • src/snake_eyes/coverage.py (+360) — parse_coverage: coverage.json + .coverage
    (analysis2) lookup, discovered-set confinement, graceful degradation.
  • src/snake_eyes/analysis/_shared.py (+177) — shared safe-file reader, function
    enumeration, package derivation, MAX_FILE_BYTES/MAX_AST_DEPTH/BROADENED_EXCEPTIONS.
  • src/snake_eyes/analysis/complexity.py (+166) — McCabe complexity (lifted gaze-py,
    no radon).
  • src/snake_eyes/server.py (+55) — analyze/complexity/coverage handlers wired into
    DEFAULT_DISPATCH; initialize unchanged.
  • src/snake_eyes/analysis/__init__.py — re-export Effect/FunctionRecord/
    function_record_to_dict.
  • pyproject.toml, uv.lock — add pinned coverage>=7.0,<8.
  • README.md, AGENTS.md — doc-sync (methods implemented; radon rejected; modules
    delivered).
  • tests/ — 12 test modules + fixtures under tests/fixtures/{effects,coverage}.
  • openspec/changes/analysis-methods/ — proposal, design, tasks, 6 capability specs
    (spec-review + code-review passed).

This PR was generated by /uf.finale (AI-assisted).

Implements the three required Gaze protocol v1.1.0 methods (issue #4),
building on the taxonomy and discovery from #3.

- analyze: Python side-effect detector covering the original taxonomy
  types that have a Python analogue plus 10 new Python-specific types;
  ambiguous calls reported (never dropped), Go-era no-analogue types
  never fabricated
- complexity: McCabe cyclomatic complexity lifted from gaze-py with
  Apache-2.0 provenance + change notice (no radon)
- coverage: coverage.py data parser (coverage.json/.coverage), confined
  to the discovered set, never runs tests
- shared safe-file/enumeration/package helpers with Constitution-V
  resource bounds (S_ISREG guard, size cap, AST-depth budget) and
  deterministic byte-identical output
- wire analyze/complexity/coverage into the JSON-RPC server; initialize
  capabilities unchanged
- add coverage>=7.0,<8 runtime dependency; 321 tests, fixtures, and
  OpenSpec change artifacts; sync README and AGENTS docs

Assisted-by: claude-opus
Generated with AI assistance (claude-opus)
Align spec artifacts with the shared-helper architecture after the
review-council refactor:
- name is_analyzable_file as the centralized stat/S_ISREG/byte-cap guard
  (iter_source_files for detector+complexity; coverage calls it directly)
- scope derive_package to detector+complexity (coverage carries no package)
- document compute_complexity as the public entry point (private
  _cyclomatic_complexity helper)
- reword tasks.md 3.16 to scope the ambiguity guarantee to unknown name
  calls + enumerated dynamic constructs

Docs-only; no requirement headers or scenarios changed.
Address review-council findings across three iterations:

Safety & resilience:
- centralize the safe-file guard (is_analyzable_file) so the stat/S_ISREG/
  byte-cap runs before Coverage.analysis2() on every path (Constitution V)
- coverage: config_file=False to ignore untrusted repo coverage config
- broaden coverage catches to include RecursionError/MemoryError/ValueError
- correct the _is_regular_file symlink comment (rejects non-regular files;
  paths stay root-confined via _confine_path)

Detection accuracy:
- drop 'input' from pure-builtins (real stdin effect -> ambiguous)
- gate FileSystemWrite co-emit on write-mode opens

Performance:
- bisect-based O(log S) line-to-function coverage counting

Server & cleanup:
- generic handler returns 'Internal error' (no str(err) on the wire)
- _discover reuses _validate_analysis_params; delete dead _BOOL_OPS

Tests (321 -> 341, coverage 93.82% -> 95.77%):
- real fault injection for stat-error paths (no self-mocking)
- full-equality detector golden + parametrized FileNotFoundError->-32602
- depth-budget guards exercised via in-memory AST injection
  (source nesting hits MAXINDENT=100 before MAX_AST_DEPTH=200)
- remove orphaned fixtures; make coverage ordering fixture discriminating

Docs:
- README Limits & Troubleshooting section + gaze-py/NOTICE provenance line
- AGENTS.md method-delivery parity

@jflowers jflowers left a comment

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.

Review Council Verdict: APPROVE

Note: This review could not be submitted as a formal GitHub APPROVE because GitHub prohibits an author from approving their own pull request (HTTP 422). It is therefore posted as a COMMENT. Original council verdict: APPROVE.

Generated by /uf.review-council (AI-assisted). Gaze quality analysis was skipped at the requester's direction — snake-eyes is a Python project and the Go gaze binary cannot analyze it.

Reviewers: 9 discovered divisor-* personas, all invoked — unanimous APPROVE.
Iterations: 3 (2 rounds of fixes). Scope: full branch diff vs main (42 files).


Per-persona verdicts (final iteration)

Reviewer Verdict
divisor-adversary (security/resilience) APPROVE
divisor-architect (structure/conventions) APPROVE (alignment 9/10)
divisor-guard (intent/governance) APPROVE (all 5 constitution principles PASS)
divisor-testing (test quality) APPROVE
divisor-sre (ops/efficiency) APPROVE
divisor-scribe (technical docs) APPROVE
divisor-curator (doc/content triage) APPROVE
divisor-envoy (PR/comms) APPROVE
divisor-herald (blog/announcements) APPROVE

Iteration history

  • Iteration 1 — REQUEST CHANGES. 5 HIGH, 11 MEDIUM, ~18 LOW consolidated findings. Notable HIGH: coverage.py bypassed the centralized safe-file reader (byte-cap applied after Coverage.analysis2() already read untrusted source — Constitution V DoS); coverage.Coverage() auto-loaded untrusted repo config (config_file=True) — non-determinism + plugin code-exec surface; self-mocking tautology tests left safety branches uncovered; detector effect tests were presence-only (no golden); FileNotFoundError -> -32602 untested at the JSON-RPC boundary.
  • Iteration 2 — REQUEST CHANGES. All iteration-1 findings resolved; 7 APPROVE, 2 MEDIUM blockers surfaced: (1) depth-budget guard tests were misattributed — nested source hits CPython's tokenizer MAXINDENT=100 before MAX_AST_DEPTH=200, so the snake-eyes guards never executed; (2) the README "Limits & Troubleshooting" section reported added had not actually landed.
  • Iteration 3 — APPROVE. Both blockers fixed: depth guards now covered via in-memory AST injection (mutation-resistant); README section added and verified line-by-line against source. coverage.py reached 100% coverage.

Changes made in response to the council (pushed, 2 commits)

  • 3c31f8a — docs: reconcile analysis-methods spec artifacts with implementation (shared-helper architecture now accurately described).
  • 640593a — fix: harden analysis methods per review council findings: centralized is_analyzable_file() guard (byte-cap before analysis2()); config_file=False for deterministic, plugin-free coverage parsing; broadened graceful-degradation exception handling; O(log S) bisect coverage counting; real fault-injection + golden + -32602 boundary + depth-guard tests; README Limits & Troubleshooting; write-mode-gated FileSystemWrite; input() no longer treated as pure; DRY/zero-waste cleanups.

Local CI-parity (all green): ruff check + ruff format --check + mypy pass; pytest 341 passed, coverage 95.77% (protected gate 85%).


Outstanding / non-blocking

  • [MEDIUM — human governance ruling required] symtable. proposal.md/design.md and Constitution III literally name symtable, but the implementation uses ast.Global/ast.Nonlocal (no symtable import). The code satisfies Principle III in substance (Python-native, no reimplemented semantics). Requires a human decision — amend Constitution III wording, or integrate symtable. Excluded from automated fixes at requester direction.
  • [LOW — deferred] .coverage SQLite container not byte-capped before cov.load() (pre-existing; capping the aggregate DB could over-restrict legitimate large projects).
  • [LOW — optional] Directly mutation-pin the _EffectVisitor/_ComplexityVisitor depth guards (the shared guard is already mutation-tested).
  • [LOW — optional] Extract a read_and_parse() helper to dedupe ~4 lines between _shared and coverage.
  • [INFO] No CHANGELOG (pre-existing convention — history lives in openspec/changes/); frame any future release note as "48-type taxonomy, of which 42 are detected in Python (6 Go-only concepts retained as vocabulary)."
  • [INFO] CoverageWarning: module-not-measured at pytest teardown (import-timing; does not affect the gate).

3-iteration council process; all actionable findings remediated and verified. The two commits above are already pushed to this PR branch.

@jflowers
jflowers merged commit a4e132f into main Aug 29, 2026
2 checks passed
@jflowers

jflowers commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

PR Cost Report: #9

Session: New session - 2026-08-28T21:01:42.217Z

ID: ses_fb5d25436ffeu3bo4QLMSlNWrU

Metric Value
Cost (parent only) $94.68
Input tokens 232
Output tokens 188,271
Reasoning tokens 117,295
Cache read tokens 14,304,876
Cache write tokens 6,618,017

Timeline: 2026-08-28 17:01:42 — 2026-08-29 17:16:16

Child sessions: 52 — additional cost: $87.15
Session tree total: $181.82

Child session breakdown
Session Cost Output Tokens
Explore snake-eyes codebase (@explore subagent) $0.00 0
Spec review: adversary (@divisor-adversary subagent) $2.69 10,401
Spec review: architect (@divisor-architect subagent) $2.91 9,109
Spec review: guard (@divisor-guard subagent) $1.60 5,721
Spec review: testing (@divisor-testing subagent) $1.37 7,801
Spec review: sre (@divisor-sre subagent) $1.50 7,984
Spec review: curator (@divisor-curator subagent) $1.72 6,091
Revise root spec artifacts (@general subagent) $0.65 14,414
Revise detector + analyze specs (@general subagent) $0.29 9,896
Revise complexity + coverage specs (@general subagent) $0.27 8,071
Spec review iter2: adversary (@divisor-adversary subagent) $0.64 1,405
Spec review iter2: architect (@divisor-architect subagent) $0.45 1,197
Spec review iter2: guard (@divisor-guard subagent) $0.45 1,248
Spec review iter2: testing (@divisor-testing subagent) $0.60 1,318
Spec review iter2: sre (@divisor-sre subagent) $0.62 1,344
Spec review iter2: curator (@divisor-curator subagent) $0.53 1,408
Spec review iter2: adversary (@divisor-adversary subagent) $2.83 7,142
Spec review iter2: architect (@divisor-architect subagent) $1.45 4,517
Spec review iter2: guard (@divisor-guard subagent) $1.33 3,611
Spec review iter2: testing (@divisor-testing subagent) $1.60 1,682
Spec review iter2: sre (@divisor-sre subagent) $1.04 3,715
Spec review iter2: curator (@divisor-curator subagent) $1.33 3,149
Apply spec fixes group A (@general subagent) $0.56 8,855
Apply spec fixes group B (@general subagent) $0.31 5,647
Apply spec fixes group C (@general subagent) $0.29 5,882
Spec review: adversary iter3 (@divisor-adversary subagent) $1.59 3,127
Spec review: architect iter3 (@divisor-architect subagent) $1.88 7,490
Spec review: guard iter3 (@divisor-guard subagent) $1.42 5,048
Spec review: testing iter3 (@divisor-testing subagent) $2.54 8,937
Spec review: sre iter3 (@divisor-sre subagent) $1.71 7,425
Spec review: curator iter3 (@divisor-curator subagent) $1.67 1,568
Apply iter-3 spec fixes (Agent A) (@general subagent) $0.48 6,743
Apply iter-3 spec fixes (Agent B) (@general subagent) $0.26 4,971
Apply iter-3 spec fixes (Agent C) (@general subagent) $0.35 7,539
Implement analysis-methods (Step 7) (@cobalt-crush-dev subagent) $2.74 3,920
Implement analysis core modules (@general subagent) $3.09 41,947
Implement tests, fixtures, docs (@general subagent) $15.13 69,546
Code review: adversary (@divisor-adversary subagent) $1.42 1,455
Code review: architect (@divisor-architect subagent) $1.44 2,331
Code review: guard (@divisor-guard subagent) $1.84 2,784
Code review: testing (@divisor-testing subagent) $3.08 3,214
Code review: sre (@divisor-sre subagent) $1.28 2,068
Code review: curator (@divisor-curator subagent) $0.59 873
Apply code-review fixes (@general subagent) $6.76 32,951
Code review iter2 adversary (@divisor-adversary subagent) $0.95 1,299
Code review iter2 architect (@divisor-architect subagent) $1.07 1,740
Code review iter2 guard (@divisor-guard subagent) $1.83 3,042
Code review iter2 testing (@divisor-testing subagent) $1.35 1,761
Code review iter2 sre (@divisor-sre subagent) $1.10 1,665
Code review iter2 curator (@divisor-curator subagent) $0.59 1,104
Apply code-review cleanup fixes (@general subagent) $3.25 14,098
Fix hanging FIFO test (@general subagent) $0.71 8,734

Session: Skip gaze testing for Python

ID: ses_fb10dd821ffevNRb5JgDcS3Dvw

Metric Value
Cost (parent only) $29.32
Input tokens 146
Output tokens 108,351
Reasoning tokens 135,945
Cache read tokens 10,435,908
Cache write tokens 1,669,817

Timeline: 2026-08-29 15:14:47 — 2026-08-29 17:07:46

Child sessions: 24 — additional cost: $60.68
Session tree total: $89.99

Child session breakdown
Session Cost Output Tokens
Adversary security review PR#9 (@divisor-adversary subagent) $4.27 7,617
Architect review PR#9 (@divisor-architect subagent) $2.49 7,087
Guard intent/constitution review PR#9 (@divisor-guard subagent) $4.81 9,555
Testing review PR#9 (@divisor-testing subagent) $4.79 11,223
SRE ops/deps review PR#9 (@divisor-sre subagent) $4.35 9,067
Curator docs-gap review PR#9 (@divisor-curator subagent) $2.39 6,746
Scribe docs review PR#9 (@divisor-scribe subagent) $3.94 6,396
Envoy review PR#9 (@divisor-envoy subagent) $0.53 1,711
Herald review PR#9 (@divisor-herald subagent) $0.84 2,897
Fix coverage subsystem (@general subagent) $3.12 28,139
Fix detector subsystem (@general subagent) $2.47 14,636
Fix complexity and server (@general subagent) $1.13 6,323
Fix docs specs and config (@general subagent) $1.18 9,383
Re-review: adversary (iter 2) (@divisor-adversary subagent) $4.20 7,406
Re-review: architect (iter 2) (@divisor-architect subagent) $1.70 6,603
Re-review: guard (iter 2) (@divisor-guard subagent) $2.12 6,675
Re-review: testing (iter 2) (@divisor-testing subagent) $5.10 8,788
Re-review: sre (iter 2) (@divisor-sre subagent) $1.34 5,684
Re-review: scribe (iter 2) (@divisor-scribe subagent) $3.12 8,902
Re-review: curator (iter 2) (@divisor-curator subagent) $1.45 5,220
Re-review: envoy (iter 2) (@divisor-envoy subagent) $1.06 4,932
Re-review: herald (iter 2) (@divisor-herald subagent) $1.12 4,336
Testing reviewer iter-3 verify (@divisor-testing subagent) $2.12 5,850
SRE reviewer iter-3 verify (@divisor-sre subagent) $1.04 2,708

Grand Total: $271.82 across 2 session tree(s) (78 sessions)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant