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'); + }); + }); });