@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.
@@ -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
- graph.addNode(`SPEC:${s.id}`, s.type, sourcePath, fact(opts, sourcePath ?? null, 'spec-store', 1));
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
- ...nodes.map((n) => `N\t${n.id}\t${n.kind}\t${n.source_path ?? ''}\t${prov(n)}`),
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.15.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",