@holmes-lab/holmes-kit 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +2 -1
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve.d.ts +14 -0
- package/dist/holmes/cli/approve.js +60 -3
- package/dist/holmes/cli/gitignore-merge.js +5 -0
- package/dist/holmes/cli/index.js +13 -1
- package/dist/holmes/governance/approval-queue.d.ts +39 -0
- package/dist/holmes/governance/approval-queue.js +105 -10
- package/dist/holmes/governance/session-context.d.ts +74 -0
- package/dist/holmes/governance/session-context.js +179 -0
- package/dist/holmes/hooks/pre-tool-use.js +5 -2
- package/dist/holmes/hooks/rtm-refresh-child.d.ts +1 -0
- package/dist/holmes/hooks/rtm-refresh-child.js +56 -0
- package/dist/holmes/hooks/rtm-refresh.d.ts +13 -0
- package/dist/holmes/hooks/rtm-refresh.js +76 -0
- package/dist/holmes/hooks/stop.js +42 -0
- package/dist/holmes/mcp/handlers.d.ts +5 -6
- package/dist/holmes/mcp/handlers.js +76 -2
- package/dist/holmes/mcp/server.js +12 -0
- package/dist/holmes/mcp/tool-schemas.js +1 -1
- package/dist/holmes/review/judgement-bundle.d.ts +49 -0
- package/dist/holmes/review/judgement-bundle.js +108 -0
- package/dist/holmes/review/run-replay.d.ts +5 -0
- package/dist/holmes/review/run-replay.js +32 -0
- package/dist/holmes/review/test-outcomes.d.ts +17 -2
- package/dist/holmes/review/test-outcomes.js +54 -15
- package/dist/holmes/rtm/impact-advisory.d.ts +48 -0
- package/dist/holmes/rtm/impact-advisory.js +175 -0
- package/dist/holmes/rtm/localize.js +7 -0
- package/dist/holmes/rtm/rtm-builder.d.ts +8 -0
- package/dist/holmes/rtm/rtm-builder.js +42 -1
- package/dist/holmes/rtm/rtm-graph.d.ts +16 -1
- package/dist/holmes/rtm/rtm-graph.js +34 -6
- package/dist/holmes/spec/approval-blockers.js +8 -0
- package/dist/holmes/spec/compat-impact.d.ts +31 -0
- package/dist/holmes/spec/compat-impact.js +141 -0
- package/dist/holmes/spec/spec-types.js +3 -1
- package/package.json +1 -1
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.DEFAULT_IMPACT = void 0;
|
|
37
|
+
exports.specSummary = specSummary;
|
|
37
38
|
exports.buildFileSubgraph = buildFileSubgraph;
|
|
38
39
|
exports.buildRtm = buildRtm;
|
|
39
40
|
exports.addCallEdges = addCallEdges;
|
|
@@ -62,6 +63,44 @@ function fact(opts, sourceLocation, derivation, confidence, evidenceDigest = nul
|
|
|
62
63
|
derivation,
|
|
63
64
|
};
|
|
64
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* @implements A-SPEC-568.1
|
|
68
|
+
* Which section states a spec's INTENT, per kind. Only the three kinds whose required sections
|
|
69
|
+
* carry a prose intent are mapped; every other kind (T-SPEC's quadrants, C-SPEC) summarizes as its
|
|
70
|
+
* title alone — a rule deferred until a consumer measures the lack (H-SPEC-568 Open Questions).
|
|
71
|
+
*/
|
|
72
|
+
const INTENT_SECTION = {
|
|
73
|
+
'REQ': 'Problem / Need',
|
|
74
|
+
'H-SPEC': 'Intent',
|
|
75
|
+
'A-SPEC': 'Objective',
|
|
76
|
+
};
|
|
77
|
+
/** Longest intent sentence carried into the graph — a bound, applied deterministically. */
|
|
78
|
+
const SUMMARY_SENTENCE_CAP = 200;
|
|
79
|
+
/**
|
|
80
|
+
* @implements A-SPEC-568.1
|
|
81
|
+
* `"<title> — <intent first sentence>"`, EXTRACTED — never generated. Same spec store, same bytes:
|
|
82
|
+
* the whole function is whitespace folding, one regex, one slice. A missing, blank or still-TODO
|
|
83
|
+
* intent section degrades to the title alone (spec_create stubs sections as `TODO`, and "TODO" as
|
|
84
|
+
* an intent sentence would be noise wearing a dash).
|
|
85
|
+
*/
|
|
86
|
+
function specSummary(s) {
|
|
87
|
+
// The Spec type promises strings, but runtime records disagree (test fixtures without a title;
|
|
88
|
+
// 20 legacy store documents with title ''). Extraction is fail-open: it degrades, never throws.
|
|
89
|
+
const title = String(s.title ?? '').trim();
|
|
90
|
+
const section = INTENT_SECTION[s.type];
|
|
91
|
+
const text = (section ? (s.sections?.[section] ?? '') : '').replace(/\s+/g, ' ').trim();
|
|
92
|
+
// Measured 2026-09-07 on this store: 20 legacy specs carry title '' — the summary falls back to
|
|
93
|
+
// the sentence alone, then to the id, because SC1 admits no empty summary on any path.
|
|
94
|
+
if (text === '' || text === 'TODO')
|
|
95
|
+
return title !== '' ? title : s.id;
|
|
96
|
+
// First sentence: up to the first . ! or ? that ends a word — the lookahead keeps `A-SPEC-129.2`
|
|
97
|
+
// whole, because its dot is followed by a digit, not by whitespace or the end.
|
|
98
|
+
const m = /^(.*?[.!?])(?=\s|$)/.exec(text);
|
|
99
|
+
let sentence = m ? m[1] : text;
|
|
100
|
+
if (sentence.length > SUMMARY_SENTENCE_CAP)
|
|
101
|
+
sentence = `${sentence.slice(0, SUMMARY_SENTENCE_CAP)}…`;
|
|
102
|
+
return title !== '' ? `${title} — ${sentence}` : sentence;
|
|
103
|
+
}
|
|
65
104
|
/**
|
|
66
105
|
* Adds one scanned file's CODE nodes and `implements` edges to the graph,
|
|
67
106
|
* tagged with that file's sourcePath so RtmGraph.removeBySource(f.sourcePath)
|
|
@@ -96,7 +135,9 @@ function buildRtm(specs, scanned, graph, opts) {
|
|
|
96
135
|
// Add spec nodes and dependencies
|
|
97
136
|
for (const s of specs) {
|
|
98
137
|
const sourcePath = opts?.specSourcePath?.(s.id);
|
|
99
|
-
|
|
138
|
+
// @implements A-SPEC-568.1 — the intent property rides in with the node; CODE/FILE/COMMIT/
|
|
139
|
+
// DECISION nodes pass nothing and stay null.
|
|
140
|
+
graph.addNode(`SPEC:${s.id}`, s.type, sourcePath, fact(opts, sourcePath ?? null, 'spec-store', 1), specSummary(s));
|
|
100
141
|
// DECLARED in frontmatter.
|
|
101
142
|
for (const p of s.dependsOn) {
|
|
102
143
|
graph.addEdge(`SPEC:${s.id}`, `SPEC:${p}`, 'depends_on', sourcePath, fact(opts, sourcePath ?? null, 'frontmatter', 1));
|
|
@@ -51,7 +51,15 @@ export declare class RtmGraph {
|
|
|
51
51
|
constructor(dbPath?: string);
|
|
52
52
|
private nodeStmt?;
|
|
53
53
|
private edgeStmt?;
|
|
54
|
-
addNode(id: string, kind: string, sourcePath?: string, provenance?: Provenance): void;
|
|
54
|
+
addNode(id: string, kind: string, sourcePath?: string, provenance?: Provenance, summary?: string | null): void;
|
|
55
|
+
/**
|
|
56
|
+
* @implements A-SPEC-568.1
|
|
57
|
+
* The node's intent sentence, extracted from the spec store at build time — or null: for a CODE
|
|
58
|
+
* node (no prose was extracted), for a node written before rtm-graph/3, for an unknown id. The
|
|
59
|
+
* summary is INFORMATION, never a verdict input (REQ-568 c6 rule) — nothing in this class or its
|
|
60
|
+
* consumers ranks, gates or filters on it.
|
|
61
|
+
*/
|
|
62
|
+
summaryOf(id: string): string | null;
|
|
55
63
|
addEdge(src: string, dst: string, rel: string, sourcePath?: string, provenance?: Provenance): void;
|
|
56
64
|
/** @implements A-SPEC-281 — how this node got here, or null if the node is unknown. */
|
|
57
65
|
provenanceOfNode(id: string): Provenance | null;
|
|
@@ -72,6 +80,13 @@ export declare class RtmGraph {
|
|
|
72
80
|
* this and not `incoming`, which would also drag in `implements` and `depends_on` and turn a
|
|
73
81
|
* "who calls me" question into "what is attached to me in any way at all".
|
|
74
82
|
*/
|
|
83
|
+
/**
|
|
84
|
+
* @implements A-SPEC-566.1
|
|
85
|
+
* Every node the graph holds for one source file — the impact advisory's entry point. Exact
|
|
86
|
+
* `source_path` equality on the indexed column (idx_nodes_source): no LIKE, so % and _ in a path
|
|
87
|
+
* are literal by construction and `a.ts` can never absorb `xa.ts`.
|
|
88
|
+
*/
|
|
89
|
+
nodeIdsInFile(relPath: string): string[];
|
|
75
90
|
callersOf(id: string): string[];
|
|
76
91
|
/**
|
|
77
92
|
* @implements A-SPEC-288
|
|
@@ -75,7 +75,7 @@ class RtmGraph {
|
|
|
75
75
|
this.db.pragma('journal_mode = WAL');
|
|
76
76
|
this.db.pragma('busy_timeout = 5000');
|
|
77
77
|
}
|
|
78
|
-
this.db.exec(`CREATE TABLE IF NOT EXISTS nodes (id TEXT PRIMARY KEY, kind TEXT NOT NULL, source_path TEXT);
|
|
78
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS nodes (id TEXT PRIMARY KEY, kind TEXT NOT NULL, source_path TEXT, summary TEXT);
|
|
79
79
|
CREATE TABLE IF NOT EXISTS edges (src TEXT NOT NULL, dst TEXT NOT NULL, rel TEXT NOT NULL, source_path TEXT, PRIMARY KEY (src,dst,rel));
|
|
80
80
|
-- The PK covers src-prefixed lookups; nothing covered dst or source_path, so every reverse
|
|
81
81
|
-- traversal and every removeBySource was a full table scan. Measured on a 240k-node graph:
|
|
@@ -103,6 +103,11 @@ class RtmGraph {
|
|
|
103
103
|
const type = column === 'confidence' ? 'REAL' : 'TEXT';
|
|
104
104
|
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
105
105
|
}
|
|
106
|
+
// @implements A-SPEC-568.1 — same in-place migration rule as provenance: an rtm-graph/2 file
|
|
107
|
+
// must keep OPENING (openReusableGraph decides reuse; an unreadable file would look corrupt).
|
|
108
|
+
if (table === 'nodes' && !present.has('summary')) {
|
|
109
|
+
this.db.exec('ALTER TABLE nodes ADD COLUMN summary TEXT');
|
|
110
|
+
}
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
113
|
// @implements A-SPEC-139
|
|
@@ -113,9 +118,20 @@ class RtmGraph {
|
|
|
113
118
|
// Semantics are unchanged: same signatures, same INSERT OR IGNORE idempotence, same columns.
|
|
114
119
|
nodeStmt;
|
|
115
120
|
edgeStmt;
|
|
116
|
-
addNode(id, kind, sourcePath, provenance) {
|
|
117
|
-
this.nodeStmt ??= this.db.prepare(`INSERT OR IGNORE INTO nodes (id,kind,source_path,${PROVENANCE_COLUMNS.join(',')}) VALUES (
|
|
118
|
-
this.nodeStmt.run(id, kind, sourcePath ?? null, ...provenanceRow(provenance));
|
|
121
|
+
addNode(id, kind, sourcePath, provenance, summary) {
|
|
122
|
+
this.nodeStmt ??= this.db.prepare(`INSERT OR IGNORE INTO nodes (id,kind,source_path,summary,${PROVENANCE_COLUMNS.join(',')}) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`);
|
|
123
|
+
this.nodeStmt.run(id, kind, sourcePath ?? null, summary ?? null, ...provenanceRow(provenance));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* @implements A-SPEC-568.1
|
|
127
|
+
* The node's intent sentence, extracted from the spec store at build time — or null: for a CODE
|
|
128
|
+
* node (no prose was extracted), for a node written before rtm-graph/3, for an unknown id. The
|
|
129
|
+
* summary is INFORMATION, never a verdict input (REQ-568 c6 rule) — nothing in this class or its
|
|
130
|
+
* consumers ranks, gates or filters on it.
|
|
131
|
+
*/
|
|
132
|
+
summaryOf(id) {
|
|
133
|
+
const row = this.db.prepare('SELECT summary FROM nodes WHERE id = ?').get(id);
|
|
134
|
+
return row?.summary ?? null;
|
|
119
135
|
}
|
|
120
136
|
addEdge(src, dst, rel, sourcePath, provenance) {
|
|
121
137
|
this.edgeStmt ??= this.db.prepare(`INSERT OR IGNORE INTO edges (src,dst,rel,source_path,${PROVENANCE_COLUMNS.join(',')}) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`);
|
|
@@ -149,6 +165,15 @@ class RtmGraph {
|
|
|
149
165
|
* this and not `incoming`, which would also drag in `implements` and `depends_on` and turn a
|
|
150
166
|
* "who calls me" question into "what is attached to me in any way at all".
|
|
151
167
|
*/
|
|
168
|
+
/**
|
|
169
|
+
* @implements A-SPEC-566.1
|
|
170
|
+
* Every node the graph holds for one source file — the impact advisory's entry point. Exact
|
|
171
|
+
* `source_path` equality on the indexed column (idx_nodes_source): no LIKE, so % and _ in a path
|
|
172
|
+
* are literal by construction and `a.ts` can never absorb `xa.ts`.
|
|
173
|
+
*/
|
|
174
|
+
nodeIdsInFile(relPath) {
|
|
175
|
+
return this.db.prepare('SELECT id FROM nodes WHERE source_path = ? ORDER BY id ASC').all(relPath).map((x) => x.id);
|
|
176
|
+
}
|
|
152
177
|
callersOf(id) {
|
|
153
178
|
return this.db.prepare("SELECT DISTINCT src FROM edges WHERE dst=? AND rel='calls' ORDER BY src ASC").all(id).map(x => x.src);
|
|
154
179
|
}
|
|
@@ -340,10 +365,13 @@ class RtmGraph {
|
|
|
340
365
|
// between a full rebuild and an incremental update is caught rather than passing as equal.
|
|
341
366
|
const cols = PROVENANCE_COLUMNS.join(', ');
|
|
342
367
|
const prov = (r) => PROVENANCE_COLUMNS.map((c) => (r[c] === null || r[c] === undefined ? '' : String(r[c]))).join('\t');
|
|
343
|
-
const nodes = this.db.prepare(`SELECT id, kind, source_path, ${cols} FROM nodes ORDER BY id`).all();
|
|
368
|
+
const nodes = this.db.prepare(`SELECT id, kind, source_path, summary, ${cols} FROM nodes ORDER BY id`).all();
|
|
344
369
|
const edges = this.db.prepare(`SELECT src, dst, rel, source_path, ${cols} FROM edges ORDER BY src, dst, rel`).all();
|
|
345
370
|
return [
|
|
346
|
-
|
|
371
|
+
// @implements A-SPEC-568.1 — summary is part of the VALUE (appended last so every positional
|
|
372
|
+
// consumer of the earlier columns is untouched); a summary-only divergence between two builds
|
|
373
|
+
// must fail the convergence comparison rather than pass as equal.
|
|
374
|
+
...nodes.map((n) => `N\t${n.id}\t${n.kind}\t${n.source_path ?? ''}\t${prov(n)}\t${n.summary ?? ''}`),
|
|
347
375
|
...edges.map((e) => `E\t${e.src}\t${e.dst}\t${e.rel}\t${e.source_path ?? ''}\t${prov(e)}`),
|
|
348
376
|
].join('\n');
|
|
349
377
|
}
|
|
@@ -10,6 +10,7 @@ exports.unactionableCriteriaBlocker = unactionableCriteriaBlocker;
|
|
|
10
10
|
const acceptance_quality_1 = require("./acceptance-quality");
|
|
11
11
|
const validator_1 = require("./validator");
|
|
12
12
|
const breaking_change_1 = require("./breaking-change");
|
|
13
|
+
const compat_impact_1 = require("./compat-impact");
|
|
13
14
|
const spec_digest_1 = require("./spec-digest");
|
|
14
15
|
const spec_types_1 = require("./spec-types");
|
|
15
16
|
const draft_1 = require("../reverse/draft");
|
|
@@ -204,6 +205,13 @@ function approvalBlockers(spec, resolve) {
|
|
|
204
205
|
const breaking = (0, breaking_change_1.checkBreakingChangeDeclared)(candidate);
|
|
205
206
|
if (breaking)
|
|
206
207
|
out.push(breaking);
|
|
208
|
+
// @implements A-SPEC-565.1 — the compat duty pre-announced where breaking_change is: whatever the
|
|
209
|
+
// act refuses with, the gate must already have told the author (A-SPEC-182's parity). No file
|
|
210
|
+
// reader here — the gate context has no root — so the OS *content* cross-check stays act-only;
|
|
211
|
+
// the declaration syntax and the FtT harness-surface contradiction are fully pre-announced.
|
|
212
|
+
const compat = (0, compat_impact_1.checkCompatDeclared)(candidate);
|
|
213
|
+
if (compat)
|
|
214
|
+
out.push(compat);
|
|
207
215
|
const stubs = placeholderSections(spec);
|
|
208
216
|
if (stubs.length > 0) {
|
|
209
217
|
out.push((0, exports.placeholderMessage)(stubs));
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Spec } from './spec-parser';
|
|
2
|
+
/**
|
|
3
|
+
* REQ-565's enforcement device: the obligation to declare HARNESS (claude/codex/agy) and OS
|
|
4
|
+
* (windows/mac/linux) impact lives on the ACT of approval — the exact shape ADR-013 gave
|
|
5
|
+
* `breaking_change`, and for the same measured reason (a static `requiredFields` predicate turned
|
|
6
|
+
* 38 already-approved specs into ART-3 violations and bricked the harness; act-time converges
|
|
7
|
+
* instead: any spec that changes re-approves and acquires the fields, a spec that never changes
|
|
8
|
+
* can introduce no new incompatibility).
|
|
9
|
+
*
|
|
10
|
+
* WHY a gate and not a habit, measured twice in one day: an observability design came out
|
|
11
|
+
* Claude-biased (Stop hook + transcript_path is a Claude-only channel — the owner caught it), and
|
|
12
|
+
* the Stop hook's own header records that voluntarily-invoked discipline fired 0/143 times. Memory
|
|
13
|
+
* is rationale storage; control is a gate.
|
|
14
|
+
*
|
|
15
|
+
* WHAT THIS DOES NOT DO: verify the declarations are TRUE. Truth belongs to the layers that measure
|
|
16
|
+
* it — adapter parity (A-SPEC-336~338), doctor's wiring checks, on-device E2E. This gate makes
|
|
17
|
+
* skipping the thought impossible and makes a false declaration an auditable record.
|
|
18
|
+
*/
|
|
19
|
+
export declare const HARNESS_CELLS: readonly ["claude", "codex", "agy"];
|
|
20
|
+
export declare const OS_CELLS: readonly ["windows", "mac", "linux"];
|
|
21
|
+
export declare const CELL_VERDICTS: readonly ["supported", "unavailable", "n-a"];
|
|
22
|
+
export interface CompatCheckOpts {
|
|
23
|
+
/** Repo-relative reader for the OS cross-check; null = file absent (skip, fail-open). */
|
|
24
|
+
readFile?: (rel: string) => string | null;
|
|
25
|
+
}
|
|
26
|
+
/** `src/…` path tokens out of the Files to Touch section — backticks, bullets and commas tolerated.
|
|
27
|
+
* Backslashes normalize to `/` FIRST (adversarial round-1): a Windows author legitimately writes
|
|
28
|
+
* `src\holmes\hooks\stop.ts`, and un-normalized it walked straight past the surface prefixes —
|
|
29
|
+
* an OS-compat gate defeated by an OS path convention would be its own counterexample. */
|
|
30
|
+
export declare function filesToTouch(spec: Spec): string[];
|
|
31
|
+
export declare function checkCompatDeclared(spec: Spec, opts?: CompatCheckOpts): string | null;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CELL_VERDICTS = exports.OS_CELLS = exports.HARNESS_CELLS = void 0;
|
|
4
|
+
exports.filesToTouch = filesToTouch;
|
|
5
|
+
exports.checkCompatDeclared = checkCompatDeclared;
|
|
6
|
+
/**
|
|
7
|
+
* REQ-565's enforcement device: the obligation to declare HARNESS (claude/codex/agy) and OS
|
|
8
|
+
* (windows/mac/linux) impact lives on the ACT of approval — the exact shape ADR-013 gave
|
|
9
|
+
* `breaking_change`, and for the same measured reason (a static `requiredFields` predicate turned
|
|
10
|
+
* 38 already-approved specs into ART-3 violations and bricked the harness; act-time converges
|
|
11
|
+
* instead: any spec that changes re-approves and acquires the fields, a spec that never changes
|
|
12
|
+
* can introduce no new incompatibility).
|
|
13
|
+
*
|
|
14
|
+
* WHY a gate and not a habit, measured twice in one day: an observability design came out
|
|
15
|
+
* Claude-biased (Stop hook + transcript_path is a Claude-only channel — the owner caught it), and
|
|
16
|
+
* the Stop hook's own header records that voluntarily-invoked discipline fired 0/143 times. Memory
|
|
17
|
+
* is rationale storage; control is a gate.
|
|
18
|
+
*
|
|
19
|
+
* WHAT THIS DOES NOT DO: verify the declarations are TRUE. Truth belongs to the layers that measure
|
|
20
|
+
* it — adapter parity (A-SPEC-336~338), doctor's wiring checks, on-device E2E. This gate makes
|
|
21
|
+
* skipping the thought impossible and makes a false declaration an auditable record.
|
|
22
|
+
*/
|
|
23
|
+
exports.HARNESS_CELLS = ['claude', 'codex', 'agy'];
|
|
24
|
+
exports.OS_CELLS = ['windows', 'mac', 'linux'];
|
|
25
|
+
exports.CELL_VERDICTS = ['supported', 'unavailable', 'n-a'];
|
|
26
|
+
/** Directory-or-file prefixes that ARE the harness surface. A trailing '/' is a directory boundary;
|
|
27
|
+
* without it the rule names a file stem (`cli/init` catches `cli/init.ts`), and the boundary test
|
|
28
|
+
* below keeps `hooks-util.ts` from matching `hooks/`. */
|
|
29
|
+
const HARNESS_SURFACES = [
|
|
30
|
+
'src/holmes/hooks/',
|
|
31
|
+
'src/holmes/cli/init',
|
|
32
|
+
'src/holmes/cli/agents',
|
|
33
|
+
'src/holmes/cli/interactive-prompt',
|
|
34
|
+
'src/holmes/mcp/server',
|
|
35
|
+
];
|
|
36
|
+
/** Conservative OS-sensitivity signals — content properties, not directories. Grown by measurement,
|
|
37
|
+
* never by guess (the goal records this as an explicitly open list). */
|
|
38
|
+
const OS_SIGNALS = ['process.platform', "'win32'", '"win32"', 'spawn(', 'spawnSync(', 'execFileSync(', '.ps1'];
|
|
39
|
+
const FORMAT_HINT = (field, cells) => `${field} 형식: 'none: <실이유>' 또는 3셀 매핑 { ${cells.map((c) => `${c}: '<supported|unavailable|n-a>: <근거>'`).join(', ')} }`;
|
|
40
|
+
/** A reason that says nothing is not a reason: empty, or still carrying the scaffold's TODO/TBD. */
|
|
41
|
+
const emptyReason = (reason) => reason.trim() === '' || /\bTODO\b|\bTBD\b/i.test(reason);
|
|
42
|
+
/**
|
|
43
|
+
* Validate one axis' declaration. Returns the refusal (naming the field) or the parsed shape:
|
|
44
|
+
* `none` (an irrelevance claim — cross-checkable) or `cells` (the axis was faced — exempt).
|
|
45
|
+
*/
|
|
46
|
+
function parseAxis(field, raw, cells) {
|
|
47
|
+
const err = (why) => ({ kind: 'error', message: `${field} ${why} — ${FORMAT_HINT(field, cells)}` });
|
|
48
|
+
if (raw === undefined || raw === null)
|
|
49
|
+
return err('선언이 없습니다 (REQ-565: 호환 고려는 봉인 의무)');
|
|
50
|
+
if (typeof raw === 'string') {
|
|
51
|
+
const i = raw.indexOf(':');
|
|
52
|
+
const grade = (i === -1 ? raw : raw.slice(0, i)).trim();
|
|
53
|
+
const reason = i === -1 ? '' : raw.slice(i + 1).trim();
|
|
54
|
+
if (grade !== 'none')
|
|
55
|
+
return err(`알 수 없는 문자열 선언 '${grade}'`);
|
|
56
|
+
if (emptyReason(reason))
|
|
57
|
+
return err('의 none 사유가 비었거나 placeholder(TODO/TBD)입니다');
|
|
58
|
+
return { kind: 'none' };
|
|
59
|
+
}
|
|
60
|
+
if (typeof raw === 'object' && !Array.isArray(raw)) {
|
|
61
|
+
const m = raw;
|
|
62
|
+
const keys = Object.keys(m);
|
|
63
|
+
for (const c of cells)
|
|
64
|
+
if (!(c in m))
|
|
65
|
+
return err(`매핑에 '${c}' 셀이 없습니다 (3셀 전부 필수)`);
|
|
66
|
+
for (const k of keys)
|
|
67
|
+
if (!cells.includes(k))
|
|
68
|
+
return err(`매핑에 알 수 없는 셀 '${k}'`);
|
|
69
|
+
for (const c of cells) {
|
|
70
|
+
const v = m[c];
|
|
71
|
+
if (typeof v !== 'string')
|
|
72
|
+
return err(`의 '${c}' 셀이 문자열이 아닙니다`);
|
|
73
|
+
const i = v.indexOf(':');
|
|
74
|
+
const verdict = (i === -1 ? v : v.slice(0, i)).trim();
|
|
75
|
+
const reason = i === -1 ? '' : v.slice(i + 1).trim();
|
|
76
|
+
if (!exports.CELL_VERDICTS.includes(verdict)) {
|
|
77
|
+
return err(`의 '${c}' 셀 어휘 '${verdict}' 는 supported|unavailable|n-a 가 아닙니다`);
|
|
78
|
+
}
|
|
79
|
+
if (emptyReason(reason))
|
|
80
|
+
return err(`의 '${c}' 셀 근거가 비었거나 placeholder 입니다`);
|
|
81
|
+
}
|
|
82
|
+
return { kind: 'cells' };
|
|
83
|
+
}
|
|
84
|
+
return err('의 형태가 문자열도 매핑도 아닙니다');
|
|
85
|
+
}
|
|
86
|
+
/** `src/…` path tokens out of the Files to Touch section — backticks, bullets and commas tolerated.
|
|
87
|
+
* Backslashes normalize to `/` FIRST (adversarial round-1): a Windows author legitimately writes
|
|
88
|
+
* `src\holmes\hooks\stop.ts`, and un-normalized it walked straight past the surface prefixes —
|
|
89
|
+
* an OS-compat gate defeated by an OS path convention would be its own counterexample. */
|
|
90
|
+
function filesToTouch(spec) {
|
|
91
|
+
const body = (spec.sections?.['Files to Touch'] ?? '')
|
|
92
|
+
.replace(/\\/g, '/')
|
|
93
|
+
.replace(/\/{2,}/g, '/'); // round-2: `src\\holmes` normalized to `src//holmes` and slid past the prefix
|
|
94
|
+
const out = [];
|
|
95
|
+
for (const m of body.matchAll(/src\/[\w./-]+/g))
|
|
96
|
+
out.push(m[0]);
|
|
97
|
+
return [...new Set(out)];
|
|
98
|
+
}
|
|
99
|
+
function checkCompatDeclared(spec, opts) {
|
|
100
|
+
if (spec.type !== 'A-SPEC')
|
|
101
|
+
return null;
|
|
102
|
+
const fm = (spec.frontmatter ?? {});
|
|
103
|
+
const harness = parseAxis('harness_impact', fm.harness_impact, exports.HARNESS_CELLS);
|
|
104
|
+
if (harness.kind === 'error')
|
|
105
|
+
return harness.message;
|
|
106
|
+
const os = parseAxis('os_impact', fm.os_impact, exports.OS_CELLS);
|
|
107
|
+
if (os.kind === 'error')
|
|
108
|
+
return os.message;
|
|
109
|
+
// Cross-checks apply ONLY to `none` — a 3-cell mapping already faced the axis, and re-litigating
|
|
110
|
+
// it here would punish exactly the declaration this gate exists to elicit (C1's lesson is that a
|
|
111
|
+
// DECLARATION must not be the only wall; an irrelevance CLAIM is what the machine can contradict).
|
|
112
|
+
const ftt = filesToTouch(spec);
|
|
113
|
+
if (harness.kind === 'none') {
|
|
114
|
+
for (const p of ftt) {
|
|
115
|
+
const hit = HARNESS_SURFACES.find((s) => (s.endsWith('/') ? p.startsWith(s) : p === s || p.startsWith(`${s}.`) || p.startsWith(`${s}/`)));
|
|
116
|
+
if (hit) {
|
|
117
|
+
return `harness_impact 는 none 인데 Files to Touch 의 '${p}' 는 하네스 표면(${hit})입니다 — 모순. 3셀 매핑으로 각 하네스의 영향을 기술하십시오.`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (os.kind === 'none' && opts?.readFile) {
|
|
122
|
+
for (const p of ftt) {
|
|
123
|
+
// round-2: an injected reader that THROWS (permissions, FIFO, anything) must degrade to
|
|
124
|
+
// "unreadable = skip", never crash the approval act — this check is a gate, not a hostage.
|
|
125
|
+
let text;
|
|
126
|
+
try {
|
|
127
|
+
text = opts.readFile(p);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
text = null;
|
|
131
|
+
}
|
|
132
|
+
if (text === null || text === undefined)
|
|
133
|
+
continue; // a file that does not exist yet has no character
|
|
134
|
+
const sig = OS_SIGNALS.find((s) => text.includes(s));
|
|
135
|
+
if (sig) {
|
|
136
|
+
return `os_impact 는 none 인데 '${p}' 의 내용이 OS 신호 '${sig}' 를 담고 있습니다 — 모순. 3셀 매핑으로 각 OS 의 영향을 기술하십시오.`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
@@ -76,7 +76,9 @@ exports.SPEC_TYPES = {
|
|
|
76
76
|
// blocked every turn, because all 38 governed A-SPECs are already approved. The duty belongs to
|
|
77
77
|
// the ACT of approval (see spec/breaking-change.ts); this line exists so the field is visible
|
|
78
78
|
// where someone looks up "what fields does an A-SPEC have", instead of hiding in a check.
|
|
79
|
-
|
|
79
|
+
// @implements A-SPEC-565.1 — same shape, same reason: the compat duty (REQ-565) lives on the
|
|
80
|
+
// act of approval (spec/compat-impact.ts), and these lines exist for the reader, not the check.
|
|
81
|
+
stubOnlyFields: ['breaking_change', 'harness_impact', 'os_impact'],
|
|
80
82
|
requiredSections: ['Objective', 'Inputs / Outputs', 'Behavior', 'Test Points', 'Files to Touch', 'Done When'],
|
|
81
83
|
},
|
|
82
84
|
'C-SPEC': {
|
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.16.0",
|
|
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",
|