Skip to content

refactor(schema)!: native v2 model — one model family + per-run passes - #97

Merged
rahlk merged 8 commits into
mainfrom
refactor/issue-096-native-v2-model
Aug 27, 2026
Merged

refactor(schema)!: native v2 model — one model family + per-run passes#97
rahlk merged 8 commits into
mainfrom
refactor/issue-096-native-v2-model

Conversation

@rahlk

@rahlk rahlk commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Closes #96. Design record: docs/design/specs/native-v2-model.md.

What

Retires the two-model architecture: the v1 TSApplication compute model and the ~720-line emit-time v1→v2 transform (src/schema/v2/emit.ts + dataflow.ts) are gone. src/schema/schema.ts now IS schema v2 — envelope TSAnalysis → root TSApplicationTSModule/TSType/TSCallable/TSField/TSBodyNode — built directly by the builders, mirroring codeanalyzer-python's post-rewrite native py_schema.py architecture.

Per-run passes stamp the derived layers (python parity: assign_ids.py et al.):

  • assignIdscan:// ids + sig→id map + collision gate (per-run because ids embed --app-name while the cache round-trips the tree)
  • l1Body — internal call_sitesbody{} call nodes (wholesale per-run rebuild; makes the pass chain idempotent)
  • heritageextends_ids/implements_ids (resolved-only)
  • homing + l2Callees — external/synthesized endpoint homing, callee null→id, call-graph re-identification + dangling gate
  • src/dataflow/attach.ts — the stage that computes the program graphs also writes them onto the tree

finalizeAnalysis (src/schema/emit.ts) runs the spine — invoked by analyze(), which now returns AnalysisResult {application (wire), internal, program_graphs, idBySig, collisions, dangling}. Serialization is dumb: JSON path writes the envelope verbatim; Neo4j projects the same envelope with typed field access (no more attribute probing). INTERNAL fields (call_sites, abs_path, cache trio) ride the model and are stripped by key in one place. The analysis cache stores only the id-free builder tree.

Wire stability (the gate)

analysis.json and the Neo4j projection are unchanged — schema_version stays 2.1.0, no SDK impact. Proven by transition goldens captured from the branch base: deep-equal analysis.json at -a 1|2|3|4 × 3 fixtures (sample-app, dataflow-app, anon-app, union provider) + sorted-line graph.cypher, green on every stage commit and retired with the transform they guarded (the standing conformance + monotonicity + Neo4j gates carry coverage from here). The goldens caught one real reconstruction bug during Stage 1 (a clamp that appended an ellipsis the original never did).

Commits = the spec's stages

  1. test(goldens) — pin the pre-rewrite wire
  2. refactor(schema) — Stage 1: native tree + passes
  3. refactor(core) — Stages 2+3 (landed together; the pass chain is order-coupled): core owns the spine, attach moves to dataflow, emission thinned
  4. refactor(schema)! — Stage 4 teardown: terminal TS* names, src/schema/v2/ deleted, typed Neo4j projection
  5. test — suite re-pointed at the terminal analyze() API
  6. test(goldens) — transition harness retired
  7. docs(design) — spec recorded as implemented

! marks the internal API break (analyzer-internal only; the wire is unchanged).

Verification

  • Full suite: 137 pass / 0 fail (6 Docker-gated skips) · tsc --noEmit clean
  • bun run build compiles; binary smoke-tested (-a 2 on sample-app: schema 2.1.0, 7 modules, 57 edges)
  • bun run gen:schema: schema.neo4j.json byte-stable
  • git grep -l "toV2|V2Application|V2Root|V2Node" → 0 files

Release (1.1.0) is deliberately not part of this PR.

rahlk added 8 commits August 26, 2026 17:57
…on (#96)

Deep-equal analysis.json goldens at -a 1..4 plus sorted-line cypher goldens
for sample-app, dataflow-app, and anon-app, captured with the default union
provider. Transition-scoped: deleted at Stage 4 teardown.
…sses (#96)

Stage 1 of the native-model rewrite (docs/design/specs/native-v2-model.md):

- src/schema/schema.ts is now the native v2 shape: v2-bucketed, span-only
  containers (types{}/functions{}/fields{}, callables{}, body{}), omit-
  instead-of-null leaves, and the INTERNAL fields (call_sites, abs_path,
  cache trio) that never reach the wire.
- builders.ts constructs that shape directly; the id-free tree is what the
  cache round-trips (ids embed --app-name, so they are per-run).
- Python-parity passes: assignIds (can:// ids + sig→id map + collision
  gate), l1Body (call_sites → body{} call nodes, wholesale per-run rebuild
  that also makes repeated emission idempotent), heritage (extends_ids/
  implements_ids), homing (externals + 2.1.0 synthesized index), l2Callees
  (callee null→id + call-graph re-identification + dangling gate).
- emit.ts no longer reshapes anything: it runs the passes, assembles the
  envelope, and returns a deep internal-field-stripped wire copy.
- callGraph.ts / dataflow readers walk the new buckets via the shared
  forEachCallable/forEachType walkers; RTA's class index keeps its
  historical module/namespace-scope reach.
- Dead v1-only baggage deleted: per-node code, local_variables, value,
  container flat line/col ints, module_name/file_path.

Wire-stable: all 12 pre-rewrite goldens (analysis.json -a 1..4 × 3
fixtures + cypher) deep-equal; full suite green.
Stages 2+3 of the native-model rewrite, landed together because the pass
chain is order-coupled (l2 passes join on assignIds output, the attach on
both) — relocating it piecemeal would have made two incoherent halves:

- finalizeAnalysis (src/schema/emit.ts) is the per-run pass spine — ids,
  body, heritage, homing, callee backfill, call-graph re-identification,
  dataflow attach, envelope, internal-field strip — and analyze() runs it
  (python-parity: core.py owns the passes). It returns both views:
  {application (wire), internal (live tree), idBySig, collisions, dangling}.
- The L3/L4 tree-attach moves to src/dataflow/attach.ts and takes the
  program-graph IR directly — the stage that computes the graphs also
  writes them onto the tree.
- serialize.ts no longer reshapes anything: JSON path writes the envelope,
  Neo4j path projects it. src/schema/v2/ is now model types + a transition
  shim (toV2/toV2Detailed → finalizeAnalysis) that Stage 4 retires.
- saveCache stays ahead of the passes: the cached tree is id-free
  (--app-name is per-invocation) and holds only source-of-truth layers.

Wire-stable: 12 goldens deep-equal; full suite + typecheck green.
Stage 4 (internal-only breaking surface; the wire is unchanged):

- src/schema/v2/ is gone. schema.ts now also holds the wire envelope
  (TSAnalysis), the application root (TSApplication — python's
  PyApplication analog), and the wire edge shapes (TSCallGraphEdge,
  TSParamEdge). The internal working set is AnalysisInternal.
- toV2/toV2Detailed shims retired; finalizeAnalysis(app, pg, opts) is the
  one pass-runner, and the program-graph IR rides AnalysisResult instead
  of the application aggregate.
- The Neo4j projection reads typed fields (no more V2Node attribute
  probing); prop semantics preserved bit-for-bit (goldens).
- The analysis cache stores only the id-free tree (the write-only cached
  call_graph is dropped).
- CLAUDE.md architecture/dir-map rewritten for the native model (also
  carries the pending caveman-compression of the guide); the local
  SCHEMA_DECISIONS.md gains the native-model decision table.

Wire-stable: 12 goldens deep-equal; full suite + typecheck green.
analyze() returns AnalysisResult; tests read .application (wire),
.internal (working set), .program_graphs (IR) instead of re-running the
retired toV2/toV2Detailed shims. The v1-superset gate becomes the id-
registration gate (its v1 premise no longer exists; completeness is
carried by the transition goldens and the standing conformance gates).
The rewrite is complete and wire-stability is proven; the deep-equal
goldens and their gate leave with the transform they guarded. The
standing schema conformance, monotonicity (L1⊆L2⊆L3⊆L4), and Neo4j
gates carry coverage from here.
@rahlk
rahlk merged commit c2f27b0 into main Aug 27, 2026
1 check passed
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.

Native v2 model: retire the v1 compute model and the emit-time transform

1 participant