@holmes-lab/holmes-kit 0.15.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 +46 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/governance/approval-queue.d.ts +6 -0
- package/dist/holmes/governance/approval-queue.js +11 -3
- 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 +57 -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/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/compat-impact.d.ts +5 -0
- package/dist/holmes/spec/compat-impact.js +1 -0
- 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
|
}
|
|
@@ -23,4 +23,9 @@ export interface CompatCheckOpts {
|
|
|
23
23
|
/** Repo-relative reader for the OS cross-check; null = file absent (skip, fail-open). */
|
|
24
24
|
readFile?: (rel: string) => string | null;
|
|
25
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[];
|
|
26
31
|
export declare function checkCompatDeclared(spec: Spec, opts?: CompatCheckOpts): string | null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CELL_VERDICTS = exports.OS_CELLS = exports.HARNESS_CELLS = void 0;
|
|
4
|
+
exports.filesToTouch = filesToTouch;
|
|
4
5
|
exports.checkCompatDeclared = checkCompatDeclared;
|
|
5
6
|
/**
|
|
6
7
|
* REQ-565's enforcement device: the obligation to declare HARNESS (claude/codex/agy) and OS
|
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",
|