@holmes-lab/holmes-kit 0.2.1 → 0.3.1

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.
Files changed (132) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/.build-id +1 -1
  3. package/dist/holmes/assoc/assoc-arm.d.ts +94 -0
  4. package/dist/holmes/assoc/assoc-arm.js +187 -0
  5. package/dist/holmes/assoc/explore.d.ts +21 -0
  6. package/dist/holmes/assoc/explore.js +160 -0
  7. package/dist/holmes/assoc/impact-baseline.d.ts +13 -0
  8. package/dist/holmes/assoc/impact-baseline.js +48 -0
  9. package/dist/holmes/assoc/ppr.d.ts +64 -0
  10. package/dist/holmes/assoc/ppr.js +110 -0
  11. package/dist/holmes/cli/agents.d.ts +24 -11
  12. package/dist/holmes/cli/agents.js +93 -21
  13. package/dist/holmes/cli/doctor.d.ts +56 -0
  14. package/dist/holmes/cli/doctor.js +311 -5
  15. package/dist/holmes/cli/index.js +13 -1
  16. package/dist/holmes/cli/semantic-key.d.ts +19 -0
  17. package/dist/holmes/cli/semantic-key.js +93 -0
  18. package/dist/holmes/config/config.d.ts +9 -0
  19. package/dist/holmes/config/config.js +8 -1
  20. package/dist/holmes/cpg/language-capability.d.ts +65 -0
  21. package/dist/holmes/cpg/language-capability.js +145 -0
  22. package/dist/holmes/cpg/language-parser-walk.js +179 -34
  23. package/dist/holmes/cpg/language-parser.d.ts +1 -1
  24. package/dist/holmes/governance/ledger-rechain.d.ts +12 -0
  25. package/dist/holmes/governance/ledger-rechain.js +17 -2
  26. package/dist/holmes/governance/provenance-ledger.js +21 -0
  27. package/dist/holmes/guardrail/blind-spots.js +12 -1
  28. package/dist/holmes/guardrail/impact-gate.d.ts +77 -0
  29. package/dist/holmes/guardrail/impact-gate.js +263 -0
  30. package/dist/holmes/guardrail/write-target.d.ts +38 -1
  31. package/dist/holmes/guardrail/write-target.js +41 -4
  32. package/dist/holmes/hooks/adapters/antigravity.js +12 -1
  33. package/dist/holmes/hooks/corrupt-state-run.d.ts +33 -0
  34. package/dist/holmes/hooks/corrupt-state-run.js +16 -0
  35. package/dist/holmes/hooks/pre-tool-use.d.ts +26 -0
  36. package/dist/holmes/hooks/pre-tool-use.js +203 -11
  37. package/dist/holmes/hooks/stop.d.ts +65 -0
  38. package/dist/holmes/hooks/stop.js +200 -2
  39. package/dist/holmes/mcp/elicit-approval.d.ts +32 -2
  40. package/dist/holmes/mcp/elicit-approval.js +30 -2
  41. package/dist/holmes/mcp/handlers.d.ts +88 -0
  42. package/dist/holmes/mcp/handlers.js +636 -35
  43. package/dist/holmes/mcp/maintenance-analyze.d.ts +435 -0
  44. package/dist/holmes/mcp/maintenance-analyze.js +994 -0
  45. package/dist/holmes/mcp/maintenance-evidence.d.ts +140 -0
  46. package/dist/holmes/mcp/maintenance-evidence.js +253 -0
  47. package/dist/holmes/mcp/server.js +13 -10
  48. package/dist/holmes/mcp/tool-schemas.js +71 -0
  49. package/dist/holmes/project/root.js +3 -1
  50. package/dist/holmes/review/baseline-arm.d.ts +37 -0
  51. package/dist/holmes/review/baseline-arm.js +51 -0
  52. package/dist/holmes/review/captured-stdin-guard.d.ts +8 -0
  53. package/dist/holmes/review/captured-stdin-guard.js +48 -0
  54. package/dist/holmes/review/coherence-verify.d.ts +31 -0
  55. package/dist/holmes/review/coherence-verify.js +144 -0
  56. package/dist/holmes/review/commit-text.d.ts +50 -0
  57. package/dist/holmes/review/commit-text.js +76 -0
  58. package/dist/holmes/review/confidence-calibration.d.ts +39 -0
  59. package/dist/holmes/review/confidence-calibration.js +39 -0
  60. package/dist/holmes/review/content-baseline.d.ts +38 -0
  61. package/dist/holmes/review/content-baseline.js +103 -0
  62. package/dist/holmes/review/content-verify.d.ts +20 -0
  63. package/dist/holmes/review/content-verify.js +73 -0
  64. package/dist/holmes/review/dense-retrieval.d.ts +66 -0
  65. package/dist/holmes/review/dense-retrieval.js +97 -0
  66. package/dist/holmes/review/edge-quality.d.ts +44 -0
  67. package/dist/holmes/review/edge-quality.js +117 -0
  68. package/dist/holmes/review/evaluation-metrics.d.ts +138 -0
  69. package/dist/holmes/review/evaluation-metrics.js +175 -0
  70. package/dist/holmes/review/graph-verifier.d.ts +34 -0
  71. package/dist/holmes/review/graph-verifier.js +62 -0
  72. package/dist/holmes/review/hop-ablation.d.ts +100 -0
  73. package/dist/holmes/review/hop-ablation.js +89 -0
  74. package/dist/holmes/review/manual-baseline.d.ts +209 -0
  75. package/dist/holmes/review/manual-baseline.js +2846 -0
  76. package/dist/holmes/review/oracle-gap.d.ts +32 -0
  77. package/dist/holmes/review/oracle-gap.js +102 -0
  78. package/dist/holmes/review/point-in-time-replay.d.ts +41 -0
  79. package/dist/holmes/review/point-in-time-replay.js +161 -0
  80. package/dist/holmes/review/rank-diagnosis.d.ts +43 -0
  81. package/dist/holmes/review/rank-diagnosis.js +163 -0
  82. package/dist/holmes/review/replay-calibration.d.ts +62 -0
  83. package/dist/holmes/review/replay-calibration.js +83 -0
  84. package/dist/holmes/review/replay-corpus.d.ts +135 -0
  85. package/dist/holmes/review/replay-corpus.js +210 -0
  86. package/dist/holmes/review/run-replay.d.ts +260 -0
  87. package/dist/holmes/review/run-replay.js +729 -0
  88. package/dist/holmes/review/semantic-arm.d.ts +271 -0
  89. package/dist/holmes/review/semantic-arm.js +717 -0
  90. package/dist/holmes/review/semantic-retrieval.d.ts +55 -0
  91. package/dist/holmes/review/semantic-retrieval.js +156 -0
  92. package/dist/holmes/review/spec-layer-stats.d.ts +38 -0
  93. package/dist/holmes/review/spec-layer-stats.js +52 -0
  94. package/dist/holmes/review/temporal-prior.d.ts +33 -0
  95. package/dist/holmes/review/temporal-prior.js +53 -0
  96. package/dist/holmes/review/test-runner.d.ts +15 -0
  97. package/dist/holmes/review/test-runner.js +41 -4
  98. package/dist/holmes/review/test-selection-breadth.d.ts +75 -0
  99. package/dist/holmes/review/test-selection-breadth.js +57 -0
  100. package/dist/holmes/review/traceability-benchmark.d.ts +81 -0
  101. package/dist/holmes/review/traceability-benchmark.js +135 -0
  102. package/dist/holmes/review/union-verify.d.ts +12 -0
  103. package/dist/holmes/review/union-verify.js +70 -0
  104. package/dist/holmes/rtm/graph-store.d.ts +51 -0
  105. package/dist/holmes/rtm/graph-store.js +122 -0
  106. package/dist/holmes/rtm/incremental.d.ts +25 -1
  107. package/dist/holmes/rtm/incremental.js +18 -1
  108. package/dist/holmes/rtm/localize.d.ts +28 -0
  109. package/dist/holmes/rtm/localize.js +272 -14
  110. package/dist/holmes/rtm/rtm-builder.d.ts +118 -3
  111. package/dist/holmes/rtm/rtm-builder.js +265 -28
  112. package/dist/holmes/rtm/rtm-graph.d.ts +117 -2
  113. package/dist/holmes/rtm/rtm-graph.js +194 -31
  114. package/dist/holmes/rtm/taint-benchmark.d.ts +97 -0
  115. package/dist/holmes/rtm/taint-benchmark.js +141 -0
  116. package/dist/holmes/rtm/test-scope.js +24 -1
  117. package/dist/holmes/semantic/credentials.d.ts +15 -0
  118. package/dist/holmes/semantic/credentials.js +134 -0
  119. package/dist/holmes/semantic/embedder.d.ts +44 -0
  120. package/dist/holmes/semantic/embedder.js +185 -0
  121. package/dist/holmes/semantic/hit-rerank.d.ts +4 -0
  122. package/dist/holmes/semantic/hit-rerank.js +38 -0
  123. package/dist/holmes/semantic/tier.d.ts +37 -0
  124. package/dist/holmes/semantic/tier.js +54 -0
  125. package/dist/holmes/semantic/vector-cache.d.ts +11 -0
  126. package/dist/holmes/semantic/vector-cache.js +91 -0
  127. package/dist/holmes/spec/acceptance-quality.d.ts +81 -0
  128. package/dist/holmes/spec/acceptance-quality.js +169 -0
  129. package/dist/holmes/spec/validator.js +33 -1
  130. package/dist/holmes/spec/yaml-scalar.d.ts +1 -0
  131. package/dist/holmes/spec/yaml-scalar.js +43 -0
  132. package/package.json +1 -1
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ // @implements A-SPEC-357
3
+ /**
4
+ * Does the confidence number mean anything?
5
+ *
6
+ * Goal Phase D asks for the closed loop, and the mechanism has been verified for a while — what is
7
+ * missing is data (`.ax/evidence` is n=0). Filling that directory with synthetic records would be
8
+ * worse than leaving it empty: it is a user-visible governance ledger the user deliberately chose
9
+ * not to git-ignore, and calibration computed from replay would read as calibration observed in use.
10
+ * So this measures in a temporary ledger and says plainly what it measured.
11
+ *
12
+ * The question it CAN answer honestly is not the one `computeCalibration` scores. That function
13
+ * grades classification correctness, and point-in-time replay carries no ground truth for "new
14
+ * requirement vs change to an existing one" — inventing that label would be scoring our own guess
15
+ * against our own guess. What replay does carry is the files the commit actually touched, so the
16
+ * answerable question is whether a HIGHER confidence goes with a MORE accurate answer.
17
+ *
18
+ * The prior is not neutral. The confidence formula is
19
+ * `0.55 + 0.15·(direct symbols exist) + 0.15·(tests found) + 0.1·(history found)` — it counts
20
+ * whether evidence is PRESENT, never whether it is any good. A number built that way can be
21
+ * expected to correlate weakly with accuracy, and if it does, saying so is the finding: a confidence
22
+ * that does not track correctness sells unearned certainty to whoever reads it.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.confidenceCalibration = confidenceCalibration;
26
+ exports.correlate = correlate;
27
+ exports.agreeingSignals = agreeingSignals;
28
+ function confidenceCalibration(obs) {
29
+ const n = obs.length;
30
+ const confidences = obs.map((o) => o.confidence);
31
+ const distinct = new Set(confidences).size;
32
+ if (n === 0) {
33
+ return { n: 0, correlation: null, distinctConfidences: 0, meanAccuracyHigh: null, meanAccuracyLow: null, meanSquaredGap: null };
34
+ }
35
+ const mean = (xs) => xs.reduce((a, b) => a + b, 0) / xs.length;
36
+ const accs = obs.map((o) => o.accuracy);
37
+ const mc = mean(confidences), ma = mean(accs);
38
+ let num = 0, dc = 0, da = 0;
39
+ for (const o of obs) {
40
+ num += (o.confidence - mc) * (o.accuracy - ma);
41
+ dc += (o.confidence - mc) ** 2;
42
+ da += (o.accuracy - ma) ** 2;
43
+ }
44
+ // A constant series has zero variance and no correlation to report. Returning 0 would say "no
45
+ // relationship measured"; null says "no relationship measurable", which is the truth.
46
+ const correlation = dc === 0 || da === 0 ? null : num / Math.sqrt(dc * da);
47
+ const sorted = [...confidences].sort((a, b) => a - b);
48
+ const median = sorted[Math.floor(sorted.length / 2)];
49
+ const high = obs.filter((o) => o.confidence >= median).map((o) => o.accuracy);
50
+ const low = obs.filter((o) => o.confidence < median).map((o) => o.accuracy);
51
+ return {
52
+ n,
53
+ correlation,
54
+ distinctConfidences: distinct,
55
+ meanAccuracyHigh: high.length === 0 ? null : mean(high),
56
+ meanAccuracyLow: low.length === 0 ? null : mean(low),
57
+ meanSquaredGap: mean(obs.map((o) => (o.confidence - o.accuracy) ** 2)),
58
+ };
59
+ }
60
+ function correlate(pairs) {
61
+ if (pairs.length < 2)
62
+ return null;
63
+ const mean = (xs) => xs.reduce((a, b) => a + b, 0) / xs.length;
64
+ const mx = mean(pairs.map((p) => p.x)), my = mean(pairs.map((p) => p.y));
65
+ let num = 0, dx = 0, dy = 0;
66
+ for (const p of pairs) {
67
+ num += (p.x - mx) * (p.y - my);
68
+ dx += (p.x - mx) ** 2;
69
+ dy += (p.y - my) ** 2;
70
+ }
71
+ return dx === 0 || dy === 0 ? null : num / Math.sqrt(dx * dy);
72
+ }
73
+ /** Signals that agree in sign across corpora AND clear the threshold in both. */
74
+ function agreeingSignals(a, b, threshold) {
75
+ const byName = new Map(b.map((s) => [s.signal, s]));
76
+ return a.filter((s) => {
77
+ const other = byName.get(s.signal);
78
+ if (s.correlation === null || other?.correlation === undefined || other.correlation === null)
79
+ return false;
80
+ return Math.sign(s.correlation) === Math.sign(other.correlation)
81
+ && Math.abs(s.correlation) >= threshold && Math.abs(other.correlation) >= threshold;
82
+ }).map((s) => s.signal);
83
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * A repository the replay benchmark can be run against.
3
+ *
4
+ * Every verdict reached in this work — three declines of contract nodes, four of the downstream
5
+ * direction, a ceiling of 0.956, an achievement of 0.914 — came from ONE repository. While
6
+ * generalisation is unmeasured, "world top tier" is not a claim that can be checked. Making the
7
+ * corpus a value is the first step to a second measurement, and the layout differences are exactly
8
+ * what the old inline logic hard-coded: `src/holmes` paths and a `.test.` filename convention.
9
+ */
10
+ export interface ReplayCorpus {
11
+ /** Absolute path to the repository root. */
12
+ root: string;
13
+ /**
14
+ * git pathspecs limiting which files this corpus considers at all.
15
+ *
16
+ * A LIST, not one path: the pathspec filters the file list git prints, so a repository that keeps
17
+ * tests in a sibling directory needs both included or its tests never appear and every commit
18
+ * looks source-only. Measured — `src` alone yielded 0 cases for a repo with 251 of them.
19
+ */
20
+ sourcePathspec: string[];
21
+ /** git pathspec for the spec store, materialised alongside the source for point-in-time replay. */
22
+ specPathspec: string;
23
+ /** Which of the changed files are tests. Layout differs per repository — that is the point. */
24
+ isTest: (file: string) => boolean;
25
+ /** Extensions that count as source at all. */
26
+ isSource: (file: string) => boolean;
27
+ /**
28
+ * @implements A-SPEC-443
29
+ * Where the case window STARTS. Absent means `HEAD`, which is what every caller had before.
30
+ *
31
+ * A window defined as "the newest N commits" measures something different every time the
32
+ * repository grows, so a digit pinned against it is not a floor. Measured 2026-08-30: the second
33
+ * corpus gained five commits in a day and `run-replay` lost a six-decimal assertion, 0.7996 ->
34
+ * 0.7663, with no product change. Re-pinning to the new digits would have hidden the fragility
35
+ * rather than removed it — and the same slide can hide a REAL regression just as easily.
36
+ */
37
+ pin?: string;
38
+ }
39
+ export interface ReplayCase {
40
+ commit: string;
41
+ subject: string;
42
+ /** Non-test source files the commit changed. */
43
+ files: string[];
44
+ /** Test files the commit changed. */
45
+ tests: string[];
46
+ }
47
+ export declare const HOLMES_CORPUS: ReplayCorpus;
48
+ /**
49
+ * The second corpus. Measured 2026-08-29: 269 source files, 329 commits, 251 eligible cases, 959
50
+ * truth files, and its own `.ax/specs`. Tests live in `tests/` rather than beside the source, which
51
+ * is precisely the assumption that had to stop being hard-coded.
52
+ *
53
+ * Absent on any machine but this one, and callers skip rather than fail — a benchmark that requires
54
+ * someone else's checkout is a benchmark nobody else can run.
55
+ */
56
+ export declare const JARVIS_CORPUS: ReplayCorpus;
57
+ /**
58
+ * A third party's repository, with no spec store at all.
59
+ *
60
+ * The first two corpora share two properties that turned out to matter: one of them is my own work,
61
+ * and both carry `.ax/specs`. A verdict recorded in the activation log — "the advantage is entirely
62
+ * the spec layer" — rested on that. Most projects have no specs, so the question that verdict left
63
+ * open is what the harness is worth without them.
64
+ *
65
+ * Measured: it beats name matching on every axis here, with `specs: []` passed to the analyzer.
66
+ * Whatever the remaining layers are worth, they are worth something on their own.
67
+ */
68
+ export declare const GEMINI_CLI_CORPUS: ReplayCorpus;
69
+ /**
70
+ * @implements A-SPEC-426
71
+ * Measured 2026-08-29 over 29 commits touching 1 to 12 source files, through the product path with
72
+ * `specs: []`. The opponent is the NAME-ONLY baseline — not a person. A human comparison on this
73
+ * corpus is a separate measurement and is not claimed here.
74
+ */
75
+ export declare const GEMINI_CLI_MEASUREMENT: {
76
+ readonly cases: 29;
77
+ readonly specsAvailable: 0;
78
+ readonly opponent: "name-only baseline (rankBaseline)";
79
+ readonly shipped: {
80
+ readonly top1: 0.241;
81
+ readonly top5Recall: 0.369;
82
+ readonly top10Recall: 0.483;
83
+ readonly top10Precision: 0.083;
84
+ };
85
+ readonly baseline: {
86
+ readonly top1: 0.172;
87
+ readonly top5Recall: 0.186;
88
+ readonly top10Recall: 0.302;
89
+ readonly top10Precision: 0.052;
90
+ };
91
+ };
92
+ /**
93
+ * The commits this corpus offers as replay cases: those that changed BOTH source and tests, most
94
+ * recent first. A commit touching only one side cannot score both axes.
95
+ */
96
+ export declare function casesFor(corpus: ReplayCorpus, limit: number, offset?: number): ReplayCase[];
97
+ /**
98
+ * @implements A-SPEC-364
99
+ * A deterministic sub-sample of a spec store, for holding a repository fixed while varying only how
100
+ * many specs it has.
101
+ *
102
+ * The advantage over a no-graph baseline is 2.5x on this repository (585 specs) and a wash on jarvis
103
+ * (54). Reading that as a DENSITY effect is, so far, a correlation: the two corpora differ in
104
+ * language, size, subject and commit style as well. Thinning one corpus is the controlled version of
105
+ * the same question — everything else stays exactly where it is.
106
+ *
107
+ * Evenly spaced over id-sorted specs, never random: a random sample makes each run a different
108
+ * experiment, and the first thing anyone would ask of a surprising curve is to see it again.
109
+ * Sampling by stride rather than by prefix matters too — spec ids here run in authoring order, so a
110
+ * prefix would select the oldest slices and confound "fewer specs" with "older specs".
111
+ */
112
+ export declare function thinSpecs<T extends {
113
+ id: string;
114
+ }>(specs: readonly T[], fraction: number): T[];
115
+ /**
116
+ * @implements A-SPEC-387
117
+ * Skip the most recent commits, and space the rest out.
118
+ *
119
+ * An audit found the benchmark measuring the benchmarker. On this repository, answering "the ten
120
+ * most recently changed files" — no text, no graph, no query at all — scores Top-10 recall **0.885**
121
+ * and Top-1 **0.650**, beating every retrieval mechanism tried. The reason is what the 60 most recent
122
+ * commits ARE: consecutive slices from this session and the ones before it, each touching the same
123
+ * handful of files. Temporal locality that extreme inflates any recency-flavoured signal, and it is
124
+ * an artefact of who wrote the commits rather than a property of software maintenance.
125
+ *
126
+ * The second corpus does not have it — recency alone scores 0.678 there against a 0.662 baseline —
127
+ * which is what makes the contamination visible rather than invisible.
128
+ *
129
+ * `skipRecent` steps past the working set of whoever ran the benchmark last; `stride` spaces the
130
+ * remaining cases so that consecutive commits from one feature push are not all sampled.
131
+ */
132
+ export declare function casesAvoidingRecent(corpus: ReplayCorpus, limit: number, opts: {
133
+ skipRecent: number;
134
+ stride: number;
135
+ }): ReplayCase[];
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.GEMINI_CLI_MEASUREMENT = exports.GEMINI_CLI_CORPUS = exports.JARVIS_CORPUS = exports.HOLMES_CORPUS = void 0;
37
+ exports.casesFor = casesFor;
38
+ exports.thinSpecs = thinSpecs;
39
+ exports.casesAvoidingRecent = casesAvoidingRecent;
40
+ // @implements A-SPEC-345
41
+ // @implements A-SPEC-387
42
+ const os = __importStar(require("node:os"));
43
+ const path = __importStar(require("node:path"));
44
+ const node_child_process_1 = require("node:child_process");
45
+ const maintenance_analyze_1 = require("../mcp/maintenance-analyze");
46
+ const TS_FAMILY = (f) => f.endsWith('.ts') && !f.endsWith('.d.ts');
47
+ exports.HOLMES_CORPUS = {
48
+ root: path.resolve(__dirname, '../../..'),
49
+ // @implements A-SPEC-443 — pinned so the floors stay floors. Our own repository moves too; the
50
+ // day it broke was only the day the OTHER corpus moved first.
51
+ pin: '9a2f304f12215561584285b45e4b326156b0a484',
52
+ sourcePathspec: ['src/holmes'],
53
+ specPathspec: '.ax/specs',
54
+ isTest: (f) => f.includes('.test.'),
55
+ isSource: TS_FAMILY,
56
+ };
57
+ /**
58
+ * The second corpus. Measured 2026-08-29: 269 source files, 329 commits, 251 eligible cases, 959
59
+ * truth files, and its own `.ax/specs`. Tests live in `tests/` rather than beside the source, which
60
+ * is precisely the assumption that had to stop being hard-coded.
61
+ *
62
+ * Absent on any machine but this one, and callers skip rather than fail — a benchmark that requires
63
+ * someone else's checkout is a benchmark nobody else can run.
64
+ */
65
+ exports.JARVIS_CORPUS = {
66
+ root: path.join(os.homedir(), 'ai_project', 'jarvis'),
67
+ // @implements A-SPEC-443 — the last commit before 2026-08-30, the day five landed and moved a
68
+ // six-decimal assertion with no product change.
69
+ pin: '7b58e40ba3723b9d85390fdadf1d407cdf7bb9ec',
70
+ sourcePathspec: ['src', 'tests'],
71
+ specPathspec: '.ax/specs',
72
+ isTest: (f) => f.startsWith('tests/') || f.includes('.test.') || f.includes('.spec.'),
73
+ isSource: (f) => (f.endsWith('.ts') || f.endsWith('.py') || f.endsWith('.js')) && !f.endsWith('.d.ts'),
74
+ };
75
+ // @implements A-SPEC-426
76
+ /**
77
+ * A third party's repository, with no spec store at all.
78
+ *
79
+ * The first two corpora share two properties that turned out to matter: one of them is my own work,
80
+ * and both carry `.ax/specs`. A verdict recorded in the activation log — "the advantage is entirely
81
+ * the spec layer" — rested on that. Most projects have no specs, so the question that verdict left
82
+ * open is what the harness is worth without them.
83
+ *
84
+ * Measured: it beats name matching on every axis here, with `specs: []` passed to the analyzer.
85
+ * Whatever the remaining layers are worth, they are worth something on their own.
86
+ */
87
+ exports.GEMINI_CLI_CORPUS = {
88
+ root: path.join(os.homedir(), 'ai_project', 'GeminiAcp', 'reference-gemini-cli'),
89
+ sourcePathspec: ['packages'],
90
+ specPathspec: '.ax/specs',
91
+ isTest: (f) => f.includes('.test.') || f.includes('.spec.'),
92
+ isSource: (f) => (f.endsWith('.ts') || f.endsWith('.tsx')) && !f.endsWith('.d.ts'),
93
+ };
94
+ /**
95
+ * @implements A-SPEC-426
96
+ * Measured 2026-08-29 over 29 commits touching 1 to 12 source files, through the product path with
97
+ * `specs: []`. The opponent is the NAME-ONLY baseline — not a person. A human comparison on this
98
+ * corpus is a separate measurement and is not claimed here.
99
+ */
100
+ exports.GEMINI_CLI_MEASUREMENT = {
101
+ cases: 29,
102
+ specsAvailable: 0,
103
+ opponent: 'name-only baseline (rankBaseline)',
104
+ shipped: { top1: 0.241, top5Recall: 0.369, top10Recall: 0.483, top10Precision: 0.083 },
105
+ baseline: { top1: 0.172, top5Recall: 0.186, top10Recall: 0.302, top10Precision: 0.052 },
106
+ };
107
+ /**
108
+ * The commits this corpus offers as replay cases: those that changed BOTH source and tests, most
109
+ * recent first. A commit touching only one side cannot score both axes.
110
+ */
111
+ // @implements A-SPEC-485 — the offset window: the holdout discipline needs cases the main
112
+ // measurement never touched. Default 0 keeps the old signature byte-identical for every pin.
113
+ function casesFor(corpus, limit, offset = 0) {
114
+ // @implements A-SPEC-443 — an absent corpus and a bad pin are DIFFERENT events, and answering
115
+ // both with `[]` lets a mis-typed pin masquerade as "this machine does not have that repository".
116
+ // A benchmark that silently measures nothing is worse than one that fails, so the corpus is
117
+ // proved present FIRST; only after that does a pin failure become an error.
118
+ const here = (() => {
119
+ try {
120
+ (0, node_child_process_1.execFileSync)('git', ['-C', corpus.root, 'rev-parse', '--git-dir'], { encoding: 'utf8', stdio: 'pipe' });
121
+ return true;
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ })();
127
+ if (!here)
128
+ return [];
129
+ const from = corpus.pin ?? 'HEAD';
130
+ let raw;
131
+ try {
132
+ raw = (0, node_child_process_1.execFileSync)('git', ['-C', corpus.root, 'log', from, '--no-merges', '--format=%H\x1f%s',
133
+ '--name-only', '--', ...corpus.sourcePathspec], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
134
+ }
135
+ catch (e) {
136
+ // The repository is here, so this is a configuration error, not a missing corpus.
137
+ throw new Error(`replay corpus ${corpus.root}: cannot read history from "${from}" — `
138
+ + 'the pin names no commit in this repository (was history rewritten?). '
139
+ + `Underlying: ${e instanceof Error ? e.message : String(e)}`);
140
+ }
141
+ const all = [];
142
+ let current = null;
143
+ for (const line of raw.split('\n')) {
144
+ if (line.includes('\x1f')) {
145
+ const [commit, subject] = line.split('\x1f');
146
+ current = { commit, subject, files: [], tests: [] };
147
+ all.push(current);
148
+ }
149
+ else if (current && line.trim() !== '') {
150
+ // @implements A-SPEC-274 — git quotes non-ASCII paths; unquoting is not optional here.
151
+ const file = (0, maintenance_analyze_1.unquoteGitPath)(line.trim());
152
+ if (!corpus.isSource(file))
153
+ continue;
154
+ (corpus.isTest(file) ? current.tests : current.files).push(file);
155
+ }
156
+ }
157
+ return all.filter((c) => c.files.length > 0 && c.tests.length > 0).slice(offset, offset + limit);
158
+ }
159
+ /**
160
+ * @implements A-SPEC-364
161
+ * A deterministic sub-sample of a spec store, for holding a repository fixed while varying only how
162
+ * many specs it has.
163
+ *
164
+ * The advantage over a no-graph baseline is 2.5x on this repository (585 specs) and a wash on jarvis
165
+ * (54). Reading that as a DENSITY effect is, so far, a correlation: the two corpora differ in
166
+ * language, size, subject and commit style as well. Thinning one corpus is the controlled version of
167
+ * the same question — everything else stays exactly where it is.
168
+ *
169
+ * Evenly spaced over id-sorted specs, never random: a random sample makes each run a different
170
+ * experiment, and the first thing anyone would ask of a surprising curve is to see it again.
171
+ * Sampling by stride rather than by prefix matters too — spec ids here run in authoring order, so a
172
+ * prefix would select the oldest slices and confound "fewer specs" with "older specs".
173
+ */
174
+ function thinSpecs(specs, fraction) {
175
+ if (fraction >= 1)
176
+ return [...specs];
177
+ if (fraction <= 0)
178
+ return [];
179
+ const sorted = [...specs].sort((a, b) => a.id.localeCompare(b.id));
180
+ const keep = Math.max(1, Math.round(sorted.length * fraction));
181
+ const stride = sorted.length / keep;
182
+ const out = [];
183
+ for (let i = 0; i < keep; i++)
184
+ out.push(sorted[Math.min(sorted.length - 1, Math.floor(i * stride))]);
185
+ return [...new Set(out)];
186
+ }
187
+ /**
188
+ * @implements A-SPEC-387
189
+ * Skip the most recent commits, and space the rest out.
190
+ *
191
+ * An audit found the benchmark measuring the benchmarker. On this repository, answering "the ten
192
+ * most recently changed files" — no text, no graph, no query at all — scores Top-10 recall **0.885**
193
+ * and Top-1 **0.650**, beating every retrieval mechanism tried. The reason is what the 60 most recent
194
+ * commits ARE: consecutive slices from this session and the ones before it, each touching the same
195
+ * handful of files. Temporal locality that extreme inflates any recency-flavoured signal, and it is
196
+ * an artefact of who wrote the commits rather than a property of software maintenance.
197
+ *
198
+ * The second corpus does not have it — recency alone scores 0.678 there against a 0.662 baseline —
199
+ * which is what makes the contamination visible rather than invisible.
200
+ *
201
+ * `skipRecent` steps past the working set of whoever ran the benchmark last; `stride` spaces the
202
+ * remaining cases so that consecutive commits from one feature push are not all sampled.
203
+ */
204
+ function casesAvoidingRecent(corpus, limit, opts) {
205
+ const skip = Math.max(0, Math.floor(opts.skipRecent));
206
+ const stride = Math.max(1, Math.floor(opts.stride));
207
+ // Ask for enough raw cases to survive both the skip and the stride, then take what was wanted.
208
+ const pool = casesFor(corpus, skip + limit * stride + stride);
209
+ return pool.slice(skip).filter((_, i) => i % stride === 0).slice(0, limit);
210
+ }