@holmes-lab/holmes-kit 0.12.0 → 0.12.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 CHANGED
@@ -5,6 +5,32 @@ 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.12.1] - 2026-09-05
9
+
10
+ Finishes the RTM dashboard's CFG/PDG story and fixes fresh-project spec numbering. Additive and
11
+ backward-compatible.
12
+
13
+ ### Added
14
+
15
+ - **CFG/PDG graph-view drilldown** (REQ-545): drilling into a symbol in the graph tab now renders that
16
+ function's **control-flow graph as a layered DAG** with **program-dependence overlays** — control
17
+ flow, data dependence, and control dependence drawn as three distinct edge styles, unreachable
18
+ blocks dimmed, with a legend. `/api/cfg` gains a server-computed `layout` (deterministic
19
+ `layoutCfg`: BFS layers from entry, cycle-safe, unreachable blocks trailing) so the client only
20
+ paints. This completes REQ-545 (2D matrix · auto-launch · CFG endpoint · CFG render).
21
+
22
+ ### Fixed
23
+
24
+ - **Fresh-project spec numbering starts at REQ-100, not REQ-201** (REQ-546): `spec_slice_init` hard-coded
25
+ a `maxId = 200` floor, so a brand-new project's first slice leapt to 201. It now shares the same id
26
+ allocator as the reverse-draft path (`nextIdBase`, floor 100). **Backward-compatible**: the next id
27
+ is always `max(existing)+1`, so a repo already numbering from 201 keeps the identical sequence and
28
+ no existing id is renamed; the change only lowers the *first* number in an empty (or sub-100-max)
29
+ project. Numbering past 999 yields 4-digit ids with no collision.
30
+ - **ADR references recognise four-digit ids** (REQ-546): the citation matcher was `\bADR-\d{3}\b`
31
+ (exactly three digits), so `ADR-1000+` was invisible. Extracted to a pure `adrRefsIn` and widened to
32
+ `\d{3,}` — three-digit matching and ordering are preserved, four-digit ADRs are now cited.
33
+
8
34
  ## [0.12.0] - 2026-09-05
9
35
 
10
36
  World-top-tier RTM dashboard, in flight. The `holmes-kit serve` dashboard stops being a table of
package/README.md CHANGED
@@ -20,7 +20,8 @@
20
20
  - 🔴 **Inbuilt TDD — RED-first, enforced not asked** *(new in 0.9.0)*: the test-first discipline is a holmes-installed `holmes-tdd-slice` skill **and** a new constitution article **ART-8**. A changed A-SPEC must show a recorded `red-assertion → green` sequence in the ledger; a `red-error` (a test that could not run) is not a valid RED, so "the covering test failed *correctly*" is judged mechanically, not on trust. `test_run` classifies each covered file (`red-assertion`/`red-error`/`green`) and records per-A-SPEC outcomes the Stop hook reads. Ships at `redFirstEvidence: track` (observe-first, non-blocking; `strict`/`off` per repo), evidence-gated and jest-only for now. A T-SPEC may also declare `kills:` mutations and `test_run --mutate` reports which SURVIVED (a coverage gap). Where superpowers *asks* for RED-first and discriminating power, holmes-kit *proves* them.
21
21
  - 🧰 **Governance UX tools** *(new in 0.10.0)*: `spec_unseal` (the inverse of `spec_approve` — return a sealed spec to editable `draft` in one act, out-of-band approval required, refuses approved dependents), `approval_status` and `ledger_timeline` (read-only observability into a spec's seal state and the governance history), and a structured `conflict` on `spec_approve`'s optimistic-concurrency refusal (read vs. current version + retry). See CHANGELOG for details.
22
22
  - ⬆️ **Zero-config upgrades** *(new in 0.11.0)*: `holmes-kit upgrade` moves **every** wired workspace to the latest in one command — plan → confirm → install → re-pin all recorded workspaces (`--dry-run`/`--yes` supported). Preparation is automatic (each `init` records the workspace; a session whose pin is behind nudges you to upgrade); the re-pin **write** stays your explicit choice, never a silent auto-install. Opt out of the nudge with `HOLMES_NO_AUTO_REPIN`.
23
- - 📊 **World-top-tier RTM dashboard** *(new in 0.12.0, in flight)*: `holmes-kit serve` — ask to *see* the RTM heatmap and the `rtm_dashboard` MCP tool launches the server idempotently and hands back the URL plus an honesty **census** (requirement/pipeline counts, coverage %, what's excluded). The heatmap is now a real **2D coverage matrix** (requirements × pipeline stages, rows seriated by completeness, sequential-ramp cells with the percent printed in each), and `/api/cfg?file=&symbol=` serves a function's **CFG + PDG** (control-flow blocks + data/control dependence) from the same engine the taint lane uses — a non-CFG language is named, never faked. Tokenised palette + light/dark; graph-view rendering of CFG/PDG lands next.
23
+ - 📊 **World-top-tier RTM dashboard** *(new in 0.12.0–0.12.1)*: `holmes-kit serve` — ask to *see* the RTM heatmap and the `rtm_dashboard` MCP tool launches the server idempotently and hands back the URL plus an honesty **census** (requirement/pipeline counts, coverage %, what's excluded). The heatmap is a real **2D coverage matrix** (requirements × pipeline stages, rows seriated by completeness, sequential-ramp cells with the percent printed in each). Drilling into a symbol renders that function's **CFG as a layered DAG** with **PDG (data/control-dependence) colour overlays**, served by `/api/cfg?file=&symbol=` from the same engine the taint lane uses — a non-CFG language is named, never faked. Tokenised palette (sequential ramp, status colours, UI/mono pairing) with light/dark.
24
+ - 🔢 **Sensible spec numbering** *(new in 0.12.1)*: a brand-new project's first slice is now **REQ-100**, not REQ-201 — `spec_slice_init` shares the same id allocator as the reverse-draft path (`nextIdBase`, floor 100). Existing projects are untouched: the next id is always `max(existing)+1`, so a repo already numbering from 201 keeps the exact same sequence. Numbering past 999 yields 4-digit ids cleanly, and ADR references now recognise 4-digit ADRs (`ADR-1000+`).
24
25
  - 🤖 **Autonomous Approval** *(new in 0.8.0)*: with the out-of-band `HOLMES_AUTONOMOUS_APPROVAL` switch on, an agent seals **low/mid-risk** specs itself (ledgered under an `autonomous:<client>` actor) — while `gate-behavior` changes, architecture/gate/taint files, and every upstream `REQ`/`H-SPEC`/`C-SPEC` still ask a human through the in-session TUI. The switch is env-only; a session cannot set it (blocked like `HOLMES_ROLE`). Off = byte-identical to before. *(new in 0.10.0)* Set **`HOLMES_ELICIT=off`** to skip the in-session prompt entirely and route every decision to the out-of-band `holmes-kit approve` queue instead — one decision surface that behaves identically across Claude Code / Antigravity / Codex.
25
26
  - 🪧 **Session Banner + Update Notice** *(new in 0.8.0)*: every session start emits an English intro (version + governance rule + npm URL) to both the human transcript and the agent context (SessionStart hook + MCP `instructions`); when a newer published version is cached, an install-mode-aware update command is appended. Registry check is detached, fail-silent, and opts out via `HOLMES_NO_UPDATE_CHECK`/`CI`.
26
27
  - 🧱 **Deterministic Gate, Hardened** *(new in 0.8.0)*: shell writes are judged at the segment's **effective working directory** (`cd sub && cat > ../src/x.ts` is sealed, legitimate out-of-tree scratch writes are freed); the governing anchor is the **whole set**, not the first match. Every gate change ships with two consecutive clean adversarial rounds.
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- 85d7b53-mtnr6dpp
1
+ 3b23a9b-mtnx2lfo
@@ -241,6 +241,7 @@ const ignore_1 = require("../project/ignore");
241
241
  // @implements A-SPEC-126
242
242
  const scan_1 = require("../reverse/scan");
243
243
  const draft_1 = require("../reverse/draft");
244
+ const adr_refs_1 = require("../spec/adr-refs");
244
245
  const spec_id_guard_1 = require("./spec-id-guard");
245
246
  const anchor_1 = require("../reverse/anchor");
246
247
  // @implements A-SPEC-128
@@ -693,10 +694,10 @@ function collectDecisions(root, scanned, specs) {
693
694
  return { decisions: [], citations: [] };
694
695
  }
695
696
  const ids = new Set(decisions.map((d) => d.id));
697
+ // @implements A-SPEC-546.1 — recognise ADR-\d{3,} (4-digit ADRs no longer invisible), via a pure fn.
696
698
  const cite = (text, from, out) => {
697
- for (const m of text.matchAll(/\bADR-\d{3}\b/g))
698
- if (ids.has(m[0]))
699
- out.push({ from, adr: m[0] });
699
+ for (const adr of (0, adr_refs_1.adrRefsIn)(text, ids))
700
+ out.push({ from, adr });
700
701
  };
701
702
  const citations = [];
702
703
  for (const f of scanned) {
@@ -3248,13 +3249,10 @@ function makeRawHandlers(store, opts) {
3248
3249
  const root = a.root ? (0, root_2.resolveProjectRoot)(a.root).root : process.cwd();
3249
3250
  const specsDir = path.join(root, '.ax', 'specs');
3250
3251
  const existing = await store.list();
3251
- let maxId = 200;
3252
- for (const s of existing) {
3253
- const num = parseInt(s.id.split('.')[0].replace(/\D/g, ''), 10);
3254
- if (!isNaN(num) && num > maxId)
3255
- maxId = num;
3256
- }
3257
- const nextId = maxId + 1;
3252
+ // @implements A-SPEC-546.1 — align with the sibling reverse-draft path (nextIdBase, floor 100)
3253
+ // instead of a hard-coded 200 floor, so a fresh project starts at REQ-100, not REQ-201. Existing
3254
+ // projects are unchanged: nextIdBase returns max(existing)+1, which dominates the floor.
3255
+ const nextId = (0, draft_1.nextIdBase)(existing.map((s) => s.id));
3258
3256
  const reqId = `REQ-${nextId}`;
3259
3257
  const hspecId = `H-SPEC-${nextId}`;
3260
3258
  const aspecId = `A-SPEC-${nextId}`;
@@ -43,3 +43,27 @@ export declare function buildCfgView(cfg: Cfg | {
43
43
  }, ast: PersistedAst, source: string): CfgView | {
44
44
  unsupported: string;
45
45
  };
46
+ export declare const CFG_COL_W = 220;
47
+ export declare const CFG_ROW_H = 120;
48
+ export interface CfgLayoutNode {
49
+ id: number;
50
+ layer: number;
51
+ col: number;
52
+ x: number;
53
+ y: number;
54
+ line: number;
55
+ label: string;
56
+ unreachable: boolean;
57
+ }
58
+ export interface CfgLayout {
59
+ nodes: CfgLayoutNode[];
60
+ width: number;
61
+ height: number;
62
+ }
63
+ /**
64
+ * @implements A-SPEC-545.5
65
+ * Pure & deterministic layered layout of a CFG: layer = BFS shortest depth from entry (cycle-safe,
66
+ * blocks visited in id order); unreachable blocks trail after the reachable ones. col = order within
67
+ * a layer. Same input ⇒ same layout.
68
+ */
69
+ export declare function layoutCfg(view: CfgView): CfgLayout;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CFG_ROW_H = exports.CFG_COL_W = void 0;
3
4
  exports.lineAtByte = lineAtByte;
4
5
  exports.buildCfgView = buildCfgView;
6
+ exports.layoutCfg = layoutCfg;
5
7
  /**
6
8
  * @implements A-SPEC-545.4
7
9
  * Pure: 1-based line number of a byte offset (count of newlines in [0, offset) + 1). Clamped.
@@ -55,3 +57,51 @@ function buildCfgView(cfg, pdg, ast, source) {
55
57
  }
56
58
  return { entry: cfg.entry, exit: cfg.exit, blocks, edges, ctrlDeps, dataDeps };
57
59
  }
60
+ exports.CFG_COL_W = 220;
61
+ exports.CFG_ROW_H = 120;
62
+ /**
63
+ * @implements A-SPEC-545.5
64
+ * Pure & deterministic layered layout of a CFG: layer = BFS shortest depth from entry (cycle-safe,
65
+ * blocks visited in id order); unreachable blocks trail after the reachable ones. col = order within
66
+ * a layer. Same input ⇒ same layout.
67
+ */
68
+ function layoutCfg(view) {
69
+ if (view.blocks.length === 0)
70
+ return { nodes: [], width: 0, height: 0 };
71
+ const succ = new Map();
72
+ for (const e of view.edges)
73
+ succ.set(e.from, [...(succ.get(e.from) ?? []), e.to]);
74
+ const layer = new Map();
75
+ const start = view.blocks.some((b) => b.id === view.entry) ? view.entry : Math.min(...view.blocks.map((b) => b.id));
76
+ let queue = [start];
77
+ layer.set(start, 0);
78
+ while (queue.length) {
79
+ const next = [];
80
+ for (const id of queue) {
81
+ const depth = layer.get(id);
82
+ for (const to of (succ.get(id) ?? []).slice().sort((a, b) => a - b)) {
83
+ if (!layer.has(to)) {
84
+ layer.set(to, depth + 1);
85
+ next.push(to);
86
+ } // BFS: cycle-safe (visit once)
87
+ }
88
+ }
89
+ queue = next;
90
+ }
91
+ const reachMax = layer.size ? Math.max(...layer.values()) : 0;
92
+ // Unreachable blocks trail after every reachable block, in id order.
93
+ const unreached = view.blocks.filter((b) => !layer.has(b.id)).sort((a, b) => a.id - b.id);
94
+ unreached.forEach((b, i) => layer.set(b.id, reachMax + 1 + i));
95
+ // col = order within a layer, by ascending block id (deterministic).
96
+ const perLayer = new Map();
97
+ const byId = [...view.blocks].sort((a, b) => a.id - b.id);
98
+ const nodes = byId.map((b) => {
99
+ const ly = layer.get(b.id);
100
+ const col = perLayer.get(ly) ?? 0;
101
+ perLayer.set(ly, col + 1);
102
+ return { id: b.id, layer: ly, col, x: col * exports.CFG_COL_W, y: ly * exports.CFG_ROW_H, line: b.line, label: b.label, unreachable: b.unreachable };
103
+ });
104
+ const maxCol = nodes.reduce((m, n) => Math.max(m, n.col), 0);
105
+ const maxLayer = nodes.reduce((m, n) => Math.max(m, n.layer), 0);
106
+ return { nodes, width: (maxCol + 1) * exports.CFG_COL_W, height: (maxLayer + 1) * exports.CFG_ROW_H };
107
+ }
@@ -271,8 +271,10 @@ async function startDashboardServer(options) {
271
271
  res.end(JSON.stringify({ ok: true, file, symbol, lang, unsupported: view.unsupported }));
272
272
  return;
273
273
  }
274
+ // @implements A-SPEC-545.5 — server-computed layered layout so the client only paints (no
275
+ // duplicated layout logic to drift from the tested one).
274
276
  res.writeHead(200, { 'Content-Type': 'application/json' });
275
- res.end(JSON.stringify({ ok: true, file, symbol, lang, cfg: view }));
277
+ res.end(JSON.stringify({ ok: true, file, symbol, lang, cfg: view, layout: (0, cfg_view_1.layoutCfg)(view) }));
276
278
  }
277
279
  catch (err) {
278
280
  if (!res.headersSent) {
@@ -654,6 +656,20 @@ function renderDashboardHtml() {
654
656
  .tool-btn { background: #1e293b; color: #f8fafc; border: 1px solid #334155; width: 38px; height: 38px; border-radius: 6px; font-weight: bold; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s ease; }
655
657
  .tool-btn:hover { background: #3b82f6; border-color: #3b82f6; color: #fff; transform: scale(1.05); }
656
658
  .ux-hint-banner { background: #1e293b; border-left: 4px solid #ec4899; color: #cbd5e1; padding: 10px 14px; border-radius: 6px; font-size: 12px; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between; }
659
+
660
+ /* @implements A-SPEC-545.5 — CFG/PDG drilldown: control-flow blocks + program-dependence overlays */
661
+ .cfg-block rect { fill: var(--surface-2); stroke: var(--border); stroke-width: 1.5; rx: 8; }
662
+ .cfg-block.entry rect { stroke: var(--ok); stroke-width: 2.5; }
663
+ .cfg-block.exitb rect { stroke: var(--accent); stroke-width: 2.5; }
664
+ .cfg-block.unreachable rect { opacity: 0.4; stroke-dasharray: 4 3; }
665
+ .cfg-block .cfg-line { font-family: var(--font-mono); font-size: 10px; fill: var(--ink-2); }
666
+ .cfg-block .cfg-label { font-family: var(--font-mono); font-size: 12px; fill: var(--ink-0); }
667
+ .cfg-edge-flow { stroke: var(--ink-1); stroke-width: 2; fill: none; marker-end: url(#cfg-arrow-flow); }
668
+ .cfg-edge-data { stroke: var(--accent); stroke-width: 2; fill: none; opacity: 0.85; marker-end: url(#cfg-arrow-data); }
669
+ .cfg-edge-ctrl { stroke: var(--warn); stroke-width: 2; fill: none; stroke-dasharray: 6 4; opacity: 0.85; marker-end: url(#cfg-arrow-ctrl); }
670
+ .cfg-legend { display: flex; gap: 18px; align-items: center; font-family: var(--font-ui); font-size: 12px; color: var(--ink-1); flex-wrap: wrap; }
671
+ .cfg-legend .swatch { display: inline-block; width: 22px; height: 0; border-top-width: 2px; border-top-style: solid; vertical-align: middle; margin-right: 6px; }
672
+ .cfg-banner { font-family: var(--font-ui); color: var(--ink-1); background: var(--surface-1); border: 1px solid var(--border); border-radius: 10px; padding: 16px 18px; }
657
673
  </style>
658
674
  </head>
659
675
  <body>
@@ -1168,12 +1184,84 @@ function renderDashboardHtml() {
1168
1184
 
1169
1185
  async function focusSpecInGraph(specId) {
1170
1186
  switchTab('graph');
1187
+ // @implements A-SPEC-545.5 — a symbol id (file#name) drills into that function's CFG/PDG;
1188
+ // every other id keeps the existing spec-graph traversal untouched.
1189
+ if (typeof specId === 'string' && specId.indexOf('#') !== -1) {
1190
+ const hash = specId.indexOf('#');
1191
+ const file = specId.slice(0, hash);
1192
+ const symbol = specId.slice(hash + 1);
1193
+ try {
1194
+ const res = await fetch('/api/cfg?file=' + encodeURIComponent(file) + '&symbol=' + encodeURIComponent(symbol));
1195
+ renderCfgDrilldown(await res.json(), file, symbol);
1196
+ } catch (err) {
1197
+ document.getElementById('graphScopeNotice').textContent = 'Failed to load CFG: ' + (err && err.message || err);
1198
+ }
1199
+ return;
1200
+ }
1171
1201
  if (!graphData) {
1172
1202
  await loadGraphCanvas();
1173
1203
  }
1174
1204
  selectAndTraverseNode(specId);
1175
1205
  }
1176
1206
 
1207
+ // @implements A-SPEC-545.5 — paint the server-computed layered CFG + PDG overlays into the canvas.
1208
+ function renderCfgDrilldown(resp, file, symbol) {
1209
+ const svg = document.getElementById('graphSvg');
1210
+ const notice = document.getElementById('graphScopeNotice');
1211
+ if (!resp || resp.ok === false) {
1212
+ svg.innerHTML = '';
1213
+ notice.innerHTML = '<div class="cfg-banner">No CFG for <strong>' + escapeHtml(symbol) + '</strong> in ' + escapeHtml(file) + (resp && resp.reason ? ' — ' + escapeHtml(resp.reason) : '') + '</div>';
1214
+ return;
1215
+ }
1216
+ if (resp.unsupported) {
1217
+ svg.innerHTML = '';
1218
+ notice.innerHTML = '<div class="cfg-banner">CFG is not built for <strong>' + escapeHtml(resp.unsupported) + '</strong> — this view is honest about the languages it cannot analyse.</div>';
1219
+ return;
1220
+ }
1221
+ const cfg = resp.cfg, layout = resp.layout;
1222
+ const pos = {};
1223
+ layout.nodes.forEach(n => { pos[n.id] = n; });
1224
+ const CW = 180, CH = 60;
1225
+ const cx = (id) => pos[id] ? pos[id].x + CW / 2 : 0;
1226
+ const top = (id) => pos[id] ? pos[id].y : 0;
1227
+ const bot = (id) => pos[id] ? pos[id].y + CH : 0;
1228
+ const line = (x1, y1, x2, y2, cls) => '<path class="' + cls + '" d="M' + x1 + ' ' + y1 + ' L' + x2 + ' ' + y2 + '"></path>';
1229
+
1230
+ let edges = '';
1231
+ (cfg.edges || []).forEach(e => { edges += line(cx(e.from), bot(e.from), cx(e.to), top(e.to), 'cfg-edge-flow'); });
1232
+ (cfg.dataDeps || []).forEach(d => { edges += line(cx(d.from) + 12, top(d.from), cx(d.to) + 12, bot(d.to), 'cfg-edge-data'); });
1233
+ (cfg.ctrlDeps || []).forEach(c => { edges += line(cx(c.ctrl) - 12, top(c.ctrl), cx(c.dep) - 12, bot(c.dep), 'cfg-edge-ctrl'); });
1234
+
1235
+ let blocks = '';
1236
+ layout.nodes.forEach(n => {
1237
+ const cls = 'cfg-block' + (n.id === cfg.entry ? ' entry' : '') + (n.id === cfg.exit ? ' exitb' : '') + (n.unreachable ? ' unreachable' : '');
1238
+ const lbl = n.label ? escapeHtml(n.label) : (n.id === cfg.entry ? 'ENTRY' : n.id === cfg.exit ? 'EXIT' : 'B' + n.id);
1239
+ blocks += '<g class="' + cls + '" transform="translate(' + n.x + ',' + n.y + ')">'
1240
+ + '<rect width="' + CW + '" height="' + CH + '" rx="8"></rect>'
1241
+ + '<text class="cfg-line" x="10" y="18">' + (n.line ? ('L' + n.line) : ('#' + n.id)) + '</text>'
1242
+ + '<text class="cfg-label" x="10" y="38">' + lbl.slice(0, 26) + '</text>'
1243
+ + '</g>';
1244
+ });
1245
+
1246
+ const defs = '<defs>'
1247
+ + '<marker id="cfg-arrow-flow" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0 0 L7 3 L0 6 z" fill="var(--ink-1)"></path></marker>'
1248
+ + '<marker id="cfg-arrow-data" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0 0 L7 3 L0 6 z" fill="var(--accent)"></path></marker>'
1249
+ + '<marker id="cfg-arrow-ctrl" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0 0 L7 3 L0 6 z" fill="var(--warn)"></path></marker>'
1250
+ + '</defs>';
1251
+ const W = Math.max(layout.width, CW) + 40, H = Math.max(layout.height, CH) + 40;
1252
+ svg.setAttribute('viewBox', '-20 -20 ' + W + ' ' + H);
1253
+ svg.innerHTML = defs + edges + blocks;
1254
+
1255
+ notice.innerHTML = '<div style="display:flex; justify-content:space-between; align-items:center; gap:16px; flex-wrap:wrap;">'
1256
+ + '<span style="font-family:var(--font-ui); color:var(--ink-0); font-weight:700;">CFG · ' + escapeHtml(symbol) + '() <span style="color:var(--ink-2); font-weight:400;">' + escapeHtml(file) + '</span></span>'
1257
+ + '<span class="cfg-legend">'
1258
+ + '<span><span class="swatch" style="border-top-color:var(--ink-1);"></span>control flow</span>'
1259
+ + '<span><span class="swatch" style="border-top-color:var(--accent);"></span>data dep</span>'
1260
+ + '<span><span class="swatch" style="border-top-color:var(--warn); border-top-style:dashed;"></span>control dep</span>'
1261
+ + '<span style="color:var(--ink-2);">' + layout.nodes.length + ' blocks</span>'
1262
+ + '</span></div>';
1263
+ }
1264
+
1177
1265
  document.getElementById('searchInput').addEventListener('input', (e) => {
1178
1266
  const q = e.target.value.toLowerCase();
1179
1267
  const filtered = allSpecs.filter(s => s.id.toLowerCase().includes(q) || (s.title && s.title.toLowerCase().includes(q)));
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @implements A-SPEC-546.1
3
+ * Pure: the ADR references cited in `text` that exist in `known`, in appearance order (duplicates
4
+ * preserved, matching the prior cite behaviour). Recognises `ADR-` followed by 3-OR-MORE digits at a
5
+ * word boundary, so four-digit ADRs (ADR-1000+) are no longer invisible.
6
+ */
7
+ export declare function adrRefsIn(text: string, known: ReadonlySet<string>): string[];
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ // @implements A-SPEC-546.1
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.adrRefsIn = adrRefsIn;
5
+ /**
6
+ * @implements A-SPEC-546.1
7
+ * Pure: the ADR references cited in `text` that exist in `known`, in appearance order (duplicates
8
+ * preserved, matching the prior cite behaviour). Recognises `ADR-` followed by 3-OR-MORE digits at a
9
+ * word boundary, so four-digit ADRs (ADR-1000+) are no longer invisible.
10
+ */
11
+ function adrRefsIn(text, known) {
12
+ const out = [];
13
+ for (const m of text.matchAll(/\bADR-\d{3,}\b/g))
14
+ if (known.has(m[0]))
15
+ out.push(m[0]);
16
+ return out;
17
+ }
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.12.0",
4
+ "version": "0.12.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",
@@ -35,6 +35,9 @@
35
35
  "spec-driven",
36
36
  "rtm",
37
37
  "cpg",
38
+ "cfg",
39
+ "pdg",
40
+ "dashboard",
38
41
  "traceability",
39
42
  "mcp",
40
43
  "guardrail"