Repository navigation
Description
Part of the epic codellm-devkit/.github#35
(canonical schema v2 consistency). The decorator shape is decided by
.github#36, the canonical projection
contract — this issue implements whatever #36 settles on.
Evidence and recommended shape:
docs/design/specs/decorator-representation-input.md.
Supersedes #127 (folded in) and #129 (answered byqualified_name, no edge family).Blocked on #36. Do not start until the projection contract fixes the shape.
Problem
decorators is a flat list of ast.unparse output — ["trace('hot')", "functools.lru_cache(maxsize=128)"] — on PyCallable only.
The keystone specifies the opposite, at L1, on type/callable/field, and says so in as many
words: "Structured { name, args[], span } — not flat strings."
codeanalyzer-typescript-v2 already ships that shape (src/schema/schema.ts, TSDecorator) on
classes, callables, parameters, and properties. Python is the divergent analyzer.
The cost of the current shape: @app.route and @blueprint.route are unequal strings even when
they resolve to the same function, arguments are not separable from the callee, and PyClass
carries no decorators at all — so @dataclass is dropped silently (verified on main 6f02581).
Scope boundary
Implements the shape settled by .github#36 in codeanalyzer-python only. Does not close
TypeScript's mirror-image gap — it has the structured data but no Neo4j decorator projection,
which is its own migration item under #35. Does not touch python-sdk
(its own work item, filed when picked up), does not add a decoration edge family (spec D5), does
not address async (#130), does not fix the PyCG call-edge gap (#131), and does not touch
codeanalyzer-java's annotations.
Goals
-
PyDecoratormodel:name,qualified_name: Optional[str],positional_arguments: List[str],keyword_arguments: Dict[str, str],span: Optional[Span] -
PyCallable.decoratorsretypedList[str]→List[PyDecorator] -
decoratorsadded toPyClass,PyClassAttribute,PyCallableParameter - Populated at L1 for all four, at every nesting depth
-
qualified_nameresolved via Jedi;Nonewhen unresolvable, never raising - Neo4j:
:PyDecoratorgainsqualified_name, keeps merging onname(spec D7);PY_DECORATED_BYstart labels widened to includePyClass - Cache-invalidation guard (
core.py:728) rejects caches built with the old decorator shape -
.claude/SCHEMA_DECISIONS.mdgains one line per decision D1–D8, linking the spec
Caveats and known risks
- No
schema_versionbump, and none is claimed. Schema v2 is a moving target until Java,
Python, and TypeScript settle on a stable shape, so there is no frozen contract number for this
change to amend.python-sdkshipsdecorators: List[str]today and breaks with no version
signal — that is a real cost, and the mitigation is the SDK update, not a number. - Stale caches are the sharp edge.
core.py:728accepts any cache matching the current
schema_versionliteral. Change the shape without moving that guard and a cached analysis
silently feeds old-shape decorators through a new analyzer. qualified_nameis best-effort. Dynamic attribute access, conditional imports, and
runtime-built decorators will not resolve.Noneis the normal path there, not an error.PyCallableParameter.decoratorswill always be empty — Python has no parameter-decorator
syntax. Carried for cross-language parity; droppable at review if judged dead weight.- Monotonicity gate applies: decorators are L1 data and must not be gated behind a level.
Definition of done
@functools.lru_cache(maxsize=128)emitsname: "lru_cache",
qualified_name: "functools.lru_cache",keyword_arguments: {"maxsize": "128"}@trace('hot')emitspositional_arguments: ["'hot'"]and resolvesqualified_nameto the
declaring module'strace@dataclassemits on the class node; a decorated class attribute and a decorated nested
class both emit@app.routeand@blueprint.routebound to the same function produce the samequalified_name- A deliberately unresolvable decorator emits
qualified_name: nulland does not raise - A cache written with the old shape is rejected and rebuilt, not round-tripped
- Every existing test that reads
decoratorsas strings is updated, not deleted analysis.json(-a 1) ⊆ ... ⊆ (-a 4)still holds- Neo4j
PY_DECORATED_BYtraverses from a:PyClassnode andqualified_nameis queryable
Activity
[-]Decorators are raw source strings with no resolved identity[/-][+]Structured decorator representation: PyDecorator on callable, class, attribute, parameter[/+]codeanalyzer-*andpython-sdk.github#35rahlk commentedon Aug 20, 2026
Three findings from a verification pass on
a0b94ff, plus an unblock.Unblocked. The "blocked on .github#36" note is withdrawn. That block assumed the decorator shape was a cross-analyzer contract decision. It is now settled that
codeanalyzer-pythonis the reference implementation and TypeScript/Java migrate to whatever it lands, so the shape is decided here.1. Class decorators are unrecoverable, not merely unstored.
ClassDef.linenopoints at theclasskeyword, so decorator lines fall outsidePyClass.span:So
module.source[span.bytes]— the documentedget_method_bodycontract — cannot recover them either. The field is the only possible home.2. The resolution this issue asks for already exists and is discarded.
accessed_symbolson a decorated callable already carries Jedi's resolution:qualified_nameis computed at build time and simply not attached to the decorator. That makes thequalified_namegoal substantially cheaper than this issue implies — it is plumbing, not new resolution.3. An internal consumer is already broken by the string shape. Filed separately as #135:
access_paths.py:255string-matches"staticmethod" not in decorators, so@builtins.staticmethodon a method whose first parameter isself/clsgets a receiver it does not have. Narrow trigger, but it demonstrates the cost inside the analyzer, not just downstream.Also worth deciding before building: a decorator
calleegoingnull → idat L1→L2 would be a second sanctioned monotonicity refinement..claude/SCHEMA_DECISIONS.mdcurrently names exactly one (call.callee), plusBodyNode.parentat L2→L3. Resolve deliberately rather than at CI.Not verified: no test asserts decorator content today —
test/test_cli.py:147-172only counts call-graph edges on the decorator fixture. Stated from a reading of that test, not from a run.