@holmes-lab/holmes-kit 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/README.md +47 -42
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/doctor.d.ts +1 -0
- package/dist/holmes/cli/doctor.js +56 -0
- package/dist/holmes/cli/init.js +13 -0
- package/dist/holmes/hooks/rtm-refresh-child.js +12 -1
- package/dist/holmes/hooks/session-start.js +22 -0
- package/dist/holmes/hooks/stop.d.ts +5 -0
- package/dist/holmes/hooks/stop.js +48 -1
- package/dist/holmes/mcp/handlers/graph-operations.d.ts +16 -0
- package/dist/holmes/mcp/handlers/graph-operations.js +69 -1
- package/dist/holmes/mcp/handlers/workspace-queries.js +20 -3
- package/dist/holmes/mcp/handlers.d.ts +1 -0
- package/dist/holmes/mcp/maintenance-analyze.js +14 -21
- package/dist/holmes/mcp/tool-schemas.js +1 -1
- package/dist/holmes/review/run-replay.js +8 -5
- package/dist/holmes/semantic/admission.d.ts +32 -0
- package/dist/holmes/semantic/admission.js +61 -0
- package/dist/holmes/semantic/tier-advice.d.ts +18 -0
- package/dist/holmes/semantic/tier-advice.js +102 -0
- package/dist/holmes/semantic/vector-coverage.d.ts +88 -0
- package/dist/holmes/semantic/vector-coverage.js +210 -0
- package/package.json +1 -1
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.MAX_CONSECUTIVE_BLOCKS = void 0;
|
|
37
37
|
exports.collectKnownDefects = collectKnownDefects;
|
|
38
|
+
exports.collectSemanticCoverage = collectSemanticCoverage;
|
|
38
39
|
exports.collectAnalysisCurrency = collectAnalysisCurrency;
|
|
39
40
|
exports.collectDistFreshness = collectDistFreshness;
|
|
40
41
|
exports.collectCiVerdicts = collectCiVerdicts;
|
|
@@ -137,6 +138,36 @@ function collectKnownDefects(root, now) {
|
|
|
137
138
|
// @implements A-SPEC-681 — the I/O half of the analysis line: count the analyses still open against
|
|
138
139
|
// the source files this turn changed. A workspace that never analysed anything did not adopt the
|
|
139
140
|
// discipline and hears nothing; a count that cannot be taken says so rather than passing.
|
|
141
|
+
// @implements A-SPEC-683 — the hook READS; it never computes. Scanning here would pull tree-sitter
|
|
142
|
+
// wasm into a gate process, which A-SPEC-510.2 forbids and which the detached refresh child exists
|
|
143
|
+
// to prevent. So the child writes its verdict and this reads it — and an absent verdict reads as
|
|
144
|
+
// NOT RUN, never as a pass, exactly like a missing CI matrix row.
|
|
145
|
+
function collectSemanticCoverage(root, tier) {
|
|
146
|
+
try {
|
|
147
|
+
// @implements A-SPEC-683 — ADOPTION FIRST. `null` means no verdict file, and a tier-none
|
|
148
|
+
// workspace never produces one: no refresh runs, so nothing is written. Without this check the
|
|
149
|
+
// shipped default — zero egress, never opted in — was told "not run" on EVERY turn about a
|
|
150
|
+
// layer it never asked for. Found by a consumer-shape probe before release, not by the suite,
|
|
151
|
+
// because the not-adopted case was exercised through a verdict object the consumer never has.
|
|
152
|
+
const { resolveSemanticTier } = require('../semantic/tier');
|
|
153
|
+
const t = tier ?? resolveSemanticTier();
|
|
154
|
+
if (t.tier === 'none')
|
|
155
|
+
return '';
|
|
156
|
+
const { readCoverageState, coverageStopLine } = require('../semantic/vector-coverage');
|
|
157
|
+
let head;
|
|
158
|
+
try {
|
|
159
|
+
const { execFileSync } = require('node:child_process');
|
|
160
|
+
head = execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || undefined;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
head = undefined;
|
|
164
|
+
}
|
|
165
|
+
return coverageStopLine(readCoverageState(root), head);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return '';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
140
171
|
function collectAnalysisCurrency(root, changedSources) {
|
|
141
172
|
if (!(0, analysis_currency_1.hasAnalysisDir)(root))
|
|
142
173
|
return undefined;
|
|
@@ -579,6 +610,7 @@ const TRACK_LABELS = {
|
|
|
579
610
|
'CI': 'matrix',
|
|
580
611
|
'DIST': 'build freshness',
|
|
581
612
|
'ANALYSIS': 'graph analysis',
|
|
613
|
+
'SEMANTIC': 'semantic vectors',
|
|
582
614
|
};
|
|
583
615
|
/**
|
|
584
616
|
* One line per ARTICLE, each under its own name.
|
|
@@ -667,6 +699,12 @@ function evaluateStop(specs, evidence) {
|
|
|
667
699
|
if (detail)
|
|
668
700
|
tracked = [...(tracked ?? []), { article: 'ANALYSIS', detail }];
|
|
669
701
|
}
|
|
702
|
+
// @implements A-SPEC-683 — the vector-coverage line rides the same non-blocking channel. It is a
|
|
703
|
+
// STRING here, not a verdict object: the hook read what the refresh child wrote and has nothing
|
|
704
|
+
// to judge. An absent verdict already reads as "not run" inside coverageStopLine.
|
|
705
|
+
if (evidence?.semantic) {
|
|
706
|
+
tracked = [...(tracked ?? []), { article: 'SEMANTIC', detail: evidence.semantic }];
|
|
707
|
+
}
|
|
670
708
|
const problems = violations.map((x) => `[${x.article}] ${x.detail}`);
|
|
671
709
|
// @implements A-SPEC-247 — structured list so the caller can ask acknowledgeStop which of these
|
|
672
710
|
// are waiting on an owner. Mirrors `problems` exactly, including the two synthesized below.
|
|
@@ -1275,7 +1313,16 @@ if (require.main === module) {
|
|
|
1275
1313
|
catch {
|
|
1276
1314
|
analysis = undefined;
|
|
1277
1315
|
}
|
|
1278
|
-
|
|
1316
|
+
// @implements A-SPEC-683 — read-only: the refresh child computed this, the hook only reports.
|
|
1317
|
+
let semantic;
|
|
1318
|
+
try {
|
|
1319
|
+
const s = collectSemanticCoverage(stopProjectRoot());
|
|
1320
|
+
semantic = s === '' ? undefined : s;
|
|
1321
|
+
}
|
|
1322
|
+
catch {
|
|
1323
|
+
semantic = undefined;
|
|
1324
|
+
}
|
|
1325
|
+
let out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable, unanchoredChangedSources: unanchored, unrecordedApprovals: unrecorded, rolledBackLedgers: rolledBack, redFirstMode, changedAspecs, outcomesByAspec, ...(knownDefects ? { knownDefects } : {}), ...(ci ? { ci } : {}), ...(dist ? { dist } : {}), ...(analysis ? { analysis } : {}), ...(semantic ? { semantic } : {}) });
|
|
1279
1326
|
// @implements A-SPEC-534.4 — track mode records ART-8 findings without blocking: surface them so
|
|
1280
1327
|
// the operator observes RED-first gaps before an owner promotes the posture to strict.
|
|
1281
1328
|
// @implements A-SPEC-559.2 — spec-evolution trigger (observe-first, NEVER blocks): a dirty
|
|
@@ -2,6 +2,7 @@ import { Spec } from '../../spec/spec-parser';
|
|
|
2
2
|
import { ScannedFile } from '../../cpg/cpg-scanner';
|
|
3
3
|
import { ChangeSet, ChangeSourceInfo } from '../../project/change-source';
|
|
4
4
|
import { type GraphBasis } from '../../rtm/graph-store';
|
|
5
|
+
import { type PersistedCoverage } from '../../semantic/vector-coverage';
|
|
5
6
|
export interface GraphOperationsContext {
|
|
6
7
|
listSpecs(): Promise<Spec[]>;
|
|
7
8
|
assertStoreReachable(tool: string, root: unknown): void;
|
|
@@ -48,6 +49,20 @@ export type SemanticWarmOutcome = {
|
|
|
48
49
|
* could not embed — a discharge must not record the last one as done.
|
|
49
50
|
*/
|
|
50
51
|
export declare function warmSemanticCache(root: string, scanned: readonly ScannedFile[]): Promise<SemanticWarmOutcome>;
|
|
52
|
+
/**
|
|
53
|
+
* @implements A-SPEC-683 — vectors refresh on the channel that keeps the graph fresh.
|
|
54
|
+
*
|
|
55
|
+
* The graph self-heals through the Stop hook's detached child; the vectors did not, because
|
|
56
|
+
* `warmSemanticCache` was reachable only from `rtm_reindex`. The cache key is a hash of the
|
|
57
|
+
* document text and that text is the file's path plus its symbol names, so every edit that adds or
|
|
58
|
+
* renames a symbol invalidates that file's vector — measured 2026-09-19, twelve days of work had
|
|
59
|
+
* taken coverage to 0 of 602 while 532 stale vectors sat in the cache answering nothing.
|
|
60
|
+
*
|
|
61
|
+
* Warming under the cloud tier is EGRESS. The owner decided it should be automatic AND observed,
|
|
62
|
+
* with a switch; silent automatic transfer was considered and refused. `HOLMES_NO_SEMANTIC_WARM`
|
|
63
|
+
* records SUPPRESSED rather than a failure, so an owner who turned it off is never shown a fault.
|
|
64
|
+
*/
|
|
65
|
+
export declare function refreshSemanticVectors(root: string, env?: NodeJS.ProcessEnv, warm?: (r: string, s: readonly ScannedFile[]) => Promise<SemanticWarmOutcome>, scan?: () => readonly ScannedFile[]): Promise<PersistedCoverage | null>;
|
|
51
66
|
export declare function createGraphOperationsHandlers(context: GraphOperationsContext): {
|
|
52
67
|
rtm_impact(a: {
|
|
53
68
|
root: string;
|
|
@@ -82,6 +97,7 @@ export declare function createGraphOperationsHandlers(context: GraphOperationsCo
|
|
|
82
97
|
tier: string;
|
|
83
98
|
computed: number;
|
|
84
99
|
cached: number;
|
|
100
|
+
stored?: number;
|
|
85
101
|
} | undefined;
|
|
86
102
|
changed: number;
|
|
87
103
|
nodes: number;
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.publishCurrentGraph = publishCurrentGraph;
|
|
37
37
|
exports.warmSemanticCache = warmSemanticCache;
|
|
38
|
+
exports.refreshSemanticVectors = refreshSemanticVectors;
|
|
38
39
|
exports.createGraphOperationsHandlers = createGraphOperationsHandlers;
|
|
39
40
|
// @implements A-SPEC-614, A-SPEC-613, A-SPEC-100.2, A-SPEC-121.2, A-SPEC-128, A-SPEC-139, A-SPEC-189, A-SPEC-280, A-SPEC-282, A-SPEC-283, A-SPEC-419, A-SPEC-433, A-SPEC-469, A-SPEC-478, A-SPEC-568.1, A-SPEC-568.2, A-SPEC-569.1, A-SPEC-569.2, A-SPEC-569.3, A-SPEC-589
|
|
40
41
|
// @implements A-SPEC-632
|
|
@@ -50,6 +51,7 @@ const trace_gaps_1 = require("../../rtm/trace-gaps");
|
|
|
50
51
|
const tier_1 = require("../../semantic/tier");
|
|
51
52
|
const vector_cache_1 = require("../../semantic/vector-cache");
|
|
52
53
|
const embedder_1 = require("../../semantic/embedder");
|
|
54
|
+
const vector_coverage_1 = require("../../semantic/vector-coverage");
|
|
53
55
|
// @implements A-SPEC-283 — bumped whenever the graph's node/edge shape changes, so a store written
|
|
54
56
|
// by an older build is rebuilt rather than read with new assumptions.
|
|
55
57
|
// @implements A-SPEC-568.1 — /3: nodes gained the intent `summary` column.
|
|
@@ -92,12 +94,78 @@ async function warmSemanticCache(root, scanned) {
|
|
|
92
94
|
const rt = (0, embedder_1.makeTierEmbedder)(tier, new vector_cache_1.VectorCache(root));
|
|
93
95
|
if (rt === null)
|
|
94
96
|
return { status: 'unavailable', tier: configured };
|
|
95
|
-
|
|
97
|
+
// @implements A-SPEC-683 — the WRITER keys through the shared definition. If the writer's key
|
|
98
|
+
// ever differs from the readers', every lookup misses for ever while both sides look correct.
|
|
99
|
+
const docTexts = scanned.map((f) => (0, vector_coverage_1.docTextOf)(f));
|
|
96
100
|
const w = await rt.warm(docTexts);
|
|
97
101
|
if (docTexts.length > 0 && w.computed + w.cached === 0)
|
|
98
102
|
return { status: 'unavailable', tier: rt.label };
|
|
103
|
+
// @implements A-SPEC-683 — `stored` is measured by refreshSemanticVectors, NOT returned here.
|
|
104
|
+
// This outcome flows into entity_integrate's discharge record, a PERSISTED artifact whose shape
|
|
105
|
+
// A-SPEC-632 pins by toEqual and validates by RECORD_KEYS; widening it is a persisted-artifact
|
|
106
|
+
// breaking change this slice never declared. The suite refused the first attempt and was right.
|
|
99
107
|
return { status: 'discharged', tier: rt.label, computed: w.computed, cached: w.cached };
|
|
100
108
|
}
|
|
109
|
+
/** The commit the verdict describes. null when this is not a Git tree — never a fabricated value. */
|
|
110
|
+
function headOf(root) {
|
|
111
|
+
try {
|
|
112
|
+
const { execFileSync } = require('node:child_process');
|
|
113
|
+
return execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || null;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* @implements A-SPEC-683 — vectors refresh on the channel that keeps the graph fresh.
|
|
121
|
+
*
|
|
122
|
+
* The graph self-heals through the Stop hook's detached child; the vectors did not, because
|
|
123
|
+
* `warmSemanticCache` was reachable only from `rtm_reindex`. The cache key is a hash of the
|
|
124
|
+
* document text and that text is the file's path plus its symbol names, so every edit that adds or
|
|
125
|
+
* renames a symbol invalidates that file's vector — measured 2026-09-19, twelve days of work had
|
|
126
|
+
* taken coverage to 0 of 602 while 532 stale vectors sat in the cache answering nothing.
|
|
127
|
+
*
|
|
128
|
+
* Warming under the cloud tier is EGRESS. The owner decided it should be automatic AND observed,
|
|
129
|
+
* with a switch; silent automatic transfer was considered and refused. `HOLMES_NO_SEMANTIC_WARM`
|
|
130
|
+
* records SUPPRESSED rather than a failure, so an owner who turned it off is never shown a fault.
|
|
131
|
+
*/
|
|
132
|
+
async function refreshSemanticVectors(root, env = process.env, warm = warmSemanticCache, scan) {
|
|
133
|
+
const tier = (0, tier_1.resolveSemanticTier)();
|
|
134
|
+
if (tier.tier === 'none')
|
|
135
|
+
return null; // never opted in: nothing transferred, nothing said
|
|
136
|
+
const at = new Date().toISOString();
|
|
137
|
+
const scanned = scan ? scan() : [];
|
|
138
|
+
const off = typeof env.HOLMES_NO_SEMANTIC_WARM === 'string' && env.HOLMES_NO_SEMANTIC_WARM !== '';
|
|
139
|
+
const head = headOf(root);
|
|
140
|
+
if (off) {
|
|
141
|
+
// Still MEASURE — the owner turned off the transfer, not the observation.
|
|
142
|
+
const rt = (0, embedder_1.makeTierEmbedder)(tier, new vector_cache_1.VectorCache(root));
|
|
143
|
+
const covered = rt === null ? 0 : (0, vector_coverage_1.countCovered)(scanned, (t) => rt.cachedDocVector(t) !== null);
|
|
144
|
+
const state = {
|
|
145
|
+
coverage: (0, vector_coverage_1.vectorCoverage)({ tier: tier.tier, modelTag: rt?.label ?? null, total: scanned.length, covered }),
|
|
146
|
+
reason: 'suppressed', at, ...(head !== null ? { head } : {}),
|
|
147
|
+
};
|
|
148
|
+
(0, vector_coverage_1.writeCoverageState)(root, state);
|
|
149
|
+
return state;
|
|
150
|
+
}
|
|
151
|
+
const outcome = await warm(root, scanned); // throws leave the previous verdict untouched
|
|
152
|
+
if (outcome.status !== 'discharged')
|
|
153
|
+
return null;
|
|
154
|
+
// What LANDED, not what was attempted. Measured 2026-09-19 the warm answered `computed: 603`
|
|
155
|
+
// while 26 vectors were never stored; a count of attempts reported as a success is the same
|
|
156
|
+
// class of dishonesty as a tier that says PASS while covering nothing.
|
|
157
|
+
const rt = (0, embedder_1.makeTierEmbedder)(tier, new vector_cache_1.VectorCache(root));
|
|
158
|
+
const stored = rt === null ? 0 : (0, vector_coverage_1.countCovered)(scanned, (t) => rt.cachedDocVector(t) !== null);
|
|
159
|
+
const state = {
|
|
160
|
+
coverage: (0, vector_coverage_1.vectorCoverage)({
|
|
161
|
+
tier: tier.tier, modelTag: outcome.tier, total: scanned.length, covered: stored,
|
|
162
|
+
}),
|
|
163
|
+
reason: 'warmed', at, stored,
|
|
164
|
+
...(headOf(root) !== null ? { head: headOf(root) } : {}),
|
|
165
|
+
};
|
|
166
|
+
(0, vector_coverage_1.writeCoverageState)(root, state);
|
|
167
|
+
return state;
|
|
168
|
+
}
|
|
101
169
|
function createGraphOperationsHandlers(context) {
|
|
102
170
|
return {
|
|
103
171
|
async rtm_impact(a) {
|
|
@@ -51,6 +51,9 @@ const embedder_1 = require("../../semantic/embedder");
|
|
|
51
51
|
// @implements A-SPEC-496 — policy parity: the direct localization path reranks with the exact
|
|
52
52
|
// A-SPEC-478 math, extracted pure.
|
|
53
53
|
const hit_rerank_1 = require("../../semantic/hit-rerank");
|
|
54
|
+
// @implements A-SPEC-682 — the shared document-text definition (see vector-coverage.ts).
|
|
55
|
+
const vector_coverage_1 = require("../../semantic/vector-coverage");
|
|
56
|
+
const admission_1 = require("../../semantic/admission");
|
|
54
57
|
function createWorkspaceQueryHandlers(context) {
|
|
55
58
|
return {
|
|
56
59
|
async rtm_check(a) {
|
|
@@ -177,12 +180,26 @@ function createWorkspaceQueryHandlers(context) {
|
|
|
177
180
|
const rt = (0, embedder_1.makeTierEmbedder)((0, tier_1.resolveSemanticTier)(), new vector_cache_1.VectorCache(root));
|
|
178
181
|
if (rt !== null) {
|
|
179
182
|
const qv = await rt.embedQuery(a.issue);
|
|
180
|
-
const
|
|
181
|
-
(f.sourcePath + ' ' + f.symbols.map((sy) => sy.qualifiedName).join(' ')).slice(0, 2000)]));
|
|
183
|
+
const docTextByFile = (0, vector_coverage_1.docTextIndex)(scanned);
|
|
182
184
|
(0, hit_rerank_1.rerankHitsBySemantic)(report.hits, qv, (file) => {
|
|
183
|
-
const dt =
|
|
185
|
+
const dt = docTextByFile.get(file);
|
|
184
186
|
return dt !== undefined ? rt.cachedDocVector(dt) : null;
|
|
185
187
|
}, rt.label);
|
|
188
|
+
// @implements A-SPEC-684 — and ADMIT what the lexical pass never produced. The rerank
|
|
189
|
+
// above can only move rows inside `report.hits`; measured 2026-09-19, asking who opens
|
|
190
|
+
// a URL in the user's browser ranked `LocalMarkdownRepository` first on the word
|
|
191
|
+
// "repository" and never returned `open-url.ts`, which the graph held throughout.
|
|
192
|
+
//
|
|
193
|
+
// PURE ADDITION, on its own field: the admitted rows do not enter `hits`, because this
|
|
194
|
+
// repository measured that precision is lost by admitting candidates into a ranked set.
|
|
195
|
+
// Cloud-only, matching A-SPEC-494's bound, and cached lookups only — no embed here.
|
|
196
|
+
if (rt.label.startsWith('cloud:')) {
|
|
197
|
+
const emitted = new Set(report.hits.map((h) => h.file));
|
|
198
|
+
const alts = (0, admission_1.semanticAdmissions)(scanned, emitted, qv, (dt) => rt.cachedDocVector(dt), 3);
|
|
199
|
+
if (alts.length > 0) {
|
|
200
|
+
report.semanticAlternates = alts;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
186
203
|
}
|
|
187
204
|
}
|
|
188
205
|
}
|
|
@@ -21,6 +21,10 @@ exports.boundAnalysis = boundAnalysis;
|
|
|
21
21
|
// @implements A-SPEC-267
|
|
22
22
|
const crypto_1 = require("crypto");
|
|
23
23
|
const localize_1 = require("../rtm/localize");
|
|
24
|
+
// @implements A-SPEC-682 — one definition of the document text; the instrument and these
|
|
25
|
+
// lookups must key identically or the coverage reported is about nothing.
|
|
26
|
+
const vector_coverage_1 = require("../semantic/vector-coverage");
|
|
27
|
+
const admission_1 = require("../semantic/admission");
|
|
24
28
|
const dense_retrieval_1 = require("../review/dense-retrieval");
|
|
25
29
|
const test_scope_1 = require("../rtm/test-scope");
|
|
26
30
|
const scope_1 = require("../review/scope");
|
|
@@ -439,12 +443,11 @@ function analyzeMaintenance(input) {
|
|
|
439
443
|
const sem = input.semantic;
|
|
440
444
|
const citedForSem = (0, localize_1.citationsIn)(input.request, new Set(specs.map((sp) => sp.id))).cited;
|
|
441
445
|
if (sem !== undefined && sem.queryVector !== null && citedForSem.length === 0 && candidates.length > 1) {
|
|
442
|
-
const
|
|
443
|
-
(f.sourcePath + ' ' + f.symbols.map((sy) => sy.qualifiedName).join(' ')).slice(0, 2000)]));
|
|
446
|
+
const docTextByFile = (0, vector_coverage_1.docTextIndex)(scanned);
|
|
444
447
|
const sim = new Map();
|
|
445
448
|
let covered = 0;
|
|
446
449
|
for (const c of candidates) {
|
|
447
|
-
const dt =
|
|
450
|
+
const dt = docTextByFile.get(c.file);
|
|
448
451
|
const dv = dt !== undefined ? sem.cachedDocVector(dt) : null;
|
|
449
452
|
if (dv !== null) {
|
|
450
453
|
sim.set(c.file, (0, dense_retrieval_1.cosine)(sem.queryVector, dv));
|
|
@@ -469,10 +472,9 @@ function analyzeMaintenance(input) {
|
|
|
469
472
|
{
|
|
470
473
|
const sem = input.semantic;
|
|
471
474
|
if (sem !== undefined && sem.queryVector !== null && sem.label.startsWith('cloud:')) {
|
|
472
|
-
const
|
|
473
|
-
(f.sourcePath + ' ' + f.symbols.map((sy) => sy.qualifiedName).join(' ')).slice(0, 2000)]));
|
|
475
|
+
const docTextByFile = (0, vector_coverage_1.docTextIndex)(scanned);
|
|
474
476
|
for (const c of candidates) {
|
|
475
|
-
const dt =
|
|
477
|
+
const dt = docTextByFile.get(c.file);
|
|
476
478
|
const dv = dt !== undefined ? sem.cachedDocVector(dt) : null;
|
|
477
479
|
if (dv === null)
|
|
478
480
|
continue;
|
|
@@ -599,10 +601,9 @@ function analyzeMaintenance(input) {
|
|
|
599
601
|
{
|
|
600
602
|
const sem = input.semantic;
|
|
601
603
|
if (sem !== undefined && sem.queryVector !== null && sem.label.startsWith('cloud:')) {
|
|
602
|
-
const
|
|
603
|
-
(f.sourcePath + ' ' + f.symbols.map((sy) => sy.qualifiedName).join(' ')).slice(0, 2000)]));
|
|
604
|
+
const docTextByFile = (0, vector_coverage_1.docTextIndex)(scanned);
|
|
604
605
|
for (const row of rankedImpact) {
|
|
605
|
-
const dt =
|
|
606
|
+
const dt = docTextByFile.get(row.file);
|
|
606
607
|
const dv = dt !== undefined ? sem.cachedDocVector(dt) : null;
|
|
607
608
|
if (dv !== null)
|
|
608
609
|
row.semCos = Math.round((0, dense_retrieval_1.cosine)(sem.queryVector, dv) * 1e4) / 1e4;
|
|
@@ -615,20 +616,12 @@ function analyzeMaintenance(input) {
|
|
|
615
616
|
{
|
|
616
617
|
const sem = input.semantic;
|
|
617
618
|
if (sem !== undefined && sem.queryVector !== null && sem.label.startsWith('cloud:')) {
|
|
619
|
+
// @implements A-SPEC-684 — the mechanism moved to one shared function so `issue_localize`
|
|
620
|
+
// could have it too. Behaviour here is unchanged; only the definition's address moved.
|
|
618
621
|
const emitted = new Set([...candidates.map((c) => c.file), ...rankedImpact.map((r) => r.file)]);
|
|
619
|
-
const scoredAlt =
|
|
620
|
-
for (const f of scanned) {
|
|
621
|
-
if (emitted.has(f.sourcePath))
|
|
622
|
-
continue;
|
|
623
|
-
const dt = (f.sourcePath + ' ' + f.symbols.map((sy) => sy.qualifiedName).join(' ')).slice(0, 2000);
|
|
624
|
-
const dv = sem.cachedDocVector(dt);
|
|
625
|
-
if (dv === null)
|
|
626
|
-
continue;
|
|
627
|
-
scoredAlt.push({ file: f.sourcePath, cos: Math.round((0, dense_retrieval_1.cosine)(sem.queryVector, dv) * 1e4) / 1e4 });
|
|
628
|
-
}
|
|
629
|
-
scoredAlt.sort((a, b) => b.cos - a.cos);
|
|
622
|
+
const scoredAlt = (0, admission_1.semanticAdmissions)(scanned, emitted, sem.queryVector, (t) => sem.cachedDocVector(t), 3);
|
|
630
623
|
if (scoredAlt.length > 0)
|
|
631
|
-
semanticAlternates = scoredAlt
|
|
624
|
+
semanticAlternates = scoredAlt;
|
|
632
625
|
}
|
|
633
626
|
}
|
|
634
627
|
// @implements A-SPEC-274 — the prediction is the RANKED set. The closure's files are reported on
|
|
@@ -308,7 +308,7 @@ exports.TOOL_SCHEMAS = {
|
|
|
308
308
|
},
|
|
309
309
|
},
|
|
310
310
|
issue_localize: {
|
|
311
|
-
description: 'N1 localization: free-text issue -> structured, ranked candidate locations (files+symbols+evidence) fusing CPG lexical match with the RTM spec link ({ terms, hits, matchedSpecs }).',
|
|
311
|
+
description: 'N1 localization: free-text issue -> structured, ranked candidate locations (files+symbols+evidence) fusing CPG lexical match with the RTM spec link ({ terms, hits, matchedSpecs }). @implements A-SPEC-684 — also read `semanticAlternates`: the top cached-vector matches among files the LEXICAL pass never returned, which the ranked `hits` by construction cannot contain. Measured on this repository, a question phrased in intent vocabulary ("who opens a URL in the browser") ranked an unrelated file first on a shared prose word and never returned the answer, while the same need in mechanism vocabulary found it — the alternates are the channel that crosses that gap. Additive evidence (cloud tier only, cached lookups): the hits are untouched by it, and selection stays with the consumer. Absent when the gate is shut or nothing off-emission is covered.',
|
|
312
312
|
inputSchema: {
|
|
313
313
|
type: 'object',
|
|
314
314
|
properties: {
|
|
@@ -68,6 +68,7 @@ const union_verify_1 = require("./union-verify");
|
|
|
68
68
|
// @implements A-SPEC-480 — coherence verification: the emission's internal graph relations.
|
|
69
69
|
const coherence_verify_1 = require("./coherence-verify");
|
|
70
70
|
const localize_1 = require("../rtm/localize");
|
|
71
|
+
const vector_coverage_1 = require("../semantic/vector-coverage");
|
|
71
72
|
// @implements A-SPEC-483 — content-level verification: the candidate's BODY, read from the
|
|
72
73
|
// materialized parent tree.
|
|
73
74
|
const content_verify_1 = require("./content-verify");
|
|
@@ -323,8 +324,10 @@ async function runReplay(corpus, limit, opts = {}) {
|
|
|
323
324
|
if (opts.caseDump !== undefined) {
|
|
324
325
|
let dumpResult = result;
|
|
325
326
|
if (opts.productSemantic !== undefined) {
|
|
326
|
-
|
|
327
|
-
|
|
327
|
+
// @implements A-SPEC-685 — the SHARED key. A benchmark that keys differently from
|
|
328
|
+
// the product scores a pipeline the product does not run, and this repository has
|
|
329
|
+
// already had an adoption verdict reversed by that class of instrument defect.
|
|
330
|
+
const dumpDocText = (0, vector_coverage_1.docTextIndex)(scanned);
|
|
328
331
|
const targets = [...new Set([...result.candidates.map((x) => x.file),
|
|
329
332
|
...(result.impacts?.rankedImpact ?? []).map((r) => r.file)])];
|
|
330
333
|
const texts = targets.map((f) => dumpDocText.get(f)).filter((t) => t !== undefined);
|
|
@@ -580,9 +583,9 @@ async function runReplay(corpus, limit, opts = {}) {
|
|
|
580
583
|
// A candidate the signal cannot see (no doc text, no vector) is KEPT: recall is spent only
|
|
581
584
|
// where the verdict actually spoke.
|
|
582
585
|
if (opts.unionVerify !== undefined && unionList.length > 0) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const known = unionList.map((f, i) => ({ text:
|
|
586
|
+
// @implements A-SPEC-685 — the SHARED key (see the dump arm above for why).
|
|
587
|
+
const docTextByFile = (0, vector_coverage_1.docTextIndex)(scanned);
|
|
588
|
+
const known = unionList.map((f, i) => ({ text: docTextByFile.get(f), i })).filter((x) => x.text !== undefined);
|
|
586
589
|
const [qv] = await opts.unionVerify.embedBatch([c.subject], 'query');
|
|
587
590
|
const dvs = known.length > 0
|
|
588
591
|
? await opts.unionVerify.embedBatch(known.map((x) => x.text), 'doc') : [];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The semantic layer ADMITS — it does not only reorder.
|
|
3
|
+
*
|
|
4
|
+
* `rerankHitsBySemantic` moves rows inside the array it is handed, so a candidate the lexical pass
|
|
5
|
+
* never produced can never appear however similar it is. Measured 2026-09-19 with the cloud tier
|
|
6
|
+
* live and the vectors warm: asking who opens a URL in the user's browser returned
|
|
7
|
+
* `LocalMarkdownRepository` at 39.97 — the prose contained the word "repository" — and
|
|
8
|
+
* `open-url.ts` was absent, while the graph held `CODE:openUrl@src/holmes/cli/open-url.ts` the
|
|
9
|
+
* whole time. Asking the same need in mechanism vocabulary returned `Supervisor.spawnChild` at
|
|
10
|
+
* rank 4. Same graph, same tool, same second; the only variable was the question's vocabulary.
|
|
11
|
+
*
|
|
12
|
+
* This is the mechanism `maintenance_analyze` already had (A-SPEC-494) and `issue_localize` did
|
|
13
|
+
* not. It lives here once so the two cannot drift.
|
|
14
|
+
*
|
|
15
|
+
* It is PURE ADDITION. The admitted rows never enter a ranked set: this repository measured that
|
|
16
|
+
* precision is lost by admitting candidates into one (7.09 -> 9.27 candidates), and practitioners
|
|
17
|
+
* reject inspecting more than about five things. Reordering what was found and surfacing what was
|
|
18
|
+
* missed are different jobs, reported separately.
|
|
19
|
+
*/
|
|
20
|
+
import { type ScannedLike } from './vector-coverage';
|
|
21
|
+
export interface Admitted {
|
|
22
|
+
file: string;
|
|
23
|
+
cos: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The top `limit` scanned files that the emission MISSED, by cosine against the query.
|
|
27
|
+
*
|
|
28
|
+
* `docVecOf` is injected and is a cached LOOKUP, never an embed: this runs on a ranking path and
|
|
29
|
+
* must not reach the network. A file with no cached vector is skipped rather than scored zero —
|
|
30
|
+
* scoring it would let an unjudged file occupy a slot a genuinely similar file needs.
|
|
31
|
+
*/
|
|
32
|
+
export declare function semanticAdmissions(scanned: readonly ScannedLike[], emitted: ReadonlySet<string>, queryVec: readonly number[] | null, docVecOf: (docText: string) => readonly number[] | null, limit: number): Admitted[];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.semanticAdmissions = semanticAdmissions;
|
|
4
|
+
// @implements A-SPEC-684
|
|
5
|
+
/**
|
|
6
|
+
* The semantic layer ADMITS — it does not only reorder.
|
|
7
|
+
*
|
|
8
|
+
* `rerankHitsBySemantic` moves rows inside the array it is handed, so a candidate the lexical pass
|
|
9
|
+
* never produced can never appear however similar it is. Measured 2026-09-19 with the cloud tier
|
|
10
|
+
* live and the vectors warm: asking who opens a URL in the user's browser returned
|
|
11
|
+
* `LocalMarkdownRepository` at 39.97 — the prose contained the word "repository" — and
|
|
12
|
+
* `open-url.ts` was absent, while the graph held `CODE:openUrl@src/holmes/cli/open-url.ts` the
|
|
13
|
+
* whole time. Asking the same need in mechanism vocabulary returned `Supervisor.spawnChild` at
|
|
14
|
+
* rank 4. Same graph, same tool, same second; the only variable was the question's vocabulary.
|
|
15
|
+
*
|
|
16
|
+
* This is the mechanism `maintenance_analyze` already had (A-SPEC-494) and `issue_localize` did
|
|
17
|
+
* not. It lives here once so the two cannot drift.
|
|
18
|
+
*
|
|
19
|
+
* It is PURE ADDITION. The admitted rows never enter a ranked set: this repository measured that
|
|
20
|
+
* precision is lost by admitting candidates into one (7.09 -> 9.27 candidates), and practitioners
|
|
21
|
+
* reject inspecting more than about five things. Reordering what was found and surfacing what was
|
|
22
|
+
* missed are different jobs, reported separately.
|
|
23
|
+
*/
|
|
24
|
+
const vector_coverage_1 = require("./vector-coverage");
|
|
25
|
+
function cosine(a, b) {
|
|
26
|
+
let dot = 0, na = 0, nb = 0;
|
|
27
|
+
const n = Math.min(a.length, b.length);
|
|
28
|
+
for (let i = 0; i < n; i++) {
|
|
29
|
+
dot += a[i] * b[i];
|
|
30
|
+
na += a[i] * a[i];
|
|
31
|
+
nb += b[i] * b[i];
|
|
32
|
+
}
|
|
33
|
+
if (na === 0 || nb === 0)
|
|
34
|
+
return 0;
|
|
35
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The top `limit` scanned files that the emission MISSED, by cosine against the query.
|
|
39
|
+
*
|
|
40
|
+
* `docVecOf` is injected and is a cached LOOKUP, never an embed: this runs on a ranking path and
|
|
41
|
+
* must not reach the network. A file with no cached vector is skipped rather than scored zero —
|
|
42
|
+
* scoring it would let an unjudged file occupy a slot a genuinely similar file needs.
|
|
43
|
+
*/
|
|
44
|
+
function semanticAdmissions(scanned, emitted, queryVec, docVecOf, limit) {
|
|
45
|
+
if (queryVec === null || !Number.isFinite(limit) || limit <= 0)
|
|
46
|
+
return [];
|
|
47
|
+
const scored = [];
|
|
48
|
+
for (const f of scanned) {
|
|
49
|
+
if (emitted.has(f.sourcePath))
|
|
50
|
+
continue;
|
|
51
|
+
// The SHARED document text. A local copy here would key on something the writer never stored,
|
|
52
|
+
// and every admission would silently miss — the failure REQ-682 found in eight places.
|
|
53
|
+
const dv = docVecOf((0, vector_coverage_1.docTextOf)(f));
|
|
54
|
+
if (dv === null)
|
|
55
|
+
continue;
|
|
56
|
+
scored.push({ file: f.sourcePath, cos: Math.round(cosine(queryVec, dv) * 1e4) / 1e4 });
|
|
57
|
+
}
|
|
58
|
+
// Deterministic under ties: cosine descending, then path, so two runs over the same tree agree.
|
|
59
|
+
scored.sort((x, y) => (y.cos - x.cos) || (x.file < y.file ? -1 : x.file > y.file ? 1 : 0));
|
|
60
|
+
return scored.slice(0, Math.floor(limit));
|
|
61
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** One spelling, shared by the writer and the reader — they cannot disagree about where it lives. */
|
|
2
|
+
export declare const INVITE_MARKER: readonly [".ax", "state", "semantic-invite.json"];
|
|
3
|
+
/** Empty when a tier is already configured: that consumer has decided, and deciding earns silence. */
|
|
4
|
+
export declare function tierAdviceLines(tier: {
|
|
5
|
+
tier: string;
|
|
6
|
+
}): string[];
|
|
7
|
+
/**
|
|
8
|
+
* True only for a consumer who has not decided AND has not been told.
|
|
9
|
+
*
|
|
10
|
+
* A throwing `exists` reads as "not invited yet" ON PURPOSE: hearing the invitation twice is a
|
|
11
|
+
* smaller harm than a consumer who never hears it, and a failed stat must not silently suppress a
|
|
12
|
+
* whole feature's existence.
|
|
13
|
+
*/
|
|
14
|
+
export declare function shouldInvite(tier: {
|
|
15
|
+
tier: string;
|
|
16
|
+
}, root: string, exists: (p: string) => boolean): boolean;
|
|
17
|
+
/** Records the invitation. A failure leaves it delivered and unrecorded — it may appear once more. */
|
|
18
|
+
export declare function markInvited(root: string, write: (p: string, data: string) => void): void;
|
|
@@ -0,0 +1,102 @@
|
|
|
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.INVITE_MARKER = void 0;
|
|
37
|
+
exports.tierAdviceLines = tierAdviceLines;
|
|
38
|
+
exports.shouldInvite = shouldInvite;
|
|
39
|
+
exports.markInvited = markInvited;
|
|
40
|
+
// @implements A-SPEC-686
|
|
41
|
+
/**
|
|
42
|
+
* The invitation. Holmes-Kit's semantic layer is measured and the measurement is good; a consumer
|
|
43
|
+
* never learns it exists.
|
|
44
|
+
*
|
|
45
|
+
* Probed 2026-09-19 against a tarball install: `init` announces the guardrail mode, the wired
|
|
46
|
+
* harness, the recovery skills and the role policy, and says nothing about the tier. The session
|
|
47
|
+
* banner says the version and the governance rule. Only `doctor` names it — and `doctor` already
|
|
48
|
+
* names it WELL, stating the default and what it costs. The gap was never the wording. It was that
|
|
49
|
+
* a consumer who does not run `doctor` never meets the sentence.
|
|
50
|
+
*
|
|
51
|
+
* Two rules shape the text.
|
|
52
|
+
*
|
|
53
|
+
* **Local comes first.** `none` is the shipped default because egress needs consent, not because
|
|
54
|
+
* nobody got to it. `local` buys most of the distance with nothing leaving the machine; `cloud` is
|
|
55
|
+
* the further step and must say what it sends. An invitation that led with cloud would contradict
|
|
56
|
+
* the posture the product deliberately holds.
|
|
57
|
+
*
|
|
58
|
+
* **The numbers are this corpus's.** 305 traceability cases measured here. They are quoted as
|
|
59
|
+
* measured, never promised for the reader's repository.
|
|
60
|
+
*/
|
|
61
|
+
const path = __importStar(require("node:path"));
|
|
62
|
+
/** One spelling, shared by the writer and the reader — they cannot disagree about where it lives. */
|
|
63
|
+
exports.INVITE_MARKER = ['.ax', 'state', 'semantic-invite.json'];
|
|
64
|
+
const markerPath = (root) => path.join(root, ...exports.INVITE_MARKER);
|
|
65
|
+
/** Empty when a tier is already configured: that consumer has decided, and deciding earns silence. */
|
|
66
|
+
function tierAdviceLines(tier) {
|
|
67
|
+
if (tier.tier !== 'none' && tier.tier !== '')
|
|
68
|
+
return [];
|
|
69
|
+
return [
|
|
70
|
+
'Semantic layer: tier none — the shipped default, and zero egress.',
|
|
71
|
+
' Measured here on 305 traceability cases: recall 0.486 lexical -> 0.667 local -> 0.887 cloud;',
|
|
72
|
+
' on requests lexical search misses entirely, recovery 0% -> 52% -> 92%.',
|
|
73
|
+
' local no egress, nothing leaves this machine holmes-kit semantic-setup',
|
|
74
|
+
' cloud sends spec prose, file paths and symbol names to an external service',
|
|
75
|
+
' holmes-kit semantic-key set (setting the key IS the consent to that transfer)',
|
|
76
|
+
' Neither is required. `holmes-kit doctor` reports which tier is live.',
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* True only for a consumer who has not decided AND has not been told.
|
|
81
|
+
*
|
|
82
|
+
* A throwing `exists` reads as "not invited yet" ON PURPOSE: hearing the invitation twice is a
|
|
83
|
+
* smaller harm than a consumer who never hears it, and a failed stat must not silently suppress a
|
|
84
|
+
* whole feature's existence.
|
|
85
|
+
*/
|
|
86
|
+
function shouldInvite(tier, root, exists) {
|
|
87
|
+
if (tier.tier !== 'none' && tier.tier !== '')
|
|
88
|
+
return false;
|
|
89
|
+
try {
|
|
90
|
+
return !exists(markerPath(root));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Records the invitation. A failure leaves it delivered and unrecorded — it may appear once more. */
|
|
97
|
+
function markInvited(root, write) {
|
|
98
|
+
try {
|
|
99
|
+
write(markerPath(root), JSON.stringify({ invitedAt: new Date().toISOString() }) + '\n');
|
|
100
|
+
}
|
|
101
|
+
catch { /* a banner is never a gate, and neither is its bookkeeping */ }
|
|
102
|
+
}
|