diff --git a/gitnexus/src/core/ingestion/utils/ast-helpers.ts b/gitnexus/src/core/ingestion/utils/ast-helpers.ts index 9b243d044..f4ec2dd2f 100644 --- a/gitnexus/src/core/ingestion/utils/ast-helpers.ts +++ b/gitnexus/src/core/ingestion/utils/ast-helpers.ts @@ -335,6 +335,15 @@ export const CLASS_CONTAINER_TYPES = new Set([ 'class_declaration', 'abstract_class_declaration', 'interface_declaration', + // A TypeScript object-type alias owns its members exactly as the interface + // beside it does — same `property_signature` members, same "who reads this + // contract field?" question. Without it an alias member is minted with a + // bare id and no owner, so two aliases in one file sharing a field name + // collapse onto one node and nothing links the field to its consumers, + // while the identical interface resolves. Aliases with no object type + // (`type Id = string`) declare no members, so they own nothing and are + // unaffected. + 'type_alias_declaration', 'struct_declaration', 'record_declaration', 'class_specifier', @@ -398,6 +407,9 @@ export const CONTAINER_TYPE_TO_LABEL: Record = { class_declaration: 'Class', abstract_class_declaration: 'Class', interface_declaration: 'Interface', + // Required by the CLASS_CONTAINER_TYPES invariant above: a container missing + // here gets orphaned member edges or a wrong owner label. + type_alias_declaration: 'TypeAlias', struct_declaration: 'Struct', struct_specifier: 'Struct', class_specifier: 'Class', diff --git a/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json b/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json index 14d56f642..91ee4fa05 100644 --- a/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json +++ b/gitnexus/test/fixtures/pipeline-golden/mini-repo/expected-graph.json @@ -3,7 +3,7 @@ "fixture": "mini-repo", "totalFileCount": 7, "symbols": 41, - "relationships": 77, + "relationships": 80, "processes": 4, "byType": { "Class": 1, @@ -17,6 +17,7 @@ "Property": 8 }, "byRelType": { + "ACCESSES": 3, "CALLS": 9, "CONTAINS": 7, "DEFINES": 16, @@ -26,5 +27,5 @@ "MEMBER_OF": 12, "STEP_IN_PROCESS": 12 }, - "edgeDigest": "1ccfc9e5de04d7e71430bcfeb3ce61fe85135b826f33aad2b1bdd2ee193e4728" + "edgeDigest": "d617ad94554d760eb0497bf5365b7b0a2e21842ab5b16568def721b954aa1594" } diff --git a/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts b/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts index 420aa7e8c..d896c68dc 100644 --- a/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts +++ b/gitnexus/test/integration/resolvers/typescript-alias-fields.test.ts @@ -73,8 +73,19 @@ describe('TypeScript type-alias and interface members (A4)', () => { // `fieldFallbackOnMethodLookup: false` (scope-resolver.ts) because name // matching over-connects in a typed language, and the unique-name pass // honors that opt-out. The precise path is the only route for TS, by design. - it.todo('links an alias field to its consumer'); - it.todo('links an interface field to its consumer'); + it('links an interface field to its consumer', () => { + expect(readersOf('ifaceSlots')).toContain('renderIface'); + }); - void readersOf; + // The ALIAS half still does not link, and the remaining blocker is now + // exact: resolving `cfg: LiveModeConfig` to its members requires the name + // `LiveModeConfig` to resolve to a CLASS-LIKE def, and `isClassLike` is + // Class|Interface|Struct|Record|Enum|Trait — no TypeAlias. That predicate is + // consulted from ~12 sites including MRO and heritage, and every language + // mints TypeAlias (Rust type_item, Kotlin/Swift/Dart typealias, C typedef), + // so widening it would enrol aliases in linearization where they do not + // belong. Widening only the scope index was tried and is NOT sufficient — + // the type-name walkers gate on it independently. Needs a deliberate + // "shape-like" concept rather than more call-site widening. + it.todo('links an alias field to its consumer'); });