@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,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.capturedStdinViolations = capturedStdinViolations;
|
|
4
|
+
// @implements A-SPEC-331
|
|
5
|
+
/**
|
|
6
|
+
* Forbid piping a CAPTURED process output (or file read) into a child process's stdin.
|
|
7
|
+
*
|
|
8
|
+
* A-SPEC-329 removed one instance of this after it failed three times in production with
|
|
9
|
+
* `spawnSync tar EPIPE`; A-SPEC-330's guard protects only the file it was fixed in. This one is
|
|
10
|
+
* repository-wide, and the difficulty is telling the hazard apart from legitimate use — `doctor.ts`
|
|
11
|
+
* and `antigravity.ts` both pass small hook payloads on stdin and must keep working. A rule that
|
|
12
|
+
* flags them is noise, and a noisy rule gets ignored, which is worse than no rule at all.
|
|
13
|
+
*
|
|
14
|
+
* Size cannot be measured statically, so the discriminator is SHAPE, not size:
|
|
15
|
+
*
|
|
16
|
+
* dangerous — `const buf = execFileSync(…); child({ input: buf })`
|
|
17
|
+
* the input grows with the repository, and nothing in the code bounds it.
|
|
18
|
+
* fine — `child({ input: JSON.stringify(payload) })`
|
|
19
|
+
* the code decides how big it is.
|
|
20
|
+
*
|
|
21
|
+
* KNOWN LIMITS, stated rather than implied by silence: this is text matching, not an AST. A capture
|
|
22
|
+
* assigned through an intermediate variable, returned from a helper, or imported from another file
|
|
23
|
+
* is missed. The goal is not soundness — it is that the specific mistake already made cannot be
|
|
24
|
+
* retyped somewhere else and go unnoticed.
|
|
25
|
+
*/
|
|
26
|
+
const CAPTURE = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?(?:\w+\.)?(?:execFileSync|execSync|spawnSync|readFileSync)\s*\(/;
|
|
27
|
+
const STDIN_INPUT = /\binput\s*:\s*([A-Za-z_$][\w$]*)\s*[,}]/;
|
|
28
|
+
function capturedStdinViolations(source, file) {
|
|
29
|
+
const lines = source.split('\n');
|
|
30
|
+
// A comment describing the pattern is documentation, not a defect — and this file is full of them.
|
|
31
|
+
const code = lines.map((line) => (line.trim().startsWith('//') || line.trim().startsWith('*') ? '' : line));
|
|
32
|
+
const captured = new Set();
|
|
33
|
+
for (const line of code) {
|
|
34
|
+
const m = CAPTURE.exec(line);
|
|
35
|
+
if (m)
|
|
36
|
+
captured.add(m[1]);
|
|
37
|
+
}
|
|
38
|
+
if (captured.size === 0)
|
|
39
|
+
return [];
|
|
40
|
+
const out = [];
|
|
41
|
+
code.forEach((line, i) => {
|
|
42
|
+
const m = STDIN_INPUT.exec(line);
|
|
43
|
+
// Whole-name comparison: `tarball` must not match a capture named `tar`.
|
|
44
|
+
if (m && captured.has(m[1]))
|
|
45
|
+
out.push({ file, variable: m[1], line: i + 1 });
|
|
46
|
+
});
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural-coherence verification — the second verifier design, built on what S-497's negative
|
|
3
|
+
* proved: re-projected path/symbol text (lexical or semantic) is evidence the ranker already
|
|
4
|
+
* consumed, and it cannot answer "does this change actually touch this file". The evidence here
|
|
5
|
+
* is NEW to the emission: the graph relations AMONG the emitted candidates. A true change set is
|
|
6
|
+
* a connected working set (siblings that call/import each other or implement the same spec); a
|
|
7
|
+
* false tail candidate is a lexical coincidence with no edge to the head.
|
|
8
|
+
*
|
|
9
|
+
* Parent-time graph only — the same point-in-time discipline every arm obeys (A-SPEC-378).
|
|
10
|
+
* Absence from the graph is a verdict, not a coverage gap: a scanned file always has nodes, so
|
|
11
|
+
* "no relation to the head" is exactly the evidence this verifier exists to read. (Contrast with
|
|
12
|
+
* S-497's null-vector KEEP, where the signal simply had not spoken.)
|
|
13
|
+
*/
|
|
14
|
+
import type { GraphView } from '../assoc/assoc-arm';
|
|
15
|
+
export interface CoherenceIndex {
|
|
16
|
+
/** file → files it shares at least one CODE↔CODE edge with (direction ignored, self excluded). */
|
|
17
|
+
fileEdges: Map<string, Set<string>>;
|
|
18
|
+
/** file → spec ids anchored to it (via SPEC↔CODE edges, either direction). */
|
|
19
|
+
fileSpecs: Map<string, Set<string>>;
|
|
20
|
+
}
|
|
21
|
+
export declare function buildCoherenceIndex(view: GraphView): CoherenceIndex;
|
|
22
|
+
/** Coupled-member count per emission member — files outside the emission never count. */
|
|
23
|
+
export declare function emissionDegree(idx: CoherenceIndex, members: readonly string[], rel: 'edge' | 'any'): Map<string, number>;
|
|
24
|
+
/**
|
|
25
|
+
* Members of the largest connected component of the emission's induced coupling graph. Ties are
|
|
26
|
+
* kept as a union — with zero edges every member is a size-1 component and everyone survives,
|
|
27
|
+
* which is the honest verdict when the structure has nothing to say.
|
|
28
|
+
*/
|
|
29
|
+
export declare function largestCluster(idx: CoherenceIndex, members: readonly string[], rel: 'edge' | 'any'): Set<string>;
|
|
30
|
+
export declare function anchoredConsensusAccept(anchor: (f: string) => boolean, deg: ReadonlyMap<string, number>, d: number, comb: 'or' | 'and'): (f: string) => boolean;
|
|
31
|
+
export declare function coherentAccept(idx: CoherenceIndex, head: ReadonlySet<string>, mode: 'edge' | 'spec' | 'any'): (f: string) => boolean;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildCoherenceIndex = buildCoherenceIndex;
|
|
4
|
+
exports.emissionDegree = emissionDegree;
|
|
5
|
+
exports.largestCluster = largestCluster;
|
|
6
|
+
exports.anchoredConsensusAccept = anchoredConsensusAccept;
|
|
7
|
+
exports.coherentAccept = coherentAccept;
|
|
8
|
+
const fileOf = (id) => {
|
|
9
|
+
if (id.startsWith('FILE:'))
|
|
10
|
+
return id.slice('FILE:'.length);
|
|
11
|
+
if (id.startsWith('CODE:')) {
|
|
12
|
+
const at = id.lastIndexOf('@');
|
|
13
|
+
return at >= 0 ? id.slice(at + 1) : null;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
};
|
|
17
|
+
const specOf = (id) => (id.startsWith('SPEC:') ? id.slice('SPEC:'.length) : null);
|
|
18
|
+
function buildCoherenceIndex(view) {
|
|
19
|
+
const fileEdges = new Map();
|
|
20
|
+
const fileSpecs = new Map();
|
|
21
|
+
const link = (m, a, b) => {
|
|
22
|
+
(m.get(a) ?? m.set(a, new Set()).get(a)).add(b);
|
|
23
|
+
};
|
|
24
|
+
for (const e of view.edges) {
|
|
25
|
+
const sf = fileOf(e.src), df = fileOf(e.dst);
|
|
26
|
+
if (sf !== null && df !== null) {
|
|
27
|
+
if (sf !== df) {
|
|
28
|
+
link(fileEdges, sf, df);
|
|
29
|
+
link(fileEdges, df, sf);
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const spec = specOf(e.src) ?? specOf(e.dst);
|
|
34
|
+
const file = sf ?? df;
|
|
35
|
+
if (spec !== null && file !== null)
|
|
36
|
+
link(fileSpecs, file, spec);
|
|
37
|
+
}
|
|
38
|
+
return { fileEdges, fileSpecs };
|
|
39
|
+
}
|
|
40
|
+
// @implements A-SPEC-481 — position-independent verdicts. S-498 located the head-error failure
|
|
41
|
+
// mode: the verdict's reference point (top-h) came from the ranking, so a wrong top-1 made
|
|
42
|
+
// coherence cut truths. Degree and cluster judge the emission's own structure and have no head
|
|
43
|
+
// to be wrong about; the explicit guard remains the only positional device, as a separate axis.
|
|
44
|
+
/** Are two files coupled under the given relation? Symmetric; self-coupling is excluded upstream. */
|
|
45
|
+
function coupled(idx, a, b, rel) {
|
|
46
|
+
if (idx.fileEdges.get(a)?.has(b) === true)
|
|
47
|
+
return true;
|
|
48
|
+
if (rel === 'edge')
|
|
49
|
+
return false;
|
|
50
|
+
const sa = idx.fileSpecs.get(a), sb = idx.fileSpecs.get(b);
|
|
51
|
+
if (sa === undefined || sb === undefined)
|
|
52
|
+
return false;
|
|
53
|
+
for (const s of sa)
|
|
54
|
+
if (sb.has(s))
|
|
55
|
+
return true;
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
/** Coupled-member count per emission member — files outside the emission never count. */
|
|
59
|
+
function emissionDegree(idx, members, rel) {
|
|
60
|
+
const deg = new Map();
|
|
61
|
+
for (const f of members)
|
|
62
|
+
deg.set(f, 0);
|
|
63
|
+
for (let i = 0; i < members.length; i++) {
|
|
64
|
+
for (let j = i + 1; j < members.length; j++) {
|
|
65
|
+
if (members[i] !== members[j] && coupled(idx, members[i], members[j], rel)) {
|
|
66
|
+
deg.set(members[i], deg.get(members[i]) + 1);
|
|
67
|
+
deg.set(members[j], deg.get(members[j]) + 1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return deg;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Members of the largest connected component of the emission's induced coupling graph. Ties are
|
|
75
|
+
* kept as a union — with zero edges every member is a size-1 component and everyone survives,
|
|
76
|
+
* which is the honest verdict when the structure has nothing to say.
|
|
77
|
+
*/
|
|
78
|
+
function largestCluster(idx, members, rel) {
|
|
79
|
+
const comp = new Map();
|
|
80
|
+
let n = 0;
|
|
81
|
+
for (const f of members) {
|
|
82
|
+
if (comp.has(f))
|
|
83
|
+
continue;
|
|
84
|
+
const stack = [f];
|
|
85
|
+
comp.set(f, n);
|
|
86
|
+
while (stack.length > 0) {
|
|
87
|
+
const cur = stack.pop();
|
|
88
|
+
for (const g of members) {
|
|
89
|
+
if (!comp.has(g) && coupled(idx, cur, g, rel)) {
|
|
90
|
+
comp.set(g, n);
|
|
91
|
+
stack.push(g);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
n++;
|
|
96
|
+
}
|
|
97
|
+
const sizes = new Array(n).fill(0);
|
|
98
|
+
for (const c of comp.values())
|
|
99
|
+
sizes[c]++;
|
|
100
|
+
const max = Math.max(0, ...sizes);
|
|
101
|
+
return new Set([...comp.entries()].filter(([, c]) => sizes[c] === max).map(([f]) => f));
|
|
102
|
+
}
|
|
103
|
+
// @implements A-SPEC-482 — anchor + consensus, the last file-granularity combination: the anchor
|
|
104
|
+
// carries the correctness prior S-499 proved indispensable, the degree carries the structure
|
|
105
|
+
// S-498's head error could not see past. OR recovers truths a wrong head cut; AND refines away
|
|
106
|
+
// coincidental couplings.
|
|
107
|
+
function anchoredConsensusAccept(anchor, deg, d, comb) {
|
|
108
|
+
return (f) => {
|
|
109
|
+
const consensus = (deg.get(f) ?? 0) >= d;
|
|
110
|
+
return comb === 'or' ? anchor(f) || consensus : anchor(f) && consensus;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function coherentAccept(idx, head, mode) {
|
|
114
|
+
const byEdge = (f) => {
|
|
115
|
+
const n = idx.fileEdges.get(f);
|
|
116
|
+
if (n === undefined)
|
|
117
|
+
return false;
|
|
118
|
+
for (const h of head)
|
|
119
|
+
if (h !== f && n.has(h))
|
|
120
|
+
return true;
|
|
121
|
+
return false;
|
|
122
|
+
};
|
|
123
|
+
const bySpec = (f) => {
|
|
124
|
+
const s = idx.fileSpecs.get(f);
|
|
125
|
+
if (s === undefined)
|
|
126
|
+
return false;
|
|
127
|
+
for (const h of head) {
|
|
128
|
+
if (h === f)
|
|
129
|
+
continue;
|
|
130
|
+
const hs = idx.fileSpecs.get(h);
|
|
131
|
+
if (hs === undefined)
|
|
132
|
+
continue;
|
|
133
|
+
for (const id of s)
|
|
134
|
+
if (hs.has(id))
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
};
|
|
139
|
+
if (mode === 'edge')
|
|
140
|
+
return byEdge;
|
|
141
|
+
if (mode === 'spec')
|
|
142
|
+
return bySpec;
|
|
143
|
+
return (f) => byEdge(f) || bySpec(f);
|
|
144
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each file's commits have said about it.
|
|
3
|
+
*
|
|
4
|
+
* Eleven mechanisms have been measured and none of them read a commit message. The temporal arm used
|
|
5
|
+
* change recency — a timestamp, not text. The spec and call arms read only the vocabulary of code and
|
|
6
|
+
* specifications. But every file carries the prose of the commits that changed it, and that prose
|
|
7
|
+
* sits exactly between the language of a requirement and the names in the code: a report saying
|
|
8
|
+
* "uploads are being rejected" reaches `sanitizeIngest` through a commit that said "fix upload
|
|
9
|
+
* rejection on malformed payload".
|
|
10
|
+
*
|
|
11
|
+
* That is query → Commit → `MODIFIED_IN` → File: the knowledge graph used to retrieve, rather than
|
|
12
|
+
* to decorate a ranking that lexical matching already produced.
|
|
13
|
+
*
|
|
14
|
+
* **The window ends at the analysis basis.** Reading the answer commit's own message is reading the
|
|
15
|
+
* answer aloud — the subject of the very commit whose files are being predicted. A test pins that
|
|
16
|
+
* the profile at the parent does not contain it.
|
|
17
|
+
*/
|
|
18
|
+
export interface CommitTextProfile {
|
|
19
|
+
/**
|
|
20
|
+
* @implements A-SPEC-386
|
|
21
|
+
* Commit hashes the window actually read.
|
|
22
|
+
*
|
|
23
|
+
* Exposed for auditing rather than for use: this mechanism produced the largest gain in the whole
|
|
24
|
+
* project (uncited Top-10 recall 0.434 -> 0.751), and a repository that has already mistaken a
|
|
25
|
+
* leak for a result at recall 0.990 should be able to CHECK that the answer commit is absent
|
|
26
|
+
* rather than trust that it is.
|
|
27
|
+
*/
|
|
28
|
+
commitIds: Set<string>;
|
|
29
|
+
/** file -> term -> how many commits in the window used that term while touching the file. */
|
|
30
|
+
byFile: Map<string, Map<string, number>>;
|
|
31
|
+
/** How many commits were read. 0 means unmeasured, not "no history". */
|
|
32
|
+
commits: number;
|
|
33
|
+
/** Commits each term appeared in, for weighting a term by how ordinary it is in this history. */
|
|
34
|
+
documentFrequency: Map<string, number>;
|
|
35
|
+
}
|
|
36
|
+
export declare function commitTextProfile(root: string, before: string, limit?: number): CommitTextProfile;
|
|
37
|
+
/**
|
|
38
|
+
* Score files by how much their commit history talks like the request.
|
|
39
|
+
*
|
|
40
|
+
* Weighted by inverse commit frequency for the same reason file scoring is: a term appearing in
|
|
41
|
+
* every commit message ("fix", "test", "add") says nothing about which file is meant, and counting
|
|
42
|
+
* it equally with a rare one is the defect that cost this ranker its precision elsewhere.
|
|
43
|
+
*
|
|
44
|
+
* Returns an empty ranking when the window is empty — a caller must be able to tell "no history was
|
|
45
|
+
* read" from "history was read and matched nothing", so `commits === 0` is exposed on the profile.
|
|
46
|
+
*/
|
|
47
|
+
export declare function rankByCommitText(issueText: string, profile: CommitTextProfile, topN: number): Array<{
|
|
48
|
+
file: string;
|
|
49
|
+
score: number;
|
|
50
|
+
}>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.commitTextProfile = commitTextProfile;
|
|
4
|
+
exports.rankByCommitText = rankByCommitText;
|
|
5
|
+
// @implements A-SPEC-385
|
|
6
|
+
// @implements A-SPEC-386
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const maintenance_analyze_1 = require("../mcp/maintenance-analyze");
|
|
9
|
+
const localize_1 = require("../rtm/localize");
|
|
10
|
+
function commitTextProfile(root, before, limit = 400) {
|
|
11
|
+
const empty = { byFile: new Map(), commits: 0, documentFrequency: new Map(), commitIds: new Set() };
|
|
12
|
+
let raw;
|
|
13
|
+
try {
|
|
14
|
+
raw = (0, node_child_process_1.execFileSync)('git', ['-C', root, 'log', '--no-merges', `-${limit}`, '--format=%x00%H%x00%s', '--name-only', before], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return empty;
|
|
18
|
+
}
|
|
19
|
+
const byFile = new Map();
|
|
20
|
+
const documentFrequency = new Map();
|
|
21
|
+
const commitIds = new Set();
|
|
22
|
+
let terms = [];
|
|
23
|
+
let commits = 0;
|
|
24
|
+
const flushTermsInto = (file) => {
|
|
25
|
+
const bucket = byFile.get(file) ?? byFile.set(file, new Map()).get(file);
|
|
26
|
+
for (const t of terms)
|
|
27
|
+
bucket.set(t, (bucket.get(t) ?? 0) + 1);
|
|
28
|
+
};
|
|
29
|
+
for (const line of raw.split('\n')) {
|
|
30
|
+
if (line.startsWith('\0')) {
|
|
31
|
+
const [hash, subject] = line.slice(1).split('\0');
|
|
32
|
+
commitIds.add(hash);
|
|
33
|
+
terms = [...new Set((0, localize_1.significantTerms)(subject ?? ''))];
|
|
34
|
+
for (const t of terms)
|
|
35
|
+
documentFrequency.set(t, (documentFrequency.get(t) ?? 0) + 1);
|
|
36
|
+
commits++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const file = line.trim();
|
|
40
|
+
if (file.length === 0 || terms.length === 0)
|
|
41
|
+
continue;
|
|
42
|
+
flushTermsInto((0, maintenance_analyze_1.unquoteGitPath)(file));
|
|
43
|
+
}
|
|
44
|
+
return { byFile, commits, documentFrequency, commitIds };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Score files by how much their commit history talks like the request.
|
|
48
|
+
*
|
|
49
|
+
* Weighted by inverse commit frequency for the same reason file scoring is: a term appearing in
|
|
50
|
+
* every commit message ("fix", "test", "add") says nothing about which file is meant, and counting
|
|
51
|
+
* it equally with a rare one is the defect that cost this ranker its precision elsewhere.
|
|
52
|
+
*
|
|
53
|
+
* Returns an empty ranking when the window is empty — a caller must be able to tell "no history was
|
|
54
|
+
* read" from "history was read and matched nothing", so `commits === 0` is exposed on the profile.
|
|
55
|
+
*/
|
|
56
|
+
function rankByCommitText(issueText, profile, topN) {
|
|
57
|
+
if (profile.commits === 0)
|
|
58
|
+
return [];
|
|
59
|
+
const query = new Set((0, localize_1.significantTerms)(issueText));
|
|
60
|
+
if (query.size === 0)
|
|
61
|
+
return [];
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const [file, bucket] of profile.byFile) {
|
|
64
|
+
let score = 0;
|
|
65
|
+
for (const t of query) {
|
|
66
|
+
const hits = bucket.get(t);
|
|
67
|
+
if (!hits)
|
|
68
|
+
continue;
|
|
69
|
+
const df = profile.documentFrequency.get(t) ?? 1;
|
|
70
|
+
score += hits * Math.log(1 + profile.commits / df);
|
|
71
|
+
}
|
|
72
|
+
if (score > 0)
|
|
73
|
+
out.push({ file, score });
|
|
74
|
+
}
|
|
75
|
+
return out.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file)).slice(0, topN);
|
|
76
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `maintenance_analyze`'s `confidence` actually predicts.
|
|
3
|
+
*
|
|
4
|
+
* The activation log's earlier entry said the field was a constant 0.25 across sixty cases, and that
|
|
5
|
+
* after a fix its correlation with accuracy was −0.062 — no signal. Re-measured through the shipped
|
|
6
|
+
* path it now takes three values, and the correlation is not merely absent. It is NEGATIVE, on both
|
|
7
|
+
* corpora, with the highest band the worst in each:
|
|
8
|
+
*
|
|
9
|
+
* holmes conf 0.25 → recall 0.751 conf 0.70 → 0.306 conf 0.85 → 0.542 r = −0.309
|
|
10
|
+
* jarvis conf 0.25 → recall 0.669 conf 0.65 → 0.646 conf 0.85 → 0.000 r = −0.212
|
|
11
|
+
*
|
|
12
|
+
* A caller who trusts this field trusts the wrong answers most. On the second corpus the two cases
|
|
13
|
+
* the harness was surest about returned nothing correct at all.
|
|
14
|
+
*
|
|
15
|
+
* The formula is NOT changed here. There is no hypothesis yet for why it inverts, and changing a
|
|
16
|
+
* scorer without one is a mistake this repository has had to undo more than once (A-SPEC-430,
|
|
17
|
+
* A-SPEC-431). What this module does is make the fact impossible to quote away.
|
|
18
|
+
*/
|
|
19
|
+
export interface ConfidenceBand {
|
|
20
|
+
confidence: number;
|
|
21
|
+
n: number;
|
|
22
|
+
meanRecall: number;
|
|
23
|
+
meanPrecision: number;
|
|
24
|
+
}
|
|
25
|
+
export interface CorpusConfidence {
|
|
26
|
+
corpus: string;
|
|
27
|
+
cases: number;
|
|
28
|
+
bands: readonly ConfidenceBand[];
|
|
29
|
+
correlationWithRecall: number;
|
|
30
|
+
}
|
|
31
|
+
/** Measured 2026-08-29 through analyzeMaintenance on each corpus's 30-case clean slice. */
|
|
32
|
+
export declare const CONFIDENCE_BY_CORPUS: readonly CorpusConfidence[];
|
|
33
|
+
/** Pearson correlation, so the number above can be recomputed rather than trusted. */
|
|
34
|
+
export declare function correlation(points: ReadonlyArray<{
|
|
35
|
+
confidence: number;
|
|
36
|
+
recall: number;
|
|
37
|
+
}>): number;
|
|
38
|
+
/** The band with the lowest recall — on both corpora it is not the lowest-confidence one. */
|
|
39
|
+
export declare function worstBin(c: CorpusConfidence): ConfidenceBand;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CONFIDENCE_BY_CORPUS = void 0;
|
|
4
|
+
exports.correlation = correlation;
|
|
5
|
+
exports.worstBin = worstBin;
|
|
6
|
+
/** Measured 2026-08-29 through analyzeMaintenance on each corpus's 30-case clean slice. */
|
|
7
|
+
exports.CONFIDENCE_BY_CORPUS = [
|
|
8
|
+
{
|
|
9
|
+
corpus: 'holmes clean-30', cases: 30, correlationWithRecall: -0.309,
|
|
10
|
+
bands: [
|
|
11
|
+
{ confidence: 0.25, n: 15, meanRecall: 0.751, meanPrecision: 0.133 },
|
|
12
|
+
{ confidence: 0.70, n: 3, meanRecall: 0.306, meanPrecision: 0.100 },
|
|
13
|
+
{ confidence: 0.85, n: 12, meanRecall: 0.542, meanPrecision: 0.133 },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
corpus: 'jarvis clean-30', cases: 30, correlationWithRecall: -0.212,
|
|
18
|
+
bands: [
|
|
19
|
+
{ confidence: 0.25, n: 12, meanRecall: 0.669, meanPrecision: 0.175 },
|
|
20
|
+
{ confidence: 0.65, n: 16, meanRecall: 0.646, meanPrecision: 0.119 },
|
|
21
|
+
{ confidence: 0.85, n: 2, meanRecall: 0.000, meanPrecision: 0.000 },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
/** Pearson correlation, so the number above can be recomputed rather than trusted. */
|
|
26
|
+
function correlation(points) {
|
|
27
|
+
if (points.length < 2)
|
|
28
|
+
return 0;
|
|
29
|
+
const mx = points.reduce((s, p) => s + p.confidence, 0) / points.length;
|
|
30
|
+
const my = points.reduce((s, p) => s + p.recall, 0) / points.length;
|
|
31
|
+
const cov = points.reduce((s, p) => s + (p.confidence - mx) * (p.recall - my), 0);
|
|
32
|
+
const vx = Math.sqrt(points.reduce((s, p) => s + (p.confidence - mx) ** 2, 0));
|
|
33
|
+
const vy = Math.sqrt(points.reduce((s, p) => s + (p.recall - my) ** 2, 0));
|
|
34
|
+
return vx === 0 || vy === 0 ? 0 : cov / (vx * vy);
|
|
35
|
+
}
|
|
36
|
+
/** The band with the lowest recall — on both corpora it is not the lowest-confidence one. */
|
|
37
|
+
function worstBin(c) {
|
|
38
|
+
return [...c.bands].sort((a, b) => a.meanRecall - b.meanRecall)[0];
|
|
39
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A baseline that reads the files.
|
|
3
|
+
*
|
|
4
|
+
* Every advantage claimed in this work compares against a baseline that matches the request's words
|
|
5
|
+
* against symbol names and paths — and never opens a file. A developer with `grep -r` reads bodies.
|
|
6
|
+
* The Goal's Phase B asked for "a strong model using plain file search" as the comparison, and a
|
|
7
|
+
* name-only matcher is weaker than that, which means every margin measured against it may be an
|
|
8
|
+
* artefact of an opponent that was not allowed to look.
|
|
9
|
+
*
|
|
10
|
+
* BM25 over file contents is what grepping approximates: it counts where the request's terms appear
|
|
11
|
+
* anywhere in the text, rewards rare terms, and corrects for length so a long file does not win by
|
|
12
|
+
* containing everything.
|
|
13
|
+
*
|
|
14
|
+
* Built to make the harness's claim HARDER to make. If this beats the shipped pipeline, the
|
|
15
|
+
* advantage recorded in this document is withdrawn.
|
|
16
|
+
*/
|
|
17
|
+
export interface ContentHit {
|
|
18
|
+
file: string;
|
|
19
|
+
score: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ContentIndex {
|
|
22
|
+
/** file -> term -> count */
|
|
23
|
+
termFrequency: Map<string, Map<string, number>>;
|
|
24
|
+
documentFrequency: Map<string, number>;
|
|
25
|
+
averageLength: number;
|
|
26
|
+
lengths: Map<string, number>;
|
|
27
|
+
/** Files that could not be read — disclosed, never counted as empty. */
|
|
28
|
+
unreadable: string[];
|
|
29
|
+
}
|
|
30
|
+
export declare function indexContents(root: string, files: readonly string[]): ContentIndex;
|
|
31
|
+
/**
|
|
32
|
+
* Rank files by BM25 over their contents.
|
|
33
|
+
*
|
|
34
|
+
* The same k1 and b as the spec retriever, untuned — tuning two constants against the corpus they
|
|
35
|
+
* are scored on would make the baseline look worse for a reason that is about the fitting rather
|
|
36
|
+
* than about the method, and this baseline exists to be strong.
|
|
37
|
+
*/
|
|
38
|
+
export declare function rankByContent(issueText: string, index: ContentIndex, topN: number): ContentHit[];
|
|
@@ -0,0 +1,103 @@
|
|
|
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.indexContents = indexContents;
|
|
37
|
+
exports.rankByContent = rankByContent;
|
|
38
|
+
// @implements A-SPEC-403
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const localize_1 = require("../rtm/localize");
|
|
42
|
+
const K1 = 1.2;
|
|
43
|
+
const B = 0.75;
|
|
44
|
+
/** Files above this are read but truncated — a megabyte of minified vendor code is not evidence. */
|
|
45
|
+
const MAX_BYTES = 200_000;
|
|
46
|
+
function indexContents(root, files) {
|
|
47
|
+
const termFrequency = new Map();
|
|
48
|
+
const documentFrequency = new Map();
|
|
49
|
+
const lengths = new Map();
|
|
50
|
+
const unreadable = [];
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
let text;
|
|
53
|
+
try {
|
|
54
|
+
text = fs.readFileSync(path.join(root, file), 'utf8').slice(0, MAX_BYTES);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
unreadable.push(file);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const terms = (0, localize_1.significantTerms)(text);
|
|
61
|
+
const counts = new Map();
|
|
62
|
+
for (const t of terms)
|
|
63
|
+
counts.set(t, (counts.get(t) ?? 0) + 1);
|
|
64
|
+
termFrequency.set(file, counts);
|
|
65
|
+
lengths.set(file, terms.length);
|
|
66
|
+
for (const t of counts.keys())
|
|
67
|
+
documentFrequency.set(t, (documentFrequency.get(t) ?? 0) + 1);
|
|
68
|
+
}
|
|
69
|
+
const total = [...lengths.values()].reduce((a, b) => a + b, 0);
|
|
70
|
+
return {
|
|
71
|
+
termFrequency, documentFrequency, lengths, unreadable,
|
|
72
|
+
averageLength: lengths.size === 0 ? 0 : total / lengths.size,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Rank files by BM25 over their contents.
|
|
77
|
+
*
|
|
78
|
+
* The same k1 and b as the spec retriever, untuned — tuning two constants against the corpus they
|
|
79
|
+
* are scored on would make the baseline look worse for a reason that is about the fitting rather
|
|
80
|
+
* than about the method, and this baseline exists to be strong.
|
|
81
|
+
*/
|
|
82
|
+
function rankByContent(issueText, index, topN) {
|
|
83
|
+
const query = [...new Set((0, localize_1.significantTerms)(issueText))];
|
|
84
|
+
const n = index.lengths.size;
|
|
85
|
+
if (query.length === 0 || n === 0)
|
|
86
|
+
return [];
|
|
87
|
+
const hits = [];
|
|
88
|
+
for (const [file, counts] of index.termFrequency) {
|
|
89
|
+
const len = index.lengths.get(file) ?? 0;
|
|
90
|
+
let score = 0;
|
|
91
|
+
for (const q of query) {
|
|
92
|
+
const f = counts.get(q) ?? 0;
|
|
93
|
+
if (f === 0)
|
|
94
|
+
continue;
|
|
95
|
+
const df = index.documentFrequency.get(q) ?? 0;
|
|
96
|
+
const idf = Math.max(Math.log(1 + (n - df + 0.5) / (df + 0.5)), 0);
|
|
97
|
+
score += idf * (f * (K1 + 1)) / (f + K1 * (1 - B + B * (len / (index.averageLength || 1))));
|
|
98
|
+
}
|
|
99
|
+
if (score > 0)
|
|
100
|
+
hits.push({ file, score });
|
|
101
|
+
}
|
|
102
|
+
return hits.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file)).slice(0, topN);
|
|
103
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-level verification — the first member of the file-CONTENT evidence class, opened
|
|
3
|
+
* after the file-granularity class (graph relations, name-projected embeddings) measured out
|
|
4
|
+
* at ×1.7–×2.5 against a modeled-human ×5.4 (S-497…S-502).
|
|
5
|
+
*
|
|
6
|
+
* The evidence here is what a human verifier actually reads when they open a candidate: does
|
|
7
|
+
* the request's rare vocabulary occur in the body at all, and does the candidate reference the
|
|
8
|
+
* change site by name (or the change site reference the candidate)? The ranker never consumed
|
|
9
|
+
* any of this — it ranked on path + symbol names alone. Bodies come from the MATERIALIZED
|
|
10
|
+
* parent tree only; reading the present tree would leak future content into a point-in-time
|
|
11
|
+
* benchmark.
|
|
12
|
+
*/
|
|
13
|
+
/** Top-k subject terms by IDF, descending, stable, lowercased. Unknown terms weigh 1.0. */
|
|
14
|
+
export declare function topIdfTerms(terms: readonly string[], idf: ReadonlyMap<string, number>, k: number): string[];
|
|
15
|
+
/** How many of the terms occur in the body (substring containment; body pre-lowercased). */
|
|
16
|
+
export declare function bodyHitCount(bodyLower: string, terms: readonly string[]): number;
|
|
17
|
+
/** The head's referenceable names: basename sans extension + symbol names; ≥3 chars, ≤20. */
|
|
18
|
+
export declare function mentionNames(headPath: string, headSymbols: readonly string[]): string[];
|
|
19
|
+
export declare function skipLicenseHeader(text: string): string;
|
|
20
|
+
export declare function mentionsAny(bodyLower: string, names: readonly string[]): boolean;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-483
|
|
3
|
+
/**
|
|
4
|
+
* Content-level verification — the first member of the file-CONTENT evidence class, opened
|
|
5
|
+
* after the file-granularity class (graph relations, name-projected embeddings) measured out
|
|
6
|
+
* at ×1.7–×2.5 against a modeled-human ×5.4 (S-497…S-502).
|
|
7
|
+
*
|
|
8
|
+
* The evidence here is what a human verifier actually reads when they open a candidate: does
|
|
9
|
+
* the request's rare vocabulary occur in the body at all, and does the candidate reference the
|
|
10
|
+
* change site by name (or the change site reference the candidate)? The ranker never consumed
|
|
11
|
+
* any of this — it ranked on path + symbol names alone. Bodies come from the MATERIALIZED
|
|
12
|
+
* parent tree only; reading the present tree would leak future content into a point-in-time
|
|
13
|
+
* benchmark.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.topIdfTerms = topIdfTerms;
|
|
17
|
+
exports.bodyHitCount = bodyHitCount;
|
|
18
|
+
exports.mentionNames = mentionNames;
|
|
19
|
+
exports.skipLicenseHeader = skipLicenseHeader;
|
|
20
|
+
exports.mentionsAny = mentionsAny;
|
|
21
|
+
/** Top-k subject terms by IDF, descending, stable, lowercased. Unknown terms weigh 1.0. */
|
|
22
|
+
function topIdfTerms(terms, idf, k) {
|
|
23
|
+
return terms
|
|
24
|
+
.map((t, i) => ({ t: t.toLowerCase(), w: idf.get(t.toLowerCase()) ?? 1.0, i }))
|
|
25
|
+
.sort((a, b) => b.w - a.w || a.i - b.i)
|
|
26
|
+
.slice(0, k)
|
|
27
|
+
.map((x) => x.t);
|
|
28
|
+
}
|
|
29
|
+
/** How many of the terms occur in the body (substring containment; body pre-lowercased). */
|
|
30
|
+
function bodyHitCount(bodyLower, terms) {
|
|
31
|
+
let n = 0;
|
|
32
|
+
for (const t of terms)
|
|
33
|
+
if (t.length > 0 && bodyLower.includes(t))
|
|
34
|
+
n++;
|
|
35
|
+
return n;
|
|
36
|
+
}
|
|
37
|
+
/** The head's referenceable names: basename sans extension + symbol names; ≥3 chars, ≤20. */
|
|
38
|
+
function mentionNames(headPath, headSymbols) {
|
|
39
|
+
const base = (headPath.split('/').pop() ?? '').replace(/\.[^.]+$/, '').toLowerCase();
|
|
40
|
+
const names = [base, ...headSymbols.map((s) => s.toLowerCase())].filter((n) => n.length >= 3);
|
|
41
|
+
return [...new Set(names)].slice(0, 20);
|
|
42
|
+
}
|
|
43
|
+
// @implements A-SPEC-490 — the excerpt fix S-507 demanded: a 400-char excerpt of a Google-style
|
|
44
|
+
// file is ALL license header, and a judge reading it learns nothing. Only blocks that actually
|
|
45
|
+
// talk about licensing are dropped; an informative leading comment (a Korean docstring) is
|
|
46
|
+
// exactly what made the JARVIS judgments strong and must survive.
|
|
47
|
+
const LICENSE_RE = /license|copyright|spdx/i;
|
|
48
|
+
function skipLicenseHeader(text) {
|
|
49
|
+
let rest = text;
|
|
50
|
+
for (;;) {
|
|
51
|
+
const lead = rest.replace(/^\s*/, '');
|
|
52
|
+
let block = null;
|
|
53
|
+
if (lead.startsWith('/*')) {
|
|
54
|
+
const end = lead.indexOf('*/');
|
|
55
|
+
if (end >= 0)
|
|
56
|
+
block = lead.slice(0, end + 2);
|
|
57
|
+
}
|
|
58
|
+
else if (lead.startsWith('//') || lead.startsWith('#')) {
|
|
59
|
+
const m = lead.match(/^(?:(?:\/\/|#)[^\n]*\n?)+/);
|
|
60
|
+
if (m !== null)
|
|
61
|
+
block = m[0];
|
|
62
|
+
}
|
|
63
|
+
if (block === null || !LICENSE_RE.test(block))
|
|
64
|
+
return rest === text ? text : rest;
|
|
65
|
+
rest = lead.slice(block.length).replace(/^\s*\n/, '');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function mentionsAny(bodyLower, names) {
|
|
69
|
+
for (const n of names)
|
|
70
|
+
if (bodyLower.includes(n))
|
|
71
|
+
return true;
|
|
72
|
+
return false;
|
|
73
|
+
}
|