@holmes-lab/holmes-kit 0.18.0 → 0.19.2
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 +149 -0
- package/README.md +3 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.d.ts +22 -0
- package/dist/holmes/cli/agents.js +76 -1
- package/dist/holmes/cli/approve.js +6 -1
- package/dist/holmes/cli/doctor.d.ts +36 -1
- package/dist/holmes/cli/doctor.js +182 -35
- package/dist/holmes/cli/index.js +7 -1
- package/dist/holmes/cli/init.js +12 -0
- package/dist/holmes/cli/native-deps.d.ts +65 -0
- package/dist/holmes/cli/native-deps.js +131 -0
- 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/cycle-observation.d.ts +65 -0
- package/dist/holmes/cpg/cycle-observation.js +146 -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/approval-queue.d.ts +23 -4
- package/dist/holmes/governance/approval-queue.js +44 -6
- 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 +29 -0
- package/dist/holmes/hooks/stop.js +119 -3
- 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 +7 -0
- package/dist/holmes/mcp/handlers.js +132 -6
- 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 +45 -0
- package/dist/holmes/mcp/maintenance-analyze.js +117 -9
- package/dist/holmes/mcp/maintenance-evidence.d.ts +41 -0
- package/dist/holmes/mcp/maintenance-evidence.js +71 -4
- package/dist/holmes/project/install-scripts-policy.d.ts +76 -0
- package/dist/holmes/project/install-scripts-policy.js +131 -0
- package/dist/holmes/project/npx-bin.d.ts +6 -0
- package/dist/holmes/project/npx-bin.js +10 -0
- package/dist/holmes/review/evaluation-metrics.d.ts +6 -0
- package/dist/holmes/review/evaluation-metrics.js +18 -1
- package/dist/holmes/review/failed-test-names.d.ts +19 -0
- package/dist/holmes/review/failed-test-names.js +43 -0
- 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.d.ts +23 -0
- package/dist/holmes/review/run-replay.js +90 -4
- package/dist/holmes/review/symbol-truth.d.ts +14 -0
- package/dist/holmes/review/symbol-truth.js +23 -0
- package/dist/holmes/review/test-runner.d.ts +27 -0
- package/dist/holmes/review/test-runner.js +59 -3
- 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/docs/install-guide.md +54 -5
- package/package.json +4 -1
- package/playbooks/author-slice/PLAYBOOK.md +14 -0
- package/playbooks/publish/PLAYBOOK.md +32 -0
- package/playbooks/tdd-slice/PLAYBOOK.md +14 -0
|
@@ -42,6 +42,7 @@ exports.escalateReappraisals = escalateReappraisals;
|
|
|
42
42
|
exports.unrecordedApprovals = unrecordedApprovals;
|
|
43
43
|
exports.rolledBackLedgers = rolledBackLedgers;
|
|
44
44
|
exports.governanceLostPreflight = governanceLostPreflight;
|
|
45
|
+
exports.trackedLines = trackedLines;
|
|
45
46
|
exports.evaluateStop = evaluateStop;
|
|
46
47
|
exports.stopDebtAction = stopDebtAction;
|
|
47
48
|
exports.acknowledgeStop = acknowledgeStop;
|
|
@@ -62,6 +63,8 @@ const node_child_process_1 = require("node:child_process");
|
|
|
62
63
|
const path = __importStar(require("node:path"));
|
|
63
64
|
const test_scope_1 = require("../rtm/test-scope");
|
|
64
65
|
const constitution_1 = require("../governance/constitution");
|
|
66
|
+
// @implements A-SPEC-574.4 — same detector as the design-time advisory and the strict article.
|
|
67
|
+
const cycle_detect_1 = require("../cpg/cycle-detect");
|
|
65
68
|
const provenance_chain_1 = require("../governance/provenance-chain");
|
|
66
69
|
const test_evidence_1 = require("../review/test-evidence");
|
|
67
70
|
const test_outcomes_1 = require("../review/test-outcomes");
|
|
@@ -430,6 +433,36 @@ function governanceLostPreflight(specsDir, projectRoot) {
|
|
|
430
433
|
const root = projectRoot ?? path.resolve(specsDir, '..', '..');
|
|
431
434
|
return (0, governance_history_1.hasGovernanceHistory)(root) ? governance_history_1.GOVERNANCE_LOST_HINT : null;
|
|
432
435
|
}
|
|
436
|
+
// @implements A-SPEC-578.1
|
|
437
|
+
/** What each article's track observations are called on the operator's screen. */
|
|
438
|
+
const TRACK_LABELS = {
|
|
439
|
+
'ART-8': 'RED-first',
|
|
440
|
+
'ART-2': 'code-graph cycles',
|
|
441
|
+
};
|
|
442
|
+
/**
|
|
443
|
+
* One line per ARTICLE, each under its own name.
|
|
444
|
+
*
|
|
445
|
+
* This used to be a single line reading `ART-8 RED-first (track)` for everything in `tracked` —
|
|
446
|
+
* and the cycle ratchet pushes ART-2 findings into that same array, so an import cycle was
|
|
447
|
+
* reported to the operator as a RED-first violation. Two observers sharing one sentence means the
|
|
448
|
+
* sentence is wrong for at least one of them.
|
|
449
|
+
*
|
|
450
|
+
* An article with no label still speaks, under its bare name: a new observer that says nothing is
|
|
451
|
+
* worse than one that says something plain.
|
|
452
|
+
*/
|
|
453
|
+
function trackedLines(tracked) {
|
|
454
|
+
if (!tracked || tracked.length === 0)
|
|
455
|
+
return [];
|
|
456
|
+
const byArticle = new Map();
|
|
457
|
+
for (const t of tracked) {
|
|
458
|
+
const key = String(t?.article ?? '');
|
|
459
|
+
byArticle.set(key, [...(byArticle.get(key) ?? []), String(t?.detail ?? '')]);
|
|
460
|
+
}
|
|
461
|
+
return [...byArticle.entries()].map(([article, details]) => {
|
|
462
|
+
const label = TRACK_LABELS[article];
|
|
463
|
+
return `[Holmes-Kit] ${article}${label ? ` ${label}` : ''} (track): ${details.join(' | ')}`;
|
|
464
|
+
});
|
|
465
|
+
}
|
|
433
466
|
function evaluateStop(specs, evidence) {
|
|
434
467
|
// L1: the Stop gate IS the constitution's re-verification point — every turn boundary re-runs the
|
|
435
468
|
// inviolable articles (ART-2 RTM, ART-3 validity incl. 4-quadrant GWT, ART-4 coverage evidence).
|
|
@@ -439,6 +472,7 @@ function evaluateStop(specs, evidence) {
|
|
|
439
472
|
const violations = (0, constitution_1.verifyConstitution)({
|
|
440
473
|
specs, testCasesByAspec: evidence?.testCasesByAspec, executedByAspec: evidence?.executedByAspec, findings: evidence?.findings,
|
|
441
474
|
redFirstMode: evidence?.redFirstMode, changedAspecs: evidence?.changedAspecs, outcomesByAspec: evidence?.outcomesByAspec,
|
|
475
|
+
cycles: evidence?.cycles,
|
|
442
476
|
});
|
|
443
477
|
// @implements A-SPEC-534.4 — `track` records ART-8 findings without blocking the turn. Computed
|
|
444
478
|
// separately (the constitution stays silent on ART-8 outside strict) and returned in `tracked` for
|
|
@@ -449,6 +483,17 @@ function evaluateStop(specs, evidence) {
|
|
|
449
483
|
if (t.length)
|
|
450
484
|
tracked = t;
|
|
451
485
|
}
|
|
486
|
+
// @implements A-SPEC-574.4 — the cycle ratchet's track posture: recorded, never blocking. Promotion
|
|
487
|
+
// to `strict` waits on the observation ledger answering the false-positive rate, which is the same
|
|
488
|
+
// path impactAdvisory and anchorDensity took.
|
|
489
|
+
if (evidence?.cycles !== undefined && evidence.cycles.mode === 'track') {
|
|
490
|
+
const scope = evidence.cycles.scope.unavailable.length > 0
|
|
491
|
+
? ` (NOT judged: ${evidence.cycles.scope.unavailable.join(', ')})` : '';
|
|
492
|
+
const t = (0, cycle_detect_1.cycleRatchetViolations)(evidence.cycles.current, evidence.cycles.allowed)
|
|
493
|
+
.map((x) => ({ article: 'ART-2', detail: `code ${x.detail}${scope}` }));
|
|
494
|
+
if (t.length)
|
|
495
|
+
tracked = [...(tracked ?? []), ...t];
|
|
496
|
+
}
|
|
452
497
|
const problems = violations.map((x) => `[${x.article}] ${x.detail}`);
|
|
453
498
|
// @implements A-SPEC-247 — structured list so the caller can ask acknowledgeStop which of these
|
|
454
499
|
// are waiting on an owner. Mirrors `problems` exactly, including the two synthesized below.
|
|
@@ -876,6 +921,77 @@ if (require.main === module) {
|
|
|
876
921
|
changedAspecs = undefined;
|
|
877
922
|
outcomesByAspec = undefined;
|
|
878
923
|
}
|
|
924
|
+
// @implements A-SPEC-574.4 — the code graph's acyclicity, judged where the code exists. The
|
|
925
|
+
// scan is the same one every other layer uses; the classification reads only the files an
|
|
926
|
+
// import edge starts from. Fail-open in every direction: any error leaves the article silent,
|
|
927
|
+
// because "we could not look" must never be reported as "there is nothing there".
|
|
928
|
+
let cycles;
|
|
929
|
+
try {
|
|
930
|
+
const root = stopProjectRoot();
|
|
931
|
+
const { CpgScanner } = require('../cpg/cpg-scanner');
|
|
932
|
+
const scanned = new CpgScanner().scan(root, root);
|
|
933
|
+
const known = new Set(scanned.map((f) => f.sourcePath));
|
|
934
|
+
const edges = [];
|
|
935
|
+
const judged = new Set();
|
|
936
|
+
const sawImports = new Set();
|
|
937
|
+
for (const f of scanned) {
|
|
938
|
+
const ext = f.sourcePath.slice(f.sourcePath.lastIndexOf('.'));
|
|
939
|
+
let text = '';
|
|
940
|
+
try {
|
|
941
|
+
text = fs.readFileSync(path.join(root, f.sourcePath), 'utf8');
|
|
942
|
+
}
|
|
943
|
+
catch {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
let sawImport = false;
|
|
947
|
+
for (const e of f.edges ?? []) {
|
|
948
|
+
if (e.rel !== 'imports' || !e.to)
|
|
949
|
+
continue;
|
|
950
|
+
sawImport = true;
|
|
951
|
+
if (!e.to.startsWith('.'))
|
|
952
|
+
continue;
|
|
953
|
+
const base = path.posix.normalize(path.posix.join(path.posix.dirname(f.sourcePath), e.to));
|
|
954
|
+
const to = ['.ts', '.tsx', '.js', '/index.ts'].map((x) => base + x).find((c) => known.has(c))
|
|
955
|
+
?? (known.has(base) ? base : null);
|
|
956
|
+
if (to === null || to === f.sourcePath)
|
|
957
|
+
continue;
|
|
958
|
+
edges.push({ from: f.sourcePath, to, kind: (0, cycle_detect_1.classifyEdgeByTarget)(text, f.sourcePath, to) });
|
|
959
|
+
judged.add(ext);
|
|
960
|
+
}
|
|
961
|
+
// A file whose imports are all bare specifiers is not evidence that its LANGUAGE is
|
|
962
|
+
// unresolvable — subtract the judged set at the end instead of marking per file, or one
|
|
963
|
+
// such file makes the whole language read as unjudged (measured: `.ts` and `.js` landed in
|
|
964
|
+
// both lists).
|
|
965
|
+
if (sawImport)
|
|
966
|
+
sawImports.add(ext);
|
|
967
|
+
}
|
|
968
|
+
cycles = {
|
|
969
|
+
current: (0, cycle_detect_1.detectCycles)(edges).cycles,
|
|
970
|
+
allowed: [],
|
|
971
|
+
mode: 'track',
|
|
972
|
+
// Unjudged = saw imports, resolved none, anywhere. Naming these is the difference between
|
|
973
|
+
// "no cycles" and "no cycles in the languages we could follow".
|
|
974
|
+
scope: {
|
|
975
|
+
judged: [...judged].sort(),
|
|
976
|
+
unavailable: [...sawImports].filter((e) => !judged.has(e)).sort(),
|
|
977
|
+
},
|
|
978
|
+
};
|
|
979
|
+
// @implements A-SPEC-578.1 — record the observation the promotion criterion waits on.
|
|
980
|
+
// EVERY run, including a clean one: a false-positive rate is violations over chances, and a
|
|
981
|
+
// ledger that only speaks when something is wrong keeps the numerator and drops the
|
|
982
|
+
// denominator. Append failure is swallowed by the outer catch below — an observation that
|
|
983
|
+
// could change a verdict would be a gate wearing another name.
|
|
984
|
+
// Its OWN try: sharing the outer one would let a fault in the observation discard the
|
|
985
|
+
// article's evidence, which is the coupling this comment exists to deny.
|
|
986
|
+
try {
|
|
987
|
+
const { appendCycleObservation, buildCycleObservation } = require('../cpg/cycle-observation');
|
|
988
|
+
appendCycleObservation(root, buildCycleObservation(cycles, new Date().toISOString()));
|
|
989
|
+
}
|
|
990
|
+
catch { /* an observation never touches the verdict */ }
|
|
991
|
+
}
|
|
992
|
+
catch {
|
|
993
|
+
cycles = undefined;
|
|
994
|
+
}
|
|
879
995
|
// Provenance-chain verification (fail-open: a verify error skips the check, never crashes).
|
|
880
996
|
let provenance;
|
|
881
997
|
// @implements A-SPEC-148
|
|
@@ -973,9 +1089,9 @@ if (require.main === module) {
|
|
|
973
1089
|
}
|
|
974
1090
|
}
|
|
975
1091
|
catch { /* the reappraisal signal is advisory; a failure never affects the stop verdict */ }
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1092
|
+
// @implements A-SPEC-578.1 — one line per article, each under its own name.
|
|
1093
|
+
for (const line of trackedLines(out.tracked))
|
|
1094
|
+
process.stderr.write(`${line}\n`);
|
|
979
1095
|
// @implements A-SPEC-247 — before deciding to re-block, ask whether every unresolved debt is
|
|
980
1096
|
// already queued for the owner. If so, tell the user ONCE and let the turn finish; a single
|
|
981
1097
|
// non-waiting violation and we block exactly as before.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DEFUSE_TOP_FILES = 10;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFUSE_TOP_FILES = void 0;
|
|
4
|
+
// @implements A-SPEC-573.4
|
|
5
|
+
// One constant, imported by both consumers, so the product path and the benchmark cannot drift on
|
|
6
|
+
// how much data flow they read. Ten is the emission tier a caller acts on; whole-repository
|
|
7
|
+
// extraction was measured at +88.8% (2572ms -> 4857ms, 2026-09-08) and ten files at roughly 95ms.
|
|
8
|
+
exports.DEFUSE_TOP_FILES = 10;
|
|
@@ -223,6 +223,11 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
223
223
|
graphPreview?: {
|
|
224
224
|
impact?: import("../rtm/impact-advisory").ImpactAdvisory;
|
|
225
225
|
density?: import("../rtm/anchor-density").AnchorDensityFinding[];
|
|
226
|
+
cycles?: {
|
|
227
|
+
findings: import("../cpg/cycle-detect").CycleFinding[];
|
|
228
|
+
note: string;
|
|
229
|
+
};
|
|
230
|
+
architecture?: import("../cpg/arch-observe").ArchObservation[];
|
|
226
231
|
graphAsOf?: string;
|
|
227
232
|
} | undefined;
|
|
228
233
|
id: string;
|
|
@@ -485,6 +490,7 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
485
490
|
survivors: import("../spec/kills").Mutation[];
|
|
486
491
|
};
|
|
487
492
|
} | {
|
|
493
|
+
calibrationClosed?: string[] | undefined;
|
|
488
494
|
baselineRecorded?: string | undefined;
|
|
489
495
|
scopeFallback?: "full" | undefined;
|
|
490
496
|
tier: import("../rtm/test-scope").RegressionTier;
|
|
@@ -513,6 +519,7 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
513
519
|
changedFiles?: string[];
|
|
514
520
|
}): Promise<MaintenanceAnalysis & {
|
|
515
521
|
persistedTo?: string;
|
|
522
|
+
bounded?: boolean;
|
|
516
523
|
}>;
|
|
517
524
|
maintenance_outcome(a: {
|
|
518
525
|
root: string;
|
|
@@ -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
|
}
|
|
@@ -2304,6 +2367,9 @@ function makeRawHandlers(store, opts) {
|
|
|
2304
2367
|
// Record ONLY an actually-executed, GREEN run (review C4/C7): a red suite must not stand as
|
|
2305
2368
|
// coverage evidence at the moment the code is broken, and a no-op run must not overwrite a real
|
|
2306
2369
|
// record with a dishonest `passed: true` over an empty result.
|
|
2370
|
+
// @implements A-SPEC-578.4 — read BEFORE the write below replaces it: the calibration window
|
|
2371
|
+
// is "since the previous evidence run", and this line is the only moment that value exists.
|
|
2372
|
+
const previousEvidenceTs = (0, test_evidence_1.readTestEvidence)(root)?.ts;
|
|
2307
2373
|
const verified = (0, baseline_1.shouldRecordBaseline)(result);
|
|
2308
2374
|
if (verified) {
|
|
2309
2375
|
(0, test_evidence_1.writeTestEvidence)(root, { ts: new Date().toISOString(), head, tier: testScope.tier, passed: true, executedByAspec });
|
|
@@ -2319,6 +2385,19 @@ function makeRawHandlers(store, opts) {
|
|
|
2319
2385
|
// actually executed and passed. A red or skipped run must never become the reference point for
|
|
2320
2386
|
// "since the last verified state" — that would silently narrow every later scope against a
|
|
2321
2387
|
// state nobody verified.
|
|
2388
|
+
// @implements A-SPEC-578.4 — close the calibration loop on real work.
|
|
2389
|
+
// The window is the PREVIOUS evidence run's timestamp, read before this run overwrote it —
|
|
2390
|
+
// an analysis older than that belonged to a previous slice, and attributing today's changes
|
|
2391
|
+
// to it would be contamination rather than measurement. Its own try: an observation that
|
|
2392
|
+
// could change `passed` would be a gate wearing another name.
|
|
2393
|
+
let calibrationClosed;
|
|
2394
|
+
try {
|
|
2395
|
+
const closed = (0, maintenance_evidence_1.closeOpenArtifacts)(path.join(root, maintenance_evidence_1.EVIDENCE_DIR), { files: changedFiles }, new Date().toISOString(), previousEvidenceTs).closed;
|
|
2396
|
+
if (closed.length > 0)
|
|
2397
|
+
calibrationClosed = closed;
|
|
2398
|
+
}
|
|
2399
|
+
catch { /* the loop is observation; it never touches the verdict */ }
|
|
2400
|
+
// @implements A-SPEC-128
|
|
2322
2401
|
let baseline;
|
|
2323
2402
|
if (verified) {
|
|
2324
2403
|
baseline = a.mark ?? DEFAULT_BASELINE;
|
|
@@ -2330,7 +2409,8 @@ function makeRawHandlers(store, opts) {
|
|
|
2330
2409
|
ranFiles: result.ranFiles, executedByAspec, tail: result.tail,
|
|
2331
2410
|
// @implements A-SPEC-130 — the remediation rides in the answer: these are the files to anchor.
|
|
2332
2411
|
unresolvedFiles: testScope.unresolvedFiles,
|
|
2333
|
-
changeSource, ...(scopeFallback ? { scopeFallback } : {}), ...(baseline ? { baselineRecorded: baseline } : {})
|
|
2412
|
+
changeSource, ...(scopeFallback ? { scopeFallback } : {}), ...(baseline ? { baselineRecorded: baseline } : {}),
|
|
2413
|
+
...(calibrationClosed ? { calibrationClosed } : {}) };
|
|
2334
2414
|
},
|
|
2335
2415
|
async issue_localize(a) {
|
|
2336
2416
|
assertSpecStoreReachable('issue_localize', store, a.root); // @implements A-SPEC-419
|
|
@@ -2515,7 +2595,7 @@ function makeRawHandlers(store, opts) {
|
|
|
2515
2595
|
commitTextBoost[hit.file] = hit.score / top;
|
|
2516
2596
|
}
|
|
2517
2597
|
catch { /* no history, no boost — the ranking falls back to lexical evidence alone */ }
|
|
2518
|
-
const
|
|
2598
|
+
const analyzeWith = (defUse) => (0, maintenance_analyze_1.analyzeMaintenance)({
|
|
2519
2599
|
semantic,
|
|
2520
2600
|
...common,
|
|
2521
2601
|
coverage: { ...common.coverage, historyStatus },
|
|
@@ -2524,7 +2604,43 @@ function makeRawHandlers(store, opts) {
|
|
|
2524
2604
|
commitTextBoost,
|
|
2525
2605
|
contextBundle,
|
|
2526
2606
|
groundTruth: a.groundTruth,
|
|
2607
|
+
defUse,
|
|
2527
2608
|
});
|
|
2609
|
+
// @implements A-SPEC-573.4 — def-use for the TOP CANDIDATES ONLY. Extracting it for the whole
|
|
2610
|
+
// repository costs +88.8% (measured 2026-09-08), well past this slice's budget; the first pass
|
|
2611
|
+
// says which handful of files are worth parsing, and the second pass reads their data flow.
|
|
2612
|
+
// Every step is fail-open: a parse failure, an unsupported language or a missing file leaves
|
|
2613
|
+
// the candidate's symbols exactly as the first pass produced them.
|
|
2614
|
+
const firstPass = analyzeWith();
|
|
2615
|
+
const analysis = (() => {
|
|
2616
|
+
const targets = firstPass.candidates.slice(0, defuse_bound_1.DEFUSE_TOP_FILES).map((c) => c.file);
|
|
2617
|
+
if (targets.length === 0)
|
|
2618
|
+
return firstPass;
|
|
2619
|
+
const defUse = {};
|
|
2620
|
+
try {
|
|
2621
|
+
const { TreeSitterTsParser, hasDataFlowWalk } = require('../cpg/language-parser');
|
|
2622
|
+
const { langForPath } = require('../cpg/cpg-scanner');
|
|
2623
|
+
const parser = new TreeSitterTsParser();
|
|
2624
|
+
for (const file of targets) {
|
|
2625
|
+
try {
|
|
2626
|
+
// The LANGUAGE matters: the first wiring omitted it and parsed Python as TypeScript,
|
|
2627
|
+
// which produced wrong facts and cost the second corpus 0.2376 -> 0.1741 on the
|
|
2628
|
+
// symbol axis. A language with no walk is skipped rather than guessed at.
|
|
2629
|
+
const lang = langForPath(file);
|
|
2630
|
+
if (!hasDataFlowWalk(lang))
|
|
2631
|
+
continue;
|
|
2632
|
+
const facts = parser.extractDataFlow(fs.readFileSync(path.join(root, file), 'utf8'), lang);
|
|
2633
|
+
if (facts !== undefined)
|
|
2634
|
+
defUse[file] = facts;
|
|
2635
|
+
}
|
|
2636
|
+
catch { /* one unreadable or unparseable file must not cost the other nine */ }
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
catch {
|
|
2640
|
+
return firstPass;
|
|
2641
|
+
}
|
|
2642
|
+
return Object.keys(defUse).length === 0 ? firstPass : analyzeWith(defUse);
|
|
2643
|
+
})();
|
|
2528
2644
|
// @implements A-SPEC-268 — persistence is OPT-IN. The tool is advertised read-only, and a
|
|
2529
2645
|
// regression pins that a cold project gains no `.ax/cpg_cache`; writing evidence by default
|
|
2530
2646
|
// would break that contract for every caller who only wanted to look.
|
|
@@ -2544,9 +2660,19 @@ function makeRawHandlers(store, opts) {
|
|
|
2544
2660
|
if (digest)
|
|
2545
2661
|
fileDigests[file] = digest;
|
|
2546
2662
|
}
|
|
2663
|
+
// The artifact takes the WHOLE analysis, never the bounded one below: a file has no
|
|
2664
|
+
// context window, and the calibration that scores this prediction must score what the
|
|
2665
|
+
// product actually predicted.
|
|
2547
2666
|
result.persistedTo = (0, maintenance_evidence_1.writeArtifact)(path.join(root, maintenance_evidence_1.EVIDENCE_DIR), (0, maintenance_evidence_1.artifactFrom)(analysis, new Date().toISOString(), fileDigests));
|
|
2548
2667
|
}
|
|
2549
|
-
|
|
2668
|
+
// @implements A-SPEC-578.5 — the response, and only the response, is bounded. Measured
|
|
2669
|
+
// 2026-09-09: the full shape is 187,174 characters (~47k tokens) and the harness refuses
|
|
2670
|
+
// it, which is why AGENTS.md step 3 had never once been obeyed with `persist: true`.
|
|
2671
|
+
// `persistedTo` rides along so the caller knows where the whole thing is.
|
|
2672
|
+
const bounded = (0, maintenance_analyze_1.boundAnalysis)(result);
|
|
2673
|
+
if (JSON.stringify(bounded).length !== JSON.stringify(result).length)
|
|
2674
|
+
bounded.bounded = true;
|
|
2675
|
+
return bounded;
|
|
2550
2676
|
}
|
|
2551
2677
|
finally {
|
|
2552
2678
|
graph.close();
|
|
@@ -3092,7 +3218,7 @@ function makeRawHandlers(store, opts) {
|
|
|
3092
3218
|
summary: `consumed single-use approval for: review-resolve ${envLiftedCriticals.join(', ')}`.slice(0, 200),
|
|
3093
3219
|
inputs: [(0, provenance_chain_1.nonceFingerprint)(String(approval.nonce))], rationale: approval.rationale,
|
|
3094
3220
|
authorization: (0, provenance_chain_1.authorizationRef)(approval.actor, approval.token),
|
|
3095
|
-
});
|
|
3221
|
+
}, { isNonceConsumed: (0, ledger_store_2.nonceConsumedIn)(ledgerFile) });
|
|
3096
3222
|
if (!won) {
|
|
3097
3223
|
throw new HandlerRefusal(`review_record: 단일 사용 승인(nonce)이 이미 소비되었습니다 — 재사용은 거부됩니다. 새 승인을 발급받으십시오`);
|
|
3098
3224
|
}
|
|
@@ -3222,7 +3348,7 @@ function makeRawHandlers(store, opts) {
|
|
|
3222
3348
|
summary: `consumed single-use approval for: ${coverTarget.kind} ${(0, provenance_chain_1.redactTarget)('command', coverTarget.target)}`.slice(0, 200),
|
|
3223
3349
|
inputs: [(0, provenance_chain_1.nonceFingerprint)(String(a.approval.nonce))], rationale: a.approval.rationale,
|
|
3224
3350
|
authorization: (0, provenance_chain_1.authorizationRef)(a.approval.actor, a.approval.token),
|
|
3225
|
-
});
|
|
3351
|
+
}, { isNonceConsumed: (0, ledger_store_2.nonceConsumedIn)(ledgerFile) });
|
|
3226
3352
|
}
|
|
3227
3353
|
catch {
|
|
3228
3354
|
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
|
|
@@ -436,3 +444,40 @@ export declare function unquoteGitPath(line: string): string;
|
|
|
436
444
|
* reproduce and compare an analysis from the exact same captured inputs.
|
|
437
445
|
*/
|
|
438
446
|
export declare function analyzeMaintenance(input: MaintenanceAnalysisInput): MaintenanceAnalysis;
|
|
447
|
+
/**
|
|
448
|
+
* Items a response lists before it starts counting instead.
|
|
449
|
+
*
|
|
450
|
+
* TEN, because that is the unit this product is measured in: localization emits Top-10 and every
|
|
451
|
+
* recall figure in this repository is recall@10. Aligning the response cap with the emission unit
|
|
452
|
+
* is a reason; picking a number that happens to hit a size target is a knob. Measured on the real
|
|
453
|
+
* 187,174-character response: cap 20 gave 34,811 with `intent` alone at 11,037 (32%), cap 10 fits.
|
|
454
|
+
*/
|
|
455
|
+
export declare const ANALYZE_LIST_CAP = 10;
|
|
456
|
+
/** Characters one listed item keeps. */
|
|
457
|
+
export declare const ANALYZE_TEXT_CAP = 400;
|
|
458
|
+
/**
|
|
459
|
+
* @implements A-SPEC-578.5
|
|
460
|
+
* The RESPONSE-EDGE projection. The artifact on disk keeps everything.
|
|
461
|
+
*
|
|
462
|
+
* Measured 2026-09-09: one `maintenance_analyze` call returns 187,174 characters (~47k tokens) and
|
|
463
|
+
* the harness refuses it outright. AGENTS.md step 3 instructs every agent to make that call before
|
|
464
|
+
* editing source — and across 2,779 spec approvals `persist:true` had never been used once. An
|
|
465
|
+
* instruction nobody can afford to follow is not an instruction.
|
|
466
|
+
*
|
|
467
|
+
* The cap is applied RECURSIVELY, which the first cut got wrong. Capping only the top level took
|
|
468
|
+
* the response from 187,174 to 142,701 and no further, because the weight is in NESTED DUPLICATES:
|
|
469
|
+
* `impacts.test` and `testScope.impactedTestFiles` carry the same 158 paths, and `impacts.contract`
|
|
470
|
+
* and `testScope.impactedSpecs` the same 160 spec ids. A cap that stops at depth one caps the
|
|
471
|
+
* cheapest arrays in the document.
|
|
472
|
+
*
|
|
473
|
+
* NEVER CALLED FROM `analyzeMaintenance`. The design-time advisory named the replay benchmark
|
|
474
|
+
* (`run-replay.ts`, `replay-corpus.ts`, `commit-text.ts`, `temporal-prior.ts`) as readers of this
|
|
475
|
+
* type, and among their anchors sits A-SPEC-402 — "the numbers reported are not what the product
|
|
476
|
+
* does". Bounding inside the analysis would make every pinned benchmark score a truncated
|
|
477
|
+
* pipeline, which is the most expensive instrument failure this repository has recorded. The
|
|
478
|
+
* benchmark keeps seeing exactly what it sees today; only the conversation gets the short form.
|
|
479
|
+
*
|
|
480
|
+
* What is dropped is COUNTED, at whatever depth it was dropped. A response that quietly shrank
|
|
481
|
+
* would be a worse lie than a long one.
|
|
482
|
+
*/
|
|
483
|
+
export declare function boundAnalysis(a: MaintenanceAnalysis): MaintenanceAnalysis;
|