mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-09 22:33:39 +00:00
feat(mcp): split the epistemic hedge into its two producers (#2766)
`impact` and `context` reported `epistemic: 'lower-bound'` for two
independent reasons that the output could not tell apart:
receiverTyping call sites dropped because the receiver could not be
typed — a RESOLVER DEFECT, and the population this
series targets
dispatchBoundary the symbol sits behind an interface with real
consumers or 2+ implementations, so callers binding
through DI or dynamic dispatch are genuinely
untraceable — NOT a defect; a compiler refuses here too
One enum plus prose meant a consumer — especially a coding agent gating
its own edits on the result — could tell THAT a count was short but not
WHY, and could not branch on the difference. It also made "the hedge
should stop appearing" unfalsifiable: with no way to see which producer
fired, there was no way to check whether fixing receiver typing had
achieved anything.
Both surfaces now carry `causes: { receiverTyping, dispatchBoundary }`
alongside the existing prose.
`receiverTyping` counts dropped SITES, not boundary notes. The first cut
counted notes and published `1` next to prose reading "2 call sites" —
an agent branching on the number would have read a different magnitude
than the human reading the text, which is precisely the failure a
structured field exists to prevent. `unresolvedReceiverBoundaries` now
returns `{ notes, sites }` so the count comes from the same place the
prose does.
Verified live. On the #2766 reproduction `WithTx` went from
`impactedCount: 0` + `lower-bound` to `impactedCount: 1` + `exact` — the
hedge is gone because its cause is gone, not because it was suppressed.
On a TypeScript repo with dropped receivers, `causes` reports
`{ receiverTyping: 2, dispatchBoundary: 0 }`, matching the prose exactly.
NOT verified: the `dispatchBoundary` path end-to-end. Go's implicit
interface satisfaction emits no IMPLEMENTS edges, so that producer
structurally cannot fire on the Go fixture, and observing it needs a Java
or TypeScript case. It is wired and typechecks; it is not claimed proven.
Only the receiverTyping producer is addressed by this series. The
dispatch boundary is untouched and will keep firing for
interface-dispatched symbols, which is correct — and any claim that the
hedge has stopped appearing must now be read per-producer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
075ad0508d
commit
59c054338d
2 changed files with 94 additions and 10 deletions
|
|
@ -1,5 +1,39 @@
|
|||
# Receiver-resolution baseline
|
||||
|
||||
## U9 — the epistemic hedge has TWO producers, and only one is a defect
|
||||
|
||||
`impact` reports `epistemic: 'lower-bound'` for two independent reasons that were
|
||||
previously indistinguishable in the output:
|
||||
|
||||
| Cause | What it means | Is it a defect? |
|
||||
|---|---|---|
|
||||
| `receiverTyping` | Call sites dropped because the analyzer could not type the receiver | **Yes** — a resolver gap. This is the population this whole series targets. |
|
||||
| `dispatchBoundary` | The symbol sits behind an interface with real consumers or 2+ implementations | **No** — callers binding through DI or dynamic dispatch are genuinely untraceable statically. A compiler refuses here too. |
|
||||
|
||||
Both collapsed into one enum plus prose, so a consumer — especially a coding
|
||||
agent gating its own edits on the result — could tell THAT a count was short but
|
||||
not WHY, and could not branch on the difference. Worse, it made "the hedge should
|
||||
stop appearing" unfalsifiable: with no way to see which producer fired, there was
|
||||
no way to check whether fixing receiver typing had done anything.
|
||||
|
||||
`impact` and `context` now carry a structured `causes: { receiverTyping,
|
||||
dispatchBoundary }` alongside the prose. `receiverTyping` counts dropped SITES,
|
||||
not boundary notes — there is one note per symbol name but it reports N sites, so
|
||||
counting notes published `1` next to prose reading "2 call sites" and a consumer
|
||||
branching on the number would have read a different magnitude than the human
|
||||
reading the text.
|
||||
|
||||
**Only the `receiverTyping` producer is addressed by this series.** The dispatch
|
||||
boundary is untouched and will keep firing for interface-dispatched symbols —
|
||||
which is correct. Any claim that the hedge has "stopped appearing" has to be read
|
||||
per-producer, and that is now possible.
|
||||
|
||||
Measured on the #2766 reproduction: `WithTx` went from `impactedCount: 0` with a
|
||||
`lower-bound` hedge to `impactedCount: 1` with `epistemic: exact`. The hedge is
|
||||
gone there because its cause is gone, not because it was suppressed.
|
||||
|
||||
---
|
||||
|
||||
## U10 — recorded drops, censused by receiver shape
|
||||
|
||||
`ResolutionOutcome`'s suppressed variant now carries `receiverShape`, set by the
|
||||
|
|
|
|||
|
|
@ -472,13 +472,48 @@ export interface CodebaseContext {
|
|||
|
||||
/** Collapse dropped-site boundary notes into an epistemic verdict: any note at
|
||||
* all means the count is a lower bound, none means it is exact (#2744). */
|
||||
function epistemicFrom(droppedBoundaries: readonly string[]): {
|
||||
/**
|
||||
* Why a count is a lower bound, as a machine-readable split.
|
||||
*
|
||||
* `epistemic` is a single enum and `boundaries` is prose, so a consumer that is
|
||||
* not a human — a coding agent gating its own edits on this result — can tell
|
||||
* THAT the answer is short but not WHY, and cannot branch on the difference.
|
||||
* The two causes are independent and have opposite remedies:
|
||||
*
|
||||
* - `receiverTyping` — the analyzer dropped call sites because it could not
|
||||
* establish the receiver's type. A resolver defect. Fixable, and shrinking:
|
||||
* this is the population the structural-receiver work targets.
|
||||
* - `dispatchBoundary` — the symbol sits behind an interface with real
|
||||
* consumers or multiple implementations, so callers binding through a DI
|
||||
* container or dynamic dispatch are genuinely untraceable statically. NOT a
|
||||
* defect; a compiler would refuse here too.
|
||||
*
|
||||
* Collapsing them told the reader "impact may be higher" for both, which made
|
||||
* the fixable cause indistinguishable from the irreducible one — and made
|
||||
* "the hedge should stop appearing" an unfalsifiable goal, because there was no
|
||||
* way to see which producer was still firing.
|
||||
*/
|
||||
export interface EpistemicCauses {
|
||||
readonly receiverTyping: number;
|
||||
readonly dispatchBoundary: number;
|
||||
}
|
||||
|
||||
function epistemicFrom(dropped: { notes: readonly string[]; sites: number }): {
|
||||
epistemic: 'exact' | 'lower-bound';
|
||||
boundaries?: string[];
|
||||
causes?: EpistemicCauses;
|
||||
} {
|
||||
return droppedBoundaries.length === 0
|
||||
return dropped.notes.length === 0
|
||||
? { epistemic: 'exact' }
|
||||
: { epistemic: 'lower-bound', boundaries: [...droppedBoundaries] };
|
||||
: {
|
||||
epistemic: 'lower-bound',
|
||||
boundaries: [...dropped.notes],
|
||||
// SITES, not notes. There is one note per symbol name but it reports N
|
||||
// dropped sites, so counting notes would have published `1` next to
|
||||
// prose saying `2 call sites` — a consumer branching on the number
|
||||
// would read a different magnitude than the human reading the text.
|
||||
causes: { receiverTyping: dropped.sites, dispatchBoundary: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
interface RepoHandle {
|
||||
|
|
@ -5738,7 +5773,11 @@ export class LocalBackend {
|
|||
symId: string,
|
||||
symType: string,
|
||||
symName: string,
|
||||
): Promise<{ epistemic: 'exact' | 'lower-bound'; boundaries?: string[] }> {
|
||||
): Promise<{
|
||||
epistemic: 'exact' | 'lower-bound';
|
||||
boundaries?: string[];
|
||||
causes?: EpistemicCauses;
|
||||
}> {
|
||||
const HERITAGE_TYPES = EPISTEMIC_HERITAGE_RELATION_TYPES;
|
||||
const CONSUMER_TYPES = EPISTEMIC_CONSUMER_RELATION_TYPES;
|
||||
// #2744 — call sites dropped for want of a receiver type. Checked BEFORE
|
||||
|
|
@ -5829,7 +5868,14 @@ export class LocalBackend {
|
|||
}
|
||||
}
|
||||
if (boundaries.length === 0) return epistemicFrom(droppedBoundaries);
|
||||
return { epistemic: 'lower-bound', boundaries: [...droppedBoundaries, ...boundaries] };
|
||||
return {
|
||||
epistemic: 'lower-bound',
|
||||
boundaries: [...droppedBoundaries.notes, ...boundaries],
|
||||
causes: {
|
||||
receiverTyping: droppedBoundaries.sites,
|
||||
dispatchBoundary: boundaries.length,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
// Never let the heritage probe's failure suppress a drop we already know
|
||||
// about — the whole point is that silence must not read as certainty.
|
||||
|
|
@ -5844,8 +5890,11 @@ export class LocalBackend {
|
|||
* index written before the summary existed, which is why the schema version
|
||||
* was bumped rather than treating "absent" as "none".
|
||||
*/
|
||||
private async unresolvedReceiverBoundaries(repo: RepoHandle, symName: string): Promise<string[]> {
|
||||
if (symName.length === 0) return [];
|
||||
private async unresolvedReceiverBoundaries(
|
||||
repo: RepoHandle,
|
||||
symName: string,
|
||||
): Promise<{ notes: string[]; sites: number }> {
|
||||
if (symName.length === 0) return { notes: [], sites: 0 };
|
||||
try {
|
||||
const meta = await loadMeta(path.dirname(repo.lbugPath));
|
||||
const summary = meta?.unresolvedReceiverMembers;
|
||||
|
|
@ -5853,8 +5902,8 @@ export class LocalBackend {
|
|||
// returns a Function for `constructor`/`toString`/… and `NaN <= 0` is false,
|
||||
// so the old guard let it through into user-facing text.
|
||||
const sites = lookupUnresolvedCallCount(summary, symName);
|
||||
if (sites === undefined) return [];
|
||||
return [
|
||||
if (sites === undefined) return { notes: [], sites: 0 };
|
||||
const notes = [
|
||||
`${sites} call ${sites === 1 ? 'site' : 'sites'} invoking \`${symName}\` ${
|
||||
sites === 1 ? 'was' : 'were'
|
||||
} dropped at index time because the receiver's type could not be ` +
|
||||
|
|
@ -5862,8 +5911,9 @@ export class LocalBackend {
|
|||
`expression). Those callers are absent from this result — actual ` +
|
||||
`impact may be higher.`,
|
||||
];
|
||||
return { notes, sites };
|
||||
} catch {
|
||||
return [];
|
||||
return { notes: [], sites: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue