mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
feat(impact): unify pdg symbol reach
This commit is contained in:
parent
0271da39e3
commit
a9fd55a91b
13 changed files with 471 additions and 214 deletions
|
|
@ -9,11 +9,11 @@
|
|||
> — prints a stratified P/R/F1 table + a plain-language decision recommendation,
|
||||
> and gates regressions with `--check`. It now also prints an additive **unified
|
||||
> impact axes** table that keeps line-level and symbol-level truth separate while
|
||||
> comparing current `callgraph`, current `pdg`, and the evaluation-only
|
||||
> `composed-current` baseline. The measured native result remains: **PDG is exact
|
||||
> at intra-procedural statement granularity; call-graph is exact at
|
||||
> inter-procedural symbol granularity; the two answer different questions and
|
||||
> neither dominates.**
|
||||
> comparing `callgraph`, unified `pdg`, and the evaluation-only
|
||||
> `composed-current` control baseline. The measured native result remains:
|
||||
> **PDG is exact at intra-procedural statement granularity; call-graph remains
|
||||
> the comparator for inter-procedural symbol granularity; unified PDG must match
|
||||
> that composed baseline before any default-switch decision.**
|
||||
|
||||
## What this measures
|
||||
|
||||
|
|
@ -23,12 +23,13 @@ granularities**:
|
|||
- `mode: 'callgraph'` (the default) — inter-procedural BFS over symbol→symbol
|
||||
edges. It answers *"what other symbols depend on / are called by this one?"* at
|
||||
**symbol granularity**, scored against `inter_AIS`.
|
||||
- `mode: 'pdg'` (opt-in) — a **statement-anchored** intra-procedural dependence
|
||||
slice from the persisted CDG + REACHING_DEF Program Dependence Graph. Seeded
|
||||
with `line: N` (`impact({mode:'pdg', line:N})`), it returns
|
||||
- `mode: 'pdg'` (opt-in) — the unified PDG-facing result. Its local
|
||||
statement slice comes from the persisted CDG + REACHING_DEF Program Dependence
|
||||
Graph. Seeded with `line: N` (`impact({mode:'pdg', line:N})`), it returns
|
||||
`affectedStatements: {line, filePath, text}[]` — the dependent **statements** of
|
||||
the changed line N. It answers *"which statements inside this function does
|
||||
changing line N affect?"* at **line granularity**, scored against `intra_AIS`.
|
||||
the changed line N — and also attaches inter-procedural symbol reach in
|
||||
`interproceduralByDepth`/`byDepth` for the same target. The native PDG row is still scored against
|
||||
`intra_AIS`; the unified axes score its statement and symbol outputs together.
|
||||
|
||||
They measure **different scopes**, so the harness scores each at its native
|
||||
granularity against its native ground truth and reports both side by side. The
|
||||
|
|
@ -39,8 +40,7 @@ neither strictly dominates*.
|
|||
## Unified impact axes
|
||||
|
||||
The harness also reports a separate unified comparison that is designed for the
|
||||
next architecture question: *could a future PDG-only / SDG-like impact engine
|
||||
replace the composition of today's engines?* This report is additive. It does not
|
||||
current architecture question: *does unified `mode:'pdg'` match the composition of today's engines?* This report is additive. It does not
|
||||
replace the native table above, and it does not change `baselines.json` gating.
|
||||
|
||||
Unified AIS has two namespaces:
|
||||
|
|
@ -51,17 +51,17 @@ Unified AIS has two namespaces:
|
|||
Each engine is adapted onto those axes without lossy projection:
|
||||
|
||||
- `callgraph` contributes only the `symbol` axis.
|
||||
- `pdg` contributes only the `statement` axis.
|
||||
- `composed-current` is an evaluation-only control row that unions current
|
||||
callgraph symbols with current PDG statements.
|
||||
- `pdg` contributes the `statement` axis from `affectedStatements` and the
|
||||
`symbol` axis from its unified `interproceduralByDepth`/`byDepth` inter-procedural reach.
|
||||
- `composed-current` remains an evaluation-only control row that unions standalone
|
||||
callgraph symbols with PDG statements.
|
||||
|
||||
The report intentionally has no single blended unified F1. A future
|
||||
`pdg-interproc` or SDG candidate must be judged axis-by-axis against
|
||||
`composed-current` so line precision cannot hide inter-symbol misses, and
|
||||
symbol recall cannot hide statement-level blindness. The control row is a recall
|
||||
baseline, not a perfection claim: current PDG can still contribute intra-line
|
||||
noise on pure-inter fixtures, so a future SDG candidate should match or exceed
|
||||
recall while reducing or bounding FPIS.
|
||||
The report intentionally has no single blended unified F1. `pdg` is now judged
|
||||
axis-by-axis against `composed-current` so line precision cannot hide
|
||||
inter-symbol misses, and symbol recall cannot hide statement-level blindness. The
|
||||
control row is a recall baseline, not a perfection claim: PDG can still
|
||||
contribute intra-line noise on pure-inter fixtures, so default-switch decisions
|
||||
should require matching recall while reducing or bounding FPIS.
|
||||
|
||||
> **A note on `line`.** A whole-symbol PDG slice (no `line`) is empty by design:
|
||||
> intra-procedural dependence stays inside the function, so every reachable block
|
||||
|
|
@ -75,11 +75,10 @@ recall while reducing or bounding FPIS.
|
|||
|
||||
`impact({mode:'pdg', line:N})` success results carry a target envelope
|
||||
(`id`, `name`, `type`, `filePath`), `risk: 'UNKNOWN'`, `affectedStatements`,
|
||||
`affectedStatementCount`, and the same empty-safe parity fields used by callgraph
|
||||
consumers (`byDepth`, `byDepthCounts`, `summary`, `affected_processes`,
|
||||
`affected_modules`). The risk stays UNKNOWN because a statement slice is
|
||||
intra-procedural; it is precise for the function body but not a whole-program
|
||||
safety verdict.
|
||||
`affectedStatementCount`, and callgraph-compatible parity fields (`byDepth`,
|
||||
`byDepthCounts`, `summary`, `affected_processes`, `affected_modules`).
|
||||
`affectedStatements` is the statement-level PDG slice; `interproceduralByDepth` is the explicit cross-function reach; `byDepth` remains the
|
||||
compatibility symbol bucket attached by unified PDG mode.
|
||||
|
||||
Degraded PDG results are explicit, not empty successes. `no-layer`,
|
||||
`sub-layer-missing`, and `unknown` responses keep `mode:'pdg'`, target metadata
|
||||
|
|
@ -206,10 +205,10 @@ call-graph row is symbol-vs-`inter_AIS`:
|
|||
- On a **mixed** fixture, both rows are real: PDG resolves the intra statement
|
||||
set, call-graph reaches the callee(s).
|
||||
|
||||
**PDG cannot cross a function boundary; call-graph cannot see below function
|
||||
granularity.** Neither is a refinement of the other — they compose. A full
|
||||
mixed-locus blast radius is the *union* of call-graph's inter-symbol reach and
|
||||
PDG's intra-statement slice.
|
||||
**The native rows still measure different units.** The PDG native row scores
|
||||
statement reach, while the callgraph native row scores symbol reach. The unified
|
||||
axes table is where `pdg` is judged as the composed result: statement reach in
|
||||
`affectedStatements`, inter-symbol reach in `byDepth`.
|
||||
|
||||
## Substrate (the load-bearing mechanism — R8)
|
||||
|
||||
|
|
@ -308,9 +307,9 @@ Read it honestly:
|
|||
pure-inter router has an empty `intra_AIS`, and the line-seeded slice returns the
|
||||
router's *own* control-dependent routing returns — FPIS against the empty truth
|
||||
(precision 0, recall `n/a`). These are **symmetric**: each engine is blind to
|
||||
the other's scope. PDG cannot cross a call boundary; call-graph cannot see below
|
||||
a function. The per-case lines surface each slice (`pdg line/intra: …`) and each
|
||||
callee set (`cg symbol/inter: …`) so this is visible, not hidden.
|
||||
the other's native scope. The per-case lines surface each statement slice (`pdg
|
||||
line/intra: …`) and each callee set (`cg symbol/inter: …`), while the unified
|
||||
table verifies whether `pdg` now carries both axes.
|
||||
|
||||
## Decision recommendation (the verdict — F2)
|
||||
|
||||
|
|
@ -329,10 +328,11 @@ Read it honestly:
|
|||
> (intra & mixed PDG F1 = 1.0, FPIS = FNIS = 0). This is a question call-graph
|
||||
> **cannot answer at all** (it has no notion of a statement).
|
||||
>
|
||||
> They **compose**: a full mixed-locus blast radius is the *union* of
|
||||
> call-graph's inter-symbol reach and PDG's intra-statement slice. The unified
|
||||
> axes table makes that composition explicit through the `composed-current` row,
|
||||
> which is the recall baseline a future SDG / `pdg-interproc` candidate must
|
||||
> `mode:'pdg'` now composes those surfaces in one result: `affectedStatements`
|
||||
> carries statement-level dependence and `interproceduralByDepth`/`byDepth` carries
|
||||
> inter-procedural symbols. `mode:'callgraph'` remains the option-driven comparator/default. The
|
||||
> unified axes table keeps `composed-current` as the control baseline that PDG
|
||||
> must match or beat before any default-switch decision.
|
||||
> match or exceed while reducing or bounding FPIS. Reach for the line-seeded
|
||||
> PDG when you need statement-level dependence *inside* a function; reach for
|
||||
> call-graph when you need
|
||||
|
|
|
|||
|
|
@ -209,14 +209,19 @@ function callgraphCisFromResult(res) {
|
|||
* U7-rework CIS: the dependent STATEMENTS the change at `criterion.line` reaches.
|
||||
*/
|
||||
function pdgCisFromResult(res) {
|
||||
const inter = callgraphCisFromResult({
|
||||
byDepth: res?.interproceduralByDepth ?? res?.pdgInterprocedural?.byDepth ?? {},
|
||||
});
|
||||
return {
|
||||
keys: pdgLineCis(res?.affectedStatements),
|
||||
lineKeys: pdgLineCis(res?.affectedStatements),
|
||||
symbolKeys: inter.keys,
|
||||
meta: {
|
||||
affectedStatementCount: res?.affectedStatementCount ?? 0,
|
||||
blockCount: res?.blockCount ?? null,
|
||||
criterionLine: res?.criterionLine ?? null,
|
||||
epistemic: res?.epistemic ?? null,
|
||||
note: res?.note ?? null,
|
||||
interprocedural: res?.pdgInterprocedural ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -429,10 +434,9 @@ function decisionRecommendation(strata, unified, underpowered, exclusions) {
|
|||
// Inter-scope: the cross-function blast radius — the question call-graph answers.
|
||||
lines.push(
|
||||
`On INTER-scope (symbol granularity), call-graph scores R=${fmt(cgInterR)} F1=${fmt(cgInterF1)} ` +
|
||||
`against inter_AIS: it recovers the cross-function callees exactly. PDG mode is ` +
|
||||
`intra-procedural, so on a pure-inter fixture it returns only the router's own ` +
|
||||
`control-dependent statements (FPIS against the empty intra_AIS — recall n/a). Call-graph is ` +
|
||||
`the engine for "what else calls/uses this?".`,
|
||||
`against inter_AIS: it recovers the cross-function callees exactly. Unified PDG mode now ` +
|
||||
`attaches the same inter-symbol reach in interproceduralByDepth/byDepth while keeping statement reach in ` +
|
||||
`affectedStatements, so the symbol axis can be compared directly against callgraph.`,
|
||||
);
|
||||
|
||||
// Mixed-scope: both engines contribute, each in its own scope.
|
||||
|
|
@ -446,25 +450,22 @@ function decisionRecommendation(strata, unified, underpowered, exclusions) {
|
|||
|
||||
if (unified) {
|
||||
lines.push(
|
||||
`Unified-axis check: current callgraph leaves the intra-line axis empty, and current PDG leaves ` +
|
||||
`the inter-symbol axis empty. The evaluation-only composed-current baseline combines both current ` +
|
||||
`outputs and reaches min defined recall=${fmt(unified['composed-current'].minRecall)} with ` +
|
||||
`FPIS=${unified['composed-current'].fpis} and FNIS=${unified['composed-current'].fnis}. ` +
|
||||
`A future PDG-only/SDG candidate must match or exceed that recall while reducing or ` +
|
||||
`bounding FPIS before any default switch.`,
|
||||
`Unified-axis check: current callgraph leaves the intra-line axis empty. Unified PDG now ` +
|
||||
`covers both axes, while composed-current remains the control baseline that combines the ` +
|
||||
`standalone callgraph symbol reach with PDG statement reach. composed-current reaches min ` +
|
||||
`defined recall=${fmt(unified['composed-current'].minRecall)} with ` +
|
||||
`FPIS=${unified['composed-current'].fpis} and FNIS=${unified['composed-current'].fnis}; ` +
|
||||
`pdg should match that recall before any default-switch discussion.`,
|
||||
);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
`VERDICT: the two engines answer DIFFERENT questions at DIFFERENT granularities, and NEITHER ` +
|
||||
`dominates. mode:'callgraph' (the default) is the correct engine for the inter-procedural ` +
|
||||
`safety question — "what else depends on / calls this symbol?" — carrying the cross-function ` +
|
||||
`reach the blast radius needs. mode:'pdg' (opt-in, seeded with line:N, where analyze --pdg ` +
|
||||
`persisted the layer) is PRECISE at intra-procedural STATEMENT granularity — "which statements ` +
|
||||
`inside this function does changing line N affect?" — a question call-graph cannot answer at ` +
|
||||
`all. Use call-graph for cross-symbol impact; reach for line-seeded PDG when you need ` +
|
||||
`statement-level dependence INSIDE a function. They compose: a full mixed-locus blast radius ` +
|
||||
`is the UNION of call-graph's inter-symbol reach and PDG's intra-statement slice.`,
|
||||
`VERDICT: keep option-driven comparison, but mode:'pdg' is now the unified PDG-facing ` +
|
||||
`answer: statement-level affectedStatements come from the persisted CDG/REACHING_DEF slice, ` +
|
||||
`and inter-procedural symbol reach is carried in interproceduralByDepth/byDepth. mode:'callgraph' remains the ` +
|
||||
`default/comparator for the established symbol-only traversal. The accuracy decision should be ` +
|
||||
`made from the unified axes: pdg must preserve statement recall while matching the composed ` +
|
||||
`inter-symbol baseline and bounding FPIS.`,
|
||||
);
|
||||
if (exclusions.length > 0) {
|
||||
lines.push(
|
||||
|
|
@ -546,11 +547,11 @@ async function run() {
|
|||
const cg = callgraphCisFromResult(results.callgraph);
|
||||
const pdg = pdgCisFromResult(results.pdg);
|
||||
const cgScore = scoreCallgraph(fx.gt, cg.keys); // symbol/inter
|
||||
const pdgScore = scorePdg(fx.gt, pdg.keys); // line/intra
|
||||
const pdgScore = scorePdg(fx.gt, pdg.lineKeys); // line/intra
|
||||
|
||||
const unifiedTruth = unifiedAis(fx.gt);
|
||||
const cgUnified = callgraphUnifiedCis(fx.gt, cg.keys);
|
||||
const pdgUnified = pdgUnifiedCis(pdg.keys);
|
||||
const pdgUnified = pdgUnifiedCis(pdg.lineKeys, pdg.symbolKeys, fx.gt);
|
||||
const composedUnified = composeUnifiedCis(cgUnified, pdgUnified);
|
||||
const unifiedScores = {
|
||||
callgraph: scoreUnifiedAxes(cgUnified, unifiedTruth),
|
||||
|
|
@ -584,7 +585,8 @@ async function run() {
|
|||
affectedStatementCount: pdg.meta.affectedStatementCount,
|
||||
blockCount: pdg.meta.blockCount,
|
||||
criterionLine: pdg.meta.criterionLine,
|
||||
lines: [...pdg.keys].sort(),
|
||||
lines: [...pdg.lineKeys].sort(),
|
||||
symbols: [...pdg.symbolKeys].sort(),
|
||||
score: pdgScore, // vs intra_AIS (line)
|
||||
},
|
||||
unified: unifiedScores,
|
||||
|
|
|
|||
|
|
@ -31,10 +31,10 @@
|
|||
* `inter_AIS` symbol set (via `aisByScope`). This is the unit at which the
|
||||
* call-graph blast radius is meaningful.
|
||||
*
|
||||
* Neither is a strict refinement of the other: PDG resolves the dependent
|
||||
* statements WITHIN a function but cannot cross a call boundary; call-graph
|
||||
* resolves the cross-function symbol reach but cannot see below function
|
||||
* granularity. The harness reports both, side by side, per locus stratum.
|
||||
* Neither native row is a strict refinement of the other: the PDG native
|
||||
* metric resolves dependent statements WITHIN a function, while call-graph
|
||||
* resolves cross-function symbol reach. The unified axes report checks whether
|
||||
* mode:'pdg' carries both outputs without blending their granularities.
|
||||
*
|
||||
* `partitionCisByScope`/`aisByScope` (symbol-level) remain for the call-graph
|
||||
* path; `pdgLineCis`/`intraLineAis` (line-level) drive the PDG path.
|
||||
|
|
@ -284,9 +284,19 @@ export function callgraphUnifiedCis(gt, symbolCisKeys) {
|
|||
return { intraLine: new Set(), interSymbol: tagSymbolKeys(inter) };
|
||||
}
|
||||
|
||||
/** Current PDG unified CIS: intra-line axis only. */
|
||||
export function pdgUnifiedCis(lineCisKeys) {
|
||||
return { intraLine: tagLineKeys(lineCisKeys), interSymbol: new Set() };
|
||||
/**
|
||||
* Unified PDG CIS: statement axis from affectedStatements plus, once runtime
|
||||
* mode:'pdg' composes interprocedural reach, symbol axis from byDepth. The
|
||||
* criterion symbol is filtered because inter_AIS is cross-function by
|
||||
* construction. Passing only lineCisKeys preserves the old intra-only shape for
|
||||
* focused metric tests.
|
||||
*/
|
||||
export function pdgUnifiedCis(lineCisKeys, symbolCisKeys = new Set(), gt = null) {
|
||||
const { criterionKey } = gt ? aisByScope(gt) : { criterionKey: null };
|
||||
const inter = criterionKey
|
||||
? new Set([...symbolCisKeys].filter((k) => k !== criterionKey))
|
||||
: new Set(symbolCisKeys);
|
||||
return { intraLine: tagLineKeys(lineCisKeys), interSymbol: tagSymbolKeys(inter) };
|
||||
}
|
||||
|
||||
/** Evaluation-only composed baseline: callgraph inter-symbol + PDG intra-line. */
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ This project is indexed by GitNexus as **${projectName}**${noStats ? '' : ` (${s
|
|||
|
||||
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run \`impact({target: "symbolName", direction: "upstream"})\` and report the blast radius (direct callers, affected processes, risk level) to the user.${
|
||||
hasPdg
|
||||
? ` For finer, intra-procedural precision within a function, add \`mode: "pdg"\` with \`line: <N>\` — it returns statement-level \`affectedStatements\` over CDG + REACHING_DEF, but does NOT model cross-function impact; no-layer/degraded PDG results are UNKNOWN-risk notes (\`--pdg\` layer).`
|
||||
? ` For unified PDG impact, add \`mode: "pdg"\` with optional \`line: <N>\` — it returns statement-level \`affectedStatements\` over CDG + REACHING_DEF and inter-procedural symbols in \`interproceduralByDepth\`/\`byDepth\`; no-layer/degraded PDG results are UNKNOWN-risk notes (\`--pdg\` layer).`
|
||||
: ''
|
||||
}
|
||||
- **MUST run \`detect_changes()\` before committing** to verify your changes only affect expected symbols and execution flows. For regression review, compare against the default branch: \`detect_changes({scope: "compare", base_ref: ${JSON.stringify(markdownSafeBranch(defaultBranch))}})\`.
|
||||
|
|
|
|||
|
|
@ -267,6 +267,49 @@ export function formatImpactResult(result: any): string {
|
|||
// reach here.
|
||||
if (result.mode === 'pdg') {
|
||||
const name = target?.name || '?';
|
||||
const appendPdgInterproceduralSymbols = (lines: string[]): boolean => {
|
||||
const byDepth =
|
||||
result.interproceduralByDepth || result.pdgInterprocedural?.byDepth || result.byDepth || {};
|
||||
const byDepthCounts =
|
||||
result.interproceduralByDepthCounts ||
|
||||
result.pdgInterprocedural?.byDepthCounts ||
|
||||
result.byDepthCounts ||
|
||||
{};
|
||||
const depthKeys = Array.from(
|
||||
new Set([...Object.keys(byDepthCounts), ...Object.keys(byDepth)]),
|
||||
)
|
||||
.map((d) => Number(d))
|
||||
.filter((d) => Number.isFinite(d))
|
||||
.sort((a, b) => a - b);
|
||||
const hasReach = depthKeys.some((depth) => {
|
||||
const items = byDepth[depth] || byDepth[String(depth)] || [];
|
||||
const count = byDepthCounts[depth] ?? byDepthCounts[String(depth)] ?? items.length;
|
||||
return count > 0;
|
||||
});
|
||||
if (!hasReach) return false;
|
||||
|
||||
const totalSymbols =
|
||||
result.pdgInterprocedural?.impactedCount ??
|
||||
(typeof result.impactedCount === 'number' ? result.impactedCount : 0);
|
||||
lines.push('');
|
||||
lines.push(`Inter-procedural symbol reach (${totalSymbols}):`);
|
||||
for (const depth of depthKeys) {
|
||||
const items = byDepth[depth] || byDepth[String(depth)] || [];
|
||||
const count = byDepthCounts[depth] ?? byDepthCounts[String(depth)] ?? items.length;
|
||||
if (count <= 0) continue;
|
||||
lines.push(` d=${depth} (${count})`);
|
||||
const shown = Math.min(items.length, 12);
|
||||
for (const item of items.slice(0, shown)) {
|
||||
const flags: string[] = [];
|
||||
if (item.unresolved) flags.push('unresolved');
|
||||
if (item.ambiguous) flags.push('ambiguous');
|
||||
const flagStr = flags.length ? ` [${flags.join(', ')}]` : '';
|
||||
lines.push(` ${item.type || ''} ${item.name} → ${item.filePath}${flagStr}`);
|
||||
}
|
||||
if (count > shown) lines.push(` ... and ${count - shown} more`);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// (1) Degradation — the PDG layer (or a sub-layer) is absent/unreadable.
|
||||
// `pdgLayer` is the non-'ready' state from `pdgLayerStatus`. Print the
|
||||
|
|
@ -288,14 +331,15 @@ export function formatImpactResult(result: any): string {
|
|||
// member / one-line declaration with no CFG. Show the caveat, never
|
||||
// "isolated / no dependencies".
|
||||
if (result.epistemic === 'no-pdg-body') {
|
||||
return (
|
||||
`${name}: PDG mode not applicable to this symbol — it has no PDG body ` +
|
||||
`(no control/data dependence edges; e.g. an interface, type alias, ` +
|
||||
`abstract/ambient member, or a one-line declaration). This is NOT a ` +
|
||||
`confident "no impact". Use \`--mode callgraph\` for its inter-procedural ` +
|
||||
`blast radius.` +
|
||||
(result.note ? `\n${result.note}` : '')
|
||||
);
|
||||
const noBodyLines = [
|
||||
`${name}: local PDG slice not applicable to this symbol — it has no PDG body ` +
|
||||
`(no control/data dependence edges; e.g. an interface, type alias, ` +
|
||||
`abstract/ambient member, or a one-line declaration). This is NOT a ` +
|
||||
`confident "no impact".`,
|
||||
];
|
||||
appendPdgInterproceduralSymbols(noBodyLines);
|
||||
if (result.note) noBodyLines.push(result.note);
|
||||
return noBodyLines.join('\n');
|
||||
}
|
||||
|
||||
// (2b) STATEMENT-ANCHORED SLICE (mode:'pdg' + line). When `criterionLine` is
|
||||
|
|
@ -333,6 +377,7 @@ export function formatImpactResult(result: any): string {
|
|||
`⚠️ Truncated${by} — the dependence slice was bounded; deeper PDG-dependent statements may exist.`,
|
||||
);
|
||||
}
|
||||
appendPdgInterproceduralSymbols(emptySliceLines);
|
||||
if (result.note) emptySliceLines.push(result.note);
|
||||
return emptySliceLines.join('\n');
|
||||
}
|
||||
|
|
@ -353,6 +398,7 @@ export function formatImpactResult(result: any): string {
|
|||
`⚠️ Truncated${by} — the dependence slice was bounded; deeper PDG-dependent statements may exist.`,
|
||||
);
|
||||
}
|
||||
appendPdgInterproceduralSymbols(slLines);
|
||||
if (result.note) {
|
||||
slLines.push('');
|
||||
slLines.push(`ℹ️ ${result.note}`);
|
||||
|
|
@ -360,57 +406,25 @@ export function formatImpactResult(result: any): string {
|
|||
return slLines.join('\n').trim();
|
||||
}
|
||||
|
||||
const items: any[] = (result.byDepth && result.byDepth[1]) || [];
|
||||
const bucketCount = result.byDepthCounts?.[1] ?? items.length;
|
||||
const pdgLines: string[] = [];
|
||||
|
||||
// (3) Has a body but no intra-procedural dependence reachability.
|
||||
// `impactedCount === 0` with no findings — still NOT "isolated": the count
|
||||
// is a per-function lower bound, and inter-procedural impact is unmodeled.
|
||||
if (total === 0 && bucketCount === 0) {
|
||||
if (!appendPdgInterproceduralSymbols(pdgLines)) {
|
||||
pdgLines.push(
|
||||
`${name} (${direction}): no intra-procedural PDG-dependent symbols found. ` +
|
||||
`This is NOT a confident "isolated / no dependencies" — cross-function ` +
|
||||
`(inter-procedural) impact is not modeled in PDG mode. Use \`--mode callgraph\` ` +
|
||||
`for the call-graph blast radius.`,
|
||||
`${name} (${direction}): no inter-procedural symbols reached. ` +
|
||||
`The local PDG statement slice may still report affectedStatements when seeded with line:<N>.`,
|
||||
);
|
||||
} else {
|
||||
// (4) Findings — render the collapsed bucket under a "PDG-dependent
|
||||
// symbols" heading (NOT "depth N"). `total` (impactedCount) is distinct
|
||||
// owning SYMBOLS; `bucketCount` includes any `unresolved` shadow rows.
|
||||
const dirLabel =
|
||||
direction === 'upstream'
|
||||
? 'this depends on (intra-procedural)'
|
||||
: 'depend on this (intra-procedural)';
|
||||
pdgLines.push(
|
||||
`PDG-dependent symbols for ${target?.kind || ''} ${name} (${direction}): ` +
|
||||
`${total} symbol(s) ${dirLabel}`,
|
||||
);
|
||||
pdgLines.push('');
|
||||
const shown = Math.min(items.length, 12);
|
||||
for (const item of items.slice(0, shown)) {
|
||||
const flags: string[] = [];
|
||||
if (item.unresolved) flags.push('unresolved');
|
||||
if (item.ambiguous) flags.push('ambiguous');
|
||||
const flagStr = flags.length ? ` [${flags.join(', ')}]` : '';
|
||||
pdgLines.push(` ${item.type || ''} ${item.name} → ${item.filePath}${flagStr}`);
|
||||
}
|
||||
if (bucketCount > shown) {
|
||||
pdgLines.push(` ... and ${bucketCount - shown} more`);
|
||||
}
|
||||
}
|
||||
|
||||
// Intra-procedural caveat — always present for a non-degraded PDG result.
|
||||
// The assembled `note` already carries the cross-function caveat + the
|
||||
// ambiguous/unresolved breakdown; surface it verbatim so the CLI reader
|
||||
// sees the same honesty the JSON consumer does.
|
||||
// The assembled note carries the local-PDG framing plus the unified
|
||||
// inter-procedural symbol-reach contract; surface it verbatim so the CLI
|
||||
// reader sees the same honesty the JSON consumer does.
|
||||
if (result.note) {
|
||||
pdgLines.push('');
|
||||
pdgLines.push(`ℹ️ ${result.note}`);
|
||||
} else {
|
||||
pdgLines.push('');
|
||||
pdgLines.push(
|
||||
'ℹ️ Intra-procedural Program Dependence Graph — cross-function impact is not modeled in this mode.',
|
||||
'ℹ️ Program Dependence Graph result — statement reach is reported in affectedStatements and inter-procedural symbol reach in interproceduralByDepth/byDepth.',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4323,25 +4323,18 @@ export class LocalBackend {
|
|||
}
|
||||
|
||||
if (mode === 'pdg') {
|
||||
// KTD12 — param-compatibility hard rejections (decided as errors, NOT
|
||||
// silent ignores and NOT an `ignoredParams` echo). Each names a symbol-
|
||||
// graph / cross-repo concept the PDG engine cannot honor:
|
||||
// relationTypes → names symbol edges (PDG walks BasicBlock edges).
|
||||
// crossDepth → cross-repo hops (PDG is single-repo intra-procedural).
|
||||
// minConfidence → CDG/RD edges may carry no confidence → would drop all.
|
||||
// A loud failure beats a quietly-wrong result. (@group targets are
|
||||
// rejected at the group-forward boundary in callToolAtGroupRepo before
|
||||
// they ever reach here; see KTD12.)
|
||||
// PDG mode is now unified inside a single repo: it combines the local
|
||||
// CDG/RD statement slice with the same inter-symbol reach used for the
|
||||
// option-driven comparison path. Cross-repo fan-out remains a callgraph
|
||||
// feature, so crossDepth is still a loud error rather than a silent ignore.
|
||||
const incompatible: string[] = [];
|
||||
if (params.relationTypes !== undefined) incompatible.push('relationTypes');
|
||||
if (params.crossDepth !== undefined) incompatible.push('crossDepth');
|
||||
if (params.minConfidence !== undefined) incompatible.push('minConfidence');
|
||||
if (incompatible.length > 0) {
|
||||
return makePdgImpactErrorResult({
|
||||
mode: 'pdg',
|
||||
error:
|
||||
`Parameter(s) ${incompatible.join(', ')} are not supported with mode:'pdg' ` +
|
||||
`(intra-procedural, single-repo, dependence-edge based). Remove them or use mode:'callgraph'.`,
|
||||
`(single-repo PDG impact). Remove them or use mode:'callgraph' for cross-repo fan-out.`,
|
||||
target: { name: target },
|
||||
direction,
|
||||
});
|
||||
|
|
@ -4612,11 +4605,16 @@ export class LocalBackend {
|
|||
}
|
||||
}
|
||||
|
||||
const effectiveRelationTypes =
|
||||
(symType === 'Class' || symType === 'Interface') &&
|
||||
!hasExplicitRelationTypes &&
|
||||
!relationTypes.includes('ACCESSES')
|
||||
? [...relationTypes, 'ACCESSES']
|
||||
: relationTypes;
|
||||
|
||||
// (4) single → route the resolved symbol to the engine selected by `mode`.
|
||||
// The PDG engine does NOT touch `_runImpactBFS`, so a `pdg` call never runs
|
||||
// callgraph.
|
||||
if (mode === 'pdg') {
|
||||
return this._runImpactPDG({
|
||||
const pdgResult = await this._runImpactPDG({
|
||||
repo,
|
||||
sym,
|
||||
symType,
|
||||
|
|
@ -4629,14 +4627,22 @@ export class LocalBackend {
|
|||
// lifecycle; `pdg-impact.ts` owns traversal/projection.
|
||||
executeParameterized,
|
||||
});
|
||||
}
|
||||
|
||||
const effectiveRelationTypes =
|
||||
(symType === 'Class' || symType === 'Interface') &&
|
||||
!hasExplicitRelationTypes &&
|
||||
!relationTypes.includes('ACCESSES')
|
||||
? [...relationTypes, 'ACCESSES']
|
||||
: relationTypes;
|
||||
try {
|
||||
const interproceduralResult = await this._runImpactBFS(repo, sym, symType, direction, {
|
||||
maxDepth,
|
||||
relationTypes: effectiveRelationTypes,
|
||||
includeTests,
|
||||
minConfidence,
|
||||
limit: Number.isFinite(params.limit) ? params.limit : 100,
|
||||
offset: Number.isFinite(params.offset) ? params.offset : 0,
|
||||
});
|
||||
return this.composeUnifiedPdgImpactResult(pdgResult, interproceduralResult);
|
||||
} catch (e) {
|
||||
logQueryError('impact:pdg-interprocedural-reach', e);
|
||||
return this.composeUnifiedPdgImpactResult(pdgResult, null, e);
|
||||
}
|
||||
}
|
||||
|
||||
return this._runImpactBFS(repo, sym, symType, direction, {
|
||||
maxDepth,
|
||||
|
|
@ -4653,9 +4659,10 @@ export class LocalBackend {
|
|||
* Delegates the PDG impact engine to `pdg-impact.ts`.
|
||||
*
|
||||
* The private method remains as the LocalBackend dispatch seam so existing
|
||||
* tests can keep asserting that `mode:'pdg'` routes here and never falls back
|
||||
* to `_runImpactBFS`. The traversal/projection/result assembly lives in the
|
||||
* extracted helper module.
|
||||
* tests can keep asserting that `mode:'pdg'` routes through the PDG
|
||||
* statement engine before LocalBackend attaches interprocedural symbol reach.
|
||||
* The traversal/projection/result assembly lives in the extracted helper
|
||||
* module.
|
||||
*/
|
||||
private async _runImpactPDG(deps: {
|
||||
repo: RepoHandle;
|
||||
|
|
@ -4670,6 +4677,139 @@ export class LocalBackend {
|
|||
return runImpactPDG(deps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose `mode:'pdg'` into one user-facing impact result:
|
||||
*
|
||||
* - `affectedStatements` / `reachableBlocks` stay owned by the persisted PDG
|
||||
* layer (CDG + REACHING_DEF), preserving the statement-level intra result.
|
||||
* - `interproceduralByDepth` / `pdgInterprocedural` expose the symbol reach;
|
||||
* `byDepth` stays as the compatibility symbol bucket so existing consumers
|
||||
* still see one PDG result shape.
|
||||
*
|
||||
* The callgraph option remains available as the comparator/default path; this
|
||||
* helper only changes the `pdg` result contract from intra-only to unified.
|
||||
*/
|
||||
private composeUnifiedPdgImpactResult(
|
||||
pdgResult: PdgImpactResult,
|
||||
interproceduralResult: any | null,
|
||||
interproceduralError?: unknown,
|
||||
): PdgImpactResult {
|
||||
if ('error' in pdgResult || 'pdgLayer' in pdgResult) return pdgResult;
|
||||
|
||||
const localByDepth = pdgResult.byDepth ?? {};
|
||||
const localByDepthCounts = pdgResult.byDepthCounts ?? {};
|
||||
const interproceduralByDepth = interproceduralResult?.byDepth ?? {};
|
||||
const interproceduralByDepthCounts = interproceduralResult?.byDepthCounts ?? {};
|
||||
const byDepth: Record<number, unknown[]> = {};
|
||||
const byDepthCounts: Record<number, number> = {};
|
||||
const depthKeys = Array.from(
|
||||
new Set([
|
||||
...Object.keys(localByDepth),
|
||||
...Object.keys(interproceduralByDepth),
|
||||
...Object.keys(localByDepthCounts),
|
||||
...Object.keys(interproceduralByDepthCounts),
|
||||
]),
|
||||
)
|
||||
.map((d) => Number(d))
|
||||
.filter((d) => Number.isFinite(d))
|
||||
.sort((a, b) => a - b);
|
||||
for (const depth of depthKeys) {
|
||||
const localItems = localByDepth[depth] ?? localByDepth[String(depth)] ?? [];
|
||||
const interItems =
|
||||
interproceduralByDepth[depth] ?? interproceduralByDepth[String(depth)] ?? [];
|
||||
const items = [...localItems, ...interItems];
|
||||
if (items.length > 0) byDepth[depth] = items;
|
||||
const localCount =
|
||||
localByDepthCounts[depth] ?? localByDepthCounts[String(depth)] ?? localItems.length;
|
||||
const interCount =
|
||||
interproceduralByDepthCounts[depth] ??
|
||||
interproceduralByDepthCounts[String(depth)] ??
|
||||
interItems.length;
|
||||
const totalCount = localCount + interCount;
|
||||
if (totalCount > 0) byDepthCounts[depth] = totalCount;
|
||||
}
|
||||
|
||||
if (Object.keys(byDepthCounts).length === 0) {
|
||||
const localZero = localByDepthCounts[1] ?? localByDepthCounts['1'];
|
||||
const interZero = interproceduralByDepthCounts[1] ?? interproceduralByDepthCounts['1'];
|
||||
if (typeof localZero === 'number' || typeof interZero === 'number') {
|
||||
byDepthCounts[1] =
|
||||
(typeof localZero === 'number' ? localZero : 0) +
|
||||
(typeof interZero === 'number' ? interZero : 0);
|
||||
}
|
||||
}
|
||||
|
||||
const localImpactedCount =
|
||||
typeof pdgResult.impactedCount === 'number' ? pdgResult.impactedCount : 0;
|
||||
const interproceduralImpactedCount =
|
||||
typeof interproceduralResult?.impactedCount === 'number'
|
||||
? interproceduralResult.impactedCount
|
||||
: 0;
|
||||
const impactedCount = localImpactedCount + interproceduralImpactedCount;
|
||||
const summary = interproceduralResult?.summary
|
||||
? {
|
||||
...interproceduralResult.summary,
|
||||
direct: (pdgResult.summary?.direct ?? 0) + (interproceduralResult.summary.direct ?? 0),
|
||||
}
|
||||
: {
|
||||
direct: pdgResult.summary?.direct ?? localImpactedCount,
|
||||
processes_affected: 0,
|
||||
modules_affected: 0,
|
||||
};
|
||||
const affectedProcesses = interproceduralResult?.affected_processes ?? [];
|
||||
const affectedModules = interproceduralResult?.affected_modules ?? [];
|
||||
const partial = Boolean(interproceduralResult?.partial || interproceduralError);
|
||||
const errorMessage =
|
||||
interproceduralError instanceof Error
|
||||
? interproceduralError.message
|
||||
: interproceduralError
|
||||
? String(interproceduralError)
|
||||
: undefined;
|
||||
|
||||
const noteParts = [
|
||||
pdgResult.note,
|
||||
`Inter-procedural symbol reach is included using the resolved symbol graph; ` +
|
||||
`statement-level PDG reach remains in affectedStatements.`,
|
||||
];
|
||||
if (errorMessage) {
|
||||
noteParts.push(
|
||||
`Inter-procedural symbol reach failed (${errorMessage}); byDepth is therefore a lower bound.`,
|
||||
);
|
||||
} else if (interproceduralResult?.epistemic === 'lower-bound') {
|
||||
noteParts.push(
|
||||
`The inter-procedural symbol reach is a lower bound because unresolved indirection was detected.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...pdgResult,
|
||||
impactedCount,
|
||||
note: noteParts.filter(Boolean).join(' '),
|
||||
summary,
|
||||
byDepthCounts,
|
||||
interproceduralByDepth,
|
||||
interproceduralByDepthCounts,
|
||||
affected_processes: affectedProcesses,
|
||||
affected_modules: affectedModules,
|
||||
byDepth,
|
||||
...(partial ? { partial: true } : {}),
|
||||
...(interproceduralResult?.epistemic
|
||||
? { interproceduralEpistemic: interproceduralResult.epistemic }
|
||||
: {}),
|
||||
...(interproceduralResult?.boundaries
|
||||
? { interproceduralBoundaries: interproceduralResult.boundaries }
|
||||
: {}),
|
||||
...(errorMessage ? { interproceduralError: errorMessage } : {}),
|
||||
pdgInterprocedural: {
|
||||
engine: 'symbol-graph',
|
||||
impactedCount: interproceduralImpactedCount,
|
||||
byDepthCounts: interproceduralByDepthCounts,
|
||||
byDepth: interproceduralByDepth,
|
||||
partial,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* #1858 — epistemic lower-bound detection.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -298,6 +298,14 @@ export interface PdgImpactParityFields {
|
|||
affected_modules: unknown[];
|
||||
}
|
||||
|
||||
export interface PdgInterproceduralImpact {
|
||||
engine: 'symbol-graph';
|
||||
impactedCount: number;
|
||||
byDepthCounts: Record<number, number>;
|
||||
byDepth: Record<number, unknown[]>;
|
||||
partial: boolean;
|
||||
}
|
||||
|
||||
export interface PdgImpactBaseResult extends PdgImpactParityFields {
|
||||
mode: 'pdg';
|
||||
target: PdgImpactTarget;
|
||||
|
|
@ -305,6 +313,13 @@ export interface PdgImpactBaseResult extends PdgImpactParityFields {
|
|||
impactedCount: number;
|
||||
risk: 'UNKNOWN';
|
||||
note?: string;
|
||||
partial?: boolean;
|
||||
interproceduralByDepth?: Record<number, unknown[]>;
|
||||
interproceduralByDepthCounts?: Record<number, number>;
|
||||
interproceduralEpistemic?: string;
|
||||
interproceduralBoundaries?: unknown[];
|
||||
interproceduralError?: string;
|
||||
pdgInterprocedural?: PdgInterproceduralImpact;
|
||||
}
|
||||
|
||||
export interface PdgImpactSuccessResult extends PdgImpactBaseResult {
|
||||
|
|
@ -494,15 +509,15 @@ function assemblePdgImpactResult(input: {
|
|||
`mode:'pdg' — intra-procedural slice from line ${input.criterionLine} of ` +
|
||||
`'${target.name}'. ${affectedStatements.length} ` +
|
||||
`${affectedStatements.length === 1 ? 'statement is' : 'statements are'} ${direction}-` +
|
||||
`dependent on it (over CDG + REACHING_DEF). Cross-function (inter-procedural) impact ` +
|
||||
`is NOT modeled in this mode — use mode:'callgraph' for the call-graph blast radius.`,
|
||||
`dependent on it (over CDG + REACHING_DEF). Inter-procedural symbol reach ` +
|
||||
`is attached by impact mode's unified PDG dispatcher in interproceduralByDepth/byDepth.`,
|
||||
]
|
||||
: [
|
||||
`mode:'pdg' — intra-procedural Program Dependence Graph. ${impactedCount} owning ` +
|
||||
`${impactedCount === 1 ? 'symbol' : 'symbols'} reached via ${reachableBlocks.length} ` +
|
||||
`dependence ${reachableBlocks.length === 1 ? 'block' : 'blocks'} ` +
|
||||
`(${direction} over CDG + REACHING_DEF). Cross-function (inter-procedural) impact is ` +
|
||||
`NOT modeled in this mode — use mode:'callgraph' for the call-graph blast radius.`,
|
||||
`(${direction} over CDG + REACHING_DEF). Inter-procedural symbol reach ` +
|
||||
`is attached by impact mode's unified PDG dispatcher in interproceduralByDepth/byDepth.`,
|
||||
];
|
||||
if (ambiguousCount > 0) {
|
||||
noteParts.push(
|
||||
|
|
@ -952,9 +967,8 @@ export async function runImpactPDG(deps: RunPdgImpactDeps): Promise<PdgImpactRes
|
|||
: `'${sym.name}' has no PDG body — no BasicBlocks / control- or data-dependence ` +
|
||||
`edges exist for this symbol (e.g. an interface, type alias, abstract/ambient ` +
|
||||
`member, or a one-line declaration with no CFG). This is NOT a confident ` +
|
||||
`"no impact": the intra-procedural PDG mode cannot model this symbol kind. ` +
|
||||
`Pass line:<N> to slice from a statement, or use mode:'callgraph' for the ` +
|
||||
`inter-procedural blast radius.`,
|
||||
`"no impact": the local PDG statement slice cannot model this symbol kind. ` +
|
||||
`Inter-procedural symbol reach may still be attached by the unified impact dispatcher.`,
|
||||
impactedCount: 0,
|
||||
risk: 'UNKNOWN',
|
||||
// KTD8 parity fields so a consumer iterating byDepth / reading the
|
||||
|
|
@ -1061,8 +1075,8 @@ export async function runImpactPDG(deps: RunPdgImpactDeps): Promise<PdgImpactRes
|
|||
: `'${sym.name}' has a PDG body but a WHOLE-SYMBOL ${direction} slice is empty: ` +
|
||||
`intra-procedural dependence stays inside the function, so every reachable block ` +
|
||||
`is already part of the seed. Pass line:<N> to slice from a specific statement ` +
|
||||
`(what depends on the code at that line), or use mode:'callgraph' for the ` +
|
||||
`inter-procedural blast radius.`,
|
||||
`(what depends on the code at that line). Inter-procedural symbol reach is attached ` +
|
||||
`separately by the unified impact dispatcher.`,
|
||||
reachableBlocks: [] as string[],
|
||||
blockCount: 0,
|
||||
affectedStatements: [],
|
||||
|
|
|
|||
|
|
@ -413,11 +413,11 @@ Each edit is tagged with confidence:
|
|||
description: `Analyze the blast radius of changing a code symbol.
|
||||
Returns affected symbols grouped by depth, plus risk assessment, affected execution flows, and affected modules.
|
||||
|
||||
MODE (opt-in): "callgraph" (default) walks symbol→symbol edges (CALLS/IMPORTS/EXTENDS/IMPLEMENTS) — inter-procedural, the established behavior. "pdg" computes the blast radius from the persisted Program Dependence Graph (control + data dependence) — finer-grained WITHIN a function but intra-procedural, and requires an index built with \`gitnexus analyze --pdg\`. The two modes answer the same question with different engines; pdg is incompatible with relationTypes/crossDepth/minConfidence and with @group targets (each rejected).
|
||||
MODE (opt-in): "callgraph" (default) walks symbol→symbol edges (CALLS/IMPORTS/EXTENDS/IMPLEMENTS) — inter-procedural, the established comparator/default behavior. "pdg" requires an index built with \`gitnexus analyze --pdg\` and returns one unified PDG-facing result: statement-level control/data dependence from the persisted PDG plus inter-procedural symbol reach. The explicit interprocedural surface is interproceduralByDepth/pdgInterprocedural; byDepth remains the compatibility symbol bucket. pdg remains incompatible with crossDepth and @group targets; relationTypes/minConfidence filter the inter-symbol reach.
|
||||
|
||||
STATEMENT-ANCHORED PDG SLICE: with mode:'pdg', pass "line" (1-based source line within the target symbol) to seed the dependence slice on the statement at that line and return what depends on it — the dependent statements (line + text), not the whole-symbol set. Without "line", a whole-symbol pdg slice is structurally empty (intra-procedural reach stays inside the function), so "line" is what makes pdg mode useful.
|
||||
STATEMENT-ANCHORED PDG SLICE: with mode:'pdg', pass "line" (1-based source line within the target symbol) to seed the dependence slice on the statement at that line and return what depends on it in affectedStatements (line + text). Inter-procedural symbols are still reported through interproceduralByDepth/pdgInterprocedural and the compatibility byDepth bucket. Without "line", pdg returns whole-symbol inter-procedural reach plus local whole-symbol PDG diagnostics.
|
||||
|
||||
PDG OUTPUT CONTRACT: successful PDG slices include mode:'pdg', a full target envelope (id/name/type/filePath), affectedStatements, affectedStatementCount, byDepth/byDepthCounts parity fields, risk:'UNKNOWN', and an intra-procedural note. Degraded PDG results (no-layer, sub-layer-missing, unknown) keep mode:'pdg', target metadata when the target resolves, risk:'UNKNOWN', note/remediation, and empty byDepth parity fields — never a false-safe zero. If depth and limit both bound the slice, truncatedByReasons reports both causes while truncatedBy remains scalar.
|
||||
PDG OUTPUT CONTRACT: successful PDG results include mode:'pdg', a full target envelope (id/name/type/filePath), affectedStatements, affectedStatementCount, interproceduralByDepth/pdgInterprocedural for cross-function reach, compatibility byDepth/byDepthCounts, risk:'UNKNOWN', and a note describing the unified contract. Degraded PDG results (no-layer, sub-layer-missing, unknown) keep mode:'pdg', target metadata when the target resolves, risk:'UNKNOWN', note/remediation, and empty byDepth parity fields — never a false-safe zero. If depth and limit both bound the slice, truncatedByReasons reports both causes while truncatedBy remains scalar.
|
||||
|
||||
WHEN TO USE: Before making code changes — especially refactoring, renaming, or modifying shared code. Shows what would break.
|
||||
AFTER THIS: Review d=1 items (WILL BREAK). Use context() on high-risk symbols.
|
||||
|
|
@ -465,13 +465,13 @@ SERVICE: optional monorepo path prefix (case-sensitive path segments). When "rep
|
|||
enum: ['callgraph', 'pdg'],
|
||||
default: 'callgraph',
|
||||
description:
|
||||
"Blast-radius engine. 'callgraph' (default) = inter-procedural symbol→symbol traversal (current behavior). 'pdg' = opt-in, intra-procedural Program Dependence Graph traversal (control + data dependence); requires an index built with `gitnexus analyze --pdg`. PDG success returns affectedStatements, while degraded/no-layer results return a structured UNKNOWN-risk note with target metadata when resolved. The pdg mode is incompatible with relationTypes/crossDepth/minConfidence and with @group targets — each is rejected, not silently ignored.",
|
||||
"Blast-radius engine. 'callgraph' (default) = inter-procedural symbol→symbol traversal (established comparator). 'pdg' = unified PDG-facing impact: statement-level affectedStatements from the persisted control/data dependence layer plus inter-procedural symbols in interproceduralByDepth/pdgInterprocedural and the compatibility byDepth bucket; requires `gitnexus analyze --pdg`. PDG is incompatible with crossDepth and @group targets; relationTypes/minConfidence filter the inter-symbol reach.",
|
||||
},
|
||||
line: {
|
||||
type: 'integer',
|
||||
minimum: 1,
|
||||
description:
|
||||
"1-based source line — PDG-only statement anchor (mode:'pdg'). Seeds the dependence slice on the statement at this line and returns what depends on it. Without it, a whole-symbol pdg slice is empty (intra-procedural reach stays inside the function).",
|
||||
"1-based source line — PDG statement anchor (mode:'pdg'). Seeds affectedStatements on the statement at this line; inter-procedural symbols are still returned in interproceduralByDepth/pdgInterprocedural and the compatibility byDepth bucket.",
|
||||
},
|
||||
file_path: {
|
||||
type: 'string',
|
||||
|
|
|
|||
|
|
@ -288,12 +288,12 @@ withTestLbugDB(
|
|||
});
|
||||
});
|
||||
|
||||
// ── KTD5 ambiguous trap: pdg+ambiguous never runs the callgraph fan-out ───
|
||||
describe('KTD5 ambiguous target never invokes the callgraph BFS', () => {
|
||||
// ── KTD5 ambiguous trap: pdg+ambiguous never runs interprocedural fan-out ──
|
||||
describe('KTD5 ambiguous target never invokes the interprocedural BFS', () => {
|
||||
it("mode:'pdg' on an ambiguous target returns candidates, never calls _runImpactBFS", async () => {
|
||||
// Spy on the private callgraph BFS; if the pdg ambiguous path leaked into
|
||||
// the callgraph fan-out, this spy would fire (the silent-fallback KTD5
|
||||
// forbids). `dupTarget` collides across dupA/dupB by NAME.
|
||||
// Spy on the private interprocedural BFS; ambiguous PDG has no single
|
||||
// resolved symbol, so it must not run the composed symbol-reach pass.
|
||||
// `dupTarget` collides across dupA/dupB by NAME.
|
||||
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
|
||||
try {
|
||||
const result = await backend.callTool('impact', {
|
||||
|
|
@ -303,7 +303,7 @@ withTestLbugDB(
|
|||
});
|
||||
expect(result.status).toBe('ambiguous');
|
||||
expect(result.mode).toBe('pdg');
|
||||
// No callgraph engine ran under the pdg call.
|
||||
// No interprocedural symbol-reach pass ran without a resolved target.
|
||||
expect(bfsSpy).not.toHaveBeenCalled();
|
||||
// And it surfaces the candidate list (no silent zero blast radius).
|
||||
expect(Array.isArray(result.candidates)).toBe(true);
|
||||
|
|
@ -321,8 +321,8 @@ withTestLbugDB(
|
|||
// (that would re-ambiguate a file_path/uid-disambiguated name, or anchor on a
|
||||
// DIFFERENT same-name symbol → wrong-symbol blast radius). With two functions
|
||||
// named `sameName` in different files, disambiguating by file_path/target_uid
|
||||
// must (a) produce the CORRECT file's blast radius, and (b) NEVER fall into
|
||||
// the callgraph `_runImpactBFS` fan-out.
|
||||
// must produce the CORRECT local PDG blast radius before the composed
|
||||
// interprocedural `_runImpactBFS` pass runs for the resolved symbol.
|
||||
describe('seed anchors on the resolved (disambiguated) symbol, not a name re-resolution', () => {
|
||||
it('file_path disambiguation reaches the right file’s downstream owner (not the other same-name fn)', async () => {
|
||||
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
|
||||
|
|
@ -348,8 +348,9 @@ withTestLbugDB(
|
|||
);
|
||||
expect(names.has('onlyB')).toBe(true);
|
||||
expect(names.has('onlyA')).toBe(false);
|
||||
// KTD5: no callgraph engine ran under the pdg call.
|
||||
expect(bfsSpy).not.toHaveBeenCalled();
|
||||
// Unified PDG now composes interprocedural symbol reach after the local
|
||||
// PDG slice anchors on the resolved symbol.
|
||||
expect(bfsSpy).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
bfsSpy.mockRestore();
|
||||
}
|
||||
|
|
@ -375,7 +376,7 @@ withTestLbugDB(
|
|||
);
|
||||
expect(names.has('onlyA')).toBe(true);
|
||||
expect(names.has('onlyB')).toBe(false);
|
||||
expect(bfsSpy).not.toHaveBeenCalled();
|
||||
expect(bfsSpy).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
bfsSpy.mockRestore();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ describe('generateAIContextFiles', () => {
|
|||
expect(withPdg).toContain('under what condition does X run');
|
||||
expect(withPdg).toContain('line: <N>');
|
||||
expect(withPdg).toContain('affectedStatements');
|
||||
expect(withPdg).toContain('byDepth');
|
||||
// hasPdg omitted (default false) → no pdg_query line; a non-pdg index must
|
||||
// not advertise a tool that only returns a "no PDG layer" note.
|
||||
const withoutPdg = generateGitNexusContent('PlainProject', stats);
|
||||
|
|
|
|||
|
|
@ -1402,8 +1402,9 @@ describe('LocalBackend.callTool', () => {
|
|||
// The MCP JSON-schema enum is advisory only (server forwards args
|
||||
// unvalidated, callTool is reachable directly), so the backend `mode`
|
||||
// validation is load-bearing. These tests pin: callgraph is the unchanged
|
||||
// default, pdg routes to the extracted traversal and NEVER the callgraph BFS,
|
||||
// invalid modes hard-error, and the KTD12 incompatible params / @group targets are rejected.
|
||||
// default, pdg routes to the extracted traversal plus interprocedural symbol
|
||||
// reach, invalid modes hard-error, and the remaining incompatible params /
|
||||
// @group targets are rejected.
|
||||
|
||||
describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
|
||||
let backend: LocalBackend;
|
||||
|
|
@ -1461,7 +1462,7 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
|
|||
expect(undef).toEqual(absent);
|
||||
});
|
||||
|
||||
it("mode:'pdg' routes to the PDG traversal and NEVER runs the callgraph BFS (KTD5)", async () => {
|
||||
it("mode:'pdg' routes to the PDG traversal and attaches interprocedural symbol reach", async () => {
|
||||
resolveSingleTarget();
|
||||
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
|
||||
const result = await backend.callTool('impact', {
|
||||
|
|
@ -1469,14 +1470,13 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
|
|||
direction: 'upstream',
|
||||
mode: 'pdg',
|
||||
});
|
||||
// U3 landed — the call reaches the real `_runImpactPDG` traversal, not the
|
||||
// old "not yet implemented" stub. It returns a pdg-shaped payload.
|
||||
// The call reaches the real `_runImpactPDG` traversal, then composes the
|
||||
// interprocedural symbol reach into the same pdg result.
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.mode).toBe('pdg');
|
||||
expect(Array.isArray(result.reachableBlocks)).toBe(true);
|
||||
// The load-bearing KTD5 invariant: the callgraph engine must NEVER be
|
||||
// invoked under a pdg call (no silent fallback).
|
||||
expect(bfsSpy).not.toHaveBeenCalled();
|
||||
expect(result.pdgInterprocedural).toBeDefined();
|
||||
expect(bfsSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([['PDG'], ['pgd'], [''], [0], [null]])(
|
||||
|
|
@ -1532,7 +1532,7 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
|
|||
},
|
||||
);
|
||||
|
||||
it("mode:'pdg' + line:8 routes to the PDG traversal (no validation error, never the BFS)", async () => {
|
||||
it("mode:'pdg' + line:8 routes to the PDG traversal and interprocedural reach", async () => {
|
||||
resolveSingleTarget();
|
||||
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
|
||||
const pdgSpy = vi.spyOn(backend as any, '_runImpactPDG');
|
||||
|
|
@ -1546,30 +1546,53 @@ describe('LocalBackend impact mode (KTD1/KTD5/KTD12)', () => {
|
|||
expect(result.error).toBeUndefined();
|
||||
expect(result.mode).toBe('pdg');
|
||||
expect(pdgSpy).toHaveBeenCalledTimes(1);
|
||||
// KTD5: the callgraph engine never runs under a pdg + line call.
|
||||
expect(bfsSpy).not.toHaveBeenCalled();
|
||||
expect(bfsSpy).toHaveBeenCalledTimes(1);
|
||||
expect(result.pdgInterprocedural).toBeDefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['relationTypes', { relationTypes: ['CALLS'] }],
|
||||
['crossDepth', { crossDepth: 2 }],
|
||||
['minConfidence', { minConfidence: 0.5 }],
|
||||
])("mode:'pdg' + %s → hard {error} (KTD12, not a silent ignore)", async (_label, extra) => {
|
||||
it("mode:'pdg' + crossDepth → hard {error} (single-repo PDG impact)", async () => {
|
||||
resolveSingleTarget();
|
||||
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS');
|
||||
const result = await backend.callTool('impact', {
|
||||
target: 'main',
|
||||
direction: 'upstream',
|
||||
mode: 'pdg',
|
||||
...extra,
|
||||
crossDepth: 2,
|
||||
});
|
||||
expect(result.error).toMatch(/not supported with mode:'pdg'/);
|
||||
expect(result.error).toContain(_label);
|
||||
// Hard error — never silently ignored, never the callgraph fan-out.
|
||||
expect(result.error).toContain('crossDepth');
|
||||
expect(bfsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ambiguous target under mode:'pdg' never invokes the callgraph fan-out (KTD5 ambiguous trap)", async () => {
|
||||
it.each([
|
||||
['relationTypes', { relationTypes: ['CALLS'] }, (opts: any) => opts.relationTypes],
|
||||
['minConfidence', { minConfidence: 0.5 }, (opts: any) => opts.minConfidence],
|
||||
])("mode:'pdg' + %s feeds the interprocedural symbol reach", async (_label, extra, readOpt) => {
|
||||
resolveSingleTarget();
|
||||
const bfsSpy = vi.spyOn(backend as any, '_runImpactBFS').mockResolvedValueOnce({
|
||||
target: { id: 'func:main', name: 'main', type: 'Function', filePath: 'src/index.ts' },
|
||||
direction: 'upstream',
|
||||
impactedCount: 0,
|
||||
risk: 'LOW',
|
||||
summary: { direct: 0, processes_affected: 0, modules_affected: 0 },
|
||||
byDepthCounts: {},
|
||||
affected_processes: [],
|
||||
affected_modules: [],
|
||||
byDepth: {},
|
||||
});
|
||||
const result = await backend.callTool('impact', {
|
||||
target: 'main',
|
||||
direction: 'upstream',
|
||||
mode: 'pdg',
|
||||
...extra,
|
||||
});
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.mode).toBe('pdg');
|
||||
expect(bfsSpy).toHaveBeenCalledTimes(1);
|
||||
expect(readOpt(bfsSpy.mock.calls[0][4])).toBeDefined();
|
||||
});
|
||||
|
||||
it("ambiguous target under mode:'pdg' never invokes interprocedural fan-out (KTD5 ambiguous trap)", async () => {
|
||||
// Two same-name Functions → resolver returns ambiguous.
|
||||
(executeParameterized as any).mockResolvedValue([
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
* U5 — CLI / consumer rendering for PDG (`mode:'pdg'`) impact results.
|
||||
*
|
||||
* Guards the KTD8 presentation contract: PDG results must render HONESTLY —
|
||||
* - findings under a "PDG-dependent symbols" heading, NOT "depth N"
|
||||
* (block-hops are not call-hops);
|
||||
* - the intra-procedural caveat ("cross-function impact not modeled");
|
||||
* - inter-procedural symbol reach under a neutral heading, NOT callgraph severity labels;
|
||||
* - the unified PDG caveat (statement reach in affectedStatements, symbol reach in interproceduralByDepth/byDepth);
|
||||
* - degradation → the "run analyze --pdg" remediation, NOT a zero blast radius;
|
||||
* - no-body (KTD6) → the "not applicable to this symbol kind" caveat, NOT
|
||||
* "isolated / no dependencies";
|
||||
|
|
@ -51,8 +50,8 @@ function pdgFindings(overrides: Record<string, unknown> = {}): Record<string, un
|
|||
epistemic: 'pdg-intra-procedural',
|
||||
note:
|
||||
"mode:'pdg' — intra-procedural Program Dependence Graph. 2 owning symbols reached via 4 " +
|
||||
'dependence blocks (downstream over CDG + REACHING_DEF). Cross-function (inter-procedural) ' +
|
||||
"impact is NOT modeled in this mode — use mode:'callgraph' for the call-graph blast radius.",
|
||||
'dependence blocks (downstream over CDG + REACHING_DEF). Inter-procedural symbol reach ' +
|
||||
'is included using the resolved symbol graph; statement-level PDG reach remains in affectedStatements.',
|
||||
reachableBlocks: ['b1', 'b2', 'b3', 'b4'],
|
||||
blockCount: 4,
|
||||
depthReached: 2,
|
||||
|
|
@ -106,12 +105,12 @@ describe('formatImpactResult — PDG (mode:pdg) rendering', () => {
|
|||
expect(out).not.toContain('[0 upstream');
|
||||
});
|
||||
|
||||
it('renders findings under PDG-dependent framing, not "depth N"', () => {
|
||||
it('renders unified PDG symbol reach without callgraph severity labels', () => {
|
||||
const out = formatImpactResult(pdgFindings());
|
||||
|
||||
// PDG framing — NOT the callgraph "depth N / WILL BREAK (direct)" labels.
|
||||
expect(out).toContain('PDG-dependent symbols');
|
||||
expect(out).not.toMatch(/d=\d/);
|
||||
expect(out).toContain('Inter-procedural symbol reach');
|
||||
expect(out).toContain('d=1 (2)');
|
||||
expect(out).not.toContain('WILL BREAK (direct)');
|
||||
expect(out).not.toContain('LIKELY AFFECTED');
|
||||
expect(out).not.toContain('MAY NEED TESTING');
|
||||
|
|
@ -124,9 +123,8 @@ describe('formatImpactResult — PDG (mode:pdg) rendering', () => {
|
|||
expect(out).toContain('finalizeTotal');
|
||||
expect(out).toContain('src/svc.ts');
|
||||
|
||||
// The intra-procedural caveat is present (cross-function not modeled).
|
||||
expect(out.toLowerCase()).toContain('cross-function');
|
||||
expect(out.toLowerCase()).toContain('not modeled');
|
||||
// The unified contract is present.
|
||||
expect(out).toContain('statement-level PDG reach remains in affectedStatements');
|
||||
|
||||
// The callgraph DI / dynamic-dispatch lower-bound copy must NEVER appear.
|
||||
expect(out).not.toContain('dynamic dispatch');
|
||||
|
|
@ -243,8 +241,8 @@ describe('formatImpactResult — PDG (mode:pdg) rendering', () => {
|
|||
note:
|
||||
"'Card' has no PDG body — no BasicBlocks / control- or data-dependence edges exist for " +
|
||||
'this symbol (e.g. an interface, type alias, abstract/ambient member, or a one-line ' +
|
||||
'declaration with no CFG). This is NOT a confident "no impact": the intra-procedural PDG ' +
|
||||
"mode cannot model this symbol kind. Use mode:'callgraph' for its inter-procedural blast radius.",
|
||||
'declaration with no CFG). This is NOT a confident "no impact": the local PDG ' +
|
||||
'statement slice cannot model this symbol kind. Inter-procedural symbol reach may still be attached.',
|
||||
impactedCount: 0,
|
||||
risk: 'UNKNOWN',
|
||||
byDepth: {},
|
||||
|
|
@ -284,8 +282,8 @@ describe('formatImpactResult — PDG (mode:pdg) rendering', () => {
|
|||
"'noop' has a PDG body but a WHOLE-SYMBOL downstream slice is empty: " +
|
||||
'intra-procedural dependence stays inside the function, so every reachable block ' +
|
||||
'is already part of the seed. Pass line:<N> to slice from a specific statement ' +
|
||||
"(what depends on the code at that line), or use mode:'callgraph' for the " +
|
||||
'inter-procedural blast radius.',
|
||||
'(what depends on the code at that line). Inter-procedural symbol reach is attached ' +
|
||||
'separately by the unified impact dispatcher.',
|
||||
reachableBlocks: [],
|
||||
blockCount: 0,
|
||||
affectedStatements: [],
|
||||
|
|
@ -299,7 +297,7 @@ describe('formatImpactResult — PDG (mode:pdg) rendering', () => {
|
|||
affected_processes: [],
|
||||
affected_modules: [],
|
||||
});
|
||||
expect(out).toContain('no intra-procedural PDG-dependent symbols');
|
||||
expect(out).toContain('no inter-procedural symbols reached');
|
||||
// The new note steers to the statement-anchored mode.
|
||||
expect(out).toContain('WHOLE-SYMBOL');
|
||||
expect(out).toMatch(/line:<N>/);
|
||||
|
|
@ -307,7 +305,7 @@ describe('formatImpactResult — PDG (mode:pdg) rendering', () => {
|
|||
// confident callgraph "appears isolated." headline must be absent.
|
||||
expect(out).not.toContain('appears isolated');
|
||||
expect(out).not.toContain('No downstream dependencies found');
|
||||
expect(out.toLowerCase()).toContain('cross-function');
|
||||
expect(out).toContain('Inter-procedural symbol reach is attached');
|
||||
});
|
||||
|
||||
// ── Statement-anchored (mode:'pdg' + line) rendering ──────────────────────
|
||||
|
|
@ -329,23 +327,23 @@ describe('formatImpactResult — PDG (mode:pdg) rendering', () => {
|
|||
{ line: 12, filePath: 'src/svc.ts', text: 'return sum;' },
|
||||
],
|
||||
affectedStatementCount: 2,
|
||||
impactedCount: 1,
|
||||
impactedCount: 0,
|
||||
risk: 'UNKNOWN',
|
||||
epistemic: 'pdg-intra-procedural',
|
||||
note:
|
||||
"mode:'pdg' — intra-procedural slice from line 8 of 'accum'. 2 statements are " +
|
||||
'downstream-dependent on it (over CDG + REACHING_DEF). Cross-function (inter-procedural) ' +
|
||||
"impact is NOT modeled in this mode — use mode:'callgraph' for the call-graph blast radius.",
|
||||
'downstream-dependent on it (over CDG + REACHING_DEF). Inter-procedural symbol reach ' +
|
||||
'is attached separately by the unified impact dispatcher.',
|
||||
reachableBlocks: ['b1', 'b2'],
|
||||
blockCount: 2,
|
||||
depthReached: 2,
|
||||
unresolvedBlockCount: 0,
|
||||
ambiguousProjectionCount: 0,
|
||||
summary: { direct: 1, processes_affected: 0, modules_affected: 0 },
|
||||
byDepthCounts: { 1: 1 },
|
||||
summary: { direct: 0, processes_affected: 0, modules_affected: 0 },
|
||||
byDepthCounts: {},
|
||||
affected_processes: [],
|
||||
affected_modules: [],
|
||||
byDepth: { 1: [] },
|
||||
byDepth: {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
|
@ -357,11 +355,37 @@ describe('formatImpactResult — PDG (mode:pdg) rendering', () => {
|
|||
// Each dependent statement renders as ` L<line>: <text>`.
|
||||
expect(out).toContain(' L10: sum = sum + x;');
|
||||
expect(out).toContain(' L12: return sum;');
|
||||
// It is the statement list — NOT the symbol-projection "PDG-dependent symbols"
|
||||
// heading (that is the whole-symbol render path).
|
||||
expect(out).not.toContain('PDG-dependent symbols');
|
||||
// The intra-procedural caveat note still surfaces.
|
||||
expect(out.toLowerCase()).toContain('cross-function');
|
||||
// It is the statement list; no inter-symbol section appears without byDepth reach.
|
||||
expect(out).not.toContain('Inter-procedural symbol reach (');
|
||||
// The unified PDG note still surfaces.
|
||||
expect(out).toContain('Inter-procedural symbol reach is attached');
|
||||
});
|
||||
|
||||
it('renders statement slices with inter-procedural symbol reach when present', () => {
|
||||
const out = formatImpactResult(
|
||||
pdgStatementSlice({
|
||||
impactedCount: 1,
|
||||
summary: { direct: 1, processes_affected: 0, modules_affected: 0 },
|
||||
byDepthCounts: { 1: 1 },
|
||||
byDepth: {
|
||||
1: [
|
||||
{
|
||||
depth: 1,
|
||||
id: 'Function:src/caller.ts:caller',
|
||||
name: 'caller',
|
||||
type: 'Function',
|
||||
filePath: 'src/caller.ts',
|
||||
processes: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
pdgInterprocedural: { engine: 'symbol-graph', impactedCount: 1, byDepthCounts: { 1: 1 } },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(out).toContain('Statements downstream-dependent on src/svc.ts:8 (2):');
|
||||
expect(out).toContain('Inter-procedural symbol reach (1):');
|
||||
expect(out).toContain('Function caller → src/caller.ts');
|
||||
});
|
||||
|
||||
it('flags slice truncation honestly', () => {
|
||||
|
|
|
|||
|
|
@ -266,6 +266,34 @@ describe('impact-pdg metric math — unified axes', () => {
|
|||
expect([...pdg.interSymbol]).toEqual([]);
|
||||
});
|
||||
|
||||
it('adapts unified PDG onto both statement and inter-symbol axes', () => {
|
||||
const pdg = M.pdgUnifiedCis(
|
||||
M.pdgLineCis([
|
||||
{ line: 16, filePath: 'src/mixed.ts' },
|
||||
{ line: 18, filePath: 'src/mixed.ts' },
|
||||
]),
|
||||
M.toKeySet([
|
||||
M.symbolKey('route', 'src/mixed.ts'),
|
||||
M.symbolKey('fast', 'src/mixed.ts'),
|
||||
M.symbolKey('slow', 'src/mixed.ts'),
|
||||
]),
|
||||
gt,
|
||||
);
|
||||
|
||||
expect([...pdg.intraLine].sort()).toEqual([
|
||||
'statement:src/mixed.ts:16',
|
||||
'statement:src/mixed.ts:18',
|
||||
]);
|
||||
expect([...pdg.interSymbol].sort()).toEqual([
|
||||
'symbol:fast@src/mixed.ts',
|
||||
'symbol:slow@src/mixed.ts',
|
||||
]);
|
||||
|
||||
const scored = M.scoreUnifiedAxes(pdg, M.unifiedAis(gt));
|
||||
expect(scored.intraLine.f1).toBe(1);
|
||||
expect(scored.interSymbol.f1).toBe(1);
|
||||
});
|
||||
|
||||
it('scores composed-current as exact on both axes without a blended F1', () => {
|
||||
const ais = M.unifiedAis(gt);
|
||||
const cg = M.callgraphUnifiedCis(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue