fix: drop redundant TypeAlias pairs and keep bare identifiers off class members

Two regressions the full suite caught after the review fixes, both real.

`schema-pair-coverage` failed with eleven hand-declared pairs that a rule now
generates. Adding `TypeAlias` to `LINKABLE_LABELS` — needed so
`resolveDefGraphId` can bridge an alias def to its node — also makes it a
SCOPE_BRIDGE source and target, so the cross-product produces `File|TypeAlias`,
`TypeAlias|Property` and nine others that round 1 had declared by hand. Removed;
the invariant is that no pair is both generated and hand-declared.

This also changes what the structural-pair sentinel means, and the comment is
corrected rather than left overstating it: `TypeAlias|Property` is no longer
load-bearing because the label is off the generated grid — it is load-bearing
because it now depends on `TypeAlias` being IN `LINKABLE_LABELS`. Remove it and
the pair stops being generated while the hand declaration is gone, which is the
same state that silently breaks alias consumer edges.

`block-scope-shadowing` failed because a bare identifier resolved to a class
`Property`. `class Box { baseUrl = '...'; pick() { const baseUrl = ...; return
baseUrl; } }` linked the block-local read to `Box.baseUrl`, duplicating the
legitimate `this.baseUrl` edge. A bare identifier is not a member access: with
no receiver there is no object whose property it could be, and in JS/TS a field
read needs `this.`. Receiver-less read/write sites no longer accept `Property`
hits; callables stay reachable, so `cb = save` naming a top-level function is
unaffected.

That defect PREDATES this branch's TypeScript captures — JavaScript has emitted
bare-identifier reads since A2 and no class fixture exercised the shadow. The
TS parity added here is what surfaced it.

Golden snapshot regenerated after verifying the drift line by line: exactly
+5 USES from type annotations in the mini-repo, every pre-existing count
unchanged, so nothing was rewired.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ReidenXerx 2026-08-06 22:27:01 +03:00
parent 411cac9b90
commit 6ce853acf0
4 changed files with 41 additions and 26 deletions

View file

@ -220,7 +220,25 @@ function lookupForSite(
...(site.explicitReceiver !== undefined ? { explicitReceiver: site.explicitReceiver } : {}),
};
const fieldHits = fieldRegistry.lookup(site.name, site.inScope, fieldOpts);
if (fieldHits.length > 0) return fieldHits;
// A BARE IDENTIFIER is not a member access. With no receiver there is no
// object whose `Property` this could be, so a hit on one is a false edge:
// in JS/TS/Python/Ruby a field read needs `this.` / `self.` / `@`, and the
// bare name means the nearest lexical binding instead.
//
// Observed: `class Box { baseUrl = '...'; pick() { const baseUrl = ...;
// return baseUrl; } }` linked the block-local read to `Box.baseUrl`,
// duplicating the legitimate `this.baseUrl` edge. That predates the
// TypeScript captures added here — JavaScript has emitted bare-identifier
// reads since A2 and no class fixture exercised the shadow.
//
// Callables are deliberately still reachable: `cb = save` naming a
// top-level function is a real bare-name reference, which is why the
// method/class fallbacks below are untouched.
const receiverlessFieldHits =
site.explicitReceiver === undefined
? fieldHits.filter((hit) => hit.def?.type !== 'Property')
: fieldHits;
if (receiverlessFieldHits.length > 0) return receiverlessFieldHits;
const methodHits = methodRegistry.lookup(site.name, site.inScope);
if (methodHits.length > 0) return methodHits;
return classRegistry.lookup(site.name, site.inScope);

View file

@ -403,7 +403,7 @@ const ATTACHMENT_TARGET_LABELS: readonly NodeTableName[] = [
*
* What survives here is characteristic, not arbitrary. Almost all of it is a
* TARGET no rule reaches — `CodeElement`, `Impl`, `Namespace`, `Template`,
* `TypeAlias`, `Typedef`, `Union`, `Static`, `Section`, `Folder` are in neither
* `Typedef`, `Union`, `Static`, `Section`, `Folder` are in neither
* `SCOPE_BRIDGE_TARGET_LABELS` nor {@link ATTACHMENT_TARGET_LABELS} — plus the
* `Impl|*` and `Template|*` member rows (Rust `impl`/`trait` bodies, C++
* templates), the two `Route|Process` / `Tool|Process` entry points whose
@ -437,7 +437,6 @@ export const STRUCTURAL_PAIR_DDL = ` FROM File TO Folder,
FROM File TO \`Union\`,
FROM File TO \`Namespace\`,
FROM File TO \`Impl\`,
FROM File TO \`TypeAlias\`,
FROM File TO \`Static\`,
FROM File TO \`Template\`,
FROM File TO Section,
@ -445,20 +444,17 @@ export const STRUCTURAL_PAIR_DDL = ` FROM File TO Folder,
FROM Folder TO File,
FROM Function TO \`Template\`,
FROM Function TO \`Namespace\`,
FROM Function TO \`TypeAlias\`,
FROM Function TO \`Impl\`,
FROM Function TO \`Typedef\`,
FROM Function TO \`Union\`,
FROM Function TO CodeElement,
FROM Class TO \`Template\`,
FROM Class TO \`TypeAlias\`,
FROM Class TO \`Impl\`,
FROM Class TO \`Union\`,
FROM Class TO \`Namespace\`,
FROM Class TO \`Typedef\`,
FROM Class TO CodeElement,
FROM Method TO \`Template\`,
FROM Method TO \`TypeAlias\`,
FROM Method TO \`Namespace\`,
FROM Method TO \`Impl\`,
FROM Method TO CodeElement,
@ -480,8 +476,6 @@ export const STRUCTURAL_PAIR_DDL = ` FROM File TO Folder,
FROM CodeElement TO \`Property\`,
FROM Section TO Section,
FROM Interface TO CodeElement,
FROM Interface TO \`TypeAlias\`,
FROM \`Enum\` TO \`TypeAlias\`,
FROM \`Namespace\` TO \`Struct\`,
FROM \`Impl\` TO Method,
FROM \`Impl\` TO Function,
@ -490,15 +484,10 @@ export const STRUCTURAL_PAIR_DDL = ` FROM File TO Folder,
FROM \`Impl\` TO \`Trait\`,
FROM \`Impl\` TO \`Struct\`,
FROM \`Impl\` TO \`Impl\`,
FROM \`TypeAlias\` TO \`Trait\`,
FROM \`TypeAlias\` TO Class,
FROM \`TypeAlias\` TO \`Property\`,
FROM \`TypeAlias\` TO Method,
FROM \`Record\` TO Method,
FROM \`Record\` TO \`Constructor\`,
FROM \`Record\` TO \`Property\`,
FROM \`Constructor\` TO \`Template\`,
FROM \`Constructor\` TO \`TypeAlias\`,
FROM \`Constructor\` TO \`Impl\`,
FROM \`Constructor\` TO \`Namespace\`,
FROM \`Constructor\` TO \`Typedef\`,

View file

@ -3,7 +3,7 @@
"fixture": "mini-repo",
"totalFileCount": 7,
"symbols": 41,
"relationships": 80,
"relationships": 85,
"processes": 4,
"byType": {
"Class": 1,
@ -25,7 +25,8 @@
"HAS_PROPERTY": 8,
"IMPORTS": 12,
"MEMBER_OF": 12,
"STEP_IN_PROCESS": 12
"STEP_IN_PROCESS": 12,
"USES": 5
},
"edgeDigest": "d617ad94554d760eb0497bf5365b7b0a2e21842ab5b16568def721b954aa1594"
"edgeDigest": "c55bd8307a5fccbfd23d5aa93f241e26ef0f45c1a9e57691f2bf4f6602e8e4ab"
}

View file

@ -117,21 +117,28 @@ const NON_BRIDGE_CORPUS = [
// died on any repo containing `type X = { ... }`. Every resolver test still
// passed, because they build an in-memory graph and never write to the DB —
// this suite is the only place that difference shows up.
// `TypeAlias|Property` is the LOAD-BEARING sentinel: `TypeAlias` is on the
// eleven-table list this suite exists for, so no cross-product generates it.
// `Interface|Property` was listed alongside it and is tautological — both
// labels are in the SCOPE_BRIDGE cross-product, so that pair is emitted by
// construction and the sentinel cannot fail. Dropped rather than left to
// read as coverage.
// `TypeAlias` USED to be off the generated grid, which is why round 1 hand-
// declared its pairs. It is now in `LINKABLE_LABELS` (the def→graph-node
// bridge needs it), which makes it a SCOPE_BRIDGE source and target, so the
// cross-product generates these pairs and the hand declarations were
// removed as redundant.
//
// The sentinel is still load-bearing, for a different reason than before:
// it now depends on `TypeAlias` being in `LINKABLE_LABELS`. Take it out and
// the pair stops being generated AND the hand declaration is gone, so this
// fails — which is exactly the state that also silently breaks alias
// consumer edges. `Interface|Property` was dropped from this entry because
// it is tautological in the ordinary way: both labels were always in the
// cross-product, so nothing about it could ever fail.
fixture: 'typescript-alias-fields',
emitter: 'object-type alias HAS_PROPERTY',
sentinels: ['TypeAlias|Property'],
},
{
// `TypeAlias|Method` is declared in `schema.ts` but no fixture contained a
// method-shaped alias member, so nothing proved it was the right pair for
// what is actually emitted — a declared pair that no emitter exercises is
// indistinguishable from a missing one until an analyze aborts on it.
// Nothing in the corpus contained a method-shaped alias member, so nothing
// proved `TypeAlias|Method` was the right pair for what is actually
// emitted — a pair no emitter exercises is indistinguishable from a missing
// one until an analyze aborts on a real repo.
fixture: 'typescript-alias-methods',
emitter: 'object-type alias HAS_METHOD',
sentinels: ['TypeAlias|Method'],