@holmes-lab/holmes-kit 0.23.2 → 0.23.3

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 CHANGED
@@ -5,6 +5,51 @@ 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.23.3] - 2026-09-18
9
+
10
+ A suite that takes half as long, a build that admits when it is stale, and a CI matrix that stopped
11
+ reporting its own noise as a verdict.
12
+
13
+ ### Fixed
14
+ - **The Files-to-Touch declarations that were anchored, and the ones that were not** (A-SPEC-670,
15
+ A-SPEC-665, A-SPEC-672, A-SPEC-674). Five trace gaps that REQ-670's parser fix uncovered are
16
+ closed: `A-SPEC-495`, `A-SPEC-549.3` and `A-SPEC-549.4` declared scanned production files and
17
+ carried no anchor in any of them. Every one of the six places was checked before it was claimed —
18
+ an anchor is an assertion, not a metric — and all six were true. Unlinked approved specs 52 → 49,
19
+ `codeLinkedPct` 91.95 → 92.41, trace gaps 5 → 0.
20
+ - **The CI matrix was reporting its own noise as a verdict** (A-SPEC-664, A-SPEC-674). Measured over
21
+ 22 runs: **every one was red**, and the reds were dominated by two suites this repository knows as
22
+ load gauges (`dashboard.test` 20/22, `entity-store-boundaries` 12/22). The cause was contention —
23
+ the Linux VM takes 11 of the host's 12 cores and ran jest with default workers. A signal that is
24
+ always red cannot separate a regression from itself. With the worker cap below, both canaries went
25
+ green on Linux for the first time in 23 runs, and the matrix immediately produced its first real
26
+ finding.
27
+ - **A dead lock holder could stop the matrix for ever, silently** (A-SPEC-674). The runner's lock was
28
+ released by `trap ... EXIT`, which SIGKILL and a process-group teardown skip; one such lock stood
29
+ for 95 minutes while nine commits went unjudged. The lock now names its holder and liveness is
30
+ judged by PID, never by elapsed time — calling a slow run dead would start a second run on the
31
+ same machine. A skipped run also leaves a `skipped` row now: REQ-664's rule is that no row means
32
+ "not run", so a silent skip erased the skip itself.
33
+
34
+ ### Added
35
+ - **The Stop hook says when `dist/` no longer represents the source** (A-SPEC-673). Thirty suites
36
+ load `dist/` while they run and nothing asserted it was current; only the release gate compared the
37
+ build id to HEAD, and only at publish time. A stale build does not go red — it verifies old code
38
+ and returns green. The judgement is built on the build id and **not** on mtime, measured: this
39
+ tree's `.build-id` had a newer mtime than every source while naming a commit nine behind, and two
40
+ changed sources were missing from `dist` entirely. Five states, of which `fresh` and `absent` say
41
+ nothing at all, and the two unjudgeable ones say **the check could not run** rather than passing
42
+ quietly. Non-blocking, on the `tracked` channel, and silent in a workspace that does not build.
43
+
44
+ ### Changed
45
+ - **The test suite runs in 353s instead of 505s** (A-SPEC-672). The wall clock equalled the longest
46
+ single file to the decimal, so more workers could not help. Splitting the two heaviest files alone
47
+ moved it 505s → 493s while the serial total rose 2,836s → 5,109s: four heavy suites running at once
48
+ contended hard enough to swallow the win. Measured 2x2, the split and a worker cap **interact** —
49
+ alone they are worth 12s and 44s, together 132s. `jest.maxWorkers` is now `"50%"`, a proportion
50
+ rather than a number, because a fixed count means something else on a machine with a different core
51
+ count. Cases are preserved exactly, per-spec execution counts included.
52
+
8
53
  ## [0.23.2] - 2026-09-18
9
54
 
10
55
  Three rules that existed only in prose, and the one number a broken parser had published.
package/README.md CHANGED
@@ -16,6 +16,7 @@
16
16
 
17
17
  ### 🛡️ Currently Supported Features (Production Features)
18
18
 
19
+ - 🧱 **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.
19
20
  - 📐 **Declarations are read as written** *(new in 0.23.2)*: `Files to Touch` is where a spec declares the files it will touch, and three things read it — fulfilment advisories, the declaration census and the approval impact note. The parser took only the **first word of a list item**, so measured over 678 approved specs here, **88 specs and 172 paths were declared and never read**; 24 of them parsed to zero while naming files plainly. Several paths on one line, an indented continuation, a Korean first word, a prose paragraph — all invisible. They are read now, wherever they sit, and a bare `name.ext` keeps its old position rule so a property access like `module.exports` is still not a file. A declared path that does not exist but is the suffix of exactly one repository file is reported as an abbreviation rather than a defect; two candidates stays an ambiguity and nothing is guessed. Cost, measured across the whole corpus: **zero** new `missing` findings.
20
21
  - 📄 **The publish gate reads the docs** *(new in 0.23.2)*: the publish playbook has demanded "bring README and CHANGELOG up to this release" since 0.16.0 and only prose enforced it, so it failed four times — including 0.21.0, 0.22.0 and 0.23.0, which each shipped with a feature list frozen at 0.20.0. The release gate now refuses two things it can decide: a missing CHANGELOG entry for the version being published, and an entry with `### Added` while `README.md` has not changed since the previous release. Replayed over eight releases it refuses exactly the three that were stale and passes the other five. What needs judgement — is the old wording still true? — stays with the person and is **reported**, never faked; a check that could not run says so instead of reading as a pass.
21
22
  - 🧩 **Your config files survive a re-wire** *(new in 0.23.0)*: `init --agent antigravity` and `init --agent codex` used to replace `.agents/mcp_config.json`, `.agents/hooks.json` and `marketplace.json` **whole**. Measured with a real `--dry-run` before the fix: a neighbour MCP server, a neighbour hook namespace, a neighbour plugin, a marketplace's own name and an operator's `disabled` flag all survived a re-wire at a rate of **zero**. They now survive — only the holmes-kit entry is refreshed, and `init` names what it kept. A `disabled` you set stays set (and `init` says the gate will not run while it stands, rather than switching it back on silently); an existing file that is not readable JSON is refused with a reason instead of being replaced. The Claude wiring already merged; the other two harnesses now have the same discipline.
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- 86f8d89c-mu6hz0ua
1
+ 970ee6cf-mu6tt9oo
@@ -48,6 +48,8 @@ exports.renderRefusals = renderRefusals;
48
48
  exports.renderNonTtyHint = renderNonTtyHint;
49
49
  exports.runInteractive = runInteractive;
50
50
  // @implements A-SPEC-246, A-SPEC-626
51
+ // @implements A-SPEC-549.4 — renderPending and renderNonTtyHint in this file are the English CLI
52
+ // surface the Hangul-absence guard reads.
51
53
  const execution_context_1 = require("../project/execution-context");
52
54
  const root_1 = require("../project/root");
53
55
  const fs = __importStar(require("node:fs"));
@@ -46,6 +46,8 @@ exports.wiredSpecsDir = wiredSpecsDir;
46
46
  exports.evaluateHook = evaluateHook;
47
47
  exports.readSpecsSync = readSpecsSync;
48
48
  // @implements A-SPEC-278
49
+ // @implements A-SPEC-549.3 — the hook's deny reasons are English here; hooks-english.test.ts drives
50
+ // evaluateHook in this file to prove it.
49
51
  // @implements A-SPEC-643
50
52
  // @implements A-SPEC-642
51
53
  // @implements A-SPEC-194, A-SPEC-195
@@ -3,7 +3,9 @@ import { Spec } from '../spec/spec-parser';
3
3
  import type { TestOutcome } from '../review/test-runner';
4
4
  import { type KnownDefectJudgement } from '../rtm/known-defects';
5
5
  import { type CiVerdict } from '../project/ci-runs';
6
+ import { type DistVerdict } from '../project/dist-freshness';
6
7
  export declare function collectKnownDefects(root: string, now: Date): KnownDefectJudgement | undefined;
8
+ export declare function collectDistFreshness(root: string): DistVerdict | undefined;
7
9
  export declare function collectCiVerdicts(root: string, now?: Date): CiVerdict[];
8
10
  /**
9
11
  * @implements A-SPEC-100.2
@@ -38,6 +40,11 @@ export interface StopEvidence {
38
40
  * hook could not look; a `not-run` verdict when it looked and found no row (never silence).
39
41
  */
40
42
  ci?: CiVerdict[];
43
+ /**
44
+ * @implements A-SPEC-673 — whether `dist/` still represents the source. Absent when the workspace
45
+ * does not build at all; a `no-build-id`/`unknown` verdict when it looked and could not judge.
46
+ */
47
+ dist?: DistVerdict;
41
48
  /** Provenance-chain verification result (CLI-supplied). A broken chain blocks the stop. */
42
49
  provenance?: {
43
50
  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.collectDistFreshness = collectDistFreshness;
38
39
  exports.collectCiVerdicts = collectCiVerdicts;
39
40
  exports.changedAnchoredAspecs = changedAnchoredAspecs;
40
41
  exports.unanchoredChangedSources = unanchoredChangedSources;
@@ -55,6 +56,8 @@ exports.guardCountOrZero = guardCountOrZero;
55
56
  exports.readGuardCount = readGuardCount;
56
57
  exports.writeGuardCount = writeGuardCount;
57
58
  const fs = __importStar(require("node:fs"));
59
+ // @implements A-SPEC-549.3 — evaluateStop's messages are English here; the Hangul-absence guard
60
+ // calls into this file.
58
61
  // @implements A-SPEC-645
59
62
  // @implements A-SPEC-642
60
63
  const npx_bin_1 = require("../project/npx-bin");
@@ -81,6 +84,7 @@ const root_1 = require("../project/root");
81
84
  const known_defects_1 = require("../rtm/known-defects");
82
85
  const test_files_1 = require("../cpg/test-files");
83
86
  const ci_runs_1 = require("../project/ci-runs");
87
+ const dist_freshness_1 = require("../project/dist-freshness");
84
88
  // @implements A-SPEC-660 — the I/O half of the known-defect marker: walk the workspace's test files
85
89
  // (the same directory rule and test predicate ART-4's anchor scan uses), parse each for markers,
86
90
  // judge them against the injected clock. A walk that cannot START is NO SIGNAL (undefined) — never
@@ -128,6 +132,32 @@ function collectKnownDefects(root, now) {
128
132
  return undefined;
129
133
  }
130
134
  }
135
+ // @implements A-SPEC-673 — the I/O half of the build-freshness line: read `dist/.build-id` and ask
136
+ // git how far that commit sits behind HEAD. A workspace that does not build hears nothing; a build
137
+ // that cannot be judged says so rather than passing quietly.
138
+ function collectDistFreshness(root) {
139
+ if (!(0, dist_freshness_1.hasDist)(root))
140
+ return undefined;
141
+ const buildId = (0, dist_freshness_1.distBuildId)(root);
142
+ let head = null;
143
+ try {
144
+ head = (0, node_child_process_1.execFileSync)('git', ['rev-parse', '--short', 'HEAD'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)() }).trim();
145
+ }
146
+ catch {
147
+ head = null;
148
+ }
149
+ let behind = null;
150
+ if (buildId && head) {
151
+ try {
152
+ const n = Number((0, node_child_process_1.execFileSync)('git', ['rev-list', '--count', `${buildId}..HEAD`], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)() }).trim());
153
+ behind = Number.isFinite(n) ? n : null;
154
+ }
155
+ catch {
156
+ behind = null;
157
+ }
158
+ }
159
+ return (0, dist_freshness_1.distFreshness)({ distExists: true, buildId, head, behind });
160
+ }
131
161
  // @implements A-SPEC-664 — the I/O half of the CI line: read the ci-runs ledger, judge Linux (always)
132
162
  // and any other OS that has a row, with git measuring how far each judged commit sits behind HEAD.
133
163
  // A tree without a ledger yields a `not-run` verdict — a line, not an absence.
@@ -529,6 +559,7 @@ const TRACK_LABELS = {
529
559
  'ART-2': 'code-graph cycles',
530
560
  'ART-9': 'known-defect debt',
531
561
  'CI': 'matrix',
562
+ 'DIST': 'build freshness',
532
563
  };
533
564
  /**
534
565
  * One line per ARTICLE, each under its own name.
@@ -602,6 +633,14 @@ function evaluateStop(specs, evidence) {
602
633
  const t = evidence.ci.map((v) => ({ article: 'CI', detail: (0, ci_runs_1.ciStatusLine)(v) }));
603
634
  tracked = [...(tracked ?? []), ...t];
604
635
  }
636
+ // @implements A-SPEC-673 — a build that no longer represents the source. Thirty suites load `dist/`
637
+ // while they run, so a stale one does not go red: it verifies old code and returns green. `fresh`
638
+ // and `absent` produce no line, because a line is spent only on something the reader can act on.
639
+ if (evidence?.dist) {
640
+ const detail = (0, dist_freshness_1.distStatusLine)(evidence.dist);
641
+ if (detail)
642
+ tracked = [...(tracked ?? []), { article: 'DIST', detail }];
643
+ }
605
644
  const problems = violations.map((x) => `[${x.article}] ${x.detail}`);
606
645
  // @implements A-SPEC-247 — structured list so the caller can ask acknowledgeStop which of these
607
646
  // are waiting on an owner. Mirrors `problems` exactly, including the two synthesized below.
@@ -1194,7 +1233,15 @@ if (require.main === module) {
1194
1233
  catch {
1195
1234
  ci = undefined;
1196
1235
  }
1197
- let out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable, unanchoredChangedSources: unanchored, unrecordedApprovals: unrecorded, rolledBackLedgers: rolledBack, redFirstMode, changedAspecs, outcomesByAspec, ...(knownDefects ? { knownDefects } : {}), ...(ci ? { ci } : {}) });
1236
+ // @implements A-SPEC-673 the build-freshness line rides the same non-blocking channel.
1237
+ let dist;
1238
+ try {
1239
+ dist = collectDistFreshness(stopProjectRoot());
1240
+ }
1241
+ catch {
1242
+ dist = undefined;
1243
+ }
1244
+ let out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable, unanchoredChangedSources: unanchored, unrecordedApprovals: unrecorded, rolledBackLedgers: rolledBack, redFirstMode, changedAspecs, outcomesByAspec, ...(knownDefects ? { knownDefects } : {}), ...(ci ? { ci } : {}), ...(dist ? { dist } : {}) });
1198
1245
  // @implements A-SPEC-534.4 — track mode records ART-8 findings without blocking: surface them so
1199
1246
  // the operator observes RED-first gaps before an owner promotes the posture to strict.
1200
1247
  // @implements A-SPEC-559.2 — spec-evolution trigger (observe-first, NEVER blocks): a dirty
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TOOL_SCHEMAS = exports.HOOK_ENFORCED_TOOLS = void 0;
4
4
  // @implements A-SPEC-629, A-SPEC-628, A-SPEC-623, A-SPEC-277, A-SPEC-624, A-SPEC-627
5
+ // @implements A-SPEC-495 — the maintenance_analyze description carries the semantic-evidence
6
+ // consumer contract (sem-verify / semCos / semanticAlternates) beside the anti-prediction warning.
5
7
  // @implements A-SPEC-642
6
8
  // @implements A-SPEC-641
7
9
  // @implements A-SPEC-639
@@ -0,0 +1,20 @@
1
+ export interface CiLockHolder {
2
+ pid: number;
3
+ host: string;
4
+ rev: string;
5
+ at: string;
6
+ }
7
+ export type CiLockState = 'free' | 'held' | 'dead' | 'unknown';
8
+ export interface CiLockInput {
9
+ holder: CiLockHolder | null;
10
+ thisHost: string;
11
+ alive: boolean;
12
+ }
13
+ /** Null for every shape we cannot trust — including the legacy empty directory. */
14
+ export declare function readCiLock(lockDir: string): CiLockHolder | null;
15
+ /**
16
+ * A holder written by ANOTHER host is `unknown`, not dead: we cannot ask that machine about its
17
+ * pids, and guessing would hand two runners the same VM. Unknown is respected like `held`.
18
+ */
19
+ export declare function ciLockState(input: CiLockInput): CiLockState;
20
+ export declare function ciLockLine(state: CiLockState, holder: CiLockHolder | null, now?: Date): string;
@@ -0,0 +1,111 @@
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.readCiLock = readCiLock;
37
+ exports.ciLockState = ciLockState;
38
+ exports.ciLockLine = ciLockLine;
39
+ // @implements A-SPEC-674
40
+ /**
41
+ * Who holds the CI runner's lock, and is that holder still alive.
42
+ *
43
+ * The runner serialises itself with a `mkdir` lock — atomic, and that atomicity is the whole reason
44
+ * it works — released by `trap ... EXIT`. SIGKILL and launchd tearing down the process group skip
45
+ * that trap. Measured 2026-09-18: one such lock survived 95 minutes while the matrix judged nothing
46
+ * and nine commits went unjudged, with zero jest processes in the VM and a load of 0.57.
47
+ *
48
+ * The lock used to be an EMPTY directory, so it could not say who held it — and a lock that cannot
49
+ * name its holder cannot be asked whether that holder is alive. It now carries a holder file, and
50
+ * the judgement below is about liveness, never about elapsed time: calling a slow run dead would
51
+ * start a second run on the same VM, which is a worse failure than waiting.
52
+ */
53
+ const fs = __importStar(require("node:fs"));
54
+ const path = __importStar(require("node:path"));
55
+ /** Null for every shape we cannot trust — including the legacy empty directory. */
56
+ function readCiLock(lockDir) {
57
+ let raw;
58
+ try {
59
+ raw = fs.readFileSync(path.join(lockDir, 'holder.json'), 'utf8');
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ let parsed;
65
+ try {
66
+ parsed = JSON.parse(raw);
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ const h = parsed;
72
+ if (typeof h?.pid !== 'number' || !Number.isFinite(h.pid))
73
+ return null;
74
+ if (typeof h.host !== 'string' || typeof h.rev !== 'string' || typeof h.at !== 'string')
75
+ return null;
76
+ return { pid: h.pid, host: h.host, rev: h.rev, at: h.at };
77
+ }
78
+ /**
79
+ * A holder written by ANOTHER host is `unknown`, not dead: we cannot ask that machine about its
80
+ * pids, and guessing would hand two runners the same VM. Unknown is respected like `held`.
81
+ */
82
+ function ciLockState(input) {
83
+ const { holder, thisHost, alive } = input;
84
+ if (!holder)
85
+ return 'free';
86
+ if (holder.host !== thisHost)
87
+ return 'unknown';
88
+ return alive ? 'held' : 'dead';
89
+ }
90
+ const elapsed = (at, now) => {
91
+ const t = Date.parse(at);
92
+ if (!Number.isFinite(t))
93
+ return null;
94
+ const mins = Math.floor((now.getTime() - t) / 60000);
95
+ // A clock that runs backwards is a fact about the clock, not about the holder; never report it as
96
+ // negative time.
97
+ return mins < 0 ? null : `${mins} min`;
98
+ };
99
+ function ciLockLine(state, holder, now = new Date()) {
100
+ if (state === 'free' || !holder)
101
+ return '';
102
+ const held = elapsed(holder.at, now);
103
+ const forHow = held ? ` for ${held}` : '';
104
+ if (state === 'dead') {
105
+ return `ci lock holder pid ${holder.pid} is gone (held${forHow}, rev ${holder.rev}) — reclaiming it; the matrix judges nothing while a dead holder stands`;
106
+ }
107
+ if (state === 'unknown') {
108
+ return `ci lock was taken by ${holder.host} (rev ${holder.rev}${forHow}) — this host cannot judge another machine's pids, so it is respected`;
109
+ }
110
+ return `ci lock is held by pid ${holder.pid} running rev ${holder.rev}${forHow}`;
111
+ }
@@ -0,0 +1,30 @@
1
+ export type DistState = 'fresh' | 'behind' | 'absent' | 'no-build-id' | 'unknown';
2
+ export interface DistVerdict {
3
+ state: DistState;
4
+ buildId: string | null;
5
+ head: string | null;
6
+ behind: number | null;
7
+ }
8
+ export interface DistInput {
9
+ distExists: boolean;
10
+ buildId: string | null;
11
+ head: string | null;
12
+ /** Commits from the build id to HEAD, or null when it could not be obtained. */
13
+ behind: number | null;
14
+ }
15
+ /**
16
+ * The adoption predicate: existence, never contents. A workspace that does not build has no build
17
+ * freshness to report, and telling it about one would be noise it can do nothing with (the same
18
+ * reasoning REQ-664 used for the CI ledger).
19
+ */
20
+ export declare function hasDist(root: string): boolean;
21
+ /** The commit the artefact came from — the part before the dash `npm run build` writes. */
22
+ export declare function distBuildId(root: string): string | null;
23
+ /**
24
+ * Five states because each calls for something different: `behind` means build, `absent` means
25
+ * nothing at all to this workspace, and the two unjudgeable ones mean the check did not run — which
26
+ * must never be reported as a check that passed.
27
+ */
28
+ export declare function distFreshness(input: DistInput): DistVerdict;
29
+ /** Empty when there is nothing to say. A line is spent only on a fact the reader can act on. */
30
+ export declare function distStatusLine(v: DistVerdict): string;
@@ -0,0 +1,114 @@
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.hasDist = hasDist;
37
+ exports.distBuildId = distBuildId;
38
+ exports.distFreshness = distFreshness;
39
+ exports.distStatusLine = distStatusLine;
40
+ // @implements A-SPEC-673
41
+ /**
42
+ * Whether the built artefact still represents the source.
43
+ *
44
+ * Thirty suites in this repository load `dist/` while they run — they spawn it or require it — and
45
+ * nothing asserted that it was current. `scripts/verify-release.js` compares the build id to HEAD,
46
+ * but only at publish time, so during development a stale build does not go red: it quietly verifies
47
+ * old code and returns green. This repository has already paid for that once.
48
+ *
49
+ * The judgement is built on the BUILD ID, never on mtime, for a measured reason. On 2026-09-18 this
50
+ * tree's `dist` was nine commits behind HEAD and two changed sources were missing from it entirely,
51
+ * while `find src -newer dist/.build-id` answered zero — the marker's mtime had been touched without
52
+ * a rebuild. mtime lies. The build id says which commit the artefact actually came from.
53
+ *
54
+ * This module only JUDGES. It never rebuilds: a hook does not change the operator's tree in silence.
55
+ */
56
+ const fs = __importStar(require("node:fs"));
57
+ const path = __importStar(require("node:path"));
58
+ /**
59
+ * The adoption predicate: existence, never contents. A workspace that does not build has no build
60
+ * freshness to report, and telling it about one would be noise it can do nothing with (the same
61
+ * reasoning REQ-664 used for the CI ledger).
62
+ */
63
+ function hasDist(root) {
64
+ try {
65
+ return fs.statSync(path.join(root, 'dist')).isDirectory();
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ /** The commit the artefact came from — the part before the dash `npm run build` writes. */
72
+ function distBuildId(root) {
73
+ let raw;
74
+ try {
75
+ raw = fs.readFileSync(path.join(root, 'dist', '.build-id'), 'utf8');
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ const id = raw.trim().split('-')[0];
81
+ // A shape we do not recognise falls through to a judgement we cannot make, never to a pass.
82
+ return /^[0-9a-f]{7,40}$/.test(id) ? id : null;
83
+ }
84
+ /**
85
+ * Five states because each calls for something different: `behind` means build, `absent` means
86
+ * nothing at all to this workspace, and the two unjudgeable ones mean the check did not run — which
87
+ * must never be reported as a check that passed.
88
+ */
89
+ function distFreshness(input) {
90
+ const { distExists, buildId, head, behind } = input;
91
+ const base = { buildId: buildId ?? null, head: head ?? null, behind: typeof behind === 'number' ? behind : null };
92
+ if (!distExists)
93
+ return { state: 'absent', ...base };
94
+ if (!buildId)
95
+ return { state: 'no-build-id', ...base };
96
+ if (!head || typeof behind !== 'number' || !Number.isFinite(behind) || behind < 0)
97
+ return { state: 'unknown', ...base };
98
+ return { state: behind === 0 ? 'fresh' : 'behind', ...base };
99
+ }
100
+ /** Empty when there is nothing to say. A line is spent only on a fact the reader can act on. */
101
+ function distStatusLine(v) {
102
+ if (v.state === 'fresh' || v.state === 'absent')
103
+ return '';
104
+ if (v.state === 'no-build-id') {
105
+ return 'dist/.build-id is missing or unreadable — the build could not be judged against the source; `npm run build` writes it';
106
+ }
107
+ if (v.state === 'unknown') {
108
+ return `dist was built from ${v.buildId ?? 'an unknown commit'} and the distance to HEAD could not be obtained — the build could not be judged`;
109
+ }
110
+ // No invented threshold: one commit is reported like nine, because which commits matter is a
111
+ // question the reader can answer and this function cannot.
112
+ const n = v.behind ?? 0;
113
+ return `dist is ${n} commit${n === 1 ? '' : 's'} behind HEAD (built from ${v.buildId}) — suites that load dist are verifying that build, not this tree; \`npm run build\``;
114
+ }
@@ -37,6 +37,8 @@ exports.resolveSemanticKey = resolveSemanticKey;
37
37
  exports.storeSemanticKey = storeSemanticKey;
38
38
  exports.removeSemanticKey = removeSemanticKey;
39
39
  // @implements A-SPEC-592
40
+ // @implements A-SPEC-549.4 — declared by that spec's Files to Touch and carries no Hangul; note that
41
+ // the guard's own SOURCES list does not cover this file (reported, not fixed here).
40
42
  // @implements A-SPEC-477
41
43
  /**
42
44
  * The cloud tier's credential: WHERE the consent lives, and in what order it is looked up.
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.23.2",
4
+ "version": "0.23.3",
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",
@@ -62,7 +62,8 @@
62
62
  "testTimeout": 30000,
63
63
  "setupFilesAfterEnv": [
64
64
  "<rootDir>/src/holmes/test-support/jest-timeouts-setup.ts"
65
- ]
65
+ ],
66
+ "maxWorkers": "50%"
66
67
  },
67
68
  "dependencies": {
68
69
  "@modelcontextprotocol/sdk": "^1.29.0",