Skip to content

snake-eyes: taxonomy (48 types), data models, and file discovery #3

Description

@jflowers

How to use this issue with /opsx-propose

/opsx-propose taxonomy-and-discovery

Use this entire issue body as the change description. Do not ask clarifying questions. Every decision is already made below. If something is still unspecified, apply these defaults in order:

  1. Match Gaze protocol v1.1.0 exactly.
  2. Lift from gaze-py only the files named below; preserve copyright headers.
  3. Do not implement analyze, complexity, coverage, test_mapping, or classify_signals.

OpenSpec change name: taxonomy-and-discovery

Depends on: #2 (scaffold-and-protocol) must be merged or present on the branch.


Context

This issue lands the shared types every later analysis method needs, plus the optional discover protocol method.

Taxonomy source of truth is Gaze internal/taxonomy/types.go (48 canonical types). gaze-py src/gaze_py/taxonomy/effects.py has 38 of those 48. Lift gaze-py's enum/map, then add the 10 missing types.

Matt Peter has given permission to lift gaze-py code (Apache 2.0). Preserve his copyright headers on lifted files.


What to build

1. Effect taxonomy

src/snake_eyes/analysis/effects.py

Lift from /Users/jflowers/Projects/github/mpeter/gaze-py/src/gaze_py/taxonomy/effects.py (or the same path in a clone of mpeter/gaze-py).

  • Keep the existing 38 SideEffectType values and TIER_MAP entries unchanged.
  • Add the 10 missing types with these exact names and tiers:
Type Tier
ErrorSignal P0
GeneratorYield P1
ContainerMutation P1
StreamOutput P1
AsyncGeneratorYield P2
MetaprogrammingMutation P2
DescriptorEffect P2
ResourceManagement P2
ImportSideEffect P2
MonkeyPatch P2

Final inventory (48, exact strings):

P0 (6): ReturnValue, ErrorReturn, SentinelError, ReceiverMutation, PointerArgMutation, ErrorSignal

P1 (11): SliceMutation, MapMutation, GlobalMutation, WriterOutput, HTTPResponseWrite, ChannelSend, ChannelClose, DeferredReturnMutation, GeneratorYield, ContainerMutation, StreamOutput

P2 (16): FileSystemWrite, FileSystemDelete, FileSystemMeta, DatabaseWrite, DatabaseTransaction, GoroutineSpawn, Panic, CallbackInvocation, LogWrite, ContextCancellation, AsyncGeneratorYield, MetaprogrammingMutation, DescriptorEffect, ResourceManagement, ImportSideEffect, MonkeyPatch

P3 (9): StdoutWrite, StderrWrite, EnvVarMutation, MutexOp, WaitGroupOp, AtomicOp, TimeDependency, ProcessExit, RecoverBehavior

P4 (6): ReflectionMutation, UnsafeMutation, CgoCall, FinalizerRegistration, SyncPoolOp, ClosureCaptureMutation

TIER_MAP is a gatekeeping value. Do not change existing gaze-py tier assignments to make tests easier. Do not invent local aliases (ArgumentMutation, ReturnValue typos, etc.) as enum members. Gaze language-neutral aliases (e.g. ArgumentMutationPointerArgMutation) are not emitted by snake-eyes; emit canonical names only.

Use enum.StrEnum (Python 3.11+).

2. Data models

src/snake_eyes/analysis/models.py

Lift structure from gaze-py taxonomy/models.py but reshape Effect to the Gaze protocol v1.1.0 analyze payload, not gaze-py's internal model.

Exact fields:

@dataclass(frozen=True)
class Effect:
    type: str                    # SideEffectType value, e.g. "ReturnValue"
    description: str             # human-readable, required
    location: str | None = None  # "file.py:25:5" (file:line:col), relative to root
    target: str | None = None    # attribute/param/exception name
    detail: dict | None = None   # opaque Python-specific metadata

@dataclass(frozen=True)
class FunctionRecord:
    name: str
    package: str                 # dotted module path, e.g. "snake_eyes.server"
    file: str                    # path relative to root_path, POSIX slashes
    line: int                    # 1-based def line
    side_effects: tuple[Effect, ...] = ()

JSON keys for FunctionRecord when serialized for analyze (later issue) must be name, package, file, line, side_effects. Do not include gaze-py-only fields (visibility, is_test, is_generator, complexity, id) on this dataclass. Those belong on later helpers if needed, not on the protocol model.

detail must serialize as a JSON object or be omitted. Never serialize detail: null if you can omit the key; if the protocol client requires the key, emit {} only when there is metadata. Decision: omit detail and target and location from JSON when None so Gaze's optional fields stay optional.

Helper: function_record_to_dict(record) -> dict used by the server later. Implement it now and unit-test it.

3. File discovery

src/snake_eyes/discovery.py

@dataclass(frozen=True)
class DiscoveryResult:
    source_files: tuple[str, ...]
    test_files: tuple[str, ...]
def discover(root_path: str, patterns: list[str] | None = None) -> DiscoveryResult: ...

Rules (do not ask):

  • Only .py files.
  • Paths in the result are relative to root_path, POSIX (/ even on Windows in tests we control).
  • patterns follows Gaze's ["./..."] convention: ./... means recursive from root. If patterns is None or ["./..."] or [], walk the whole tree. If a pattern is a relative directory (src, src/), walk that subtree. If a pattern looks like a glob (**/*.py), use it relative to root. Do not implement Go's ./pkg/... package semantics beyond "directory prefix + recursive".
  • Test file if any of:
    • filename starts with test_
    • filename ends with _test.py
    • any path component is tests or test
  • A file cannot appear in both lists. If it matches test rules, it is test, not source.
  • Exclude a directory (do not descend) if its name is any of:
    .venv, venv, env, .env, __pycache__, .git, .hg, .svn, dist, build, .tox, .nox, .mypy_cache, .ruff_cache, .pytest_cache, node_modules, .eggs, and any name ending in .egg-info
  • Exclude files named __pycache__ (already covered) and .pyi stub files (not .py).
  • root_path missing or not a directory: raise FileNotFoundError with the path in the message. The server maps this to JSON-RPC -32602 invalid params.
  • Symlinks: do not follow directory symlinks (avoid cycles). Follow file symlinks only if the target is a .py file inside root (optional; skipping symlink files entirely is also acceptable — pick skip all symlinks).

4. Wire discover method

Request params:

{"root_path": "/abs/path", "patterns": ["./..."]}

Result:

{"source_files": ["src/foo.py"], "test_files": ["tests/test_foo.py"]}
  • Register method discover on the server from issue 1.
  • Flip capabilities.discover to true in initialize. Leave the other three capability flags unchanged (false unless a later issue already flipped them).
  • Invalid/missing root_path: error -32602.

Do not have discover parse Python or detect effects.

5. Package init

src/snake_eyes/analysis/__init__.py may re-export SideEffectType and TIER_MAP. Keep it thin.


Tests required

  • tests/test_effects.py:
    • len(SideEffectType) == 48
    • every enum member has a TIER_MAP entry
    • the 10 new types have the tiers in the table above
    • existing P0 set still includes ReturnValue, ErrorReturn, SentinelError, ReceiverMutation, PointerArgMutation
  • tests/test_models.py:
    • function_record_to_dict omits None optionals
    • type is the canonical string
    • frozen: assignment raises
  • tests/test_discovery.py using tmp_path:
    • mixed src/ + tests/ layout → correct split
    • test_foo.py at repo root classified as test
    • foo_test.py classified as test
    • .venv/lib/python3.12/site.py excluded
    • __pycache__/x.py excluded
    • empty project → both lists empty, no error
    • missing root → FileNotFoundError (or server -32602 in the RPC test)
  • tests/test_discover_method.py:
    • JSON-RPC roundtrip against a temp project
    • initialize reports "discover": true

Coverage strategy (Constitution IV)

Layer Target
effects.py 100%
models.py 100%
discovery.py 95%+
Project gate still 85% overall

Out of scope

  • Detector, complexity, coverage parser
  • Implementing the 10 new types' detection (enum members only)
  • gaze-py taxonomy/models.py fields that are not in the protocol

Done when

  • 48 types exist with correct tiers
  • discover works over JSON-RPC
  • initialize.capabilities.discover is true
  • Copyright header retained on lifted effects.py

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions