@holmes-lab/holmes-kit 0.24.1 → 0.25.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 +89 -0
- package/README.md +4 -0
- 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
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,95 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
<!-- @implements A-SPEC-209 -->
|
|
8
|
+
## [0.25.1] - 2026-09-19
|
|
9
|
+
|
|
10
|
+
Test-only. No product behaviour changed; `dist/` behaves exactly as 0.25.0.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **The adopter outcome space is derived from the contract, not from the last observation**
|
|
14
|
+
(A-SPEC-638). A race in which adopters compete with initializers AND with each other sits on two
|
|
15
|
+
axes at once — *is there an identity yet* (`missing-workspace`, `unregistered`) and *is another
|
|
16
|
+
adopter winning* (`active`, `store-busy`, `entity-state-present`, `recovery-required`) — and the
|
|
17
|
+
mixed case had only ever enumerated the first. On Linux it met `store-busy`; widened by one, it
|
|
18
|
+
met `entity-state-present` on the very next run. Every one of those refusals is the product
|
|
19
|
+
behaving correctly and safely: `store-busy` is raised at lock acquisition before any mutation and
|
|
20
|
+
the staging directory is removed in the same catch, and the other two are planning-time refusals
|
|
21
|
+
that never reach a write. The two axes are now named once and shared by both halves of the case,
|
|
22
|
+
so they cannot drift again, and the assertion that a refused adopter left nothing behind covers
|
|
23
|
+
the whole axis rather than the one outcome that happened to be seen.
|
|
24
|
+
|
|
25
|
+
Measured: Linux 3-in-5 failing before the first repair, 1-in-5 after it, **20 in 20 passing**
|
|
26
|
+
after deriving the set from the contract. The CI matrix had been red on 38 of 45 rows since
|
|
27
|
+
2026-09-17 — a matrix that is always red reports nothing, which is the cost this closes.
|
|
28
|
+
|
|
29
|
+
## [0.25.0] - 2026-09-19
|
|
30
|
+
|
|
31
|
+
The semantic layer was dead on this repository and nothing said so. It is alive, it keeps itself
|
|
32
|
+
alive, it can now introduce a candidate the lexical pass never found, and a consumer learns it
|
|
33
|
+
exists without running `doctor`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- **`holmes-kit doctor` reports document-vector coverage** (A-SPEC-682). The tier line says what is
|
|
37
|
+
CONFIGURED; it never said whether one lookup would succeed. Measured here 2026-09-19: the resolved
|
|
38
|
+
tier was `cloud` and **0 of 602 scanned files** had a cached vector under it, so every lookup
|
|
39
|
+
returned nothing — reranking reordered nothing, alternates emitted nothing, and the output was
|
|
40
|
+
byte-identical to a layer that examined everything and agreed. A naive check would have counted
|
|
41
|
+
532 cached vectors and called it healthy; those 532 keyed on symbol lists that no longer existed.
|
|
42
|
+
The count is therefore taken against the files scanned NOW, through the runtime's own accessor.
|
|
43
|
+
Four states: `covered`, `inert`, `not-adopted`, `unknown` — the two that cannot be judged say so
|
|
44
|
+
rather than passing quietly.
|
|
45
|
+
- **Vectors refresh on the channel that keeps the graph fresh** (A-SPEC-683). The graph self-heals
|
|
46
|
+
through the Stop hook's detached child; the vectors did not, because warming was reachable only
|
|
47
|
+
from an explicit `rtm_reindex`. The cache key is a hash of the document text and that text is the
|
|
48
|
+
file's path plus its symbol names, so every edit that renames a symbol invalidates that file's
|
|
49
|
+
vector: twelve days of work had taken coverage to zero. The detached refresh now warms too, the
|
|
50
|
+
Stop hook reports the verdict the child recorded (it never scans — tree-sitter wasm must not ride
|
|
51
|
+
into a gate process), and **an absent verdict reads as "not run", never as a pass**. Warming
|
|
52
|
+
under the cloud tier is egress, so it is automatic AND observed, with `HOLMES_NO_SEMANTIC_WARM`
|
|
53
|
+
to switch the transfer off — an owner who switches it off is shown a switch, not a fault. The
|
|
54
|
+
verdict records the commit it was taken at: work merged from another machine changes the symbols
|
|
55
|
+
the vectors key on, so a verdict from another head reports **needs re-measuring** rather than
|
|
56
|
+
yesterday's numbers as today's truth.
|
|
57
|
+
- **`issue_localize` admits what the lexical pass never returned** (A-SPEC-684). Its semantic
|
|
58
|
+
rerank could only reorder the array it was handed, so a question phrased in intent vocabulary
|
|
59
|
+
never reached the file that answers it. Measured with the tier live and the vectors warm: asking
|
|
60
|
+
*who opens a URL in the user's browser* ranked `LocalMarkdownRepository` first — the prose
|
|
61
|
+
contained the word "repository" — at 3.3x the second score, and never returned `open-url.ts`,
|
|
62
|
+
which the graph held throughout. The same need in mechanism vocabulary found its answer at rank 4.
|
|
63
|
+
`semanticAlternates` now carries the top cached-vector matches among files the emission missed;
|
|
64
|
+
`open-url.ts` comes back at cosine 0.754. **Pure addition on its own field**: the ranked `hits`
|
|
65
|
+
are untouched in set, order and score, because this repository measured that precision is lost by
|
|
66
|
+
admitting candidates INTO a ranked set. The mechanism is the one `maintenance_analyze` already
|
|
67
|
+
had (A-SPEC-494, measured ×1.25–×2.25 in weak windows across three corpora); it now exists once
|
|
68
|
+
and both surfaces call it.
|
|
69
|
+
- **A consumer meets the semantic ladder without running `doctor`** (A-SPEC-686). The layer is
|
|
70
|
+
measured — on 305 traceability cases here, recall 0.486 lexical → 0.667 local → **0.887 cloud**,
|
|
71
|
+
and on requests lexical search misses entirely, recovery 0% → 52% → **92%** — and a consumer
|
|
72
|
+
never learned it existed. Probed against a tarball install: `init` announced the guardrail mode,
|
|
73
|
+
the wired harness and the role policy and said nothing about the tier; the session banner said
|
|
74
|
+
the version and the governance rule. `init` now names the ladder with its measured numbers and
|
|
75
|
+
the command for each, and the session banner says it **once** per workspace on a machine and then
|
|
76
|
+
never again. `local` is named before `cloud` deliberately: `none` is the default because egress
|
|
77
|
+
needs consent, not because nobody got to it, and the cloud line states what leaves the machine.
|
|
78
|
+
A consumer who has already chosen a tier hears nothing at either moment.
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
- **A project that never adopted the semantic layer hears nothing about it** (A-SPEC-683). Caught
|
|
82
|
+
by a consumer-shape probe before release, not by the suite: a tier-`none` workspace — the shipped
|
|
83
|
+
default, zero egress, never opted in — was told `semantic vectors: not run` on every turn. A
|
|
84
|
+
`null` verdict carries no tier, so "adopted but never refreshed" and "never adopted" looked
|
|
85
|
+
identical. The suite missed it because the not-adopted case was exercised through a verdict object
|
|
86
|
+
the consumer never has; the consumer reaches that code with no file at all.
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
- **One definition of the semantic document text, enforced by a census** (A-SPEC-685). The formula
|
|
90
|
+
existed in eight places; A-SPEC-682 gave it one definition and pinned it with a hand-written list
|
|
91
|
+
of two files — a note of which call sites that slice happened to touch, not a rule. The pin now
|
|
92
|
+
walks the source tree, and it immediately found what the list could not: two copies in the replay
|
|
93
|
+
benchmark. They were byte-identical, so live impact was zero — but a benchmark that keys
|
|
94
|
+
differently from the product scores a pipeline the product does not run, and this repository has
|
|
95
|
+
had an adoption verdict reversed by exactly that.
|
|
96
|
+
|
|
8
97
|
## [0.24.1] - 2026-09-19
|
|
9
98
|
|
|
10
99
|
Documentation only. No code changed; `dist/` is byte-identical in behaviour to 0.24.0.
|
package/README.md
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
### 🛡️ Currently Supported Features (Production Features)
|
|
18
18
|
|
|
19
|
+
- 🔦 **A semantic layer that is inert says so** *(new in 0.25.0)*: the tier line said what was CONFIGURED, never whether one lookup would succeed. Measured on this repository: the resolved tier was `cloud` and **0 of 602 scanned files** had a cached document vector under it, so every lookup returned nothing — reranking reordered nothing, alternates emitted nothing, and the output was byte-identical to a layer that examined everything and agreed. A naive check would have counted the 532 vectors sitting in the cache and called it healthy; they keyed on symbol lists that no longer existed. `doctor` now reports coverage against the files scanned NOW, through the runtime's own accessor, in four states — the two that cannot be judged say so rather than passing quietly.
|
|
20
|
+
- 🔁 **Vectors refresh on the channel that keeps the graph fresh** *(new in 0.25.0)*: the graph self-heals through the Stop hook's detached child; the vectors did not, because warming was reachable only from an explicit `rtm_reindex`. The cache key hashes the file's path plus its symbol names, so every edit that renames a symbol invalidates that file's vector — twelve days of work had taken coverage to zero. The refresh now warms too, and the Stop hook reports the verdict the child recorded rather than scanning (tree-sitter wasm must not ride into a gate process); **a missing verdict reads as "not run", never as a pass**. Warming under the cloud tier is egress, so it is automatic AND observed, with `HOLMES_NO_SEMANTIC_WARM` to switch the transfer off — an owner who switches it off is shown a switch, not a fault. The verdict records the commit it was taken at, so work merged from another machine reports **needs re-measuring** instead of yesterday's numbers as today's truth.
|
|
21
|
+
- 🎣 **The graph can introduce a candidate your words never named** *(new in 0.25.0)*: `issue_localize`'s semantic rerank could only reorder what lexical matching already found, so a question phrased in intent vocabulary never reached the file that answers it. Measured with the tier live and the vectors warm: asking *who opens a URL in the user's browser* ranked an unrelated file first on the shared prose word "repository", at 3.3× the second score, and never returned `open-url.ts` — which the graph held the whole time. The same need in mechanism vocabulary found its answer at rank 4. `semanticAlternates` now carries the top cached-vector matches among files the emission missed, and that file comes back at cosine 0.754. **Pure addition on its own field**: the ranked hits are untouched in set, order and score, because admitting candidates INTO a ranked set is where this project measured precision being lost.
|
|
22
|
+
- 🪧 **The semantic ladder reaches you without running `doctor`** *(new in 0.25.0)*: measured here on 305 traceability cases, recall goes 0.486 lexical → 0.667 local → **0.887 cloud**, and on requests lexical search misses entirely, recovery goes 0% → 52% → **92%** — and a consumer never learned any of it, because only `doctor` said so. `init` now names the ladder with those numbers and the command for each, and the session banner says it **once** per workspace on a machine and then never again. `local` is named before `cloud` on purpose: `none` is the default because egress needs consent, not because nobody got to it, and the cloud line states what leaves the machine. A consumer who has already chosen hears nothing.
|
|
19
23
|
- 📮 **`holmes-kit report` — a defect can reach the maintainers** *(new in 0.24.0)*: until now a consumer's holmes-kit defect had no way back to us; the one we learned about arrived because someone pasted a transcript, and it had been reproducing for every consumer on every slice. The command writes a **redacted** report to `.ax/reports/<fingerprint>.md` and prints a prefilled GitHub issue link — title, assignee, body — plus a search link for the same fingerprint so you can see whether it is already known. `--open` opens it; on a headless box or over SSH the printed link is the whole of it. **No token, no API, nothing sent automatically**: you press Submit on GitHub's own page with the body in front of you and editable. Redaction is an allowlist rather than a scrubber — this project's own remote carries a token before the `@`, its replica ids carry a person's name, and its spec titles are unreleased product intent, so a path, a credential or a machine identifier withholds the field and the report says which. A spec id passes by shape; a spec title does not. And what holmes-kit cannot know, it says: no ledger keeps its own refusal text, so a report with no description states that rather than pretending.
|
|
20
24
|
- 🧭 **A skipped graph analysis is visible** *(new in 0.24.0)*: `AGENTS.md` asks for `maintenance_analyze` before editing source, and nothing checked. Measured on this repository, the step had been skipped for seventeen consecutive commits — and the run that followed named child-process precedents a name search had missed completely, because the question was "who opens a browser" while the answer lived under "who spawns a child". The Stop hook now reports source changed with no analysis standing open: non-blocking, judged by commit rather than by clock, and silent in a workspace that never adopted the habit.
|
|
21
25
|
- 🧱 **A stale build is told, not discovered** *(new in 0.23.3)*: thirty suites in this project load `dist/` while they run, and nothing asserted that it still represented the source — only the release gate compared the build id to HEAD, and only at publish time. A stale build does not go red; it verifies old code and returns green. The Stop hook now reports it on the non-blocking `tracked` channel, judged by the **build id and never by mtime**: measured here, `.build-id` had a newer mtime than every source file while naming a commit nine behind HEAD, with two changed sources missing from `dist` entirely. A workspace that does not build hears nothing, `fresh` says nothing, and the two states that cannot be judged say **that** rather than passing quietly.
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
6277a6d6-mu7vpg1b
|
|
@@ -200,6 +200,7 @@ export declare function wiringHandshakeChecks(target: string): Promise<Check[]>;
|
|
|
200
200
|
* quotes the measured stakes (lexical-zero recovery 0% -> 52% local / 92% cloud, S-491) so the
|
|
201
201
|
* user decides the trade with numbers rather than adjectives.
|
|
202
202
|
*/
|
|
203
|
+
export declare function semanticCoverageCheck(root: string, tier: SemanticTier): Check | null;
|
|
203
204
|
export declare function semanticTierVerdict(t: SemanticTier): {
|
|
204
205
|
level: 'PASS' | 'WARN';
|
|
205
206
|
detail: string;
|
|
@@ -50,6 +50,7 @@ exports.pushGateCheck = pushGateCheck;
|
|
|
50
50
|
exports.ciMatrixCheck = ciMatrixCheck;
|
|
51
51
|
exports.formatChecks = formatChecks;
|
|
52
52
|
exports.wiringHandshakeChecks = wiringHandshakeChecks;
|
|
53
|
+
exports.semanticCoverageCheck = semanticCoverageCheck;
|
|
53
54
|
exports.semanticTierVerdict = semanticTierVerdict;
|
|
54
55
|
exports.detectTreeKeyTemporary = detectTreeKeyTemporary;
|
|
55
56
|
// @implements A-SPEC-264, A-SPEC-423, A-SPEC-549.1, A-SPEC-590
|
|
@@ -68,6 +69,7 @@ const tier_1 = require("../semantic/tier");
|
|
|
68
69
|
const probe_process_1 = require("./probe-process");
|
|
69
70
|
const npx_cache_check_1 = require("./npx-cache-check");
|
|
70
71
|
const ci_runs_1 = require("../project/ci-runs");
|
|
72
|
+
const vector_coverage_1 = require("../semantic/vector-coverage");
|
|
71
73
|
const path = __importStar(require("node:path"));
|
|
72
74
|
const role_policy_1 = require("../governance/role-policy");
|
|
73
75
|
const blind_spots_1 = require("../guardrail/blind-spots");
|
|
@@ -1248,6 +1250,16 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
1248
1250
|
const t = (0, tier_1.resolveSemanticTier)();
|
|
1249
1251
|
const v = semanticTierVerdict(t);
|
|
1250
1252
|
add('semantic tier', v.level, v.detail, v.fix);
|
|
1253
|
+
// @implements A-SPEC-682 — the tier line says what is CONFIGURED. Measured 2026-09-19 on this
|
|
1254
|
+
// repository, the configured tier was cloud while 0 of 602 scanned files had a cached document
|
|
1255
|
+
// vector under it, so every lookup returned nothing and both semantic surfaces were inert —
|
|
1256
|
+
// output-identical to a layer that examined everything and agreed. doctor renders the verdict;
|
|
1257
|
+
// it does not compute it (this file already carries 51 anchors against a p90 of 10).
|
|
1258
|
+
{
|
|
1259
|
+
const cov = semanticCoverageCheck(target ?? process.cwd(), t);
|
|
1260
|
+
if (cov)
|
|
1261
|
+
checks.push(cov);
|
|
1262
|
+
}
|
|
1251
1263
|
// @implements A-SPEC-477 — the ".env stopgap" detection: a key in a project-tree file is a
|
|
1252
1264
|
// commit-accident surface, world-readable at 644, and readable by every in-session tool. The
|
|
1253
1265
|
// value is read to detect the pattern and never printed.
|
|
@@ -1689,6 +1701,50 @@ function entryOf(value) {
|
|
|
1689
1701
|
* quotes the measured stakes (lexical-zero recovery 0% -> 52% local / 92% cloud, S-491) so the
|
|
1690
1702
|
* user decides the trade with numbers rather than adjectives.
|
|
1691
1703
|
*/
|
|
1704
|
+
// @implements A-SPEC-682 — the coverage CHECK: the I/O half, kept out of the pure judgement.
|
|
1705
|
+
//
|
|
1706
|
+
// The count is taken against the files scanned NOW, through the runtime's own `cachedDocVector` —
|
|
1707
|
+
// the exact accessor the ranking surfaces call. Rebuilding the cache tag here would duplicate a
|
|
1708
|
+
// rule that differs by tier (local carries a revision and a pooling mode, cloud does not), and a
|
|
1709
|
+
// duplicated rule is the drift this whole slice exists to remove.
|
|
1710
|
+
//
|
|
1711
|
+
// Counting cache ENTRIES instead would have read 532 vectors on this repository on 2026-09-19 and
|
|
1712
|
+
// called it healthy, while 0 of them answered a lookup for a current file: the key is
|
|
1713
|
+
// sha256(document text), and that text changes whenever a file's symbols do. The reassuring
|
|
1714
|
+
// number is the wrong number.
|
|
1715
|
+
function semanticCoverageCheck(root, tier) {
|
|
1716
|
+
if (tier.tier === 'none')
|
|
1717
|
+
return null; // never opted in; it hears nothing about this
|
|
1718
|
+
const unjudgeable = () => ({
|
|
1719
|
+
name: 'semantic coverage', level: 'WARN',
|
|
1720
|
+
detail: (0, vector_coverage_1.coverageLine)((0, vector_coverage_1.vectorCoverage)({ tier: tier.tier, modelTag: null, total: 1, covered: 0 })),
|
|
1721
|
+
});
|
|
1722
|
+
try {
|
|
1723
|
+
const { CpgScanner } = require('../cpg/cpg-scanner');
|
|
1724
|
+
const { ScanFileCache } = require('../cpg/scan-cache');
|
|
1725
|
+
const { VectorCache } = require('../semantic/vector-cache');
|
|
1726
|
+
const { makeTierEmbedder } = require('../semantic/embedder');
|
|
1727
|
+
const crypto = require('node:crypto');
|
|
1728
|
+
const runtime = makeTierEmbedder(tier, new VectorCache(root));
|
|
1729
|
+
if (runtime === null)
|
|
1730
|
+
return unjudgeable();
|
|
1731
|
+
const dir = path.join(os.tmpdir(), 'holmes-cpg-cache-' + crypto.createHash('sha256').update(root).digest('hex').slice(0, 16));
|
|
1732
|
+
const scanned = new CpgScanner(undefined, new ScanFileCache(dir)).scan(root, root);
|
|
1733
|
+
const covered = (0, vector_coverage_1.countCovered)(scanned, (t) => runtime.cachedDocVector(t) !== null);
|
|
1734
|
+
const v = (0, vector_coverage_1.vectorCoverage)({ tier: tier.tier, modelTag: runtime.label, total: scanned.length, covered });
|
|
1735
|
+
const line = (0, vector_coverage_1.coverageLine)(v);
|
|
1736
|
+
if (line === '')
|
|
1737
|
+
return null;
|
|
1738
|
+
return v.state === 'inert'
|
|
1739
|
+
? { name: 'semantic coverage', level: 'WARN', detail: line,
|
|
1740
|
+
fix: 'Re-warm the vectors for the resolved model — `rtm_reindex` carries the only warm pass.' }
|
|
1741
|
+
: { name: 'semantic coverage', level: 'PASS', detail: line };
|
|
1742
|
+
}
|
|
1743
|
+
catch {
|
|
1744
|
+
// A count that could not be taken says so — it must not read as 0 (inert) or as full.
|
|
1745
|
+
return unjudgeable();
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1692
1748
|
function semanticTierVerdict(t) {
|
|
1693
1749
|
if (t.tier === 'cloud') {
|
|
1694
1750
|
// @implements A-SPEC-477 — the SOURCE of the consent is shown (env/keychain/file); the value
|
package/dist/holmes/cli/init.js
CHANGED
|
@@ -606,5 +606,18 @@ function runInit(opts) {
|
|
|
606
606
|
(0, roles_readme_1.installRolesReadme)(opts.target);
|
|
607
607
|
messages.push('Role policy is OPTIONAL and currently off — see .ax/roles/README.md to enable it.');
|
|
608
608
|
}
|
|
609
|
+
// @implements A-SPEC-686 — the tier, said where the consumer is already reading posture.
|
|
610
|
+
// Probed 2026-09-19: init announced the guardrail mode, the harness, the skills and the role
|
|
611
|
+
// policy, and said nothing about the semantic layer; only `doctor` did, and a consumer who never
|
|
612
|
+
// runs doctor never met it. One-time by the nature of the act, so no marker is needed here.
|
|
613
|
+
// Fail-open: an invitation is not worth failing a wiring over.
|
|
614
|
+
try {
|
|
615
|
+
const { resolveSemanticTier } = require('../semantic/tier');
|
|
616
|
+
const { tierAdviceLines } = require('../semantic/tier-advice');
|
|
617
|
+
for (const line of tierAdviceLines(resolveSemanticTier()))
|
|
618
|
+
messages.push(line);
|
|
619
|
+
}
|
|
620
|
+
catch { /* the wiring is the point; the invitation is not */
|
|
621
|
+
}
|
|
609
622
|
return { ok: true, exitCode: 0, messages, changes, removals };
|
|
610
623
|
}
|
|
@@ -49,7 +49,18 @@ if (require.main === module) {
|
|
|
49
49
|
const { makeHandlers } = require('../mcp/handlers');
|
|
50
50
|
const { LocalMarkdownRepository } = require('../spec/spec-store');
|
|
51
51
|
const h = makeHandlers(new LocalMarkdownRepository(path.join(root, '.ax', 'specs')));
|
|
52
|
-
void h.rtm_impact({ root, changed: [] }).
|
|
52
|
+
void h.rtm_impact({ root, changed: [] }).then(() => {
|
|
53
|
+
// @implements A-SPEC-683 — the vectors ride the channel that already refreshes the graph.
|
|
54
|
+
// Detached, TTL-gated and fail-soft are properties this child already has; warming inherits
|
|
55
|
+
// all three rather than needing a schedule of its own. Measured 2026-09-19: without this,
|
|
56
|
+
// coverage reached 0 of 602 in twelve days while the graph stayed current.
|
|
57
|
+
const { refreshSemanticVectors } = require('../mcp/handlers/graph-operations');
|
|
58
|
+
const { CpgScanner } = require('../cpg/cpg-scanner');
|
|
59
|
+
const { ScanFileCache } = require('../cpg/scan-cache');
|
|
60
|
+
const crypto = require('node:crypto');
|
|
61
|
+
const dir = path.join(require('node:os').tmpdir(), 'holmes-cpg-cache-' + crypto.createHash('sha256').update(root).digest('hex').slice(0, 16));
|
|
62
|
+
return refreshSemanticVectors(root, process.env, undefined, () => new CpgScanner(undefined, new ScanFileCache(dir)).scan(root, root));
|
|
63
|
+
}).catch(() => undefined);
|
|
53
64
|
}
|
|
54
65
|
}
|
|
55
66
|
catch { /* fail-soft: a failed refresh leaves the old graph, which the advisory tolerates */ }
|
|
@@ -175,6 +175,28 @@ if (require.main === module) {
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
catch { /* the banner is never a gate */ }
|
|
178
|
+
// @implements A-SPEC-686 — invite ONCE. The measured layer reached consumers only through
|
|
179
|
+
// the README and doctor; a consumer who runs neither never learns it exists. This is an
|
|
180
|
+
// every-session surface, so it says it once per workspace on this machine and then never
|
|
181
|
+
// again — a recurring advertisement for an opt-in feature is the noise class this project
|
|
182
|
+
// removed the same day. Fail-open, exactly like the autonomy banner above it.
|
|
183
|
+
try {
|
|
184
|
+
const { resolveSemanticTier } = require('../semantic/tier');
|
|
185
|
+
const { tierAdviceLines, shouldInvite, markInvited } = require('../semantic/tier-advice');
|
|
186
|
+
const cwd = process.cwd();
|
|
187
|
+
const tier = resolveSemanticTier();
|
|
188
|
+
if (shouldInvite(tier, cwd, (p) => fs.existsSync(p))) {
|
|
189
|
+
const lines = tierAdviceLines(tier);
|
|
190
|
+
if (lines.length > 0) {
|
|
191
|
+
out.hookSpecificOutput.additionalContext += '\n[Holmes-Kit] ' + lines.join('\n');
|
|
192
|
+
markInvited(cwd, (p, data) => {
|
|
193
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
194
|
+
fs.writeFileSync(p, data);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch { /* the banner is never a gate */ }
|
|
178
200
|
process.stdout.write(JSON.stringify({ hookSpecificOutput: out.hookSpecificOutput }));
|
|
179
201
|
if (out.shouldRefresh) {
|
|
180
202
|
// Detached, unref'd child so the session start does not wait on the network. The refresh
|
|
@@ -6,6 +6,9 @@ import { type CiVerdict } from '../project/ci-runs';
|
|
|
6
6
|
import { type DistVerdict } from '../project/dist-freshness';
|
|
7
7
|
import { type AnalysisVerdict } from '../project/analysis-currency';
|
|
8
8
|
export declare function collectKnownDefects(root: string, now: Date): KnownDefectJudgement | undefined;
|
|
9
|
+
export declare function collectSemanticCoverage(root: string, tier?: {
|
|
10
|
+
tier: string;
|
|
11
|
+
}): string;
|
|
9
12
|
export declare function collectAnalysisCurrency(root: string, changedSources: number): AnalysisVerdict | undefined;
|
|
10
13
|
export declare function collectDistFreshness(root: string): DistVerdict | undefined;
|
|
11
14
|
export declare function collectCiVerdicts(root: string, now?: Date): CiVerdict[];
|
|
@@ -52,6 +55,8 @@ export interface StopEvidence {
|
|
|
52
55
|
* turn's source edits. Absent when the workspace never analysed anything.
|
|
53
56
|
*/
|
|
54
57
|
analysis?: AnalysisVerdict;
|
|
58
|
+
/** @implements A-SPEC-683 — the rendered coverage line the refresh child recorded; already judged. */
|
|
59
|
+
semantic?: string;
|
|
55
60
|
/** Provenance-chain verification result (CLI-supplied). A broken chain blocks the stop. */
|
|
56
61
|
provenance?: {
|
|
57
62
|
ok: boolean;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the semantic layer is doing anything at all.
|
|
3
|
+
*
|
|
4
|
+
* Measured on this repository 2026-09-19: the resolved tier was `cloud`, and of 602 scanned files
|
|
5
|
+
* ZERO had a cached document vector under that model — while 327 sat in the same cache under
|
|
6
|
+
* `bge-m3`, a model this project no longer resolves to. Every `cachedDocVector` returned null, so
|
|
7
|
+
* `rerankHitsBySemantic` reordered nothing and `semanticAlternates` emitted nothing. Neither said
|
|
8
|
+
* so, and `doctor` printed `PASS semantic tier` throughout — a statement about a key and a model
|
|
9
|
+
* name, not about whether one lookup would succeed.
|
|
10
|
+
*
|
|
11
|
+
* An inert layer and a layer that examined every candidate and agreed with the lexical ordering
|
|
12
|
+
* produce byte-identical output. That is the defect this module removes: it makes zero say zero.
|
|
13
|
+
*
|
|
14
|
+
* OBSERVATION ONLY. Nothing here reads into a ranking, a selection or any verdict.
|
|
15
|
+
*/
|
|
16
|
+
/** The shape the scanner returns, narrowed to what the document text needs. */
|
|
17
|
+
export interface ScannedLike {
|
|
18
|
+
sourcePath: string;
|
|
19
|
+
symbols: ReadonlyArray<{
|
|
20
|
+
qualifiedName: string;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export type CoverageState = 'covered' | 'inert' | 'not-adopted' | 'unknown';
|
|
24
|
+
export interface CoverageInput {
|
|
25
|
+
/** The RESOLVED tier. `none` is the shipped default (zero egress) and means never opted in. */
|
|
26
|
+
tier: string;
|
|
27
|
+
/** The resolved model's cache tag. null when it cannot be determined. */
|
|
28
|
+
modelTag: string | null;
|
|
29
|
+
total: number;
|
|
30
|
+
covered: number;
|
|
31
|
+
}
|
|
32
|
+
export interface VectorCoverage {
|
|
33
|
+
state: CoverageState;
|
|
34
|
+
covered: number;
|
|
35
|
+
total: number;
|
|
36
|
+
/** null whenever a ratio would be invented — an unjudgeable count or an empty scan. */
|
|
37
|
+
ratio: number | null;
|
|
38
|
+
modelTag: string | null;
|
|
39
|
+
}
|
|
40
|
+
/** The maximum document text length; part of the cache key, so it is part of the contract. */
|
|
41
|
+
export declare const DOC_TEXT_LIMIT = 2000;
|
|
42
|
+
/**
|
|
43
|
+
* THE definition of the text whose hash keys a document vector.
|
|
44
|
+
*
|
|
45
|
+
* It existed as an inline expression in five places, four of them in `maintenance-analyze.ts` and
|
|
46
|
+
* one in `issue_localize`'s rerank — two of which are the surfaces this module's coverage claims to
|
|
47
|
+
* describe. A separate copy here would be free to drift, and a drifted instrument reports a
|
|
48
|
+
* coverage the product does not have. One definition, or the build fails.
|
|
49
|
+
*/
|
|
50
|
+
export declare function docTextOf(file: ScannedLike): string;
|
|
51
|
+
/** The same text, indexed by source path — the form the three ranking passes consume. */
|
|
52
|
+
export declare function docTextIndex(scanned: ReadonlyArray<ScannedLike>): Map<string, string>;
|
|
53
|
+
/**
|
|
54
|
+
* Pure: the caller supplies the counts. Keeping the filesystem out means the judgement is testable
|
|
55
|
+
* without a cache on disk, and it cannot be wrong about ordering or locking.
|
|
56
|
+
*/
|
|
57
|
+
export declare function vectorCoverage(input: CoverageInput): VectorCoverage;
|
|
58
|
+
/** Empty when there is nothing to say. Never silent when there is. */
|
|
59
|
+
export declare function coverageLine(v: VectorCoverage): string;
|
|
60
|
+
/**
|
|
61
|
+
* How many scanned files have a cached document vector, judged by a lookup the CALLER supplies.
|
|
62
|
+
*
|
|
63
|
+
* The lookup is injected so this module stays free of the cache's I/O and locking, and so the
|
|
64
|
+
* count is taken through the same public accessor the ranking surfaces use — a private key
|
|
65
|
+
* recomputed here could agree with the cache today and drift from it tomorrow.
|
|
66
|
+
*/
|
|
67
|
+
export declare function countCovered(scanned: ReadonlyArray<ScannedLike>, isCached: (docText: string) => boolean): number;
|
|
68
|
+
export type WarmReason = 'warmed' | 'suppressed' | 'not-adopted';
|
|
69
|
+
export interface PersistedCoverage {
|
|
70
|
+
coverage: VectorCoverage;
|
|
71
|
+
reason: WarmReason;
|
|
72
|
+
at: string;
|
|
73
|
+
stored?: number;
|
|
74
|
+
head?: string;
|
|
75
|
+
}
|
|
76
|
+
export declare const COVERAGE_STATE_FILE = "semantic-coverage.json";
|
|
77
|
+
/** Best-effort: a verdict that cannot be written is not worth failing a refresh over. */
|
|
78
|
+
export declare function writeCoverageState(root: string, p: PersistedCoverage): void;
|
|
79
|
+
/** null means NOT RUN — missing, unreadable, or not shaped like a verdict. Never throws. */
|
|
80
|
+
export declare function readCoverageState(root: string): PersistedCoverage | null;
|
|
81
|
+
/**
|
|
82
|
+
* The turn-boundary line. Empty when there is nothing to say; NEVER empty when there is.
|
|
83
|
+
*
|
|
84
|
+
* `null` is the case this exists for. A workspace whose refresh never ran looks exactly like a
|
|
85
|
+
* healthy one if absence is allowed to be silent, and that silence is what hid zero coverage for
|
|
86
|
+
* twelve days.
|
|
87
|
+
*/
|
|
88
|
+
export declare function coverageStopLine(p: PersistedCoverage | null, currentHead?: string): string;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-682
|
|
3
|
+
/**
|
|
4
|
+
* Whether the semantic layer is doing anything at all.
|
|
5
|
+
*
|
|
6
|
+
* Measured on this repository 2026-09-19: the resolved tier was `cloud`, and of 602 scanned files
|
|
7
|
+
* ZERO had a cached document vector under that model — while 327 sat in the same cache under
|
|
8
|
+
* `bge-m3`, a model this project no longer resolves to. Every `cachedDocVector` returned null, so
|
|
9
|
+
* `rerankHitsBySemantic` reordered nothing and `semanticAlternates` emitted nothing. Neither said
|
|
10
|
+
* so, and `doctor` printed `PASS semantic tier` throughout — a statement about a key and a model
|
|
11
|
+
* name, not about whether one lookup would succeed.
|
|
12
|
+
*
|
|
13
|
+
* An inert layer and a layer that examined every candidate and agreed with the lexical ordering
|
|
14
|
+
* produce byte-identical output. That is the defect this module removes: it makes zero say zero.
|
|
15
|
+
*
|
|
16
|
+
* OBSERVATION ONLY. Nothing here reads into a ranking, a selection or any verdict.
|
|
17
|
+
*/
|
|
18
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
21
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
22
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(o, k2, desc);
|
|
25
|
+
}) : (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
o[k2] = m[k];
|
|
28
|
+
}));
|
|
29
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
30
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
31
|
+
}) : function(o, v) {
|
|
32
|
+
o["default"] = v;
|
|
33
|
+
});
|
|
34
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
35
|
+
var ownKeys = function(o) {
|
|
36
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
37
|
+
var ar = [];
|
|
38
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
39
|
+
return ar;
|
|
40
|
+
};
|
|
41
|
+
return ownKeys(o);
|
|
42
|
+
};
|
|
43
|
+
return function (mod) {
|
|
44
|
+
if (mod && mod.__esModule) return mod;
|
|
45
|
+
var result = {};
|
|
46
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
47
|
+
__setModuleDefault(result, mod);
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.COVERAGE_STATE_FILE = exports.DOC_TEXT_LIMIT = void 0;
|
|
53
|
+
exports.docTextOf = docTextOf;
|
|
54
|
+
exports.docTextIndex = docTextIndex;
|
|
55
|
+
exports.vectorCoverage = vectorCoverage;
|
|
56
|
+
exports.coverageLine = coverageLine;
|
|
57
|
+
exports.countCovered = countCovered;
|
|
58
|
+
exports.writeCoverageState = writeCoverageState;
|
|
59
|
+
exports.readCoverageState = readCoverageState;
|
|
60
|
+
exports.coverageStopLine = coverageStopLine;
|
|
61
|
+
const fs = __importStar(require("node:fs"));
|
|
62
|
+
const path = __importStar(require("node:path"));
|
|
63
|
+
/** The maximum document text length; part of the cache key, so it is part of the contract. */
|
|
64
|
+
exports.DOC_TEXT_LIMIT = 2000;
|
|
65
|
+
/**
|
|
66
|
+
* THE definition of the text whose hash keys a document vector.
|
|
67
|
+
*
|
|
68
|
+
* It existed as an inline expression in five places, four of them in `maintenance-analyze.ts` and
|
|
69
|
+
* one in `issue_localize`'s rerank — two of which are the surfaces this module's coverage claims to
|
|
70
|
+
* describe. A separate copy here would be free to drift, and a drifted instrument reports a
|
|
71
|
+
* coverage the product does not have. One definition, or the build fails.
|
|
72
|
+
*/
|
|
73
|
+
function docTextOf(file) {
|
|
74
|
+
return (file.sourcePath + ' ' + file.symbols.map((sy) => sy.qualifiedName).join(' ')).slice(0, exports.DOC_TEXT_LIMIT);
|
|
75
|
+
}
|
|
76
|
+
/** The same text, indexed by source path — the form the three ranking passes consume. */
|
|
77
|
+
function docTextIndex(scanned) {
|
|
78
|
+
return new Map(scanned.map((f) => [f.sourcePath, docTextOf(f)]));
|
|
79
|
+
}
|
|
80
|
+
const isCount = (n) => typeof n === 'number' && Number.isFinite(n) && n >= 0;
|
|
81
|
+
/**
|
|
82
|
+
* Pure: the caller supplies the counts. Keeping the filesystem out means the judgement is testable
|
|
83
|
+
* without a cache on disk, and it cannot be wrong about ordering or locking.
|
|
84
|
+
*/
|
|
85
|
+
function vectorCoverage(input) {
|
|
86
|
+
const total = isCount(input.total) ? Math.floor(input.total) : 0;
|
|
87
|
+
const covered = isCount(input.covered) ? Math.min(Math.floor(input.covered), total) : 0;
|
|
88
|
+
const modelTag = typeof input.modelTag === 'string' && input.modelTag !== '' ? input.modelTag : null;
|
|
89
|
+
const base = { covered, total, modelTag };
|
|
90
|
+
// A project at the shipped default never opted in. It is not failing at something it declined.
|
|
91
|
+
if (input.tier === 'none' || input.tier === '')
|
|
92
|
+
return { state: 'not-adopted', ratio: null, ...base };
|
|
93
|
+
// No model tag means no lookup key: the count could not be taken. Saying `covered: 0` here would
|
|
94
|
+
// report an inert layer that may well be working — the two must not share a shape.
|
|
95
|
+
if (modelTag === null)
|
|
96
|
+
return { state: 'unknown', ratio: null, ...base };
|
|
97
|
+
// Nothing was scanned, so nothing was missed. An empty scan is evidence about the scan, not the
|
|
98
|
+
// cache, and a ratio over zero files would be invented.
|
|
99
|
+
if (total === 0)
|
|
100
|
+
return { state: 'unknown', ratio: null, ...base };
|
|
101
|
+
if (covered === 0)
|
|
102
|
+
return { state: 'inert', ratio: 0, ...base };
|
|
103
|
+
return { state: 'covered', ratio: covered / total, ...base };
|
|
104
|
+
}
|
|
105
|
+
/** Empty when there is nothing to say. Never silent when there is. */
|
|
106
|
+
function coverageLine(v) {
|
|
107
|
+
if (v.state === 'not-adopted')
|
|
108
|
+
return '';
|
|
109
|
+
if (v.state === 'unknown') {
|
|
110
|
+
// An empty scan has nothing to report; an unjudgeable count with files present does.
|
|
111
|
+
if (v.total === 0)
|
|
112
|
+
return '';
|
|
113
|
+
return 'document-vector coverage could not be judged — the resolved model has no cache tag, '
|
|
114
|
+
+ 'so whether a lookup would succeed is unknown';
|
|
115
|
+
}
|
|
116
|
+
if (v.state === 'inert') {
|
|
117
|
+
return `the semantic layer is INERT: 0 of ${v.total} scanned files have a cached document vector `
|
|
118
|
+
+ `under ${v.modelTag ?? 'the resolved model'} — every lookup returns nothing, so reranking `
|
|
119
|
+
+ 'reorders nothing and alternates emit nothing. This reads identically to agreement.';
|
|
120
|
+
}
|
|
121
|
+
const pct = v.ratio === null ? '' : ` (${(v.ratio * 100).toFixed(1)}%)`;
|
|
122
|
+
return `document-vector coverage ${v.covered} of ${v.total}${pct} under ${v.modelTag ?? 'the resolved model'}`;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* How many scanned files have a cached document vector, judged by a lookup the CALLER supplies.
|
|
126
|
+
*
|
|
127
|
+
* The lookup is injected so this module stays free of the cache's I/O and locking, and so the
|
|
128
|
+
* count is taken through the same public accessor the ranking surfaces use — a private key
|
|
129
|
+
* recomputed here could agree with the cache today and drift from it tomorrow.
|
|
130
|
+
*/
|
|
131
|
+
function countCovered(scanned, isCached) {
|
|
132
|
+
let n = 0;
|
|
133
|
+
for (const f of scanned) {
|
|
134
|
+
if (isCached(docTextOf(f)))
|
|
135
|
+
n++;
|
|
136
|
+
}
|
|
137
|
+
return n;
|
|
138
|
+
}
|
|
139
|
+
exports.COVERAGE_STATE_FILE = 'semantic-coverage.json';
|
|
140
|
+
const statePath = (root) => path.join(root, '.ax', 'state', exports.COVERAGE_STATE_FILE);
|
|
141
|
+
/** Best-effort: a verdict that cannot be written is not worth failing a refresh over. */
|
|
142
|
+
function writeCoverageState(root, p) {
|
|
143
|
+
try {
|
|
144
|
+
const file = statePath(root);
|
|
145
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
146
|
+
fs.writeFileSync(file, JSON.stringify(p, null, 2) + '\n');
|
|
147
|
+
}
|
|
148
|
+
catch { /* the refresh is fail-soft; so is recording it */ }
|
|
149
|
+
}
|
|
150
|
+
/** null means NOT RUN — missing, unreadable, or not shaped like a verdict. Never throws. */
|
|
151
|
+
function readCoverageState(root) {
|
|
152
|
+
let parsed;
|
|
153
|
+
try {
|
|
154
|
+
parsed = JSON.parse(fs.readFileSync(statePath(root), 'utf8'));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
160
|
+
return null;
|
|
161
|
+
const o = parsed;
|
|
162
|
+
const c = o.coverage;
|
|
163
|
+
if (typeof c !== 'object' || c === null)
|
|
164
|
+
return null;
|
|
165
|
+
if (typeof c.state !== 'string' || typeof c.covered !== 'number' || typeof c.total !== 'number')
|
|
166
|
+
return null;
|
|
167
|
+
if (o.reason !== 'warmed' && o.reason !== 'suppressed' && o.reason !== 'not-adopted')
|
|
168
|
+
return null;
|
|
169
|
+
return { coverage: c, reason: o.reason, at: typeof o.at === 'string' ? o.at : '',
|
|
170
|
+
...(typeof o.stored === 'number' ? { stored: o.stored } : {}),
|
|
171
|
+
...(typeof o.head === 'string' && o.head !== '' ? { head: o.head } : {}) };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The turn-boundary line. Empty when there is nothing to say; NEVER empty when there is.
|
|
175
|
+
*
|
|
176
|
+
* `null` is the case this exists for. A workspace whose refresh never ran looks exactly like a
|
|
177
|
+
* healthy one if absence is allowed to be silent, and that silence is what hid zero coverage for
|
|
178
|
+
* twelve days.
|
|
179
|
+
*/
|
|
180
|
+
function coverageStopLine(p, currentHead) {
|
|
181
|
+
if (p === null) {
|
|
182
|
+
return 'semantic vectors: not run — no refresh has recorded a coverage verdict for this '
|
|
183
|
+
+ 'workspace, so whether the semantic layer contributes anything is unknown';
|
|
184
|
+
}
|
|
185
|
+
if (p.reason === 'not-adopted' || p.coverage.state === 'not-adopted')
|
|
186
|
+
return '';
|
|
187
|
+
// The verdict is machine-local; the CODE it describes arrives from other machines through Git.
|
|
188
|
+
// A pull brings in symbols this verdict never saw, and the document text those symbols key is
|
|
189
|
+
// what every vector lookup hashes — so a verdict from another commit describes a tree that no
|
|
190
|
+
// longer exists. Report it as needing re-measurement: neither healthy nor broken. Absence of a
|
|
191
|
+
// head on either side is not a mismatch; it is simply unjudgeable, and silence is correct there
|
|
192
|
+
// because the other states already speak for themselves.
|
|
193
|
+
if (typeof p.head === 'string' && p.head !== '' && typeof currentHead === 'string'
|
|
194
|
+
&& currentHead !== '' && p.head !== currentHead) {
|
|
195
|
+
return `semantic vectors: the coverage verdict was recorded at another commit (${p.head.slice(0, 7)}, `
|
|
196
|
+
+ `now ${currentHead.slice(0, 7)}) — work merged since then changes the symbols the vectors key on, `
|
|
197
|
+
+ 'so coverage needs re-measuring';
|
|
198
|
+
}
|
|
199
|
+
if (p.reason === 'suppressed') {
|
|
200
|
+
// An owner who turned the transfer off must not be shown a fault. Report the number, name the
|
|
201
|
+
// switch, and say nothing that reads as breakage.
|
|
202
|
+
return `semantic vectors: automatic warming is switched off (HOLMES_NO_SEMANTIC_WARM); `
|
|
203
|
+
+ `coverage stands at ${p.coverage.covered} of ${p.coverage.total} — run rtm_reindex to warm on demand`;
|
|
204
|
+
}
|
|
205
|
+
const line = coverageLine(p.coverage);
|
|
206
|
+
if (p.coverage.state === 'covered' && p.stored !== undefined && p.stored < p.coverage.covered) {
|
|
207
|
+
return `${line} — ${p.coverage.covered - p.stored} vector(s) were computed but never stored`;
|
|
208
|
+
}
|
|
209
|
+
return p.coverage.state === 'covered' ? '' : line;
|
|
210
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "@implements A-SPEC-209",
|
|
3
3
|
"name": "@holmes-lab/holmes-kit",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.25.1",
|
|
5
5
|
"description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
|
|
6
6
|
"main": "dist/holmes/mcp/server.js",
|
|
7
7
|
"types": "dist/holmes/mcp/server.d.ts",
|