@holmes-lab/holmes-kit 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/README.md +3 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/release-docs.d.ts +27 -0
- package/dist/holmes/cli/release-docs.js +68 -0
- package/dist/holmes/cpg/arch-observe.d.ts +15 -0
- package/dist/holmes/cpg/arch-observe.js +19 -0
- package/dist/holmes/cpg/cpg-scanner.d.ts +10 -36
- package/dist/holmes/cpg/cpg-scanner.js +27 -3
- package/dist/holmes/cpg/cycle-detect.d.ts +87 -0
- package/dist/holmes/cpg/cycle-detect.js +251 -0
- package/dist/holmes/cpg/scan-cache.d.ts +1 -1
- package/dist/holmes/cpg/scanned-file.d.ts +36 -0
- package/dist/holmes/cpg/scanned-file.js +2 -0
- package/dist/holmes/governance/constitution.d.ts +20 -0
- package/dist/holmes/governance/constitution.js +17 -0
- package/dist/holmes/governance/ledger-store.d.ts +9 -0
- package/dist/holmes/governance/ledger-store.js +47 -0
- package/dist/holmes/governance/provenance-chain.d.ts +16 -1
- package/dist/holmes/governance/provenance-chain.js +5 -3
- package/dist/holmes/hooks/pre-tool-use.js +3 -1
- package/dist/holmes/hooks/stop.d.ts +14 -0
- package/dist/holmes/hooks/stop.js +73 -0
- package/dist/holmes/mcp/defuse-bound.d.ts +1 -0
- package/dist/holmes/mcp/defuse-bound.js +8 -0
- package/dist/holmes/mcp/handlers.d.ts +5 -0
- package/dist/holmes/mcp/handlers.js +103 -4
- package/dist/holmes/mcp/history-admission.d.ts +15 -0
- package/dist/holmes/mcp/history-admission.js +37 -0
- package/dist/holmes/mcp/maintenance-analyze.d.ts +8 -0
- package/dist/holmes/mcp/maintenance-analyze.js +44 -8
- package/dist/holmes/review/evaluation-metrics.d.ts +6 -0
- package/dist/holmes/review/evaluation-metrics.js +18 -1
- package/dist/holmes/review/paired-power.d.ts +14 -0
- package/dist/holmes/review/paired-power.js +57 -0
- package/dist/holmes/review/replay-corpus.d.ts +11 -0
- package/dist/holmes/review/replay-corpus.js +34 -0
- package/dist/holmes/review/run-replay.js +60 -4
- package/dist/holmes/review/symbol-truth.d.ts +14 -0
- package/dist/holmes/review/symbol-truth.js +23 -0
- package/dist/holmes/rtm/defuse-symbols.d.ts +17 -0
- package/dist/holmes/rtm/defuse-symbols.js +91 -0
- package/dist/holmes/rtm/incremental.js +5 -0
- package/dist/holmes/rtm/rtm-builder.d.ts +8 -0
- package/dist/holmes/rtm/rtm-builder.js +32 -5
- package/dist/holmes/rtm/rtm-graph.d.ts +11 -0
- package/dist/holmes/rtm/rtm-graph.js +13 -0
- package/dist/holmes/spec/legacy-fields.d.ts +2 -0
- package/dist/holmes/spec/legacy-fields.js +9 -0
- package/dist/holmes/spec/legacy-format.d.ts +1 -1
- package/dist/holmes/spec/legacy-format.js +4 -1
- package/dist/holmes/spec/spec-parser.js +5 -3
- package/package.json +1 -1
- package/playbooks/author-slice/PLAYBOOK.md +14 -0
- package/playbooks/publish/PLAYBOOK.md +32 -0
- package/playbooks/tdd-slice/PLAYBOOK.md +14 -0
|
@@ -45,6 +45,8 @@ exports.collectDecisions = collectDecisions;
|
|
|
45
45
|
// @implements A-SPEC-277
|
|
46
46
|
// @implements A-SPEC-269
|
|
47
47
|
// @implements A-SPEC-267
|
|
48
|
+
// @implements A-SPEC-573.4 — shared with the benchmark so the two cannot drift.
|
|
49
|
+
const defuse_bound_1 = require("./defuse-bound");
|
|
48
50
|
const fs = __importStar(require("node:fs"));
|
|
49
51
|
const http = __importStar(require("node:http"));
|
|
50
52
|
const assoc_arm_1 = require("../assoc/assoc-arm");
|
|
@@ -205,6 +207,8 @@ const ledger_timeline_1 = require("../governance/ledger-timeline");
|
|
|
205
207
|
const version_conflict_1 = require("../spec/version-conflict");
|
|
206
208
|
const ledger_store_1 = require("../governance/ledger-store");
|
|
207
209
|
const provenance_chain_1 = require("../governance/provenance-chain");
|
|
210
|
+
// @implements A-SPEC-574.3 — the caller owns the store and hands the chain the question.
|
|
211
|
+
const ledger_store_2 = require("../governance/ledger-store");
|
|
208
212
|
const ledger_lock_1 = require("../governance/ledger-lock");
|
|
209
213
|
const decision_ledger_1 = require("../guardrail/decision-ledger");
|
|
210
214
|
const cspec_change_1 = require("../guardrail/cspec-change");
|
|
@@ -1375,7 +1379,63 @@ function makeRawHandlers(store, opts) {
|
|
|
1375
1379
|
return null;
|
|
1376
1380
|
} });
|
|
1377
1381
|
const density = anchorDensityFindings(ftt, graph.implementsAnchorCounts());
|
|
1378
|
-
|
|
1382
|
+
// @implements A-SPEC-574.2 — the computation lives in cycle-detect; this file holds
|
|
1383
|
+
// the wiring only (measured: 85 anchors here against a p90 of 7).
|
|
1384
|
+
const { cycleAdvisory, classifyEdgeByTarget, CYCLE_ADVISORY_NOTE } = require('../cpg/cycle-detect');
|
|
1385
|
+
const readCache = new Map();
|
|
1386
|
+
const readSource = (rel) => {
|
|
1387
|
+
const hit = readCache.get(rel);
|
|
1388
|
+
if (hit !== undefined)
|
|
1389
|
+
return hit;
|
|
1390
|
+
// A read failure degrades to the conservative kind rather than losing the finding.
|
|
1391
|
+
let text = '';
|
|
1392
|
+
try {
|
|
1393
|
+
text = fs.readFileSync(path.join(a.root, rel), 'utf8');
|
|
1394
|
+
}
|
|
1395
|
+
catch {
|
|
1396
|
+
text = '';
|
|
1397
|
+
}
|
|
1398
|
+
readCache.set(rel, text);
|
|
1399
|
+
return text;
|
|
1400
|
+
};
|
|
1401
|
+
const cycleFindings = cycleAdvisory(ftt, graph.importEdges().map((edge) => ({
|
|
1402
|
+
...edge,
|
|
1403
|
+
kind: classifyEdgeByTarget(readSource(edge.from), edge.from, edge.to),
|
|
1404
|
+
})));
|
|
1405
|
+
// @implements A-SPEC-574.5 — the same import edges the cycle pass already read, plus
|
|
1406
|
+
// the parent-time symbol spans the graph already holds. No new scan, no new parse.
|
|
1407
|
+
const { architectureObservation } = require('../cpg/arch-observe');
|
|
1408
|
+
const { TreeSitterTsParser } = require('../cpg/language-parser');
|
|
1409
|
+
const { langForPath } = require('../cpg/cpg-scanner');
|
|
1410
|
+
// Bounded to the DECLARED files, whose text is read once and used for both numbers.
|
|
1411
|
+
const archParser = new TreeSitterTsParser();
|
|
1412
|
+
const archText = new Map();
|
|
1413
|
+
const readArch = (f) => {
|
|
1414
|
+
if (!archText.has(f)) {
|
|
1415
|
+
try {
|
|
1416
|
+
archText.set(f, fs.readFileSync(path.join(a.root, f), 'utf8'));
|
|
1417
|
+
}
|
|
1418
|
+
catch {
|
|
1419
|
+
archText.set(f, null);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return archText.get(f) ?? null;
|
|
1423
|
+
};
|
|
1424
|
+
const spans = new Map(ftt.map((f) => {
|
|
1425
|
+
const text = readArch(f);
|
|
1426
|
+
if (text === null)
|
|
1427
|
+
return [f, []];
|
|
1428
|
+
try {
|
|
1429
|
+
return [f, archParser.extractSymbols(text, langForPath(f))
|
|
1430
|
+
.filter((sy) => sy.kind !== 'class')
|
|
1431
|
+
.map((sy) => ({ startLine: sy.startLine, endLine: sy.endLine }))];
|
|
1432
|
+
}
|
|
1433
|
+
catch {
|
|
1434
|
+
return [f, []];
|
|
1435
|
+
}
|
|
1436
|
+
}));
|
|
1437
|
+
const arch = architectureObservation(ftt, spans, graph.importEdges(), (f) => { const t = readArch(f); return t === null ? null : t.split('\n').length; });
|
|
1438
|
+
if (impact || density.length > 0 || cycleFindings.length > 0 || arch.length > 0) {
|
|
1379
1439
|
const graphAsOf = (() => { try {
|
|
1380
1440
|
return fs.statSync(dbPath).mtime.toISOString();
|
|
1381
1441
|
}
|
|
@@ -1385,6 +1445,9 @@ function makeRawHandlers(store, opts) {
|
|
|
1385
1445
|
graphPreview = {
|
|
1386
1446
|
...(impact ? { impact } : {}),
|
|
1387
1447
|
...(density.length > 0 ? { density } : {}),
|
|
1448
|
+
...(cycleFindings.length > 0
|
|
1449
|
+
? { cycles: { findings: cycleFindings, note: CYCLE_ADVISORY_NOTE } } : {}),
|
|
1450
|
+
...(arch.length > 0 ? { architecture: arch } : {}),
|
|
1388
1451
|
...(graphAsOf ? { graphAsOf } : {}),
|
|
1389
1452
|
};
|
|
1390
1453
|
}
|
|
@@ -2515,7 +2578,7 @@ function makeRawHandlers(store, opts) {
|
|
|
2515
2578
|
commitTextBoost[hit.file] = hit.score / top;
|
|
2516
2579
|
}
|
|
2517
2580
|
catch { /* no history, no boost — the ranking falls back to lexical evidence alone */ }
|
|
2518
|
-
const
|
|
2581
|
+
const analyzeWith = (defUse) => (0, maintenance_analyze_1.analyzeMaintenance)({
|
|
2519
2582
|
semantic,
|
|
2520
2583
|
...common,
|
|
2521
2584
|
coverage: { ...common.coverage, historyStatus },
|
|
@@ -2524,7 +2587,43 @@ function makeRawHandlers(store, opts) {
|
|
|
2524
2587
|
commitTextBoost,
|
|
2525
2588
|
contextBundle,
|
|
2526
2589
|
groundTruth: a.groundTruth,
|
|
2590
|
+
defUse,
|
|
2527
2591
|
});
|
|
2592
|
+
// @implements A-SPEC-573.4 — def-use for the TOP CANDIDATES ONLY. Extracting it for the whole
|
|
2593
|
+
// repository costs +88.8% (measured 2026-09-08), well past this slice's budget; the first pass
|
|
2594
|
+
// says which handful of files are worth parsing, and the second pass reads their data flow.
|
|
2595
|
+
// Every step is fail-open: a parse failure, an unsupported language or a missing file leaves
|
|
2596
|
+
// the candidate's symbols exactly as the first pass produced them.
|
|
2597
|
+
const firstPass = analyzeWith();
|
|
2598
|
+
const analysis = (() => {
|
|
2599
|
+
const targets = firstPass.candidates.slice(0, defuse_bound_1.DEFUSE_TOP_FILES).map((c) => c.file);
|
|
2600
|
+
if (targets.length === 0)
|
|
2601
|
+
return firstPass;
|
|
2602
|
+
const defUse = {};
|
|
2603
|
+
try {
|
|
2604
|
+
const { TreeSitterTsParser, hasDataFlowWalk } = require('../cpg/language-parser');
|
|
2605
|
+
const { langForPath } = require('../cpg/cpg-scanner');
|
|
2606
|
+
const parser = new TreeSitterTsParser();
|
|
2607
|
+
for (const file of targets) {
|
|
2608
|
+
try {
|
|
2609
|
+
// The LANGUAGE matters: the first wiring omitted it and parsed Python as TypeScript,
|
|
2610
|
+
// which produced wrong facts and cost the second corpus 0.2376 -> 0.1741 on the
|
|
2611
|
+
// symbol axis. A language with no walk is skipped rather than guessed at.
|
|
2612
|
+
const lang = langForPath(file);
|
|
2613
|
+
if (!hasDataFlowWalk(lang))
|
|
2614
|
+
continue;
|
|
2615
|
+
const facts = parser.extractDataFlow(fs.readFileSync(path.join(root, file), 'utf8'), lang);
|
|
2616
|
+
if (facts !== undefined)
|
|
2617
|
+
defUse[file] = facts;
|
|
2618
|
+
}
|
|
2619
|
+
catch { /* one unreadable or unparseable file must not cost the other nine */ }
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
catch {
|
|
2623
|
+
return firstPass;
|
|
2624
|
+
}
|
|
2625
|
+
return Object.keys(defUse).length === 0 ? firstPass : analyzeWith(defUse);
|
|
2626
|
+
})();
|
|
2528
2627
|
// @implements A-SPEC-268 — persistence is OPT-IN. The tool is advertised read-only, and a
|
|
2529
2628
|
// regression pins that a cold project gains no `.ax/cpg_cache`; writing evidence by default
|
|
2530
2629
|
// would break that contract for every caller who only wanted to look.
|
|
@@ -3092,7 +3191,7 @@ function makeRawHandlers(store, opts) {
|
|
|
3092
3191
|
summary: `consumed single-use approval for: review-resolve ${envLiftedCriticals.join(', ')}`.slice(0, 200),
|
|
3093
3192
|
inputs: [(0, provenance_chain_1.nonceFingerprint)(String(approval.nonce))], rationale: approval.rationale,
|
|
3094
3193
|
authorization: (0, provenance_chain_1.authorizationRef)(approval.actor, approval.token),
|
|
3095
|
-
});
|
|
3194
|
+
}, { isNonceConsumed: (0, ledger_store_2.nonceConsumedIn)(ledgerFile) });
|
|
3096
3195
|
if (!won) {
|
|
3097
3196
|
throw new HandlerRefusal(`review_record: 단일 사용 승인(nonce)이 이미 소비되었습니다 — 재사용은 거부됩니다. 새 승인을 발급받으십시오`);
|
|
3098
3197
|
}
|
|
@@ -3222,7 +3321,7 @@ function makeRawHandlers(store, opts) {
|
|
|
3222
3321
|
summary: `consumed single-use approval for: ${coverTarget.kind} ${(0, provenance_chain_1.redactTarget)('command', coverTarget.target)}`.slice(0, 200),
|
|
3223
3322
|
inputs: [(0, provenance_chain_1.nonceFingerprint)(String(a.approval.nonce))], rationale: a.approval.rationale,
|
|
3224
3323
|
authorization: (0, provenance_chain_1.authorizationRef)(a.approval.actor, a.approval.token),
|
|
3225
|
-
});
|
|
3324
|
+
}, { isNonceConsumed: (0, ledger_store_2.nonceConsumedIn)(ledgerFile) });
|
|
3226
3325
|
}
|
|
3227
3326
|
catch {
|
|
3228
3327
|
won = false;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow history-derived candidates to files that could be source at all.
|
|
3
|
+
*
|
|
4
|
+
* The predicate is the SCANNER's, not the replay corpus's truth predicate — scoring against a
|
|
5
|
+
* filter copied from the metric would be gaming it. Deliberately NOT membership in the scanned
|
|
6
|
+
* set: a file a commit CREATES does not exist in the parent-time scan, and admitting exactly such
|
|
7
|
+
* files is A-SPEC-388's boundary contract. Every offender measured above is excluded by the
|
|
8
|
+
* extension test alone (.jsonl / .json / .md), so the stronger predicate would have cost that
|
|
9
|
+
* contract and bought nothing.
|
|
10
|
+
*
|
|
11
|
+
* Order is preserved because the downstream RRF lists are position-indexed.
|
|
12
|
+
*/
|
|
13
|
+
export declare function admitHistoryFiles(historyFiles: readonly string[]): string[];
|
|
14
|
+
/** The lexical path's predicate (localize.ts), restated so both channels demote the same trees. */
|
|
15
|
+
export declare const isVendorPath: (p: string) => boolean;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isVendorPath = void 0;
|
|
4
|
+
exports.admitHistoryFiles = admitHistoryFiles;
|
|
5
|
+
// @implements A-SPEC-573.1
|
|
6
|
+
// The commit-prose channel (A-SPEC-388) lets files the lexical layer never scored ENTER the
|
|
7
|
+
// candidate pool — that is where its measured gain comes from (Top-10 recall 0.393 -> 0.601). But
|
|
8
|
+
// its keys come from git history, so it also admits files that CANNOT be the answer: this
|
|
9
|
+
// repository's ledger JSONL, the last-green baseline, CHANGELOG.md. Measured 2026-09-08 over 12
|
|
10
|
+
// real commit-subject requests: 76 of 120 emitted candidate slots (63.3%) went to such files, and
|
|
11
|
+
// one took the emission head. The channel is not the problem; its POPULATION is.
|
|
12
|
+
const cpg_scanner_1 = require("../cpg/cpg-scanner");
|
|
13
|
+
/**
|
|
14
|
+
* Narrow history-derived candidates to files that could be source at all.
|
|
15
|
+
*
|
|
16
|
+
* The predicate is the SCANNER's, not the replay corpus's truth predicate — scoring against a
|
|
17
|
+
* filter copied from the metric would be gaming it. Deliberately NOT membership in the scanned
|
|
18
|
+
* set: a file a commit CREATES does not exist in the parent-time scan, and admitting exactly such
|
|
19
|
+
* files is A-SPEC-388's boundary contract. Every offender measured above is excluded by the
|
|
20
|
+
* extension test alone (.jsonl / .json / .md), so the stronger predicate would have cost that
|
|
21
|
+
* contract and bought nothing.
|
|
22
|
+
*
|
|
23
|
+
* Order is preserved because the downstream RRF lists are position-indexed.
|
|
24
|
+
*/
|
|
25
|
+
function admitHistoryFiles(historyFiles) {
|
|
26
|
+
const admitted = historyFiles.filter((file) => !file.endsWith('.d.ts') && cpg_scanner_1.SCANNABLE_EXTENSIONS.some((ext) => file.endsWith(ext)));
|
|
27
|
+
// @implements A-SPEC-573.2 — the lexical path halves a vendored file's score AND orders vendored
|
|
28
|
+
// last (localize.ts). Entering at score 0, this channel bypassed both: measured 2026-09-08, the
|
|
29
|
+
// only two non-source candidates the extension test let through were vendored files and BOTH held
|
|
30
|
+
// the emission head. Demote, never drop — vendored code is the answer in some projects, which is
|
|
31
|
+
// why the lexical path orders rather than filters. Stable within each group: the downstream RRF
|
|
32
|
+
// lists are position-indexed.
|
|
33
|
+
return [...admitted.filter((f) => !(0, exports.isVendorPath)(f)), ...admitted.filter(exports.isVendorPath)];
|
|
34
|
+
}
|
|
35
|
+
/** The lexical path's predicate (localize.ts), restated so both channels demote the same trees. */
|
|
36
|
+
const isVendorPath = (p) => /(^|\/)(reference|vendor|vendors|third_party|third-party|external)\//i.test(p);
|
|
37
|
+
exports.isVendorPath = isVendorPath;
|
|
@@ -117,6 +117,14 @@ export interface MaintenanceAnalysisInput {
|
|
|
117
117
|
* that is the whole result.
|
|
118
118
|
*/
|
|
119
119
|
commitTextBoost?: Record<string, number>;
|
|
120
|
+
/**
|
|
121
|
+
* @implements A-SPEC-573.4
|
|
122
|
+
* Def-use facts for a BOUNDED set of files — the caller extracts them, because this core does no
|
|
123
|
+
* I/O. Extracting them for the whole repository costs +88.8% (measured 2026-09-08, 2572ms ->
|
|
124
|
+
* 4857ms), which is why the callers pass the top candidates only. Absent means "not extracted",
|
|
125
|
+
* and the symbols then read exactly as they did before this existed.
|
|
126
|
+
*/
|
|
127
|
+
defUse?: Record<string, import('../cpg/language-parser').DataFlowFacts>;
|
|
120
128
|
contextBundle?: ContextBundle | null;
|
|
121
129
|
/**
|
|
122
130
|
* @implements A-SPEC-290
|
|
@@ -34,6 +34,8 @@ const decision_context_1 = require("../rtm/decision-context");
|
|
|
34
34
|
const taint_1 = require("../rtm/taint");
|
|
35
35
|
const test_evidence_1 = require("../review/test-evidence");
|
|
36
36
|
const language_capability_1 = require("../cpg/language-capability");
|
|
37
|
+
const history_admission_1 = require("./history-admission");
|
|
38
|
+
const defuse_symbols_1 = require("../rtm/defuse-symbols");
|
|
37
39
|
// @implements A-SPEC-409 — the SHAPE gained a field (`rerankPool`), so a consumer can detect it.
|
|
38
40
|
// @implements A-SPEC-405 A-SPEC-407 — the EXTRACTOR changed too: the same request now returns a
|
|
39
41
|
// differently ordered candidate list (keep-head rank fusion, then the graph-hop list), so two
|
|
@@ -181,10 +183,12 @@ function analyzeMaintenance(input) {
|
|
|
181
183
|
// Files the commit prose names but the lexical layer never scored have to be able to ENTER, or the
|
|
182
184
|
// fusion can only reorder what lexical already found — and reaching what it missed is where the
|
|
183
185
|
// measured gain came from (Top-10 recall 0.393 -> 0.601 on the second corpus).
|
|
186
|
+
// @implements A-SPEC-573.1 — but only files that could be source at all. The keys come from git
|
|
187
|
+
// history, so without this the pool also admits ledger JSONL and build artifacts that cannot be
|
|
188
|
+
// the answer (measured: 63.3% of emitted slots). The lexical hits are untouched, and a file the
|
|
189
|
+
// scan has never seen still enters — that is the boundary contract directly above.
|
|
184
190
|
const seeded = hasBoost
|
|
185
|
-
? [...localization.hits, ...Object.keys(boost)
|
|
186
|
-
.filter((file) => !localization.hits.some((h) => h.file === file))
|
|
187
|
-
.map((file) => ({ file, score: 0, matchedSymbols: [], viaSpecs: [], why: ['reached through commit history'] }))]
|
|
191
|
+
? [...localization.hits, ...(0, history_admission_1.admitHistoryFiles)(Object.keys(boost).filter((file) => !localization.hits.some((h) => h.file === file))).map((file) => ({ file, score: 0, matchedSymbols: [], viaSpecs: [], why: ['reached through commit history'] }))]
|
|
188
192
|
: localization.hits;
|
|
189
193
|
const reranked = !hasPrior && !hasBoost
|
|
190
194
|
? localization.hits
|
|
@@ -202,7 +206,13 @@ function analyzeMaintenance(input) {
|
|
|
202
206
|
// than prose spread evenly. Kept, with the cost named rather than defended.
|
|
203
207
|
+ (hasBoost ? COMMIT_TEXT_WEIGHT * (boost[hit.file] ?? 0) * Math.max(1, hit.score) : 0),
|
|
204
208
|
}))
|
|
205
|
-
.
|
|
209
|
+
// @implements A-SPEC-573.2 — vendored last, exactly as the lexical path orders it
|
|
210
|
+
// (localize.ts). A history-seeded file scores 0 lexically, so the boost term alone can carry
|
|
211
|
+
// it to 2.0 and hand it the head — measured, a vendored file took the head in 2 of 12 real
|
|
212
|
+
// requests. This is the ordering rule that path already applies; the channel was bypassing
|
|
213
|
+
// it. Demotion, not exclusion: vendored code is the answer in some projects.
|
|
214
|
+
.sort((a, b) => Number((0, history_admission_1.isVendorPath)(a.file)) - Number((0, history_admission_1.isVendorPath)(b.file))
|
|
215
|
+
|| b.score - a.score || a.file.localeCompare(b.file));
|
|
206
216
|
// @implements A-SPEC-405
|
|
207
217
|
// S-405 decomposed the recall loss measured in S-404: on the eight blind cases 18 of 18 truth
|
|
208
218
|
// files were already in this pool and 7 sat at ranks 12-93, so widening retrieval could not have
|
|
@@ -294,7 +304,10 @@ function analyzeMaintenance(input) {
|
|
|
294
304
|
;
|
|
295
305
|
const lists = [
|
|
296
306
|
desc((f) => byFile.get(f).score),
|
|
297
|
-
|
|
307
|
+
// @implements A-SPEC-573.2 — vendored last inside this list. Ordering `seeded` was not enough:
|
|
308
|
+
// measured, the head is decided HERE, and a vendored file with a high boost took it twice in
|
|
309
|
+
// twelve requests. The boost VALUES are untouched, so the lexical scoring above is unchanged.
|
|
310
|
+
hasBoost ? desc((f) => boost[f] ?? 0).sort((a, b) => Number((0, history_admission_1.isVendorPath)(a)) - Number((0, history_admission_1.isVendorPath)(b))) : [],
|
|
298
311
|
hasPrior ? desc((f) => (prior[f] ?? 1) - 1) : [],
|
|
299
312
|
cited.length > 0 ? [] : hops,
|
|
300
313
|
];
|
|
@@ -345,10 +358,23 @@ function analyzeMaintenance(input) {
|
|
|
345
358
|
// @implements A-SPEC-486 — the S-502/503 calibration point for gemini cosines. Display only:
|
|
346
359
|
// nothing in this module compares against it to drop or reorder anything.
|
|
347
360
|
const SEM_VERIFY_TAU = 0.65;
|
|
361
|
+
// @implements A-SPEC-573.4 — def-use rides BEHIND the lexical match, never in front of it: the
|
|
362
|
+
// symbols the request actually named keep the front, and the data-flow neighbours are appended.
|
|
363
|
+
const defUseTerms = input.defUse === undefined ? [] : (0, localize_1.significantTerms)(input.request);
|
|
364
|
+
const symbolsByFile = new Map(scanned.map((f) => [f.sourcePath, f.symbols.map((sy) => sy.qualifiedName)]));
|
|
365
|
+
const symbolsOf = (file, matched) => {
|
|
366
|
+
const facts = input.defUse?.[file];
|
|
367
|
+
if (facts === undefined)
|
|
368
|
+
return matched;
|
|
369
|
+
const inFile = symbolsByFile.get(file) ?? [];
|
|
370
|
+
if (inFile.length === 0)
|
|
371
|
+
return matched;
|
|
372
|
+
return (0, defuse_symbols_1.enrichCandidateSymbols)(matched, (0, defuse_symbols_1.rankSymbolsByDefUse)(facts, defUseTerms, inFile));
|
|
373
|
+
};
|
|
348
374
|
const shape = (hit) => ({
|
|
349
375
|
file: hit.file,
|
|
350
376
|
score: hit.score,
|
|
351
|
-
symbols: sortedUnique(hit.matchedSymbols),
|
|
377
|
+
symbols: symbolsOf(hit.file, sortedUnique(hit.matchedSymbols)),
|
|
352
378
|
evidence: hit.why,
|
|
353
379
|
});
|
|
354
380
|
// @implements A-SPEC-428
|
|
@@ -376,10 +402,15 @@ function analyzeMaintenance(input) {
|
|
|
376
402
|
const rescuedFiles = new Set(rescued.map((h) => h.file));
|
|
377
403
|
const rerankPool = [...rescued, ...exposedTail.filter((h) => !rescuedFiles.has(h.file))]
|
|
378
404
|
.slice(0, RERANK_POOL_N).map(shape);
|
|
405
|
+
const lexicalSymbolsOf = new Map(ordered.slice(0, LOCALIZATION_TOP_N).map((hit) => [hit.file, sortedUnique(hit.matchedSymbols)]));
|
|
379
406
|
const candidates = ordered.slice(0, LOCALIZATION_TOP_N).map((hit) => ({
|
|
380
407
|
file: hit.file,
|
|
381
408
|
score: hit.score,
|
|
382
|
-
|
|
409
|
+
// @implements A-SPEC-573.4 — the emitted list, which is what a caller reads and what the
|
|
410
|
+
// benchmark scores on the symbol axis. `shape` above serves the rerank pool only; wiring the
|
|
411
|
+
// enrichment there and not here would have enriched a surface nobody grades (caught by the
|
|
412
|
+
// symbol axis reporting NO movement — the instrument earned its keep on its first use).
|
|
413
|
+
symbols: symbolsOf(hit.file, sortedUnique(hit.matchedSymbols)),
|
|
383
414
|
evidence: [...hit.why],
|
|
384
415
|
}));
|
|
385
416
|
// @implements A-SPEC-572.3
|
|
@@ -460,7 +491,12 @@ function analyzeMaintenance(input) {
|
|
|
460
491
|
.flatMap((f) => f.symbols.map((sym) => sym.qualifiedName));
|
|
461
492
|
// A file you edited is not unaffected by your edit, so these are direct impact and not merely a
|
|
462
493
|
// traversal starting point.
|
|
463
|
-
|
|
494
|
+
// @implements A-SPEC-573.4 — the impact axis seeds from what the LEXICAL layer matched, not from
|
|
495
|
+
// the def-use symbols appended for emission. Measured: seeding the enriched list moved the union
|
|
496
|
+
// axis on both corpora (precision 0.1667->0.2075 / 0.1476->0.1689, recall 0.8755->0.8698 /
|
|
497
|
+
// 0.8302->0.7996) — a different axis than this slice targets, and axis transfer is exactly what
|
|
498
|
+
// this repository's record forbids. The emitted symbols stay enriched; the seeds do not.
|
|
499
|
+
const direct = sortedUnique([...lexicalSymbolsOf.values()].flat().concat(changedSymbols));
|
|
464
500
|
const transitiveIds = new Set();
|
|
465
501
|
const maxCallDepth = 3;
|
|
466
502
|
const hubInDegree = 12;
|
|
@@ -20,6 +20,10 @@ export interface ReplayOutcome {
|
|
|
20
20
|
truthFiles: string[];
|
|
21
21
|
selectedTests: string[];
|
|
22
22
|
truthTests: string[];
|
|
23
|
+
/** Symbols the change actually touched (parent-time ranges ∩ changed lines). */
|
|
24
|
+
truthSymbols?: string[];
|
|
25
|
+
/** Predicted symbols, best first — the qualified names the emitted candidates carried. */
|
|
26
|
+
rankedSymbols?: string[];
|
|
23
27
|
}
|
|
24
28
|
export interface CutMetrics {
|
|
25
29
|
/** Share of cases where at least one truth file appears in the first K. */
|
|
@@ -33,6 +37,8 @@ export interface EvaluationMetrics {
|
|
|
33
37
|
scorableCases: number;
|
|
34
38
|
topK: Record<number, CutMetrics>;
|
|
35
39
|
testRecall: number | null;
|
|
40
|
+
symbolTopK: Record<number, CutMetrics>;
|
|
41
|
+
symbolCases: number;
|
|
36
42
|
}
|
|
37
43
|
export declare function evaluationMetrics(outcomes: readonly ReplayOutcome[]): EvaluationMetrics;
|
|
38
44
|
/**
|
|
@@ -42,7 +42,24 @@ function evaluationMetrics(outcomes) {
|
|
|
42
42
|
}
|
|
43
43
|
const testScorable = outcomes.filter((o) => o.truthTests.length > 0);
|
|
44
44
|
const testRecall = mean(testScorable.map((o) => o.truthTests.filter((t) => o.selectedTests.includes(t)).length / o.truthTests.length));
|
|
45
|
-
|
|
45
|
+
// @implements A-SPEC-573.3
|
|
46
|
+
const symbolScorable = outcomes.filter((o) => (o.truthSymbols?.length ?? 0) > 0);
|
|
47
|
+
const symbolTopK = {};
|
|
48
|
+
for (const k of exports.TOP_K) {
|
|
49
|
+
const hits = [];
|
|
50
|
+
const recalls = [];
|
|
51
|
+
const precisions = [];
|
|
52
|
+
for (const o of symbolScorable) {
|
|
53
|
+
const cut = (o.rankedSymbols ?? []).slice(0, k);
|
|
54
|
+
const found = cut.filter((s2) => o.truthSymbols.includes(s2)).length;
|
|
55
|
+
hits.push(found > 0 ? 1 : 0);
|
|
56
|
+
recalls.push(found / o.truthSymbols.length);
|
|
57
|
+
precisions.push(cut.length === 0 ? 0 : found / cut.length);
|
|
58
|
+
}
|
|
59
|
+
symbolTopK[k] = { hitRate: mean(hits), recall: mean(recalls), precision: mean(precisions) };
|
|
60
|
+
}
|
|
61
|
+
return { cases: outcomes.length, scorableCases: scorable.length, topK, testRecall,
|
|
62
|
+
symbolTopK, symbolCases: symbolScorable.length };
|
|
46
63
|
}
|
|
47
64
|
/**
|
|
48
65
|
* Impact had no benchmark until this existed, so an impact regression could not fail anything.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** F1 over the SET of files, which is the edit set a caller actually acts on. */
|
|
2
|
+
export declare function editSetF1(predicted: readonly string[], truth: readonly string[]): number;
|
|
3
|
+
export interface PairedPower {
|
|
4
|
+
n: number;
|
|
5
|
+
meanDiff: number | null;
|
|
6
|
+
/** Sample standard deviation of the paired differences (n-1). Null below two observations. */
|
|
7
|
+
sdDiff: number | null;
|
|
8
|
+
sem: number | null;
|
|
9
|
+
/** Smallest difference detectable at 80% power, two-sided alpha 0.05. */
|
|
10
|
+
mde80: number | null;
|
|
11
|
+
/** True when the degrees of freedom fell outside the table and the normal approximation was used. */
|
|
12
|
+
approx: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function pairedPower(diffs: readonly number[]): PairedPower;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-573.6
|
|
3
|
+
// Every verdict in REQ-573 — S1 adopted, S2 held at 60, S3 conditional, S4 rejected — was reached
|
|
4
|
+
// without asking whether the observed difference was large enough to be detectable at all. A
|
|
5
|
+
// rejection that a study could never have detected is not evidence of absence, and an adoption of
|
|
6
|
+
// a difference smaller than the noise is not evidence of presence. This computes both, from paired
|
|
7
|
+
// observations, with no dependency and no randomness.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.editSetF1 = editSetF1;
|
|
10
|
+
exports.pairedPower = pairedPower;
|
|
11
|
+
/** F1 over the SET of files, which is the edit set a caller actually acts on. */
|
|
12
|
+
function editSetF1(predicted, truth) {
|
|
13
|
+
const p = new Set(predicted);
|
|
14
|
+
const t = new Set(truth);
|
|
15
|
+
if (p.size === 0 || t.size === 0)
|
|
16
|
+
return 0;
|
|
17
|
+
let hit = 0;
|
|
18
|
+
for (const f of p)
|
|
19
|
+
if (t.has(f))
|
|
20
|
+
hit++;
|
|
21
|
+
if (hit === 0)
|
|
22
|
+
return 0;
|
|
23
|
+
const precision = hit / p.size;
|
|
24
|
+
const recall = hit / t.size;
|
|
25
|
+
return (2 * precision * recall) / (precision + recall);
|
|
26
|
+
}
|
|
27
|
+
// Two-sided 0.975 and one-sided 0.80 critical values, by degrees of freedom. Small and explicit
|
|
28
|
+
// rather than a dependency: the numbers a verdict rests on should be readable in the file that uses
|
|
29
|
+
// them. Anything past the table is close enough to normal that the approximation is honest — and it
|
|
30
|
+
// is reported as such rather than passed off as exact.
|
|
31
|
+
const T975 = {
|
|
32
|
+
1: 12.706, 2: 4.303, 3: 3.182, 4: 2.776, 5: 2.571, 6: 2.447, 7: 2.365, 8: 2.306, 9: 2.262,
|
|
33
|
+
10: 2.228, 11: 2.201, 12: 2.179, 13: 2.160, 14: 2.145, 15: 2.131, 16: 2.120, 17: 2.110,
|
|
34
|
+
18: 2.101, 19: 2.093, 20: 2.086, 25: 2.060, 30: 2.042, 40: 2.021, 60: 2.000, 120: 1.980,
|
|
35
|
+
};
|
|
36
|
+
const T80 = {
|
|
37
|
+
1: 1.376, 2: 1.061, 3: 0.978, 4: 0.941, 5: 0.920, 6: 0.906, 7: 0.896, 8: 0.889, 9: 0.883,
|
|
38
|
+
10: 0.879, 11: 0.876, 12: 0.873, 13: 0.870, 14: 0.868, 15: 0.866, 16: 0.865, 17: 0.863,
|
|
39
|
+
18: 0.862, 19: 0.861, 20: 0.860, 25: 0.856, 30: 0.854, 40: 0.851, 60: 0.848, 120: 0.845,
|
|
40
|
+
};
|
|
41
|
+
const NORMAL_975 = 1.96;
|
|
42
|
+
const NORMAL_80 = 0.8416;
|
|
43
|
+
function pairedPower(diffs) {
|
|
44
|
+
const n = diffs.length;
|
|
45
|
+
if (n === 0)
|
|
46
|
+
return { n: 0, meanDiff: null, sdDiff: null, sem: null, mde80: null, approx: false };
|
|
47
|
+
const meanDiff = diffs.reduce((s, x) => s + x, 0) / n;
|
|
48
|
+
if (n < 2)
|
|
49
|
+
return { n, meanDiff, sdDiff: null, sem: null, mde80: null, approx: false };
|
|
50
|
+
const variance = diffs.reduce((s, x) => s + (x - meanDiff) ** 2, 0) / (n - 1);
|
|
51
|
+
const sdDiff = Math.sqrt(variance);
|
|
52
|
+
const sem = sdDiff / Math.sqrt(n);
|
|
53
|
+
const df = n - 1;
|
|
54
|
+
const exact = T975[df] !== undefined && T80[df] !== undefined;
|
|
55
|
+
const [tAlpha, tBeta] = exact ? [T975[df], T80[df]] : [NORMAL_975, NORMAL_80];
|
|
56
|
+
return { n, meanDiff, sdDiff, sem, mde80: (tAlpha + tBeta) * sem, approx: !exact };
|
|
57
|
+
}
|
|
@@ -44,6 +44,17 @@ export interface ReplayCase {
|
|
|
44
44
|
/** Test files the commit changed. */
|
|
45
45
|
tests: string[];
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The PARENT-side line numbers a commit changed in one file.
|
|
49
|
+
*
|
|
50
|
+
* Parent-side, not head-side, because the symbol ranges these are intersected with come from the
|
|
51
|
+
* parent-time scan — the tree the analysis actually saw. Reading head-side numbers would line up
|
|
52
|
+
* the diff against a file the analyzer never had.
|
|
53
|
+
*
|
|
54
|
+
* Returns an empty list rather than throwing: a file the parent did not have (the commit created
|
|
55
|
+
* it) has no parent lines, and that is an answer, not a failure.
|
|
56
|
+
*/
|
|
57
|
+
export declare function changedParentLines(root: string, commit: string, file: string): number[];
|
|
47
58
|
export declare const HOLMES_CORPUS: ReplayCorpus;
|
|
48
59
|
/**
|
|
49
60
|
* The second corpus. Measured 2026-08-29: 269 source files, 329 commits, 251 eligible cases, 959
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.GEMINI_CLI_MEASUREMENT = exports.GEMINI_CLI_CORPUS = exports.JARVIS_CORPUS = exports.HOLMES_CORPUS = void 0;
|
|
37
|
+
exports.changedParentLines = changedParentLines;
|
|
37
38
|
exports.casesFor = casesFor;
|
|
38
39
|
exports.thinSpecs = thinSpecs;
|
|
39
40
|
exports.casesAvoidingRecent = casesAvoidingRecent;
|
|
@@ -44,6 +45,39 @@ const path = __importStar(require("node:path"));
|
|
|
44
45
|
const node_child_process_1 = require("node:child_process");
|
|
45
46
|
const maintenance_analyze_1 = require("../mcp/maintenance-analyze");
|
|
46
47
|
const TS_FAMILY = (f) => f.endsWith('.ts') && !f.endsWith('.d.ts');
|
|
48
|
+
// @implements A-SPEC-573.3
|
|
49
|
+
/**
|
|
50
|
+
* The PARENT-side line numbers a commit changed in one file.
|
|
51
|
+
*
|
|
52
|
+
* Parent-side, not head-side, because the symbol ranges these are intersected with come from the
|
|
53
|
+
* parent-time scan — the tree the analysis actually saw. Reading head-side numbers would line up
|
|
54
|
+
* the diff against a file the analyzer never had.
|
|
55
|
+
*
|
|
56
|
+
* Returns an empty list rather than throwing: a file the parent did not have (the commit created
|
|
57
|
+
* it) has no parent lines, and that is an answer, not a failure.
|
|
58
|
+
*/
|
|
59
|
+
function changedParentLines(root, commit, file) {
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = (0, node_child_process_1.execFileSync)('git', ['-C', root, 'show', '--unified=0', '--format=', commit, '--', file], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const lines = [];
|
|
68
|
+
for (const line of raw.split('\n')) {
|
|
69
|
+
// `@@ -12,3 +12,4 @@` — the old-side start and count. A count of 0 marks a pure insertion, which
|
|
70
|
+
// touched no existing line and therefore no existing symbol.
|
|
71
|
+
const m = /^@@ -(\d+)(?:,(\d+))? /.exec(line);
|
|
72
|
+
if (m === null)
|
|
73
|
+
continue;
|
|
74
|
+
const start = Number(m[1]);
|
|
75
|
+
const count = m[2] === undefined ? 1 : Number(m[2]);
|
|
76
|
+
for (let i = 0; i < count; i++)
|
|
77
|
+
lines.push(start + i);
|
|
78
|
+
}
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
|
47
81
|
exports.HOLMES_CORPUS = {
|
|
48
82
|
root: path.resolve(__dirname, '../../..'),
|
|
49
83
|
// @implements A-SPEC-443 — pinned so the floors stay floors. Our own repository moves too; the
|
|
@@ -46,6 +46,9 @@ const node_child_process_1 = require("node:child_process");
|
|
|
46
46
|
const os = __importStar(require("node:os"));
|
|
47
47
|
const path = __importStar(require("node:path"));
|
|
48
48
|
const cpg_scanner_1 = require("../cpg/cpg-scanner");
|
|
49
|
+
const language_parser_1 = require("../cpg/language-parser");
|
|
50
|
+
// @implements A-SPEC-573.4 — same bound as the handler, one constant so the two cannot drift.
|
|
51
|
+
const defuse_bound_1 = require("../mcp/defuse-bound");
|
|
49
52
|
const rtm_builder_1 = require("../rtm/rtm-builder");
|
|
50
53
|
const rtm_graph_1 = require("../rtm/rtm-graph");
|
|
51
54
|
const test_scope_1 = require("../rtm/test-scope");
|
|
@@ -55,6 +58,7 @@ const maintenance_analyze_1 = require("../mcp/maintenance-analyze");
|
|
|
55
58
|
const point_in_time_replay_1 = require("./point-in-time-replay");
|
|
56
59
|
const evaluation_metrics_1 = require("./evaluation-metrics");
|
|
57
60
|
const replay_corpus_1 = require("./replay-corpus");
|
|
61
|
+
const symbol_truth_1 = require("./symbol-truth");
|
|
58
62
|
const semantic_arm_1 = require("./semantic-arm");
|
|
59
63
|
// @implements A-SPEC-479 — pre-emission verification of the union answer (measurement only).
|
|
60
64
|
// @implements A-SPEC-488 — lexPoverty drives the gate simulation ("uncited ∨ lexically poor").
|
|
@@ -212,25 +216,77 @@ async function runReplay(corpus, limit, opts = {}) {
|
|
|
212
216
|
// reported from it understated what a caller actually gets — a benchmark measuring something
|
|
213
217
|
// adjacent to the product is worse than one measuring nothing, because it reads as if it
|
|
214
218
|
// measured the product.
|
|
215
|
-
|
|
219
|
+
// @implements A-SPEC-573.3 — and the boost is handed over UNFILTERED. This benchmark used to
|
|
220
|
+
// narrow it to the scanned set on its own, which is a filter the product did not have: the
|
|
221
|
+
// pipeline being scored was not the pipeline that shipped, so a defect worth 63.3% of the
|
|
222
|
+
// product's emitted slots read as six-decimal NO MOVEMENT here (measured, S1). Admission is
|
|
223
|
+
// the product's business now (A-SPEC-573.1/573.2), and the bench inherits whatever it does.
|
|
216
224
|
const profile = (0, commit_text_1.commitTextProfile)(corpus.root, parent, 400);
|
|
217
|
-
const ct = (0, commit_text_1.rankByCommitText)(c.subject, profile, 300)
|
|
225
|
+
const ct = (0, commit_text_1.rankByCommitText)(c.subject, profile, 300);
|
|
218
226
|
const top = ct[0]?.score ?? 0;
|
|
219
227
|
const commitTextBoost = {};
|
|
220
228
|
if (top > 0)
|
|
221
229
|
for (const h of ct)
|
|
222
230
|
commitTextBoost[h.file] = h.score / top;
|
|
223
|
-
const
|
|
231
|
+
const analyzeWith = (defUse) => (0, maintenance_analyze_1.analyzeMaintenance)({
|
|
224
232
|
request: c.subject, scanned, specs, graph, testAnchors: anchors, history: [], changePrior, commitTextBoost,
|
|
225
233
|
groundTruth: { files: c.files, tests: c.tests },
|
|
226
234
|
basis: { head: parent, loadedBuild: 'b', diskBuild: 'b', specFingerprint: 'fp' },
|
|
227
235
|
coverage: { scannedFiles: scanned.length, skippedFiles: [], unsupportedLanguages: [] },
|
|
236
|
+
defUse,
|
|
228
237
|
});
|
|
238
|
+
// @implements A-SPEC-573.4 — the same two-pass the handler runs, for the same reason the
|
|
239
|
+
// bench stopped filtering the boost on its own: what is measured has to be what ships.
|
|
240
|
+
const firstPass = analyzeWith();
|
|
241
|
+
const result = (() => {
|
|
242
|
+
const targets = firstPass.candidates.slice(0, defuse_bound_1.DEFUSE_TOP_FILES).map((x) => x.file);
|
|
243
|
+
if (targets.length === 0)
|
|
244
|
+
return firstPass;
|
|
245
|
+
const defUse = {};
|
|
246
|
+
const parser = new language_parser_1.TreeSitterTsParser();
|
|
247
|
+
for (const file of targets) {
|
|
248
|
+
try {
|
|
249
|
+
const lang = (0, cpg_scanner_1.langForPath)(file);
|
|
250
|
+
if (!(0, language_parser_1.hasDataFlowWalk)(lang))
|
|
251
|
+
continue;
|
|
252
|
+
const facts = parser.extractDataFlow(fs.readFileSync(path.join(dest, file), 'utf8'), lang);
|
|
253
|
+
if (facts !== undefined)
|
|
254
|
+
defUse[file] = facts;
|
|
255
|
+
}
|
|
256
|
+
catch { /* fail-open, per file */ }
|
|
257
|
+
}
|
|
258
|
+
return Object.keys(defUse).length === 0 ? firstPass : analyzeWith(defUse);
|
|
259
|
+
})();
|
|
229
260
|
const ranked = result.candidates.map((x) => x.file);
|
|
230
261
|
// @implements A-SPEC-469 — the union scores what a caller actually RECEIVES as the impact
|
|
231
262
|
// answer, and that surface is now the graded rankedImpact (the closure stays gate-facing).
|
|
232
263
|
const impacted = (result.impacts?.rankedImpact ?? []).map((r) => r.file);
|
|
233
|
-
|
|
264
|
+
// @implements A-SPEC-573.3 — the function axis, derived from what this loop already holds:
|
|
265
|
+
// the commit's parent-side changed lines and the parent-time scan's symbol ranges. A case
|
|
266
|
+
// whose change fell outside every symbol contributes no truth and drops out of that
|
|
267
|
+
// denominator; it is not scored zero.
|
|
268
|
+
const byPathScanned = new Map(scanned.map((f) => [f.sourcePath, f]));
|
|
269
|
+
const truthSymbols = [];
|
|
270
|
+
for (const file of c.files) {
|
|
271
|
+
const sf = byPathScanned.get(file);
|
|
272
|
+
if (sf === undefined)
|
|
273
|
+
continue;
|
|
274
|
+
const ranges = sf.symbols.map((sy) => ({ name: sy.qualifiedName, startLine: sy.startLine, endLine: sy.endLine }));
|
|
275
|
+
for (const name of (0, symbol_truth_1.symbolsTouched)(ranges, (0, replay_corpus_1.changedParentLines)(corpus.root, c.commit, file))) {
|
|
276
|
+
if (!truthSymbols.includes(name))
|
|
277
|
+
truthSymbols.push(name);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// The prediction is the symbols the emitted candidates carried, in candidate order — the
|
|
281
|
+
// same list a caller reads, never a separately computed one.
|
|
282
|
+
const rankedSymbols = [];
|
|
283
|
+
for (const cand of result.candidates) {
|
|
284
|
+
for (const sy of cand.symbols)
|
|
285
|
+
if (!rankedSymbols.includes(sy))
|
|
286
|
+
rankedSymbols.push(sy);
|
|
287
|
+
}
|
|
288
|
+
outcomes.push({ ranked, truthFiles: c.files, selectedTests: [...result.relevantTests], truthTests: c.tests,
|
|
289
|
+
truthSymbols, rankedSymbols });
|
|
234
290
|
// @implements A-SPEC-487 — the dump: 2-pass with semantic injected when asked (vectors
|
|
235
291
|
// only for the files the 1-pass surfaced — the hot-path lookup contract holds), 1-pass
|
|
236
292
|
// otherwise. Nothing here feeds outcomes or any pin.
|