mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
9 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
42cde0b359 |
feat(resolution): separate the program boundary from analysis uncertainty (#2766)
`impact` reported `epistemic: lower-bound` for calls it could not resolve.
Dumping all 102 call drops with source context shows the count was
measuring two different things and reporting both as uncertainty:
external 76 System.out.println, fetch(...), os.environ.setdefault,
document.body.appendChild, .stream() — the callee is
NOT IN THE GRAPH, so no node exists for an edge to
point at and nothing was lost
in-program 20 a real gap in principle
unknown 6 casts, ternaries, globalThis.x ??= []
Three quarters of the hedge was the program boundary, not doubt about
the program. A compiler resolves `System.out.println` against the JDK;
lacking one, the honest statement is "this call leaves the analyzed
program", NOT "this analysis is incomplete". Collapsing the two is what
made the signal fire on essentially every real codebase, which is what
teaches readers to ignore it.
`ResolutionOutcome.receiverOrigin` records which applies, and
`summarizeUnresolvedReceivers` skips `external`. `unknown` still counts —
assuming a completeness we cannot demonstrate is the unsafe direction.
ORIGIN IS DECIDED BY THE BASE'S DECLARED TYPE, NOT ITS NAME. A first cut
asked whether the base was a local, which marked `inputs.stream()`
in-program: `inputs` IS a local, but its type `List<String>` is JDK, so
the target is external. Asking whether the base's TYPE is one this index
contains moved 28 sites to the correct bucket.
Measured end to end: `impact save` on a TypeScript repo went from
`lower-bound` (2 dropped sites, both `fetch(...).then(...)`) to
`impactedCount: 3, epistemic: exact`.
WHAT THIS DOES NOT DO. The remaining 20 in-program drops are mostly not
product defects either: `user.Address.Save()` resolves cleanly in
isolation — the csharp-deep-field-chain fixture alone emits both expected
edges with ZERO drops — and drops in the count arm only because the
corpus is ~200 mini-projects in one directory and 55 files define
`Address`, so the resolver correctly declines on ambiguity. The genuinely
untypeable population is the 6 `unknown`, and those are the real targets
for type resolution: a cast GIVES you the type, a ternary needs a join of
its branch types. They were invisible under 76 stdlib calls until now.
Full external-target resolution needs stdlib type stubs (JDK / BCL /
lib.d.ts) so those callees exist as nodes at all. That is a program of
work; this change makes the boundary measured rather than guessed.
`callDropsByOrigin` joins the gated projection so the split cannot drift
silently. Both benches pass; scope-capture unchanged (this is
diagnostic + summary, not capture).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
a4002c3c43 |
fix(go): drop the phantom read site on a member call's callee (#2766)
Found by a three-agent investigation into an A1 regression. The
regression was the symptom; this is the disease, and it SHIPPED on this
branch.
Go's `@reference.read` pattern matches every `selector_expression` with
no call-position exclusion, so `h.dep.Work()` minted THREE sites:
call Work on the call_expression — the member call
read Work on the outer `h.dep.Work` — PHANTOM, this is the callee
read dep on the inner `h.dep` — the genuine field read
The phantom resolves through `findOwnedMember`, which prefers methods
over fields, and emits an ACCESSES edge to the METHOD duplicating the
CALLS edge at the same position.
MY U8 CLAIM WAS WRONG. I asserted that typing the base made the ACCESSES
"correctly retarget to the property". It did not. The property edge was
always a separate site, and the U8 test passed by ACCIDENT: the row it
asserts on has a pointer receiver whose text-cascade lookup failed for an
unrelated reason. The value-receiver twin was emitting the bad edge the
whole time — verified on the pushed branch:
ACCESSES RunFromValueReceiver -> DoWork:Method
ACCESSES RunLocal -> DoWork:Method
A selector in FUNCTION position is never a read. Dropped at the emitter.
A method VALUE (`f := h.dep.Work`) is not in function position and is
untouched.
Deliberately NOT fixed by gating on `handledSites`: the three co-located
Go sites share one site key, so that would suppress the correct
`RunSamePackage -> dep` edge depending on match order. Nor by flipping
`findOwnedMember` to prefer fields on reads — that is cross-language and
would break legitimate method-value reads.
The weak assertion is replaced by an exact-set check over the whole
fixture, so a new phantom fails even on a row nobody wrote a targeted
assertion for. My first attempt at that check compared `undefined ===
undefined` and would have passed against anything; caught and rewritten.
Numbers:
callDrops 102 -> 102 no call lost
read drops 27 -> 22 five phantoms were being RECORDED AS DROPS
chain-field 61 -> 60 one reclassified...
chain-unwrap 0 -> 1 ...to the real call's shape, because the
phantom and the call share a site key and the
phantom's chain was the one being censused
Baselines: go scope-capture fingerprint and the go capture golden
rebaselined with reasons — fewer capture matches for Go, and go was the
only fingerprint of 15 that moved, which is the check that this touches
Go's emitter alone. 4369 tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
10d872529e |
fix(resolution): name-free steps were vetoed by the construction selector (#2766)
A defect I introduced two commits earlier, not a pre-existing one.
`foldReceiverChain` opens each step with:
if (options.constructionSyntax?.selector === step.name) return undefined;
Correct as written. It became wrong the moment name-free step kinds were
added: `step.name` is `undefined` for `await` and `index`, and
`constructionSyntax?.selector` is `undefined` for any language that
declares none — so `undefined === undefined` matched EVERY name-free step
and vetoed the whole fold before it ran.
That is why subscript and await receivers minted a chain, fired the Case 0
gate, had a resolvable base binding, and still produced no edge. The fold
logic downstream was fine; nothing downstream ever executed. Three reading
passes missed it because the line looks obviously correct — you have to
watch `step.name` be `undefined` at runtime. An instrumented run found it
immediately, which is the lesson: when captures, plumbing, payload and
gate all verify clean, stop reading and instrument.
Guarded on `step.name !== undefined`. Load-bearing, not defensive.
Shape matrix, RESOLVES 46 -> 55, VISIBLE-GAP 32 -> 23:
awaitParen now resolves in typescript, python, csharp, kotlin, dart
indexElement now resolves in typescript, cpp, go, python, rust, kotlin
Every canonical TypeScript shape now resolves, which broke the #2744
drop-recorder test: it needs a receiver that FAILS to type, and its own
comment records that `!` served until structural typing resolved it, then
await-paren served until this fix. Both were shapes the resolver merely
did not SUPPORT yet, so each improvement moved the goalposts. Re-fixtured
to an UNANNOTATED parameter — no type information exists, so no resolver
work can type it. Stable by construction rather than by
not-yet-implemented.
Verified: resolver integration + scope-resolution unit 4367 green,
typescript resolver 270 green, scope-capture unchanged (this is resolution,
not capture), callDrops unchanged at 102.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
a4b75ffdb2 |
fix(resolution): decouple the wrapper peel bound; leave the chain cap at 3 (#2766)
Two halves, and the headline one is a negative result. DECOUPLED (the real fix). `unwrapTransparentReceiver` used `MAX_CHAIN_DEPTH` as its iteration bound. The two answer unrelated questions — how many chain hops do we type, versus how many redundant parens might someone write — so raising the chain cap would silently widen the paren peel as a side effect, and the shared name reads as intentional enough to hide it. The coupling got worse when the await/subscript work added a peel call at loop entry. Now `MAX_TRANSPARENT_WRAPPER_DEPTH`, its own constant. The parse-worker comment that hardcoded "(3)" no longer restates the number. NOT RAISED, on measurement. The premise was that a chain deeper than the cap is discarded whole rather than truncated, so a 4-hop builder chain "contributes nothing at all". The first half is true. The second is not. `fourHopChain` was added to the TypeScript corpus as a declared extra to make the question answerable at all — without a chain longer than the cap, raising the cap measures nothing: cap 3: NO chain minted (confirmed by probing the emitter) -> RESOLVES cap 4: chain minted -> RESOLVES It resolves at both. At 3 the TEXT CASCADE answers, because it owns the fallback path and runs to its own `COMPOUND_RECEIVER_MAX_DEPTH` of 8. So the cap bounds which chains are typed STRUCTURALLY, not which calls resolve — raising it moves work from the cascade to the fold and changes no edge. Verified across the whole matrix: totals identical at 3 and 4, callDrops 102 at both. Left at 3. The fixture is committed so the next person to reach for this number inherits the measurement rather than the intuition. (An earlier version of that fixture was three steps, not four — `root.getSvc().getUser().address` counts hops in the source text, not steps in the receiver — so it fit inside the old cap and measured nothing. Corrected before drawing any conclusion.) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
075ad0508d |
feat(resolution): mint chains through await and subscript receivers (#2766)
Two changes, one of which matters more than the shape work it was meant
to enable.
EMISSION. `extractMixedChain` stopped at await and subscript nodes —
neither is a call or a field access — so `(await svc.getUserAsync()).save()`
and `repos[0].save()` minted NO chain at all. Adds the node vocabularies
for both, records them as the codec's name-free `await` / `index` steps,
and peels transparent wrappers at LOOP ENTRY rather than only where a base
is returned (an await receiver arrives wrapped in a
`parenthesized_expression`, which matched no branch and fell straight
through with an empty chain). Verified directly: the two shapes now mint
`2|svc|cgetUserAsync|a` and `2|repos|i`.
CASE 0's GATE. The bigger one. That case fired on
`receiverName.includes('.') || includes('(')` — a C-family punctuation
test. A subscript receiver contains neither, so the case never ran, the
fold was never consulted, AND no drop was recorded: the call vanished with
the instrument blind to it. The bench's own KNOWN BLIND SPOTS section
names this. PHP `->` and `::` receivers are lost the same way.
A minted chain answers the same question structurally — the capture layer
walked the real AST and found the receiver is an expression, whatever
punctuation it is spelled with. The gate now accepts that signal, which is
the textual-shape-dispatch removal this whole line of work exists to make.
Measured on the shape matrix: 13 cells move INVISIBLE-GAP -> VISIBLE-GAP,
and kotlin `awaitParen` -> RESOLVES.
RESOLVES 43 -> 45
VISIBLE-GAP 21 -> 32
INVISIBLE-GAP 31 -> 18
INCOMPLETE, deliberately stated: most subscript and await receivers still
do not RESOLVE. The chain mints, the gate fires, and the base binding
exists (`repos` -> `User[]`, confirmed) — the edge is still absent and I
have not isolated why. Three attempts (identity step, collection-element
hook, base carrying an unresolved container) did not produce it. The hook
and the relaxed base are kept because both are correct in their own right
and are needed by whatever the real fix turns out to be; they are not
load-bearing for anything claimed here.
`callDrops` 101 -> 102 on the fixture corpus. That is previously-invisible
loss becoming countable, NOT a regression — and it means the drop ratchet
must be baselined after this lands, or it would ratchet against a number
that understated reality.
Baselines: go and kotlin scope-capture fingerprints and the go capture
golden rebaselined — more sites carry a chain; no existing chain changed
shape. Only 2 of 15 languages drifted, the two whose fixtures contain such
receivers. Unit 1402 green, resolver integration 2995 green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
0f98f16765 |
feat(resolution): census receiver drops by structural shape (#2766)
`callDropsByExtension` was the finest split available, so a language's bucket said nothing about whether its drops were one defect or five. The suppressed `ResolutionOutcome` now carries `receiverShape`, set by the emitting case from the site's ENCODED CHAIN — the compact string the capture emitters mint by walking the real AST. Never re-derived from the source line. `resolution-outcome.ts` already rejects that for `siteKind`, and for the same reason: regex-classifying the number that gates this work is the textual-shape dispatch the structural-receiver line exists to remove. Diagnostic only, so the persisted `RepoMeta.unresolvedReceiverMembers` artifact is unchanged. Census of the 101 call drops on the committed corpus: chain-field 60 (59%) h.repo.save() chain-call 27 (27%) svc.getUser().save() no-chain 12 (12%) no nameable base survived the walk chain-mixed 2 (2%) svc.getUser().addr.save() Two questions the plan could not answer are now answered. The `.java` bucket is NOT one defect: its 49 drops split 30 field-chain / 14 call-chain / 5 no-chain — the same mix as everywhere else, just more of it. And field-receiver chains dominate at 59%, which is precisely the shape the pointer-receiver fix closed for Go (now down to 3). The same class remains in java (30), csharp (6), cpp (4), php (4), py (3), rust (3). Also records what the census CANNOT decide: await-wrapped and subscript receivers appear nowhere in it, because the committed fixtures contain no such sites — not because they are rare. `indexElement` is an INVISIBLE-GAP in all 14 languages in the shape arm, so that population is real but structurally invisible here. Funding those shapes has to be read off the shape arm; reading it off this census would confuse "absent from these fixtures" with "does not happen". Also closes the ACCESSES-vs-CALLS question as RESOLVED BY the pointer-receiver fix, diagnosed rather than assumed. Go `h.dep.Work()` previously emitted ACCESSES to the METHOD and no CALLS: the member-name path emitted the access while the CALLS leg needed the receiver's class, which failed to type. With the base typed, CALLS emits and the ACCESSES correctly retargets to the PROPERTY being read. Verified across the fixture that no Method target now carries ACCESSES without a matching CALLS. Locked by a same-package control asserting both directions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
809d2eb63b |
fix(resolution): type Go pointer-receiver bases at the class lookup (#2766)
A Go method with a pointer receiver binds its receiver to the literal string `*Holder` — `synthesizeGoReceiverBinding` stores `typeNode.text` raw. `findClassBindingInScope` normalizes exactly one decoration, a dotted qualifier, so `*Holder` matched nothing: the scope walk missed, the qualified-name index missed, and the dotted-tail fallback never fired because there is no dot. Receiver typing declined at the BASE, so every `h.field.Method()` in a pointer-receiver method — the dominant Go idiom — emitted no CALLS edge. The reporter measured 178 handler and 585 service call sites lost on a 470k LOC codebase. The defect is the base ALONE. Go already normalizes field type bindings at capture through `normalizeGoTypeName`, so the step lookup was always sound. Isolating the two independently proves it: value receiver + value field resolves, value receiver + POINTER field resolves, pointer receiver + value field does not. Adds an OPT-IN decoration fallback to the class lookup, consulted only after every undecorated branch has failed: - `findAllClassBindingsInScope` enumerates every class-like candidate from both the scope chain and the qualified-name index. Needed because `walkScopeChain` returns the FIRST match and structurally cannot report a collision, so widening what a name matches without it would pick the nearest of several and mint a confident wrong edge. It stops at the first scope that binds the name, so an inner binding shadowing an outer one is not misreported as ambiguity. - The fallback strips one layer at a time and requires exactly ONE surviving nodeId, or it declines. - `stripTypePreservingDecoration` on the ScopeResolver contract carries the per-language vocabulary, so the core names no language (AGENTS.md R6). Go strips `*` only — `[]` and `map[…]` are CONTAINERS whose member set differs from the element's, and stripping one would let `repos: Repo[]` fold `repos.find(x)` to `Repo.find`. Those are unwrapped only by an index step that consumed a subscript. Opt-in rather than global because ~two dozen call sites are shaped `findClassBindingInScope(...) ?? otherResolver(...)`: turning a former `undefined` into a hit SUPPRESSES the fallback that used to answer, which would retarget inheritance edges and bypass generic-specialization selection. Only receiver-chain base and step resolution opts in. The stored `*T` binding is left decorated — `method-owners.ts` consumes the `*T` vs `T` distinction to model Go's value and pointer method sets, so this normalizes at LOOKUP, never by rewriting the binding. Verification: - bench cell `go.decoratedReceiverBase` VISIBLE-GAP -> RESOLVES, and it is the ONLY cell that moved of 164. - #2766's reproduction goes from 4 CALLS edges to 10; cross-package interface field, cross-package concrete field and same-package field all resolve. - Regression test proven to fail without the fix: with the stripper disabled the 3 pointer-receiver assertions fail while both controls (local-variable receiver, value receiver) still pass — so the test targets the changed line and the fix adds edges rather than moving one. - Full resolver suite green (2988 tests). Baseline movement, both fixture-corpus growth rather than code: receiver-resolution callDrops unchanged at 101; scope-capture go fingerprint rebaselined with a documented reason, and go was the only language of 15 that drifted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b442e04bf4 |
test(bench): canonical shape axis across all 14 languages (#2766)
The shape arm was three languages with an ad-hoc shape list each, so "covers all supported languages" was an assertion rather than a measurement. Replace it with a canonical 10-shape axis every language must answer for, and add two states so a hole stops masquerading as a result: - `N/A` — the grammar does not admit the spelling. A reason is REQUIRED; an omitted cell and an inapplicable one are otherwise identical in a diff, which is how coverage rots. - `GRAMMAR-UNAVAILABLE` — the parser could not load, so nothing was measured. `drift` skips it on BOTH sides, so the gate cannot fail for the environment it ran in. Dart, Kotlin and Swift are vendored OPTIONAL grammars, absent under GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 or when no vendored prebuild matches the host. All 14 load on glibc linux-x64, so this state has no producer in the committed baseline — it guards the skip-flag and unsupported-host cases. `assertMatrixComplete` throws on a missing cell, an unknown id, or a reasonless N/A. It caught four real ids on its first run; those became declared `extraShapeIds`, because PHP's annotated/unannotated pair and C++'s pointer/value pair discriminate things the axis cannot express. 164 cells: 42 RESOLVES, 22 VISIBLE-GAP, 31 INVISIBLE-GAP, 69 N/A. Count arm unmoved at 101 call drops — shape fixtures build in temp directories and never touch the committed corpus. The measurement contradicts what was assumed about the decoration bug: - Go isolates it to ONE cell. Varying receiver and field decoration independently: value/value RESOLVES, value/pointer RESOLVES, pointer/value is a VISIBLE-GAP. Only the pointer receiver fails; field bindings already go through normalizeGoTypeName, so the step lookup is sound and the defect is entirely the base. - Go is the ONLY language whose receiver decoration defeats the lookup. Rust `&mut self` resolves. - The field-type gap is real in rust (Box<User>), typescript (User|null), csharp (User?), swift (User?) and cpp (User*) — but php, python, kotlin and dart already resolve theirs. - PHP's sigil hypothesis is dead: `$svc->getUser()->save()` (unannotated return) is an INVISIBLE-GAP while `$svc->getUserTyped()->save()` (annotated) RESOLVES. Same chain, same `->`, same base. Also surfaced, previously unknown: Swift resolves almost no chained receiver; Ruby's chains are VISIBLE-GAPs; C++ `this->` field receivers are INVISIBLE; `indexElement` is INVISIBLE-GAP in all 14; and Dart is the only language where `awaitParen` already resolves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
27ab37c432
|
feat(resolution): type receiver chains from AST structure across all 14 languages (#2708) + epistemic lower-bound (#2744) (#2747) |