@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.
- package/CHANGELOG.md +55 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/assoc/assoc-arm.d.ts +94 -0
- package/dist/holmes/assoc/assoc-arm.js +187 -0
- package/dist/holmes/assoc/explore.d.ts +21 -0
- package/dist/holmes/assoc/explore.js +160 -0
- package/dist/holmes/assoc/impact-baseline.d.ts +13 -0
- package/dist/holmes/assoc/impact-baseline.js +48 -0
- package/dist/holmes/assoc/ppr.d.ts +64 -0
- package/dist/holmes/assoc/ppr.js +110 -0
- package/dist/holmes/cli/agents.d.ts +24 -11
- package/dist/holmes/cli/agents.js +93 -21
- package/dist/holmes/cli/doctor.d.ts +56 -0
- package/dist/holmes/cli/doctor.js +311 -5
- package/dist/holmes/cli/index.js +13 -1
- package/dist/holmes/cli/semantic-key.d.ts +19 -0
- package/dist/holmes/cli/semantic-key.js +93 -0
- package/dist/holmes/config/config.d.ts +9 -0
- package/dist/holmes/config/config.js +8 -1
- package/dist/holmes/cpg/language-capability.d.ts +65 -0
- package/dist/holmes/cpg/language-capability.js +145 -0
- package/dist/holmes/cpg/language-parser-walk.js +179 -34
- package/dist/holmes/cpg/language-parser.d.ts +1 -1
- package/dist/holmes/governance/ledger-rechain.d.ts +12 -0
- package/dist/holmes/governance/ledger-rechain.js +17 -2
- package/dist/holmes/governance/provenance-ledger.js +21 -0
- package/dist/holmes/guardrail/blind-spots.js +12 -1
- package/dist/holmes/guardrail/impact-gate.d.ts +77 -0
- package/dist/holmes/guardrail/impact-gate.js +263 -0
- package/dist/holmes/guardrail/write-target.d.ts +38 -1
- package/dist/holmes/guardrail/write-target.js +41 -4
- package/dist/holmes/hooks/adapters/antigravity.js +12 -1
- package/dist/holmes/hooks/corrupt-state-run.d.ts +33 -0
- package/dist/holmes/hooks/corrupt-state-run.js +16 -0
- package/dist/holmes/hooks/pre-tool-use.d.ts +26 -0
- package/dist/holmes/hooks/pre-tool-use.js +203 -11
- package/dist/holmes/hooks/stop.d.ts +65 -0
- package/dist/holmes/hooks/stop.js +200 -2
- package/dist/holmes/mcp/elicit-approval.d.ts +32 -2
- package/dist/holmes/mcp/elicit-approval.js +30 -2
- package/dist/holmes/mcp/handlers.d.ts +88 -0
- package/dist/holmes/mcp/handlers.js +636 -35
- package/dist/holmes/mcp/maintenance-analyze.d.ts +435 -0
- package/dist/holmes/mcp/maintenance-analyze.js +994 -0
- package/dist/holmes/mcp/maintenance-evidence.d.ts +140 -0
- package/dist/holmes/mcp/maintenance-evidence.js +253 -0
- package/dist/holmes/mcp/server.js +13 -10
- package/dist/holmes/mcp/tool-schemas.js +71 -0
- package/dist/holmes/project/root.js +3 -1
- package/dist/holmes/review/baseline-arm.d.ts +37 -0
- package/dist/holmes/review/baseline-arm.js +51 -0
- package/dist/holmes/review/captured-stdin-guard.d.ts +8 -0
- package/dist/holmes/review/captured-stdin-guard.js +48 -0
- package/dist/holmes/review/coherence-verify.d.ts +31 -0
- package/dist/holmes/review/coherence-verify.js +144 -0
- package/dist/holmes/review/commit-text.d.ts +50 -0
- package/dist/holmes/review/commit-text.js +76 -0
- package/dist/holmes/review/confidence-calibration.d.ts +39 -0
- package/dist/holmes/review/confidence-calibration.js +39 -0
- package/dist/holmes/review/content-baseline.d.ts +38 -0
- package/dist/holmes/review/content-baseline.js +103 -0
- package/dist/holmes/review/content-verify.d.ts +20 -0
- package/dist/holmes/review/content-verify.js +73 -0
- package/dist/holmes/review/dense-retrieval.d.ts +66 -0
- package/dist/holmes/review/dense-retrieval.js +97 -0
- package/dist/holmes/review/edge-quality.d.ts +44 -0
- package/dist/holmes/review/edge-quality.js +117 -0
- package/dist/holmes/review/evaluation-metrics.d.ts +138 -0
- package/dist/holmes/review/evaluation-metrics.js +175 -0
- package/dist/holmes/review/graph-verifier.d.ts +34 -0
- package/dist/holmes/review/graph-verifier.js +62 -0
- package/dist/holmes/review/hop-ablation.d.ts +100 -0
- package/dist/holmes/review/hop-ablation.js +89 -0
- package/dist/holmes/review/manual-baseline.d.ts +209 -0
- package/dist/holmes/review/manual-baseline.js +2846 -0
- package/dist/holmes/review/oracle-gap.d.ts +32 -0
- package/dist/holmes/review/oracle-gap.js +102 -0
- package/dist/holmes/review/point-in-time-replay.d.ts +41 -0
- package/dist/holmes/review/point-in-time-replay.js +161 -0
- package/dist/holmes/review/rank-diagnosis.d.ts +43 -0
- package/dist/holmes/review/rank-diagnosis.js +163 -0
- package/dist/holmes/review/replay-calibration.d.ts +62 -0
- package/dist/holmes/review/replay-calibration.js +83 -0
- package/dist/holmes/review/replay-corpus.d.ts +135 -0
- package/dist/holmes/review/replay-corpus.js +210 -0
- package/dist/holmes/review/run-replay.d.ts +260 -0
- package/dist/holmes/review/run-replay.js +729 -0
- package/dist/holmes/review/semantic-arm.d.ts +271 -0
- package/dist/holmes/review/semantic-arm.js +717 -0
- package/dist/holmes/review/semantic-retrieval.d.ts +55 -0
- package/dist/holmes/review/semantic-retrieval.js +156 -0
- package/dist/holmes/review/spec-layer-stats.d.ts +38 -0
- package/dist/holmes/review/spec-layer-stats.js +52 -0
- package/dist/holmes/review/temporal-prior.d.ts +33 -0
- package/dist/holmes/review/temporal-prior.js +53 -0
- package/dist/holmes/review/test-runner.d.ts +15 -0
- package/dist/holmes/review/test-runner.js +41 -4
- package/dist/holmes/review/test-selection-breadth.d.ts +75 -0
- package/dist/holmes/review/test-selection-breadth.js +57 -0
- package/dist/holmes/review/traceability-benchmark.d.ts +81 -0
- package/dist/holmes/review/traceability-benchmark.js +135 -0
- package/dist/holmes/review/union-verify.d.ts +12 -0
- package/dist/holmes/review/union-verify.js +70 -0
- package/dist/holmes/rtm/graph-store.d.ts +51 -0
- package/dist/holmes/rtm/graph-store.js +122 -0
- package/dist/holmes/rtm/incremental.d.ts +25 -1
- package/dist/holmes/rtm/incremental.js +18 -1
- package/dist/holmes/rtm/localize.d.ts +28 -0
- package/dist/holmes/rtm/localize.js +272 -14
- package/dist/holmes/rtm/rtm-builder.d.ts +118 -3
- package/dist/holmes/rtm/rtm-builder.js +265 -28
- package/dist/holmes/rtm/rtm-graph.d.ts +117 -2
- package/dist/holmes/rtm/rtm-graph.js +194 -31
- package/dist/holmes/rtm/taint-benchmark.d.ts +97 -0
- package/dist/holmes/rtm/taint-benchmark.js +141 -0
- package/dist/holmes/rtm/test-scope.js +24 -1
- package/dist/holmes/semantic/credentials.d.ts +15 -0
- package/dist/holmes/semantic/credentials.js +134 -0
- package/dist/holmes/semantic/embedder.d.ts +44 -0
- package/dist/holmes/semantic/embedder.js +185 -0
- package/dist/holmes/semantic/hit-rerank.d.ts +4 -0
- package/dist/holmes/semantic/hit-rerank.js +38 -0
- package/dist/holmes/semantic/tier.d.ts +37 -0
- package/dist/holmes/semantic/tier.js +54 -0
- package/dist/holmes/semantic/vector-cache.d.ts +11 -0
- package/dist/holmes/semantic/vector-cache.js +91 -0
- package/dist/holmes/spec/acceptance-quality.d.ts +81 -0
- package/dist/holmes/spec/acceptance-quality.js +169 -0
- package/dist/holmes/spec/validator.js +33 -1
- package/dist/holmes/spec/yaml-scalar.d.ts +1 -0
- package/dist/holmes/spec/yaml-scalar.js +43 -0
- 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
|
+
}
|