From 19974b49d8830a0da07d6486e9b9f9aa3aaa13e1 Mon Sep 17 00:00:00 2001 From: ReidenXerx Date: Thu, 6 Aug 2026 20:32:38 +0300 Subject: [PATCH] fix(scope-resolution): link type consumers to the type they name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An exported contract type owned its members after round 1 and still answered `incoming: {}`, so "what breaks if I remove this field?" — the question a contract type exists to answer — had no edge to walk. Measured on the reporting repo: all 324 TypeAlias nodes AND every Interface node had DEFINES as their only incoming edge. Two independent causes, and the second is why the first was not enough. TypeScript captured no type references at all — only cpp and csharp did — so an annotation naming a declared type minted no reference site. Added for annotations, generic arguments and `as` assertions, anchored to those contexts rather than a bare `(type_identifier)`, which would also match the name in `type X = …` and make every declaration a consumer of itself. That alone fixed interfaces and left aliases still empty. `TypeAlias` was missing from `LINKABLE_LABELS`, so alias graph nodes were never indexed in `nodeLookup` and `resolveDefGraphId` could not bridge a def to its node — the edge was dropped AFTER a successful lookup. `CLASS_KINDS` has always listed TypeAlias and the ClassRegistry returned the def correctly, which is what made this read as a resolution failure; instrumenting the lookup showed it returning the right def all along and moved the search one table over. Exactly the bug already documented two entries above it for Trait. Fixes every language that spells an alias this way — TypeScript, Kotlin, Dart and Rust all emit `@declaration.type_alias`. Co-Authored-By: Claude Opus 5 (1M context) --- .../ingestion/languages/typescript/query.ts | 31 +++++++++++++++++++ .../graph-bridge/node-lookup.ts | 15 +++++++++ .../typescript-alias-fields/contracts.ts | 1 + .../resolvers/typescript-alias-fields.test.ts | 21 +++++++++++++ 4 files changed, 68 insertions(+) diff --git a/gitnexus/src/core/ingestion/languages/typescript/query.ts b/gitnexus/src/core/ingestion/languages/typescript/query.ts index 04886ccb4..b2c0181f9 100644 --- a/gitnexus/src/core/ingestion/languages/typescript/query.ts +++ b/gitnexus/src/core/ingestion/languages/typescript/query.ts @@ -1218,6 +1218,37 @@ export const TYPESCRIPT_SCOPE_QUERY = ` (object (shorthand_property_identifier) @reference.name @reference.property-key @reference.value-ref) + +;; References — TYPE POSITION (R2-2). An annotation naming a declared type is +;; the only thing that makes that type's declaration reachable from the code +;; that depends on it, and TypeScript captured none: only cpp and csharp emitted +;; type references at all. So an exported API-contract type owned its members +;; (round 1) but had \`incoming: {}\`, and "what breaks if I remove this field?" +;; — the question a contract type exists to answer — had no edge to walk. +;; +;; The resolution path was already complete on the other side: +;; \`type-reference\` routes to the ClassRegistry, whose CLASS_KINDS already +;; lists TypeAlias, Interface and Enum, and \`edges.ts\` already maps the kind to +;; USES. Only the capture was missing. +;; +;; Anchored to the CONTEXTS a type is used in — annotations, type arguments, +;; and heritage \`implements\` — never a bare \`(type_identifier)\`. A blanket rule +;; would also match the identifier in \`type X = …\` and \`interface X\`, making +;; every declaration a consumer of itself. +(type_annotation + (type_identifier) @reference.name @reference.type_reference) + +(type_annotation + (generic_type + name: (type_identifier) @reference.name @reference.type_reference)) + +(type_arguments + (type_identifier) @reference.name @reference.type_reference) + +;; \`x as SomeType\` / \`satisfies SomeType\` — an assertion is a claim ABOUT a +;; declared type, so the code making it depends on that declaration. +(as_expression + (type_identifier) @reference.name @reference.type_reference) `; /** diff --git a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts index a8f90393a..81bf84e58 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts @@ -273,6 +273,21 @@ export const LINKABLE_LABELS: ReadonlySet = new Set([ // IMPLEMENTS edges from classes to traits are otherwise invisible to // the scope-resolution MRO pass. 'Trait', + // TypeAlias is linkable for the same reason Trait is (R2-2). The alias + // resolves fine — `CLASS_KINDS` has always listed it, and the ClassRegistry + // returns the def — but without an entry here `resolveDefGraphId` cannot + // bridge that def to its graph node, so the edge is dropped after a + // SUCCESSFUL lookup. That is why an exported contract type owned its members + // and still reported `incoming: {}`: the failure was one table away from + // everything that appeared to be responsible. + // + // Covers every language that spells an alias this way — TypeScript, Kotlin, + // Dart and Rust all emit `@declaration.type_alias`. The remaining + // `CLASS_KINDS` entries (Typedef, Record, Union, Delegate, Annotation, + // Template) plausibly have the same gap, but nothing exercises them today + // and adding labels no test covers is how this list drifts out of sync with + // what it claims. + 'TypeAlias', // Variable / Property are linkable too — receiver-bound write/read // ACCESSES edges target field nodes (e.g. `user.name = "x"` → // ACCESSES edge to User's `name` Variable/Property node). diff --git a/gitnexus/test/fixtures/lang-resolution/typescript-alias-fields/contracts.ts b/gitnexus/test/fixtures/lang-resolution/typescript-alias-fields/contracts.ts index 640cd2788..e9382cb7f 100644 --- a/gitnexus/test/fixtures/lang-resolution/typescript-alias-fields/contracts.ts +++ b/gitnexus/test/fixtures/lang-resolution/typescript-alias-fields/contracts.ts @@ -17,3 +17,4 @@ export function renderAlias(cfg: LiveModeConfig): number { export function renderIface(cfg: LiveModeIface): number { return cfg.ifaceSlots; } + diff --git a/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts b/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts index 4911054b9..8b9f22100 100644 --- a/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts +++ b/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts @@ -80,4 +80,25 @@ describe('TypeScript type-alias and interface members (A4)', () => { it('links an alias field to its consumer', () => { expect(readersOf('bookNotionalUsdt')).toContain('renderAlias'); }); + + // R2-2. Owning the members was only half of it: with no edge INTO the type, + // `context()` on an exported contract answered `incoming: {}`, so "what + // breaks if I remove this field?" — the question a contract type exists to + // answer — had nothing to walk. Measured on the reporting repo, all 324 + // TypeAlias nodes and every Interface node had DEFINES as their ONLY + // incoming edge, because TypeScript captured no type references at all. + describe('type consumers (R2-2)', () => { + const usersOf = (typeName: string): string[] => + getRelationships(result, 'USES') + .filter((e) => e.target === typeName) + .map((e) => e.source); + + it('links a parameter annotation to the alias it names', () => { + expect(usersOf('LiveModeConfig')).toContain('renderAlias'); + }); + + it('links a parameter annotation to the interface it names', () => { + expect(usersOf('LiveModeIface')).toContain('renderIface'); + }); + }); });