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:
- Match Gaze protocol v1.1.0 exactly.
- Lift from gaze-py only the files named below; preserve copyright headers.
- 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. ArgumentMutation → PointerArgMutation) 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
How to use this issue with
/opsx-proposeUse 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:
analyze,complexity,coverage,test_mapping, orclassify_signals.OpenSpec change name:
taxonomy-and-discoveryDepends 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
discoverprotocol method.Taxonomy source of truth is Gaze
internal/taxonomy/types.go(48 canonical types). gaze-pysrc/gaze_py/taxonomy/effects.pyhas 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.pyLift 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).SideEffectTypevalues andTIER_MAPentries unchanged.ErrorSignalGeneratorYieldContainerMutationStreamOutputAsyncGeneratorYieldMetaprogrammingMutationDescriptorEffectResourceManagementImportSideEffectMonkeyPatchFinal inventory (48, exact strings):
P0 (6):
ReturnValue,ErrorReturn,SentinelError,ReceiverMutation,PointerArgMutation,ErrorSignalP1 (11):
SliceMutation,MapMutation,GlobalMutation,WriterOutput,HTTPResponseWrite,ChannelSend,ChannelClose,DeferredReturnMutation,GeneratorYield,ContainerMutation,StreamOutputP2 (16):
FileSystemWrite,FileSystemDelete,FileSystemMeta,DatabaseWrite,DatabaseTransaction,GoroutineSpawn,Panic,CallbackInvocation,LogWrite,ContextCancellation,AsyncGeneratorYield,MetaprogrammingMutation,DescriptorEffect,ResourceManagement,ImportSideEffect,MonkeyPatchP3 (9):
StdoutWrite,StderrWrite,EnvVarMutation,MutexOp,WaitGroupOp,AtomicOp,TimeDependency,ProcessExit,RecoverBehaviorP4 (6):
ReflectionMutation,UnsafeMutation,CgoCall,FinalizerRegistration,SyncPoolOp,ClosureCaptureMutationTIER_MAPis a gatekeeping value. Do not change existing gaze-py tier assignments to make tests easier. Do not invent local aliases (ArgumentMutation,ReturnValuetypos, etc.) as enum members. Gaze language-neutral aliases (e.g.ArgumentMutation→PointerArgMutation) are not emitted by snake-eyes; emit canonical names only.Use
enum.StrEnum(Python 3.11+).2. Data models
src/snake_eyes/analysis/models.pyLift structure from gaze-py
taxonomy/models.pybut reshapeEffectto the Gaze protocol v1.1.0 analyze payload, not gaze-py's internal model.Exact fields:
JSON keys for
FunctionRecordwhen serialized foranalyze(later issue) must bename,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.detailmust serialize as a JSON object or be omitted. Never serializedetail: nullif you can omit the key; if the protocol client requires the key, emit{}only when there is metadata. Decision: omitdetailandtargetandlocationfrom JSON when None so Gaze's optional fields stay optional.Helper:
function_record_to_dict(record) -> dictused by the server later. Implement it now and unit-test it.3. File discovery
src/snake_eyes/discovery.pyRules (do not ask):
.pyfiles.root_path, POSIX (/even on Windows in tests we control).patternsfollows Gaze's["./..."]convention:./...means recursive from root. Ifpatternsis 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__test.pytestsortest.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__pycache__(already covered) and.pyistub files (not.py).root_pathmissing or not a directory: raiseFileNotFoundErrorwith the path in the message. The server maps this to JSON-RPC-32602invalid params..pyfile inside root (optional; skipping symlink files entirely is also acceptable — pick skip all symlinks).4. Wire
discovermethodRequest params:
{"root_path": "/abs/path", "patterns": ["./..."]}Result:
{"source_files": ["src/foo.py"], "test_files": ["tests/test_foo.py"]}discoveron the server from issue 1.capabilities.discovertotrueininitialize. Leave the other three capability flags unchanged (falseunless a later issue already flipped them).root_path: error-32602.Do not have
discoverparse Python or detect effects.5. Package init
src/snake_eyes/analysis/__init__.pymay re-exportSideEffectTypeandTIER_MAP. Keep it thin.Tests required
tests/test_effects.py:len(SideEffectType) == 48TIER_MAPentryReturnValue,ErrorReturn,SentinelError,ReceiverMutation,PointerArgMutationtests/test_models.py:function_record_to_dictomits None optionalstypeis the canonical stringtests/test_discovery.pyusingtmp_path:src/+tests/layout → correct splittest_foo.pyat repo root classified as testfoo_test.pyclassified as test.venv/lib/python3.12/site.pyexcluded__pycache__/x.pyexcludedFileNotFoundError(or server-32602in the RPC test)tests/test_discover_method.py:initializereports"discover": trueCoverage strategy (Constitution IV)
effects.pymodels.pydiscovery.pyOut of scope
taxonomy/models.pyfields that are not in the protocolDone when
discoverworks over JSON-RPCinitialize.capabilities.discoveristrueeffects.py