* Initial plan
* feat(SM-20): extract registries into model/ module with SemanticModel interface
- Create model/type-registry.ts — TypeRegistry interface + factory
- Create model/method-registry.ts — MethodRegistry interface + factory
- Create model/field-registry.ts — FieldRegistry interface + factory
- Create model/semantic-model.ts — SemanticModel interface + factory
- Create model/heritage-map.ts — re-export HeritageMap types
- Create model/binding-accumulator.ts — re-export BindingAccumulator types
- Create model/resolve.ts — move lookupMethodByOwnerWithMRO from call-processor
- Update symbol-table.ts — delegate to SemanticModel for registry ops
- Update call-processor.ts — re-export lookupMethodByOwnerWithMRO from model/resolve
No circular dependencies: model/resolve.ts does NOT import resolution-context.ts.
All 775 related unit tests pass with no regressions.
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/27ad2975-1a31-4f50-815b-178ee8a95277
* fix: clarify re-export comment per code review feedback
Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/27ad2975-1a31-4f50-815b-178ee8a95277
* refactor(SM-20): wire up SemanticModel as first-class resolution input
PR #786 extracted TypeRegistry/MethodRegistry/FieldRegistry into model/
behind SemanticModel, but consumers still routed through SymbolTable
delegates. This change completes Phase 6 of the fuzzy-lookup elimination
roadmap by making call-processor, resolution-context, type-env, and
heritage-map query the model directly via `table.model.{types,methods,fields}`.
Also absorbs the open PR #786 review findings so the branch lands clean:
- Removed duplicate JSDoc block on lookupMethodByOwner (symbol-table.ts)
- Added model/index.ts barrel for the public model/ surface
- Fixed O(n) buildParentMapFromHeritage BFS via head-pointer queue
- Clarified re-export facade framing on binding-accumulator.ts and
heritage-map.ts inside model/
- Refined @internal JSDoc on lookupMethodByOwnerWithMRO
Changes:
- symbol-table.ts: expose `readonly model: SemanticModel` on the
SymbolTable interface. SymbolTable delegate wrappers (lookupClassByName
etc.) stay as thin pass-throughs for backward compat; deletion is a
follow-up once all internal callers are migrated.
- model/resolve.ts: lookupMethodByOwnerWithMRO now takes SemanticModel
instead of SymbolTable, removing the last SymbolTable import from the
model/ module. Preserves circular-dependency firewall.
- call-processor.ts: 6 call sites in D0 member resolution, field
resolution, ctor override, and ctor disambiguation migrated to
model.types/methods/fields.
- resolution-context.ts: tier 3 class+impl lookup migrated.
- type-env.ts: 5 sites across lookupClassDefsByName, resolveFieldType,
and resolveMethodReturnType migrated.
- heritage-map.ts: parent/child class-name resolution migrated.
Tests:
- symbol-table.test.ts: +10 parity and feeding-audit tests covering
every model.{types,methods,fields} path (Class, Method, Property,
Impl, Function-with-ownerId, Property-without-ownerId skip, arity
filtering, clear cascade).
- call-processor.test.ts: classLookupSpy now targets
ctx.symbols.model.types since the wrapper is bypassed.
- type-env.test.ts: createMockSymbolTable and the destructured-call
makeSymbolTable helpers gained a model shim that forwards to the
(possibly overridden) top-level lookup stubs.
Validation: full suite 5603 passed / 159 skipped, resolver integration
suite (19 files, 1766 tests) clean, tsc --noEmit clean.
* refactor(SM-21): invert ownership — SemanticModel contains SymbolTable
Follow-up to SM-20. Previously SymbolTable owned a `model` subfield;
this commit turns the ownership direction around so the SemanticModel
is the top-level container and SymbolTable is nested as `.symbols`:
SemanticModel (top-level, passed everywhere)
├── types (TypeRegistry)
├── methods (MethodRegistry)
├── fields (FieldRegistry)
└── symbols (SymbolTable — file-indexed + callable-name index)
The owner-scoped registries live directly on the model; file and
callable-name lookups go through `.symbols`. Consumers receive a
`SemanticModel` and reach into the appropriate field — no more
`table.model.types.X` double-hop.
Core changes:
- symbol-table.ts: createSymbolTable now takes injected
TypeRegistry/MethodRegistry/FieldRegistry via a SymbolTableDeps
argument. When omitted (test fallback), it creates standalone
registries locally and clears them in clear() — production callers
always inject. The five registry convenience delegates
(lookupClassByName, lookupMethodByOwner, lookupFieldByOwner,
lookupClassByQualifiedName, lookupImplByName) remain as thin
forwards to the injected registries so standalone SymbolTable use
(chiefly tests) stays ergonomic.
- model/semantic-model.ts: createSemanticModel() now creates the
three registries AND a SymbolTable wired to them, exposing the
SymbolTable as `.symbols`. clear() cascades through all four.
- resolution-context.ts: `readonly symbols: SymbolTable` field is
replaced with `readonly model: SemanticModel`. Internal factory
builds a SemanticModel and keeps a local `symbols` alias for
backward-compatible inner body.
Consumer migrations (src/):
- call-processor.ts: ctx.symbols.add/.lookupExactAll/
.lookupCallableByName → ctx.model.symbols.*; ctx.symbols.model.X →
ctx.model.X. buildTypeEnv option key renamed symbolTable → model.
- type-env.ts: symbolTable parameter renamed model (type
SemanticModel), all internal call sites rewritten to use
model.types.*, model.methods.*, model.fields.*,
model.symbols.lookupExactAll / .lookupCallableByName.
- heritage-map.ts: 2 class-lookup sites migrated.
- pipeline.ts: ctx.symbols → ctx.model.symbols throughout.
Test migrations:
- symbol-table.test.ts: parity tests (which validated the old
table.model.X hop) replaced with direct SemanticModel coverage via
createSemanticModel(). New tests exercise types/methods/fields/
symbols feeding end-to-end.
- type-env.test.ts: createMockSymbolTable rebuilt as a
SemanticModel-shaped mock that still accepts the legacy flat
override bag for backward compat; inline `makeSymbolTable` helpers
for destructured-call and importedReturnTypes suites rewritten to
match the new shape; buildTypeEnv options `symbolTable: X` and
`{ symbolTable }` shorthand renamed to `model:`; one real
createSymbolTable-based test rewritten to use createSemanticModel.
- call-processor.test.ts, heritage-map.test.ts,
heritage-processor.test.ts, symbol-resolver.test.ts: bulk sed
`ctx.symbols.` → `ctx.model.symbols.`. call-processor.test.ts spy
updated to target `ctx.model.types.lookupClassByName`.
Validation: full test suite 5589 passed / 169 skipped / 0 failed;
tsc --noEmit clean; pre-commit eslint + prettier + typecheck all
green. CLAUDE.md / AGENTS.md stats bumped from an earlier `npx
gitnexus analyze` refresh (3965 symbols / 10012 edges / 243 flows).
* refactor(SM-22/SM-23): dispatch table + DAG rearchitecture
SM-22: Extract registration dispatch table into model/registration-table.ts.
Replaces the if/else ladder inside SymbolTable.add() with an O(1)
Map<NodeLabel, RoutingDecision> fan-out. SemanticModel wires the table
per-instance so hooks close over the correct registries.
SM-23: DAG rearchitecture. symbol-table.ts is now a pure 2-index leaf
(fileIndex + callableByName) with zero imports from model/. All
type/method/field routing lives in the model/ layer. Tests migrated to
createSemanticModel() + model.symbols access pattern.
Tests: 5632 passed, 0 failures.
* refactor: delete dead code (skipCallableIndex + model/ facades)
Removes the unused skipCallableIndex flag from the registration dispatch
table and deletes two facade files that had zero consumers.
skipCallableIndex was declared on RoutingDecision and populated for all
10 entries but never read at runtime — semantic-model.ts explicitly
documented that the flag was NOT consulted. The callable-index gate
lives inside SymbolTable.add() via CALLABLE_TYPES.has(type), which is
the single source of truth. Deleting the flag keeps SymbolTable as the
sole decision point and removes documentation-as-data.
model/binding-accumulator.ts and model/heritage-map.ts were facade
pass-throughs of their parent-directory counterparts. Grep confirms no
consumer imports either from the model/ path — all usage goes through
../binding-accumulator.js and ../heritage-map.js directly. model/index.ts
was the only "user" and re-exported them with a note about unifying the
import boundary, but that boundary has no actual consumers today.
Resolves review findings M-01 and M-03 from
.context/compound-engineering/ce-review/20260411-144641-59605d93/maintainability.json
Tests: 5631 passed, 0 failures (1 less than pre-Unit-1: the
skipCallableIndex-specific assertion was removed).
* refactor: remove lookupMethodByOwnerWithMRO backward-compat shim
call-processor.ts re-exported lookupMethodByOwnerWithMRO from
./model/resolve.js as a backward-compat shim for symbol-table.test.ts.
The function already lives in model/resolve.ts and is re-exported
properly from model/index.ts (the barrel) — the call-processor shim
was a duplicate export path with no durable reason to exist.
Migrated the test import from call-processor.js to model/index.js
(the canonical barrel). Deleted the re-export statement and the stale
"re-exported for backward compatibility" comment block. Hoisted the
remaining import to the top of the file with the other imports; the
bottom-of-file position was a relic of the shim pattern.
Resolves review finding M-02 from
.context/compound-engineering/ce-review/20260411-144641-59605d93/maintainability.json
Tests: 5631 passed, 0 failures.
* refactor: harden registration dispatch runtime safety
Two hardening changes in semantic-model.ts, both closing silent-failure
paths in the SM-series dispatcher-bypass failure mode.
1. model.symbols.clear() now cascades to the owner-scoped registries.
Previously, the SymbolTable facade exposed rawSymbols.clear directly,
which only emptied fileIndex + callableByName — the types/methods/
fields registries stayed populated. Any caller holding a SymbolTable
reference that invoked .clear() left the model in a split state where
subsequent .add() calls double-registered in the registries. No
current caller exercises this path, but it was a latent phantom-
resolution risk that didn't belong in a public API. Extracted the
cascade into a single cascadeClear closure wired into both
model.clear() and the facade's clear field.
2. runExhaustivenessGuard now throws instead of console.warn on drift.
The production short-circuit via NODE_ENV === 'production' is
preserved, so real users never see the throw — but CI and dev runs
now fail loudly if a NodeLabel is added to gitnexus-shared without
being placed in one of the three registration-table allowlists. The
previous warn-only behavior was silent in test output volume; SM-19
already documented dispatcher-bypass as the dominant silent-failure
mode in this codebase.
Test-first: added test/unit/model/semantic-model.test.ts covering
model.symbols.clear() cascade (4 registries × clear = 4 tests), the
existing model.clear() cascade (regression guard), and a happy-path
construction test that verifies the current allowlists have zero drift.
Resolves correctness P2 finding (symbols.clear() partial clear),
correctness P3 (exhaustiveness warn-only), and kieran-typescript KT-03
(same exhaustiveness finding, agreement boost).
Tests: 5638 passed (+7 new), 0 failures.
* docs: fix stale JSDoc references in resolveStaticCall
call-processor.ts:2215-2216 referenced SymbolTable.lookupClassByName
and SymbolTable.lookupMethodByOwner via {@link}. Both methods were
removed from SymbolTable during SM-20 — they now live on TypeRegistry
and MethodRegistry respectively, accessible via model.types and
model.methods.
Other SymbolTable.* references in the codebase (lookupExactFull, add,
lookupCallableByName in call-processor.ts:593, symbol-table.ts:86,
type-extractors/types.ts:57) target methods that are still on
SymbolTable and remain valid.
Resolves correctness P3 and kieran-typescript KT-02 (same finding,
agreement boost).
* refactor: deduplicate ALL_NODE_LABELS constant
ALL_NODE_LABELS was private in semantic-model.ts and duplicated
verbatim in registration-table.test.ts. Two hardcoded lists meant a
new NodeLabel added to gitnexus-shared could land in one copy but not
the other, silently drifting the exhaustiveness invariant.
Exported ALL_NODE_LABELS from semantic-model.ts, re-exported through
model/index.ts for barrel consistency, and switched the test to import
it instead of redeclaring. The explanatory comment now describes the
single-source-of-truth contract.
Resolves maintainability M-04.
Tests: 5638 passed, 0 failures.
* refactor: add compile-time NodeLabel exhaustiveness check
The runtime exhaustiveness guard in semantic-model.ts caught drift at
test time. Added a type-level check in registration-table.ts that
catches drift at BUILD time — if a new NodeLabel is added to
gitnexus-shared without being classified into one of the three
allowlists, TypeScript fails the _exhaustiveCheck assignment and
names the missing label.
The runtime guard stays as belt-and-suspenders: if a future contributor
bypasses the type check with @ts-ignore, the runtime guard still fires
in dev/test.
Implementation: converted the three allowlist Set<NodeLabel> initializers
to use `as const` tuples, then derived a union type from the tuples and
asserted `Exclude<NodeLabel, union> extends never`. Zero runtime impact
— the exported Sets are unchanged, Map.get hot-path performance is
unchanged, the test API is unchanged.
Resolves kieran-typescript KT-04.
Tests: 21/21 registration-table tests pass with zero modifications.
* refactor(test): restore type safety to createMockSymbolTable
createMockSymbolTable was widened to (overrides: any = {}): any with an
eslint-disable-next-line, and every buildTypeEnv call site passed the
mock as `model: mockSymbolTable as any`. The widening masked silent
false-green tests: buildTypeEnv accesses model.types/methods/fields,
and a flat any-typed override could silently return undefined from a
path that TypeScript should have caught at compile time.
Defined LegacyMockOverrides interface with typed stubs for each method
the mock can override (SymbolTable reads + TypeRegistry/MethodRegistry/
FieldRegistry lookups). Return type is now SemanticModel, so the mock
object is compile-checked against the real interface — a missing
registry method is a type error, not a silent runtime undefined.
Removed the eslint-disable and all 9 `as any` casts at call sites
(lines 1287, 1300, 1307, 2124, 2138, 5823, 5835, 5850, 5870). The
mock's return value now flows through buildTypeEnv's typed `model`
option without coercion.
Resolves kieran-typescript KT-01 and testing gap TG-02. This was the
highest-value cleanup in the plan — the only finding representing real
hidden test weakness.
Tests: 360 passed | 7 skipped (type-env.test.ts), typecheck clean.
* test: close coverage gaps in model/ registries
Added direct unit tests for the three owner-scoped registries that
previously had only transitive coverage via symbol-table.test.ts and
registration-table.test.ts. These new tests pin behaviors that were
flagged by the testing reviewer as untested or undertested.
method-registry.test.ts (14 tests):
- T-01: arity-fallback branch — when argCount matches no overload,
fall back to the full pool so fuzzy resolution still has candidates.
Previously untested and would have returned undefined instead of
a valid candidate if the branch regressed.
- T-02: requiredParameterCount range filtering — methods with default
parameters accept any argCount in [requiredParameterCount,
parameterCount]. Previously untested at the registry level.
- Variadic fallback (parameterCount=undefined is retained during arity
narrowing, bypassing range check).
- Return-type dedup paths: shared returnType → first wins, differing
returnTypes → undefined, firstReturnType=undefined → undefined,
single-overload skips dedup entirely.
type-registry.test.ts (9 tests):
- classByName homonym accumulation (two User classes in different
packages both returned).
- classByQualifiedName disambiguation — same simple name, different
FQNs resolve independently.
- Partial classes with identical simple + qualified name accumulate
in both indexes.
- registerImpl stores Rust impl blocks separately from classes.
- Multiple impl blocks per type accumulate.
field-registry.test.ts (6 tests):
- register/lookup round-trip, owner-scope isolation, last-wins on
duplicate key (flat map, not overload list).
- clear + re-register round-trip.
Extended symbol-table.test.ts cascade test (renamed from "both
registries" to "all three registries and the nested symbol table") to
also assert model.methods and model.fields are cleared — the test
name previously implied full coverage but only asserted types + symbols.
Resolves testing findings T-01, T-02, T-03, T-05.
Tests: 5667 passed (+29 new), 0 failures.
* refactor(test): replace brittle reference-equality tests + add intent comments
Two cleanups flagged as low-severity P3 by the testing reviewer:
1. registration-table.test.ts: Replaced three reference-equality tests
(hook identity via toBe) with behavioral tests that survive a future
refactor to per-label closures. The new "class-like behavior group"
describe iterates Class/Struct/Interface/Enum/Record/Trait and
verifies each one writes to types.registerClass. Same pattern for
Method/Constructor. A separate "behavior group isolation" describe
verifies class-like hooks don't leak into methods/fields and Impl
never pollutes registerClass. Strictly more coverage than the
reference-equality tests provided and implementation-independent.
2. symbol-resolver.test.ts: Added a comment above the lookupExactFull
and SM-16: getFiles() describes explaining why they intentionally
use createSymbolTable() directly instead of createSemanticModel().
The DAG leaf-only behaviors they test do not involve registries, so
testing the bare SymbolTable keeps the unit isolated. Prevents a
future reader from "fixing" the inconsistency.
3. qualified-class-lookups.test.ts: Added a comment above
`const symbolTable = model.symbols` explaining that processParsing
writes still reach the owner-scoped registries via SemanticModel's
fan-out — the alias is convenience, not a leaf in isolation.
Resolves testing T-04, kieran-typescript KT-05, kieran-typescript KT-06.
Tests: affected files all green (112 passed in registration-table +
symbol-resolver + qualified-class-lookups).
* refactor(model): collapse RoutingDecision wrapper and trim barrel surface
Two cleanups against the advanced-review findings on post-Unit-9 state:
S2 (cross-reviewer agreement — architecture-strategist + code-simplicity):
Delete the RoutingDecision single-field wrapper interface. Post-Unit-1
it held exactly one field (hook: RegistrationHook) and added pure
ceremony at every call site — `dispatchTable.get(key)!.hook(name, def)`
vs the now-direct `dispatchTable.get(key)!(name, def)`. Change the Map
type from Map<NodeLabel, RoutingDecision> to Map<NodeLabel,
RegistrationHook>, drop the interface, and update 17 test call sites.
A3 (architecture-strategist): Trim model/index.ts barrel surface.
createRegistrationTable, RegistrationHook, and RegistrationTableDeps
were re-exported from the barrel despite having zero legitimate
consumers outside model/ itself. The only callers (semantic-model.ts
and registration-table.test.ts) import directly from
./registration-table.js. Barrel exposure invited external callers to
construct orphan dispatch tables with independent registries,
weakening the SM-21 ownership inversion where SemanticModel is the
composition root. Kept CALLABLE_ONLY_LABELS, INERT_LABELS,
DISPATCH_LABELS exported since those remain useful for downstream
resolution logic and have no construction risk.
Resolves review findings:
- S2 (code-simplicity P3, 0.85) + architecture-strategist residual
- A3 (architecture-strategist P3, 0.82)
Tests: 5674 passed, 0 failures. Typecheck clean.
* refactor(model): replace runtime exhaustiveness guard with compile-time bijection
Replace the three-layer drift protection (hardcoded ALL_NODE_LABELS
array + 3 tuple consts + _ExhaustiveLabelCheck type + runExhaustivenessGuard
runtime + CI taxonomy test) with a single Record<NodeLabel, LabelBehavior>
map that structurally proves every invariant at compile time.
## Before
- ALL_NODE_LABELS hardcoded in semantic-model.ts (36 entries, could drift)
- DISPATCH_LABELS_TUPLE / CALLABLE_ONLY_LABELS_TUPLE / INERT_LABELS_TUPLE
private tuples (36 more entries total, could overlap or miss)
- _ClassifiedLabel / _UncoveredLabel type-level check (caught missing
labels but NOT duplicates across tuples)
- runExhaustivenessGuard runtime throw (only defense against duplicates)
- NodeLabel taxonomy coverage test in CI (same check as runtime guard)
Four defenses for invariants that the type system can express directly.
## After
```ts
type LabelBehavior = 'dispatch' | 'callable-only' | 'inert';
const LABEL_BEHAVIOR = {
Class: 'dispatch',
// ...36 entries...
Tool: 'inert',
} as const satisfies Record<NodeLabel, LabelBehavior>;
```
The `as const satisfies Record<NodeLabel, LabelBehavior>` combo enforces:
1. **Every NodeLabel must be a key** — Record requires all K keys.
Adding a NodeLabel to gitnexus-shared without classifying it here
fails with "Property 'X' is missing in type ..." naming the drifted label.
2. **No non-NodeLabel keys allowed** — `satisfies` with object literals
triggers excess-property checking. A typo'd key fails to compile.
3. **No duplicate classification** — impossible by construction; object
keys are unique at the source level.
4. **Valid category** — LabelBehavior is a narrow union, typos caught.
`ALL_NODE_LABELS`, `DISPATCH_LABELS`, `CALLABLE_ONLY_LABELS`, and
`INERT_LABELS` are now derived via `Object.keys(LABEL_BEHAVIOR)` and
`filter(l => LABEL_BEHAVIOR[l] === ...)` — single source of truth,
structurally impossible to drift.
## Deleted
- runExhaustivenessGuard() function in semantic-model.ts (~18 lines)
- ALL_NODE_LABELS hardcoded array in semantic-model.ts (~38 lines)
- DISPATCH_LABELS_TUPLE / CALLABLE_ONLY_LABELS_TUPLE / INERT_LABELS_TUPLE
private consts in registration-table.ts (~30 lines)
- _ClassifiedLabel / _UncoveredLabel / _exhaustiveCheck type machinery
(~20 lines)
## Kept named proofs: none
The `as const satisfies` on the object literal already catches all four
drift modes. Named type-level proofs (_MissingFromMap / _ExtraKeysInMap)
are pure duplication and were removed per review.
## Also in this commit
- S6: trim wrappedAdd narration comments in semantic-model.ts
(Step 1/2/3 block comments removed; kept the Function+ownerId WHY note)
- A3: tighten model/index.ts barrel — createRegistrationTable,
RegistrationHook, RegistrationTableDeps remain direct-imports only;
ALL_NODE_LABELS and LabelBehavior re-exported from the new home in
registration-table.ts
## Resolves
- Advanced-review S4 (runtime guard per-call cost) — guard no longer exists
- Advanced-review S1 (tuple three-defenses indirection) — single Record replaces all tuples
- Correctness P3 (exhaustiveness warns-only) — structurally impossible to drift
- Unit 6 type-level check — subsumed by the Record type
- Unit 3 runtime throw — no longer needed
Tests: 5674 passed, 0 failures. Typecheck clean.
* test(model): delete duplicate closure-isolation spy tests
S5 (code-simplicity P3): The 'closure isolation — each hook can only
write to its registry' describe block duplicated the 'behavior group
isolation' block's coverage via a different mechanism.
Behavioral tests (lines 151-174, kept):
table.get('Class')!('User', def);
expect(deps.methods.lookupMethodByOwner('unrelated', 'User')).toBeUndefined();
expect(deps.fields.lookupFieldByOwner('unrelated', 'User')).toBeUndefined();
Spy tests (deleted, ~55 lines):
vi.spyOn(deps.methods, 'register')
table.get('Class')!('User', def);
expect(methodsSpy).not.toHaveBeenCalled();
Both assert the same invariant — classHook does not touch the methods or
fields registries. The behavioral form observes the END STATE of the
registry (lookup returns undefined), which is the actual contract.
The spy form asserts the IMPLEMENTATION (a specific method was not
called), which couples to internal wiring — a refactor to a different
register function name would break the spy test while the behavioral
test would still pass.
Also dropped the now-unused `vi` import from vitest.
Tests: 24/24 registration-table.test.ts pass (-4 from spy deletion).
* refactor(model): compile-time cross-invariant between CLASS_TYPES and dispatch classHook
A1 (architecture-strategist P2, 0.90): CLASS_TYPES in symbol-table.ts
and the class-like entries of the dispatch table were two independent
hardcoded sets. Adding a new class-like label (e.g. Swift 'Extension')
to one but not the other would silently degrade qualifiedName
population — the symptom is subtle (partial qualified-name lookups)
and no test asserted the co-extensive invariant.
Fixed with a single source of truth and a two-layer compile-time
enforcement:
## symbol-table.ts
- Add `CLASS_TYPES_TUPLE` as `readonly [...] as const satisfies
readonly NodeLabel[]`. The `satisfies` forces every tuple entry to
be a valid NodeLabel at compile time.
- Export derived type `ClassLikeLabel = typeof CLASS_TYPES_TUPLE[number]`.
- Derive `CLASS_TYPES` Set from the tuple — same runtime shape as
before, now typed `ReadonlySet<NodeLabel>`.
## registration-table.ts
- Import `CLASS_TYPES_TUPLE` and `ClassLikeLabel` from symbol-table.ts.
- Narrow the `satisfies` on `LABEL_BEHAVIOR` via intersection:
Record<NodeLabel, LabelBehavior> & Record<ClassLikeLabel, 'dispatch'>
This forces every class-like label to have value 'dispatch' at
compile time. Adding a label to CLASS_TYPES_TUPLE without
classifying it as dispatch in LABEL_BEHAVIOR fails to compile with
a type error naming the drifted label.
- Build the class-like entries of the dispatch Map by iterating
`CLASS_TYPES_TUPLE` at factory time. Adding a label to the tuple
automatically wires it to classHook — no second place to update.
## What the design prevents
1. Drift scenario A (A1 original): 'Extension' added to CLASS_TYPES_TUPLE
but not to LABEL_BEHAVIOR → compile error on LABEL_BEHAVIOR's
satisfies.
2. Drift scenario B: 'Extension' added to CLASS_TYPES_TUPLE but not
wired to classHook → impossible because the Map is derived from the
tuple.
3. Drift scenario C: class-like label classified as something other
than 'dispatch' in LABEL_BEHAVIOR → compile error on the narrowed
intersection.
Runtime behavior unchanged: same 6 labels in CLASS_TYPES, same 6
class-like entries in the dispatch Map. Tests pin the behavior via
the existing behavior-group tests in registration-table.test.ts.
DAG unchanged: registration-table.ts already imported from symbol-table.ts
(the allowed upward direction). symbol-table.ts still imports nothing
from model/.
Tests: 5670 passed, 0 failures. Typecheck clean.
* test(field-extraction): use SemanticModel facade instead of raw SymbolTable
A6 (architecture-strategist P3, 0.85): field-extraction.test.ts created
its FieldExtractorContext fixture with `symbolTable: createSymbolTable()` —
a raw SymbolTable leaf, not the facade. In production, the context's
symbolTable field is always `model.symbols` (the SemanticModel-wrapped
facade where .add() dispatches through the owner-scoped registries).
The current field extractors don't call symbolTable.add() at all, so
this change is behavior-neutral today. The value is architectural
consistency — matching the test fixture to the production shape
prevents silent drift if a future field extractor starts registering
dynamically-discovered properties via the context. Without the fix,
such writes would hit the raw leaf and skip the fan-out, and tests
would pass even though the symptom (empty FieldRegistry) would
manifest in production.
Tests: 50/50 field-extraction.test.ts pass. Production tsc --noEmit
clean. Test-tsconfig error count unchanged (634 pre-existing errors
in unrelated test files, out of scope).
* refactor(A5): decouple model/resolve.ts from language registry
Move the MroStrategy type into gitnexus-shared and replace the
language: SupportedLanguages parameter on lookupMethodByOwnerWithMRO
with a direct mroStrategy: MroStrategy literal. Callers derive the
strategy from their language provider before invoking the resolver.
model/resolve.ts no longer imports from ../languages/index.js, so the
model/ layer is free of cross-layer coupling with the language
registry — this closes finding A5 from the SM-20/21/22/23 advanced
review (plan 006).
* feat(A4): add MethodRegistry.lookupMethodByName flat-by-name index
Add a secondary `methodsByName: Map<string, SymbolDefinition[]>` index
on MethodRegistry that returns every method with a given unqualified
name, accumulated across owners and overloads. The new index shares
SymbolDefinition references with methodByOwner — no duplication.
This is step 1 of the A4 double-index removal (plan 006). Tier 3
global resolution will switch to this index in Unit 3 so Method and
Constructor can be removed from CALLABLE_TYPES in Unit 4.
* refactor(A4): extend Tier 3 + memberCallByFile to consult method registry
Add model.methods.lookupMethodByName to Tier 3 global resolution in
resolution-context.ts and to the callable-pool build in
call-processor.ts (resolveMemberCallByFile + D2 widen path).
Intentionally behavior-preserving: Method and Constructor are still
in CALLABLE_TYPES so the new lookup returns identical candidates that
already reach Tier 3 through callableByName. Both paths dedup by
nodeId during this intermediate state — Unit 4 shrinks CALLABLE_TYPES
and the dedup is removed.
Part of plan 006 A4 step 2.
* refactor(A4): shrink CALLABLE_TYPES to free callables only
CALLABLE_TYPES = {Function, Macro, Delegate}. Method and Constructor
are no longer double-indexed in callableByName — they reach resolvers
through model.methods.lookupMethodByName instead.
Companion changes:
- Introduce CALL_TARGET_TYPES = CALLABLE_TYPES ∪ {Method, Constructor}
for the resolver's kind filter (filterCallableCandidates,
countCallableCandidates). Separates registration semantics (narrow)
from the resolver's acceptable-target set (wide).
- type-env.ts for-loop return-type inference consults both indexes,
treating the union as the authoritative call pool.
- resolveMemberCallByFile + D2 widen path keep the nodeId dedup in
place: Python/Rust/Kotlin class methods emitted as Function+ownerId
still land in both indexes until Unit 5 unblocks the normalization.
- Tier 3 global resolution (resolution-context.ts) keeps the same
dedup for the same reason.
Test updates reflect the new contract: Method/Constructor live in
methodsByName, not callableByName. Orphan Method-without-ownerId now
lives only in the file index (no registry coverage).
Part of plan 006 — closes A4 for strictly-labeled methods. Python/
Rust/Kotlin Function+ownerId normalization is tracked as Unit 5
(blocked).
* refactor: rename CALLABLE_TYPES → FREE_CALLABLE_TYPES
Pure rename. The constant's meaning changed in Unit 4 (free callables
only — no methods, no constructors) so the name now reflects that
scope: "callables that have no owner scope". Updates the constant
declaration and every consumer in src/ and test/.
Closes plan 006 Unit 6.
* refactor(A2): strict SymbolTableReader (pure reads) + SymbolTableWriter (+add)
Split the SymbolTable interface into three strictly layered surfaces:
- SymbolTableReader: lookups + iteration. NO add, NO clear. Holders
cannot mutate the table in any way.
- SymbolTableWriter extends Reader: + add. NO clear. Holders can
register new symbols but cannot trigger a leaf-index reset.
- InternalSymbolTable (private, not exported): + clear. The cascading
reset capability is reachable only through createSymbolTable's
return type, held exclusively by SemanticModel.rawSymbols.
SemanticModel.symbols is now typed as SymbolTableWriter — external
consumers (workers, processors, pipelines) can register symbols and
query them, but cannot reach .clear(). The A2 LSP fix holds: callers
holding any public reference cannot desync the leaf indexes from the
owner-scoped registries.
Delete the transitional `type SymbolTable = SymbolTableReader` alias
and migrate every consumer (src + test) to the explicit names:
- Field and parameter annotations use SymbolTableReader by default;
only code that calls .add() uses SymbolTableWriter.
- parsing-processor (workers + sequential paths) takes
SymbolTableWriter so it can register extracted symbols.
- field-types, call-processor, named-binding-processor,
workers/parse-worker: use SymbolTableReader (query-only).
- Tests: drop the stale `clear` fields from mock factories and
migrate the semantic-model cascade tests from the removed
model.symbols.clear() path to model.clear().
Closes plan 006 Unit 7. Industry sources: TypeScript compiler API
builder pattern, Salsa ParallelDatabase, .NET IReadOnlyList. See the
a2-lsp-clear-contract-research artifact for full citations.
* feat(A2): add SemanticModel.resetFileIndex() partial-reset entry point
Add a named method that clears only the leaf file and callable
indexes without cascading to the three owner-scoped registries
(types, methods, fields). Replaces the rare partial-reset use case
that was previously reachable via the now-removed symbols.clear()
path from A2 (plan 006 Unit 7).
JSDoc makes the semantic difference with model.clear() explicit so
future readers don't have to guess which method to call for a given
reingestion scenario.
Test-first: three scenarios cover the partial-vs-full semantics,
re-add after reset, and idempotency.
Closes plan 006 Unit 8.
* docs(S7): trim registration-table module JSDoc
Remove the ~24 lines of design-provenance citations from the module
JSDoc. The rust-analyzer, TypeScript-compiler, and Fowler references
are preserved in git history via the original SM-22 commits and in
plan 006 Unit 9.
Keep the ownership diagram, behavior-group table, and the
'How to add a new NodeLabel' checklist — those are load-bearing for
future contributors.
Closes plan 006 Unit 9 (S7 advanced-review finding).
* test(S3): migrate type-env.test.ts off LegacyMockOverrides
Replace the createMockSymbolTable bridge and LegacyMockOverrides
interface with real createSemanticModel() + add() calls across all
14 call sites. Where a test needs a specific registry lookup that
can't be pre-populated cleanly, use vi.spyOn on the real registry
instead.
Pattern breakdown:
- Pattern A (pre-populate via model.symbols.add): 13 sites
- Pattern B (vi.spyOn on registry lookup): 1 site
Deletes LegacyMockOverrides + createMockSymbolTable entirely. The
real MethodRegistry arity/returnType semantics match the hand-rolled
mock behavior in every migrated case, and no 'as any' casts remain
in the file.
Closes plan 006 Unit 10 (S3 advanced-review finding).
* refactor: remove unused MroStrategy type exports from language-provider and resolve modules
* refactor: relocate symbol-table, heritage-map, resolution-context into model/
Use git mv so blame and history follow each file:
- gitnexus/src/core/ingestion/symbol-table.ts → model/symbol-table.ts
- gitnexus/src/core/ingestion/heritage-map.ts → model/heritage-map.ts
- gitnexus/src/core/ingestion/resolution-context.ts → model/resolution-context.ts
These three files are part of the SemanticModel layer (file/callable
indexes, heritage parent map, tiered resolver) and now sit alongside
the registries they collaborate with. Updates every consumer import
path across src/ and test/ to the new locations.
* refactor(model): enforce pure-leaf DAG + delete legacy re-exports
model/ is now a pure leaf: zero upward imports and zero compat
shims in its parent processors. Completes the DAG cleanup started
in the previous commit.
1. walkBindingChain — moved into model/resolution-context.ts;
named-binding-processor.ts deleted.
2. NamedImportMap + NamedImportBinding + isFileInPackageDir —
moved into model/resolution-context.ts. Every consumer now
imports from the canonical location directly. Legacy re-exports
in import-processor.ts deleted.
3. c3Linearize + gatherAncestors — moved into model/resolve.ts.
mro-processor.ts imports them back for computeMRO. Legacy
c3Linearize re-export from mro-processor.ts deleted.
4. ExtractedHeritage type — moved into model/heritage-map.ts.
call-processor.ts, parsing-processor.ts, pipeline.ts,
heritage-processor.ts, and the test files now import it from
the canonical location. Legacy re-exports in parse-worker.ts
and heritage-processor.ts deleted.
5. resolveExtendsType — rewritten in model/heritage-map.ts to
take an explicit HeritageResolutionStrategy (A5-style DI).
buildHeritageMap accepts an optional getHeritageStrategy
callback; production uses getHeritageStrategyForLanguage from
heritage-processor.ts. Legacy resolveExtendsType re-export
from heritage-processor.ts deleted.
Verified:
- grep 'from "..' gitnexus/src/core/ingestion/model → empty
- grep 'Re-export for legacy' gitnexus/src/core/ingestion → empty
- npx tsc --noEmit → clean
- npx vitest run → 5686 passing
* docs(model): strip phase/plan references from module comments
Remove SM-20/21/22/23, A2/A4/A5, plan 006, Unit N labels and historical
phrasing ("previously", "legacy", "model-leaf DAG cleanup") from all 10
files in src/core/ingestion/model/. Preserve domain vocabulary (Tier
1/2/3), invariants, and caveats — only the plan archaeology is gone.
* refactor(model): tighten interface segregation + compile-time invariants
Apply four gated findings from branch-wide code review:
- SemanticModel.symbols now typed as SymbolTableReader; MutableSemanticModel
widens it back to SymbolTableWriter. ResolutionContext.model is typed as
MutableSemanticModel since it owns the lifecycle. Resolvers that only
query symbols can annotate their own fields as SemanticModel to drop
write access at the type level.
- Lookup methods (lookupExactAll, lookupCallableByName, lookupClassByName,
lookupClassByQualifiedName, lookupImplByName) now return
readonly SymbolDefinition[]. The returned arrays are live views into
the internal indexes; the readonly marker prevents accidental caller
mutation. walkBindingChain return type narrowed to match.
- FREE_CALLABLE_TUPLE + FreeCallableLabel exported from symbol-table.ts
as the single source of truth for free-callable labels. LABEL_BEHAVIOR
now satisfies Record<FreeCallableLabel, 'callable-only'> as a second
cross-invariant alongside Record<ClassLikeLabel, 'dispatch'>. Adding a
label to the tuple without classifying it as 'callable-only' fails at
build time. CALLABLE_ONLY_LABELS is now a re-export alias of
FREE_CALLABLE_TYPES so the two sets cannot drift.
- walkBindingChain fast-exits before allocating its cycle-detection Set
when the caller's file has no named bindings. Skips ~200k transient
Set allocations per large-repo resolution pass.
Also fixes five stale comments flagged by the review: duplicate JSDoc
block on RegistrationHook merged; resolve.ts "delegates to mro-processor"
direction corrected; RegistrationTableDeps JSDoc names
createRegistrationTable (not createSymbolTable); mro-processor.ts
"re-exported at top" stale comment removed; gatherAncestors export
comment matches reality.
tsc --noEmit clean, full test suite green (5786 tests).
* refactor(model): resolve four deferred P2 review findings
Address the four gated items from the branch-wide review that needed
design decisions before applying:
F#3 — Method/Constructor without ownerId fallback to callable index.
The dispatch hook silently skips owner-scoped labels that lack an owner
(an extractor contract violation — AST-degraded parse, or a buggy
language extractor). Pre-dispatch-table code let such defs fall through
to callableByName and stay reachable at Tier 3 global resolution. This
restores that fallback in SymbolTable.add so orphaned Methods and
Constructors don't silently vanish. Property deliberately does NOT
participate in the fallback to avoid polluting common names like
id / name / type.
F#4 — Delete MutableSemanticModel.resetFileIndex. The method had zero
production callers (only three tests), documented a "rare partial-
reingestion flow" that was never implemented, and contained the
adversarial-reviewer's double-populate trap: calling resetFileIndex
followed by re-adding the same class symbol would push a duplicate
SymbolDefinition into TypeRegistry.classByName without ever clearing
the first one. If incremental reingestion is ever needed, it can be
designed properly with per-file TypeRegistry invalidation. For now,
deleting the footgun is safer than documenting it.
F#5 — Compile-time dispatch-table completeness check. `LABEL_BEHAVIOR`
already enforces "every NodeLabel is classified" via
`Record<NodeLabel, LabelBehavior>`, but the dispatch-table factory
populated its Map with manual `table.set(...)` calls that TypeScript
could not correlate back to the `'dispatch'` classification. Add a
type-level `DispatchLabel` extracted from `LABEL_BEHAVIOR` via a
conditional mapped type, and build the table from an object literal
that satisfies `Record<DispatchLabel, RegistrationHook>`. Adding a new
dispatch-classified label without wiring it to a hook now fails the
build with a named-key error — no more silent no-op hooks.
F#7 — Tier 3 dedup fast-path via MethodRegistry.hasFunctionMethods.
The Set-based dedup between callableDefs and methodDefs is only needed
when a Python/Rust/Kotlin class method (emitted as Function+ownerId by
the worker) lands in both indexes. For TS/Java/C#/C++/Ruby-only repos
— where the two indexes are disjoint by construction — the dedup was
pure overhead on every global-tier hit. MethodRegistry now tracks
whether any Function-typed def was ever registered, and resolution-
context branches Tier 3 into a concat-only fast path when that flag
is false. Slow path with dedup survives unchanged for mixed-language
repos.
New tests pin the invariants: hasFunctionMethods flag transitions,
Method/Constructor orphan fallback, Property non-fallback, and the
MethodRegistry clear() reset. Full test suite green (5756 tests).
* refactor(model): close remaining P3 review findings + coverage gaps
Address the remaining review items in one batch.
Production refactors:
- Rename classHook → classLikeHook (M05). The hook handles Class /
Struct / Interface / Enum / Record / Trait; the vocabulary used in
surrounding docs and the behavior-group table is "class-like". The
rename makes the code match the taxonomy without forcing readers
through a mental glossary.
- Extract MAX_BINDING_CHAIN_DEPTH constant in resolution-context.ts
and document it as a known silent false-negative source (ADV-003).
Five hops cover the common TypeScript monorepo pattern; raising the
cap is a one-line change if a real repo exceeds it. walkBindingChain
consumes the constant so the 5 magic number no longer floats free.
- Replace defs.filter() allocation in MethodRegistry.lookupMethodByOwner
with a two-pass streaming count + conditional materialization
(PERF-04). Pure-match and pure-reject arity paths now skip the
filtered-array allocation entirely; only the discriminating case
(at least one match AND at least one rejection) pays it.
- Rewrite NOOP_SYMBOL_TABLE in parse-worker.ts and NOOP_SYMBOL_TABLE_SEQ
in parsing-processor.ts to implement all six SymbolTableReader
methods (ADV-005). The `as unknown as SymbolTableReader` cast is
removed in favor of a direct SymbolTableReader annotation, so future
additions to the interface surface as compile errors on the stubs
instead of silently falling through.
- type-env.ts getCallableUnionCount and getFirstCallable now take
`model: SemanticModel` as an explicit argument instead of reaching
into the enclosing `model!` non-null assertion (KT-003). Callers
enter via an `if (model)` guard and pass the narrowed reference, so
the non-null precondition is visible at the type level and the
closures cannot be accidentally extracted into a context without
the guard.
- Tier 3 dedup in resolution-context.ts now covers all four index reads
(classDefs, implDefs, callableDefs, methodDefs) via a pushUnique
helper (C-03). Previously classDefs and implDefs were spread directly
without dedup; any theoretical nodeId collision would have produced
duplicates in globalDefs.
Test infrastructure:
- Extract makeDef / makeMethod factory helpers into
test/unit/model/helpers.ts (T-07). The four registry/table test
files now import the shared helper and specialize with overrides,
removing ~25 lines of duplicated boilerplate and creating a single
point of maintenance.
New test coverage:
- T-01: c3 BFS fallback — cyclic Python hierarchy that fails c3
linearization and must fall back to heritageMap.getAncestors() BFS
order. Added to the lookupMethodByOwnerWithMRO describe block.
- T-02: Tier 2a-named precedence — verifies the binding chain walker
fires before Tier 2a import-scoped when an aliased import
`import { User as U } from B` competes with a raw same-name Tier 2a
hit. Also pins Tier 1 same-file precedence over Tier 2a-named.
- T-03: Tier 3 Function+ownerId dedup — end-to-end test that a Python
class method emitted as `Function + ownerId` yields exactly ONE Tier
3 candidate (not two). Companion test pins the fast-path branch for
hasFunctionMethods === false repos.
- T-06: walkBindingChain guards — circular re-export detection,
depth-cap exceeded drop, and boundary case at exactly
MAX_BINDING_CHAIN_DEPTH hops resolving successfully.
All tests added to a new test/unit/model/resolution-context.test.ts
dedicated to ResolutionContext.resolve() tier-precedence invariants.
Full suite: 5708 passing (minus the known Windows LBUG lock flake
that passes in isolation).
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>