@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.
@@ -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
- 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 } : {}) });
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
- const docTexts = scanned.map((f) => (f.sourcePath + ' ' + f.symbols.map((sy) => sy.qualifiedName).join(' ')).slice(0, 2000));
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 docTextOf = new Map(scanned.map((f) => [f.sourcePath,
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 = docTextOf.get(file);
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
  }
@@ -428,6 +428,7 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
428
428
  tier: string;
429
429
  computed: number;
430
430
  cached: number;
431
+ stored?: number;
431
432
  } | undefined;
432
433
  changed: number;
433
434
  nodes: number;
@@ -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 docTextOf = new Map(scanned.map((f) => [f.sourcePath,
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 = docTextOf.get(c.file);
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 docTextOf = new Map(scanned.map((f) => [f.sourcePath,
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 = docTextOf.get(c.file);
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 docTextOf = new Map(scanned.map((f) => [f.sourcePath,
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 = docTextOf.get(row.file);
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.slice(0, 3);
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
- const dumpDocText = new Map(scanned.map((f) => [f.sourcePath,
327
- (f.sourcePath + ' ' + f.symbols.map((sy) => sy.qualifiedName).join(' ')).slice(0, 2000)]));
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
- const docTextOf = new Map(scanned.map((f) => [f.sourcePath,
584
- (f.sourcePath + ' ' + f.symbols.map((sy) => sy.qualifiedName).join(' ')).slice(0, 2000)]));
585
- const known = unionList.map((f, i) => ({ text: docTextOf.get(f), i })).filter((x) => x.text !== undefined);
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
+ }