@holmes-lab/holmes-kit 0.11.0 → 0.12.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 CHANGED
@@ -5,6 +5,42 @@ 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.0] - 2026-09-05
9
+
10
+ World-top-tier RTM dashboard, in flight. The `holmes-kit serve` dashboard stops being a table of
11
+ accordions and becomes a real coverage matrix, gains a one-call launcher, and learns to serve CFG/PDG
12
+ for a function. Additive and backward-compatible — every existing `/api/*` payload keeps its shape and
13
+ values; these are new fields, a new endpoint, a new tool, and a rebuilt heatmap view.
14
+
15
+ ### Added
16
+
17
+ - **2D coverage matrix** (REQ-545): the heatmap tab is now a single **requirement × pipeline-stage
18
+ grid** — rows are requirements ordered by completeness (seriation), each cell a sequential-ramp
19
+ colour with the coverage percent printed in it (so magnitude never rides on colour alone). Replaces
20
+ the per-REQ accordion-of-tables. `/api/rtm/heatmap` gains an additive `matrix` field
21
+ (`{reqs, stages, values, order, buckets}`) built by pure, unit-tested `buildStageMatrix` +
22
+ `seriateRows` + `coverageBucket`.
23
+ - **`rtm_dashboard` MCP tool** (REQ-545): ask to *see* the RTM heatmap / coverage matrix and the
24
+ server launches (idempotently — a second call reuses the live one) and returns `{ url, running,
25
+ census }`, where `census` is an honesty summary (`reqCount, pipelineCount, coveredCount,
26
+ coveragePct, retiredCount, findingsScanned`). No more starting `serve` by hand.
27
+ - **`/api/cfg` — control-flow + program-dependence for one function** (REQ-545): `?file=&symbol=`
28
+ returns that function's CFG (blocks with source line/label, control-flow edges) and PDG (block-level
29
+ control deps + reaching-def data deps), consumed from the same `cfgOf`/`pdgOf` the taint lane uses.
30
+ A non-CFG language is named as `unsupported`, never faked as an empty graph. (Graph-view rendering
31
+ of this lands next.)
32
+
33
+ ### Changed
34
+
35
+ - **Dashboard design tokens** (REQ-545): the palette moved off the Tailwind-default + emoji-marker
36
+ look to a tokenised system — a single-hue sequential coverage ramp, reserved status colours, a
37
+ UI/mono type pairing (data in tabular-nums), surfaces and ink, with a light and a dark palette via
38
+ `prefers-color-scheme`. Applied to the new matrix view and the tab bar; structure emoji removed
39
+ (meaning markers only). The rest of the dashboard tokenises incrementally in the follow-on slice.
40
+ - **Upgrade nudges point at one command** (REQ-544): the session banner's drift line and doctor's
41
+ version-drift fix hint now name `holmes-kit upgrade` (the right binary per platform — `npx.cmd` on
42
+ win32) instead of spelling out a manual `npm i` + `init --force`.
43
+
8
44
  ## [0.11.0] - 2026-09-05
9
45
 
10
46
  Zero-config upgrades — preparation is automatic, execution is your explicit choice. Additive and
package/README.md CHANGED
@@ -20,6 +20,7 @@
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
24
  - 🤖 **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.
24
25
  - 🪧 **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`.
25
26
  - 🧱 **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
- 2fb1cad-mtnmh3yo
1
+ 85d7b53-mtnr6dpp
@@ -757,7 +757,9 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
757
757
  const launch = (0, mcp_version_1.mcpLaunchVersion)({ command: server.command, args: server.args }, readPkgVersionAt);
758
758
  const verdict = (0, mcp_version_1.versionDriftVerdict)(launch, cli);
759
759
  if (verdict === 'drift') {
760
- add('mcp server version', 'WARN', `이 프로젝트의 MCP 서버는 holmes-kit ${launch} 를 띄우지만 이 CLI 는 ${cli} 입니다 — 재배선 전까지 옛 서버가 계속 뜹니다`, `${(0, npx_bin_1.npxBin)()} @holmes-lab/holmes-kit@latest init 을 다시 실행해 .mcp.json 을 최신 버전으로 재배선하십시오 (전역 -g 업그레이드 불필요).`);
760
+ add('mcp server version', 'WARN', `이 프로젝트의 MCP 서버는 holmes-kit ${launch} 를 띄우지만 이 CLI 는 ${cli} 입니다 — 재배선 전까지 옛 서버가 계속 뜹니다`,
761
+ // @implements A-SPEC-544.1 — one command installs + re-pins every workspace; init --force is the per-workspace path.
762
+ `${(0, npx_bin_1.npxBin)()} holmes-kit upgrade 로 최신 설치 + 전 워크스페이스 재핀 (이 워크스페이스만 고치려면 ${(0, npx_bin_1.npxBin)()} holmes-kit init --force).`);
761
763
  }
762
764
  else if (verdict === 'match') {
763
765
  add('mcp server version', 'PASS', `MCP 서버 버전 ${launch} 가 이 CLI 와 일치합니다`);
@@ -237,6 +237,22 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
237
237
  events: import("../governance/ledger-timeline").TimelineEntry[];
238
238
  reason?: undefined;
239
239
  }>;
240
+ rtm_dashboard(a: {
241
+ root?: string;
242
+ port?: number;
243
+ }): Promise<{
244
+ ok: boolean;
245
+ reason: string;
246
+ url?: undefined;
247
+ running?: undefined;
248
+ census?: undefined;
249
+ } | {
250
+ ok: boolean;
251
+ url: string;
252
+ running: boolean;
253
+ census: import("../server/dashboard-launcher").DashboardCensus;
254
+ reason?: undefined;
255
+ }>;
240
256
  spec_approve(a: {
241
257
  root?: string;
242
258
  id: string;
@@ -45,6 +45,7 @@ exports.makeHandlers = makeHandlers;
45
45
  // @implements A-SPEC-269
46
46
  // @implements A-SPEC-267
47
47
  const fs = __importStar(require("node:fs"));
48
+ const http = __importStar(require("node:http"));
48
49
  const assoc_arm_1 = require("../assoc/assoc-arm");
49
50
  // @implements A-SPEC-478 — the semantic tier runtime, resolved HERE (the async edge) and injected
50
51
  // into the sync analysis; the replay harness injects nothing, keeping every pin untouched.
@@ -213,6 +214,21 @@ class HandlerRefusal extends Error {
213
214
  constructor(message) { super(message); this.name = 'HandlerRefusal'; }
214
215
  }
215
216
  exports.HandlerRefusal = HandlerRefusal;
217
+ /** GET a localhost JSON endpoint (used by rtm_dashboard to read the census off the server it launched). */
218
+ function fetchJson(url) {
219
+ return new Promise((resolve, reject) => {
220
+ http.get(url, (res) => {
221
+ let data = '';
222
+ res.on('data', (c) => (data += c));
223
+ res.on('end', () => { try {
224
+ resolve(JSON.parse(data));
225
+ }
226
+ catch (e) {
227
+ reject(e);
228
+ } });
229
+ }).on('error', reject);
230
+ });
231
+ }
216
232
  /** Marker test that survives realm boundaries (jest's instanceof does not). */
217
233
  function isHandlerRefusal(e) {
218
234
  return !!e && typeof e === 'object' && e.holmesRefusal === true;
@@ -1322,6 +1338,24 @@ function makeRawHandlers(store, opts) {
1322
1338
  const events = fs.existsSync(dir) ? new ledger_store_1.FileLedgerStore(dir).loadAll() : [];
1323
1339
  return { ok: true, events: (0, ledger_timeline_1.timelineFrom)(events, a.id) };
1324
1340
  },
1341
+ // @implements A-SPEC-545.3 — "show me the RTM dashboard" launches (idempotently) the server the
1342
+ // agent would otherwise start by hand, and returns its URL plus an honesty census of what it shows.
1343
+ async rtm_dashboard(a) {
1344
+ const dest = resolveLedgerRoot(a.root);
1345
+ if (!dest.ok)
1346
+ return { ok: false, reason: dest.reason };
1347
+ try {
1348
+ const { startDashboardServer } = await Promise.resolve().then(() => __importStar(require('../server/dashboard')));
1349
+ const { ensureDashboard, dashboardCensus } = await Promise.resolve().then(() => __importStar(require('../server/dashboard-launcher')));
1350
+ const launch = await ensureDashboard(dest.root, a.port, (opts) => startDashboardServer(opts));
1351
+ const rtm = await fetchJson(`${launch.url}/api/rtm`);
1352
+ const heatmap = await fetchJson(`${launch.url}/api/rtm/heatmap`);
1353
+ return { ok: true, url: launch.url, running: launch.running, census: dashboardCensus(rtm, heatmap) };
1354
+ }
1355
+ catch (err) {
1356
+ return { ok: false, reason: `대시보드 기동 실패: ${err?.message ?? String(err)}` };
1357
+ }
1358
+ },
1325
1359
  async spec_approve(a) {
1326
1360
  const approvalRaw = process.env.HOLMES_APPROVAL;
1327
1361
  let approval;
@@ -133,6 +133,16 @@ exports.TOOL_SCHEMAS = {
133
133
  },
134
134
  },
135
135
  },
136
+ rtm_dashboard: {
137
+ description: "Launch (idempotently) the interactive RTM/CPG dashboard the agent would otherwise start by hand, and return { ok, url, running, census } — use it when a human asks to SEE the RTM heatmap / coverage matrix / dashboard. `url` is the localhost address to open; `running` is true when an already-live server for this project was reused (a second call never starts a second server). `census` is an honesty summary of what the dashboard shows — { reqCount, pipelineCount, coveredCount, coveragePct, retiredCount, findingsScanned } — so the caller can report coverage without scraping the page. No spec writes, no ledger append. Refuses when a supplied `root` points at a different project than the server is bound to.",
138
+ inputSchema: {
139
+ type: 'object',
140
+ properties: {
141
+ root: str('Optional when the server is bound to a file store; if supplied it must resolve to the SAME project.'),
142
+ port: { type: 'number', description: 'Optional port to bind when first launching (default 8080). Ignored if a server for this project is already running.' },
143
+ },
144
+ },
145
+ },
136
146
  spec_approve: {
137
147
  description: 'Approve a spec as a sealing ACT: confirm the ledger destination BEFORE sealing → validate (zero errors) → record approved_digest + parent_digests snapshots → status: approved (written only at the version this act read; a concurrent edit wins and the approval is refused for retry) → append spec-approved to the provenance ledger. Requires a valid out-of-band HOLMES_APPROVAL in the SERVER environment (fail-closed; nothing in the request can substitute). Refuses an unsealed approved parent — seal parents first.',
138
148
  inputSchema: {
@@ -0,0 +1,45 @@
1
+ import type { Cfg, CfgEdge } from '../cpg/foundation/cfg';
2
+ import type { Pdg } from '../cpg/foundation/cdg';
3
+ import type { PersistedAst } from '../cpg/foundation/ast-store';
4
+ export interface CfgViewBlock {
5
+ id: number;
6
+ line: number;
7
+ label: string;
8
+ unreachable: boolean;
9
+ }
10
+ export interface CfgView {
11
+ entry: number;
12
+ exit: number;
13
+ blocks: CfgViewBlock[];
14
+ edges: {
15
+ from: number;
16
+ to: number;
17
+ kind: CfgEdge['kind'];
18
+ }[];
19
+ ctrlDeps: {
20
+ ctrl: number;
21
+ dep: number;
22
+ }[];
23
+ dataDeps: {
24
+ from: number;
25
+ to: number;
26
+ name: string;
27
+ }[];
28
+ }
29
+ /**
30
+ * @implements A-SPEC-545.4
31
+ * Pure: 1-based line number of a byte offset (count of newlines in [0, offset) + 1). Clamped.
32
+ */
33
+ export declare function lineAtByte(source: string, offset: number): number;
34
+ /**
35
+ * @implements A-SPEC-545.4
36
+ * Pure: fold a foundation Cfg + Pdg into a display-ready graph — block labels/lines, control-flow
37
+ * edges, block-level control deps, and reaching defs projected to block pairs (deduped, no self-edge).
38
+ */
39
+ export declare function buildCfgView(cfg: Cfg | {
40
+ unsupported: string;
41
+ }, pdg: Pdg | {
42
+ unsupported: string;
43
+ }, ast: PersistedAst, source: string): CfgView | {
44
+ unsupported: string;
45
+ };
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.lineAtByte = lineAtByte;
4
+ exports.buildCfgView = buildCfgView;
5
+ /**
6
+ * @implements A-SPEC-545.4
7
+ * Pure: 1-based line number of a byte offset (count of newlines in [0, offset) + 1). Clamped.
8
+ */
9
+ function lineAtByte(source, offset) {
10
+ const at = Math.max(0, Math.min(offset, source.length));
11
+ let line = 1;
12
+ for (let i = 0; i < at; i++)
13
+ if (source.charCodeAt(i) === 10)
14
+ line++;
15
+ return line;
16
+ }
17
+ /**
18
+ * @implements A-SPEC-545.4
19
+ * Pure: fold a foundation Cfg + Pdg into a display-ready graph — block labels/lines, control-flow
20
+ * edges, block-level control deps, and reaching defs projected to block pairs (deduped, no self-edge).
21
+ */
22
+ function buildCfgView(cfg, pdg, ast, source) {
23
+ if ('unsupported' in cfg)
24
+ return cfg;
25
+ if ('unsupported' in pdg)
26
+ return pdg;
27
+ // stmt node index → owning block id
28
+ const blockOf = new Map();
29
+ for (const b of cfg.blocks)
30
+ for (const s of b.stmts)
31
+ blockOf.set(s, b.id);
32
+ const unreachable = new Set(cfg.unreachable);
33
+ const blocks = cfg.blocks.map((b) => {
34
+ const first = b.stmts.length ? b.stmts[0] : -1;
35
+ const node = first >= 0 ? ast.nodes[first] : undefined;
36
+ const line = node ? lineAtByte(source, node.start) : 0;
37
+ const raw = node ? source.slice(node.start, node.end) : '';
38
+ const label = raw.split('\n', 1)[0].slice(0, 80);
39
+ return { id: b.id, line, label, unreachable: unreachable.has(b.id) };
40
+ });
41
+ const edges = cfg.edges.map((e) => ({ from: e.from, to: e.to, kind: e.kind }));
42
+ const ctrlDeps = pdg.controlDeps.map((c) => ({ ctrl: c.ctrl, dep: c.dep }));
43
+ const seen = new Set();
44
+ const dataDeps = [];
45
+ for (const d of pdg.reachingDefs) {
46
+ const from = blockOf.get(d.defStmt);
47
+ const to = blockOf.get(d.useStmt);
48
+ if (from === undefined || to === undefined || from === to)
49
+ continue; // no self-edge / unresolved
50
+ const key = `${from}>${to}:${d.name}`;
51
+ if (seen.has(key))
52
+ continue;
53
+ seen.add(key);
54
+ dataDeps.push({ from, to, name: d.name });
55
+ }
56
+ return { entry: cfg.entry, exit: cfg.exit, blocks, edges, ctrlDeps, dataDeps };
57
+ }
@@ -0,0 +1,40 @@
1
+ /** Honesty summary of what a launched dashboard shows — derived from the endpoints it serves. */
2
+ export interface DashboardCensus {
3
+ reqCount: number;
4
+ pipelineCount: number;
5
+ coveredCount: number;
6
+ coveragePct: number;
7
+ retiredCount: number;
8
+ findingsScanned: boolean;
9
+ }
10
+ /**
11
+ * @implements A-SPEC-545.3
12
+ * Pure: fold the /api/rtm and /api/rtm/heatmap payloads into a census. Missing fields read as 0/false;
13
+ * coveragePct never divides by zero.
14
+ */
15
+ export declare function dashboardCensus(rtm: any, heatmap: any): DashboardCensus;
16
+ export interface LaunchResult {
17
+ url: string;
18
+ port: number;
19
+ running: boolean;
20
+ }
21
+ export interface StartedHandle {
22
+ url: string;
23
+ port: number;
24
+ stop?: () => Promise<void>;
25
+ }
26
+ type Starter = (opts: {
27
+ root: string;
28
+ port?: number;
29
+ }) => Promise<StartedHandle>;
30
+ /**
31
+ * @implements A-SPEC-545.3
32
+ * Idempotent per-root dashboard registry: start once, reuse thereafter. `start` is injected so the
33
+ * idempotency is testable without a real server. A failing start registers nothing.
34
+ */
35
+ export declare function ensureDashboard(root: string, port: number | undefined, start: Starter): Promise<LaunchResult>;
36
+ /** Test hook: forget all launched servers (without stopping them). */
37
+ export declare function _resetLauncher(): void;
38
+ /** Test/shutdown hook: stop every launched server and clear the registry. */
39
+ export declare function _stopAll(): Promise<void>;
40
+ export {};
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ // @implements A-SPEC-545.3
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.dashboardCensus = dashboardCensus;
5
+ exports.ensureDashboard = ensureDashboard;
6
+ exports._resetLauncher = _resetLauncher;
7
+ exports._stopAll = _stopAll;
8
+ /**
9
+ * @implements A-SPEC-545.3
10
+ * Pure: fold the /api/rtm and /api/rtm/heatmap payloads into a census. Missing fields read as 0/false;
11
+ * coveragePct never divides by zero.
12
+ */
13
+ function dashboardCensus(rtm, heatmap) {
14
+ const reqCount = heatmap?.matrix?.reqs?.length ?? 0;
15
+ const pipelineCount = heatmap?.pipelineCount ?? 0;
16
+ const coveredCount = heatmap?.completeCount ?? 0;
17
+ const coveragePct = pipelineCount > 0 ? Math.round((coveredCount / pipelineCount) * 100) : 0;
18
+ return {
19
+ reqCount,
20
+ pipelineCount,
21
+ coveredCount,
22
+ coveragePct,
23
+ retiredCount: rtm?.retired?.count ?? 0,
24
+ findingsScanned: heatmap?.findingsScanned === true,
25
+ };
26
+ }
27
+ const live = new Map();
28
+ /**
29
+ * @implements A-SPEC-545.3
30
+ * Idempotent per-root dashboard registry: start once, reuse thereafter. `start` is injected so the
31
+ * idempotency is testable without a real server. A failing start registers nothing.
32
+ */
33
+ async function ensureDashboard(root, port, start) {
34
+ const existing = live.get(root);
35
+ if (existing)
36
+ return { url: existing.url, port: existing.port, running: true };
37
+ const h = await start({ root, port }); // a throw here registers nothing and propagates
38
+ live.set(root, h);
39
+ return { url: h.url, port: h.port, running: false };
40
+ }
41
+ /** Test hook: forget all launched servers (without stopping them). */
42
+ function _resetLauncher() {
43
+ live.clear();
44
+ }
45
+ /** Test/shutdown hook: stop every launched server and clear the registry. */
46
+ async function _stopAll() {
47
+ for (const h of live.values()) {
48
+ try {
49
+ await h.stop?.();
50
+ }
51
+ catch { /* best effort */ }
52
+ }
53
+ live.clear();
54
+ }
@@ -50,6 +50,11 @@ const cpg_scanner_1 = require("../cpg/cpg-scanner");
50
50
  const language_parser_1 = require("../cpg/language-parser");
51
51
  const findings_1 = require("../review/findings");
52
52
  const ast_mutation_1 = require("../cpg/ast-mutation");
53
+ const rtm_matrix_1 = require("./rtm-matrix");
54
+ const ast_store_1 = require("../cpg/foundation/ast-store");
55
+ const cfg_1 = require("../cpg/foundation/cfg");
56
+ const cdg_1 = require("../cpg/foundation/cdg");
57
+ const cfg_view_1 = require("./cfg-view");
53
58
  /**
54
59
  * Start a lightweight standalone Node.js HTTP server for interactive dashboard & RTM visualization.
55
60
  *
@@ -183,6 +188,8 @@ async function startDashboardServer(options) {
183
188
  const findingsLedger = new findings_1.FindingsLedger(findingsPath);
184
189
  const allFindings = findingsScanned ? findingsLedger.list() : [];
185
190
  const pipelines = buildPipelineRows(specs, files, allFindings, { findingsScanned });
191
+ // @implements A-SPEC-545.2 — seriated REQ × stage coverage matrix (additive; pipelines unchanged).
192
+ const matrix = (0, rtm_matrix_1.heatmapMatrix)(pipelines);
186
193
  const body = JSON.stringify({
187
194
  ok: true,
188
195
  pipelineCount: pipelines.length,
@@ -190,6 +197,7 @@ async function startDashboardServer(options) {
190
197
  findingsScanned,
191
198
  mutationScoreMeasured: false,
192
199
  pipelines,
200
+ matrix,
193
201
  });
194
202
  res.writeHead(200, { 'Content-Type': 'application/json' });
195
203
  res.end(body);
@@ -202,6 +210,78 @@ async function startDashboardServer(options) {
202
210
  }
203
211
  return;
204
212
  }
213
+ // @implements A-SPEC-545.4 — one function's CFG + PDG (data/control deps), from the same cfgOf/pdgOf
214
+ // the taint lane uses. Honest envelope: a non-CFG language is named, never faked as an empty graph.
215
+ if (parsedUrl === '/api/cfg') {
216
+ try {
217
+ const q = new URL(req.url || '/', 'http://localhost').searchParams;
218
+ const file = q.get('file');
219
+ const symbol = q.get('symbol');
220
+ if (!file || !symbol) {
221
+ res.writeHead(400, { 'Content-Type': 'application/json' });
222
+ res.end(JSON.stringify({ ok: false, reason: 'file and symbol query params are required' }));
223
+ return;
224
+ }
225
+ const files = scanner.scan(root);
226
+ const scanned = files.find((f) => f.sourcePath === file);
227
+ const sym = scanned?.symbols.find((s) => s.name === symbol);
228
+ const lang = (0, ast_store_1.languageFor)(file)?.lang ?? null;
229
+ if (!scanned || !sym) {
230
+ res.writeHead(200, { 'Content-Type': 'application/json' });
231
+ res.end(JSON.stringify({ ok: false, reason: `symbol ${symbol} not found in ${file}`, file, symbol, lang }));
232
+ return;
233
+ }
234
+ if (!lang || !cfg_1.CFG_LANGUAGES.has(lang)) {
235
+ res.writeHead(200, { 'Content-Type': 'application/json' });
236
+ res.end(JSON.stringify({ ok: true, file, symbol, lang, unsupported: lang ?? 'unknown' }));
237
+ return;
238
+ }
239
+ const source = fs.readFileSync(path.join(root, file), 'utf8');
240
+ const ast = await (0, ast_store_1.parseAst)(source, file);
241
+ if (!ast) {
242
+ res.writeHead(200, { 'Content-Type': 'application/json' });
243
+ res.end(JSON.stringify({ ok: false, reason: `could not parse ${file}`, file, symbol, lang }));
244
+ return;
245
+ }
246
+ // Pick the function whose declaration line is closest to the scanned symbol's start line
247
+ // (absorbs any 0/1-based drift between the scanner and the AST byte→line mapping).
248
+ const fns = (0, cfg_1.functionsIn)(ast);
249
+ let best = null;
250
+ let bestDist = Infinity;
251
+ for (const fn of fns) {
252
+ const node = ast.nodes[fn.nodeIndex];
253
+ if (!node)
254
+ continue;
255
+ const dist = Math.abs((0, cfg_view_1.lineAtByte)(source, node.start) - sym.startLine);
256
+ if (dist < bestDist) {
257
+ bestDist = dist;
258
+ best = fn;
259
+ }
260
+ }
261
+ if (!best) {
262
+ res.writeHead(200, { 'Content-Type': 'application/json' });
263
+ res.end(JSON.stringify({ ok: false, reason: `no function body for ${symbol} in ${file}`, file, symbol, lang }));
264
+ return;
265
+ }
266
+ const cfg = (0, cfg_1.cfgOf)(ast, best, source);
267
+ const pdg = (0, cdg_1.pdgOf)(ast, cfg, best, source);
268
+ const view = (0, cfg_view_1.buildCfgView)(cfg, pdg, ast, source);
269
+ if ('unsupported' in view) {
270
+ res.writeHead(200, { 'Content-Type': 'application/json' });
271
+ res.end(JSON.stringify({ ok: true, file, symbol, lang, unsupported: view.unsupported }));
272
+ return;
273
+ }
274
+ res.writeHead(200, { 'Content-Type': 'application/json' });
275
+ res.end(JSON.stringify({ ok: true, file, symbol, lang, cfg: view }));
276
+ }
277
+ catch (err) {
278
+ if (!res.headersSent) {
279
+ res.writeHead(500, { 'Content-Type': 'application/json' });
280
+ res.end(JSON.stringify({ ok: false, error: 'Internal Server Error' }));
281
+ }
282
+ }
283
+ return;
284
+ }
205
285
  if (parsedUrl === '/api/cpg') {
206
286
  try {
207
287
  const files = scanner.scan(root);
@@ -388,7 +468,57 @@ function renderDashboardHtml() {
388
468
  <title>Holmes-Kit World Top-Tier Quantitative RTM Matrix & AST/CPG Graph Canvas</title>
389
469
  <script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
390
470
  <style>
391
- body { font-family: system-ui, -apple-system, sans-serif; background-color: #0b0f19; color: #f8fafc; margin: 0; padding: 24px; user-select: none; }
471
+ /* @implements A-SPEC-545.2 design tokens: sequential coverage ramp, status, surfaces, 2 fonts.
472
+ Dark-first; light palette under prefers-color-scheme. The green ramp reads "traced". */
473
+ :root {
474
+ --surface-0: #0d1417; --surface-1: #131e22; --surface-2: #1b2b30; --border: #26383d;
475
+ --ink-0: #eaf2f0; --ink-1: #9fb2b0; --ink-2: #6b807e;
476
+ --ramp-0: #17211d; --ramp-1: #1f3a2c; --ramp-2: #26543a; --ramp-3: #2c7449; --ramp-4: #33a35f; --ramp-5: #46d97e;
477
+ --ramp-ink-lo: #8ba39c; --ramp-ink-hi: #06120c;
478
+ --ok: #46d97e; --warn: #e0a83c; --bad: #e5595c; --accent: #46b0d9;
479
+ --font-ui: ui-sans-serif, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
480
+ --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;
481
+ }
482
+ @media (prefers-color-scheme: light) {
483
+ :root {
484
+ --surface-0: #f4f7f6; --surface-1: #ffffff; --surface-2: #eaf0ee; --border: #d3ddda;
485
+ --ink-0: #12201c; --ink-1: #4a5c58; --ink-2: #7b8d89;
486
+ --ramp-0: #e7efe9; --ramp-1: #c3e0cd; --ramp-2: #8fc9a5; --ramp-3: #54ac77; --ramp-4: #2f8a54; --ramp-5: #166b3c;
487
+ --ramp-ink-lo: #5b6f69; --ramp-ink-hi: #f4faf6;
488
+ --ok: #166b3c; --warn: #b7791f; --bad: #c53539; --accent: #1f7fa6;
489
+ }
490
+ }
491
+ body { font-family: var(--font-ui); background-color: var(--surface-0); color: var(--ink-0); margin: 0; padding: 24px; user-select: none; }
492
+
493
+ /* Seriated REQ × stage coverage matrix (2D grid) */
494
+ .hm-toolbar { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; margin-bottom: 12px; color: var(--ink-1); font-size: 12px; }
495
+ .hm-legend { display: inline-flex; align-items: center; gap: 6px; }
496
+ .hm-legend-swatch { width: 16px; height: 12px; border-radius: 2px; display: inline-block; border: 1px solid var(--border); }
497
+ .hm-scroll { max-height: 74vh; overflow: auto; border: 1px solid var(--border); border-radius: 12px; background: var(--surface-1); }
498
+ table.hm-grid { border-collapse: separate; border-spacing: 0; width: 100%; font-family: var(--font-mono); }
499
+ table.hm-grid th, table.hm-grid td { border-bottom: 1px solid var(--surface-0); border-right: 1px solid var(--surface-0); }
500
+ table.hm-grid thead th { position: sticky; top: 0; z-index: 3; background: var(--surface-2); color: var(--ink-1);
501
+ font-family: var(--font-ui); font-size: 10px; font-weight: 700; letter-spacing: 0.6px; text-transform: uppercase;
502
+ padding: 10px 8px; text-align: center; white-space: nowrap; }
503
+ table.hm-grid thead th.hm-corner { left: 0; z-index: 4; text-align: left; }
504
+ table.hm-grid td.hm-rowlabel, table.hm-grid th.hm-corner { position: sticky; left: 0; z-index: 2; background: var(--surface-1);
505
+ min-width: 260px; max-width: 320px; padding: 7px 12px; border-right: 1px solid var(--border); }
506
+ .hm-req-id { font-family: var(--font-mono); font-weight: 700; color: var(--ink-0); font-size: 12px; }
507
+ .hm-req-title { font-family: var(--font-ui); color: var(--ink-2); font-size: 11px; margin-left: 8px;
508
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
509
+ .hm-rowlabel-flex { display: flex; align-items: baseline; overflow: hidden; }
510
+ table.hm-grid td.hm-cell { text-align: center; font-variant-numeric: tabular-nums; font-size: 11px; padding: 7px 6px;
511
+ color: var(--ramp-ink-hi); cursor: default; min-width: 62px; }
512
+ table.hm-grid tbody tr:hover td.hm-cell { outline: 1px solid var(--accent); outline-offset: -2px; }
513
+ .hm-b0 { background: var(--ramp-0); color: var(--ramp-ink-lo); }
514
+ .hm-b1 { background: var(--ramp-1); color: var(--ramp-ink-lo); }
515
+ .hm-b2 { background: var(--ramp-2); color: var(--ramp-ink-hi); }
516
+ .hm-b3 { background: var(--ramp-3); color: var(--ramp-ink-hi); }
517
+ .hm-b4 { background: var(--ramp-4); color: var(--ramp-ink-hi); }
518
+ .hm-b5 { background: var(--ramp-5); color: var(--ramp-ink-hi); }
519
+ .hm-summary-pill { font-family: var(--font-ui); font-weight: 700; font-size: 11px; padding: 2px 8px; border-radius: 999px; }
520
+ .hm-pill-ok { background: color-mix(in srgb, var(--ok) 22%, transparent); color: var(--ok); border: 1px solid var(--ok); }
521
+ .hm-pill-warn { background: color-mix(in srgb, var(--warn) 22%, transparent); color: var(--warn); border: 1px solid var(--warn); }
392
522
  .header { margin-bottom: 20px; border-bottom: 1px solid rgba(255,255,255,0.12); padding-bottom: 16px; display: flex; justify-content: space-between; align-items: center; }
393
523
  .nav-tabs { display: flex; gap: 12px; margin-bottom: 20px; }
394
524
  .tab-btn { background: #1e293b; color: #94a3b8; border: 1px solid #334155; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-weight: bold; }
@@ -541,10 +671,10 @@ function renderDashboardHtml() {
541
671
  </div>
542
672
 
543
673
  <div class="nav-tabs">
544
- <button id="tabRtmBtn" class="tab-btn active" onclick="switchTab('rtm')">📊 Quantitative REQ RTM Matrix Grid</button>
545
- <button id="tabHeatmapBtn" class="tab-btn" onclick="switchTab('heatmap')">🔥 REQ-Grouped Pipeline Heatmap (REQ ➔ AST)</button>
546
- <button id="tabGraphBtn" class="tab-btn" onclick="switchTab('graph')">🕸️ End-to-End Human-Insight Pipeline Canvas</button>
547
- <button id="tabLegacyBtn" class="tab-btn" onclick="switchTab('legacy')">🏷️ Unmapped Specs (C-SPEC · JOB)</button>
674
+ <button id="tabRtmBtn" class="tab-btn active" onclick="switchTab('rtm')">Quantitative REQ RTM</button>
675
+ <button id="tabHeatmapBtn" class="tab-btn" onclick="switchTab('heatmap')">Coverage Matrix</button>
676
+ <button id="tabGraphBtn" class="tab-btn" onclick="switchTab('graph')">Pipeline Canvas</button>
677
+ <button id="tabLegacyBtn" class="tab-btn" onclick="switchTab('legacy')">Unmapped Specs</button>
548
678
  </div>
549
679
 
550
680
  <div id="rtmTab">
@@ -570,30 +700,28 @@ function renderDashboardHtml() {
570
700
  </div>
571
701
 
572
702
  <div id="heatmapTab" style="display: none;">
573
- <div class="ux-hint-banner" style="border-left-color: #f59e0b;">
574
- <div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap;">
575
- <div style="display: flex; align-items: center; gap: 8px;">
576
- <span>🔥 <strong>3-Tier Multi-Lens:</strong></span>
577
- <select id="lensSelect" class="select-box" style="border-color: #f59e0b; font-weight: bold; background: #0f172a;" onchange="changeHeatmapLens()">
578
- <option value="lens1" selected>🎯 Lens 1: SDLC Trace Spine (REQ ➔ AST/CPG Core Chain)</option>
579
- <option value="lens2">🛡️ Lens 2: Audit & Security Risk Overlay (Findings Ledger — shows “not scanned” when absent)</option>
580
- <option value="lens3">🧪 Lens 3: Mutation & Robustness Overlay (Static AST Mutants — score unmeasured)</option>
581
- </select>
582
- </div>
583
-
584
- <div style="display: flex; align-items: center; gap: 8px;">
585
- <span>🔍 <strong>Filter Status:</strong></span>
586
- <select id="pipelineFilterSelect" class="select-box" style="border-color: #38bdf8; font-weight: bold; background: #0f172a;" onchange="changeHeatmapLens()">
587
- <option value="all" selected>All REQ Pipelines</option>
588
- <option value="uncovered">Only Uncovered / Active Traces</option>
589
- <option value="findings">Only Traces with Open Findings</option>
590
- </select>
591
- </div>
592
- </div>
593
- </div>
594
- <div class="heatmap-wrapper">
595
- <div id="heatmapContainer">Loading REQ-grouped 6-stage pipeline heatmap...</div>
703
+ <div class="hm-toolbar">
704
+ <span style="font-family: var(--font-ui); font-weight: 700; color: var(--ink-0); font-size: 13px;">Requirement × Pipeline-Stage Coverage</span>
705
+ <span style="color: var(--ink-2);">rows seriated by completeness · cell = share of a REQ's chains reaching that stage</span>
706
+ <span style="display: inline-flex; align-items: center; gap: 8px;">
707
+ <label for="pipelineFilterSelect" style="color: var(--ink-1);">Rows</label>
708
+ <select id="pipelineFilterSelect" class="select-box" onchange="changeHeatmapLens()">
709
+ <option value="all" selected>All requirements</option>
710
+ <option value="uncovered">Incomplete only</option>
711
+ </select>
712
+ </span>
713
+ <span class="hm-legend">
714
+ <span style="color: var(--ink-2);">0%</span>
715
+ <span class="hm-legend-swatch" style="background: var(--ramp-0);"></span>
716
+ <span class="hm-legend-swatch" style="background: var(--ramp-1);"></span>
717
+ <span class="hm-legend-swatch" style="background: var(--ramp-2);"></span>
718
+ <span class="hm-legend-swatch" style="background: var(--ramp-3);"></span>
719
+ <span class="hm-legend-swatch" style="background: var(--ramp-4);"></span>
720
+ <span class="hm-legend-swatch" style="background: var(--ramp-5);"></span>
721
+ <span style="color: var(--ink-2);">100%</span>
722
+ </span>
596
723
  </div>
724
+ <div id="heatmapContainer">Loading coverage matrix…</div>
597
725
  </div>
598
726
 
599
727
  <div id="graphTab" style="display: none;">
@@ -844,144 +972,70 @@ function renderDashboardHtml() {
844
972
  }
845
973
  }
846
974
 
847
- // Render one pipeline stage cell. A stage with no artifact renders as an inert “missing” cell
848
- // instead of a clickable id, so an empty stage can never be mistaken for a traced one.
849
- function stageCell(cls, icon, label, jumpId, withArrow) {
850
- const arrow = withArrow ? '<span class="arrow-sep">➔</span> ' : '';
851
- if (!label) {
852
- return '<td class="' + cls + ' stage-missing" title="No artifact for this stage">' + arrow + '— missing</td>';
853
- }
854
- const onclick = jumpId ? ' onclick="focusSpecInGraph(\\'' + escapeHtml(String(jumpId)) + '\\')"' : '';
855
- return '<td class="' + cls + '"' + onclick + '>' + arrow + icon + ' ' + escapeHtml(String(label)) + '</td>';
856
- }
857
- // REQ-Grouped Structured 6-Stage Pipeline Heatmap Matrix Renderer
975
+ // @implements A-SPEC-545.2 seriated REQ × stage coverage matrix (2D grid), painted from the
976
+ // server-built data.matrix. Rows are ordered by matrix.order (completeness), each cell coloured
977
+ // by its ramp bucket with the coverage percent shown inline (tabular-nums), so magnitude never
978
+ // relies on colour alone.
858
979
  function render6StagePipelineMatrix(data) {
859
980
  const container = document.getElementById('heatmapContainer');
860
- const rawPipelines = data.pipelines || [];
861
- const lens = document.getElementById('lensSelect').value || 'lens1';
862
- const statusFilter = document.getElementById('pipelineFilterSelect').value || 'all';
981
+ const matrix = data.matrix || { reqs: [], stages: [], values: [], order: [], buckets: [] };
982
+ const filterEl = document.getElementById('pipelineFilterSelect');
983
+ const statusFilter = filterEl ? (filterEl.value || 'all') : 'all';
863
984
 
864
- if (rawPipelines.length === 0) {
865
- container.innerHTML = '<div style="color:#94a3b8; font-size:14px;">No pipeline trace chains found.</div>';
985
+ if (!matrix.reqs || matrix.reqs.length === 0) {
986
+ container.innerHTML = '<div style="color: var(--ink-1); font-size: 14px; padding: 16px;">No pipeline trace chains found.</div>';
866
987
  return;
867
988
  }
868
989
 
869
- // Filter pipelines based on user status selection
870
- let pipelines = rawPipelines;
871
- if (statusFilter === 'uncovered') {
872
- pipelines = rawPipelines.filter(p => p.status !== 'COVERED');
873
- } else if (statusFilter === 'findings') {
874
- pipelines = rawPipelines.filter(p => p.findingsCount > 0);
875
- }
990
+ const isRowComplete = (i) => (matrix.values[i] || []).every(v => v >= 1);
991
+ let rowIdx = (matrix.order && matrix.order.length ? matrix.order : matrix.reqs.map((_, i) => i)).slice();
992
+ if (statusFilter === 'uncovered') rowIdx = rowIdx.filter(i => !isRowComplete(i));
876
993
 
877
- // Group pipelines by reqId
878
- const groupedMap = new Map();
879
- pipelines.forEach(p => {
880
- if (!groupedMap.has(p.reqId)) {
881
- groupedMap.set(p.reqId, { reqId: p.reqId, reqTitle: p.reqTitle, items: [] });
882
- }
883
- groupedMap.get(p.reqId).items.push(p);
884
- });
994
+ const fullyTraced = matrix.reqs.filter((_, i) => isRowComplete(i)).length;
995
+ const summaryCls = fullyTraced === matrix.reqs.length ? 'hm-pill-ok' : 'hm-pill-warn';
885
996
 
886
- if (groupedMap.size === 0) {
887
- const unscanned = statusFilter === 'findings' && data.findingsScanned === false;
888
- container.innerHTML = unscanned
889
- ? '<div style="color:#fbbf24; font-size:14px; padding:16px; border:1px dashed #64748b; border-radius:8px;">⚪ No findings ledger exists (<code>.ax/ledger/findings.jsonl</code>). This is <strong>not</strong> a clean result — no audit has been run.</div>'
890
- : '<div style="color:#94a3b8; font-size:14px; padding:16px;">No pipelines match the selected filter.</div>';
997
+ if (rowIdx.length === 0) {
998
+ container.innerHTML = '<div style="color: var(--ink-1); font-size: 14px; padding: 16px;">Every requirement is fully traced — no incomplete rows to show.</div>';
891
999
  return;
892
1000
  }
893
1001
 
894
- let extraHeader = 'Status';
895
- if (lens === 'lens2') extraHeader = heatmapData && heatmapData.findingsScanned === false ? '🛡️ Security Audit (never scanned)' : '🛡️ Live Security Findings';
896
- else if (lens === 'lens3') extraHeader = '🧪 Static AST Mutants (score unmeasured)';
897
-
898
- let html = '';
899
-
900
- groupedMap.forEach((group, reqId) => {
901
- const items = group.items;
902
- const totalItems = items.length;
903
- const coveredItems = items.filter(i => i.status === 'COVERED').length;
904
- const isReqHealthy = coveredItems === totalItems;
905
-
906
- const reqStatusBadge = isReqHealthy
907
- ? '<span class="neighbor-rel badge-covered">🟢 100% HEALTHY TRACE</span>'
908
- : '<span class="neighbor-rel badge-uncovered">🔴 CONTAINS UNCOVERED TRACES</span>';
909
-
910
- html += \`
911
- <div class="req-card" style="margin-bottom: 20px;">
912
- <div class="req-header" onclick="toggleAccordion('pipe-body-\${escapeHtml(reqId)}')">
913
- <div class="req-title-group">
914
- <span class="req-id-badge">\${escapeHtml(reqId)}</span>
915
- <span class="req-title">\${escapeHtml(group.reqTitle || 'Business Requirement Pipeline')}</span>
916
- \${reqStatusBadge}
917
- </div>
918
- <div style="display: flex; align-items: center; gap: 12px;">
919
- <span style="font-size: 12px; color: #94a3b8; font-weight: bold;">\${totalItems} End-to-End Trace Chain\${totalItems > 1 ? 's' : ''}</span>
920
- <button class="expand-btn">▶ Expand Pipeline Matrix</button>
1002
+ const pct = (v) => Math.round((v || 0) * 100);
1003
+ const headCells = matrix.stages.map(s => \`<th>\${escapeHtml(s)}</th>\`).join('');
1004
+
1005
+ let rows = '';
1006
+ rowIdx.forEach(i => {
1007
+ const req = matrix.reqs[i];
1008
+ const vals = matrix.values[i] || [];
1009
+ const bkts = matrix.buckets[i] || [];
1010
+ const cells = matrix.stages.map((stage, s) => {
1011
+ const v = vals[s] || 0;
1012
+ const b = bkts[s] || 0;
1013
+ const title = escapeHtml(req.id) + ' · ' + escapeHtml(stage) + ': ' + pct(v) + '% of chains reached';
1014
+ return \`<td class="hm-cell hm-b\${b}" title="\${title}">\${pct(v)}</td>\`;
1015
+ }).join('');
1016
+ rows += \`
1017
+ <tr>
1018
+ <td class="hm-rowlabel" onclick="focusSpecInGraph('\${escapeHtml(req.id)}')" title="\${escapeHtml(req.title || '')}">
1019
+ <div class="hm-rowlabel-flex">
1020
+ <span class="hm-req-id">\${escapeHtml(req.id)}</span>
1021
+ <span class="hm-req-title">\${escapeHtml(req.title || '')}</span>
921
1022
  </div>
922
- </div>
923
-
924
- <div class="accordion-body open" id="pipe-body-\${escapeHtml(reqId)}" style="padding:0;">
925
- <table class="pipeline-table">
926
- <thead>
927
- <tr>
928
- <th>Stage 1: REQ Business</th>
929
- <th>Stage 2: H-SPEC Functional</th>
930
- <th>Stage 3: A-SPEC Architecture</th>
931
- <th>Stage 4: T-SPEC Verification</th>
932
- <th>Stage 5: Source Code File</th>
933
- <th>Stage 6: AST / CPG Symbol</th>
934
- <th>\${extraHeader}</th>
935
- </tr>
936
- </thead>
937
- <tbody>
938
- \`;
939
-
940
- items.forEach(p => {
941
- const missing = p.missingStages || [];
942
- let statusBadge = missing.length === 0
943
- ? '<span class="neighbor-rel badge-covered">COVERED (6/6)</span>'
944
- : \`<span class="neighbor-rel badge-uncovered" title="Missing: \${escapeHtml(missing.join(', '))}">UNCOVERED (\${p.stagesComplete}/6)</span>\`;
945
-
946
- if (lens === 'lens2') {
947
- if (!p.findingsScanned) {
948
- statusBadge = '<span class="neighbor-rel badge-unmeasured" title="No .ax/ledger/findings.jsonl — this chain has never been audited">⚪ Not scanned</span>';
949
- } else if (p.criticalCount > 0) {
950
- statusBadge = \`<span class="neighbor-rel badge-uncovered">🔴 \${p.criticalCount} Critical Finding\${p.criticalCount > 1 ? 's' : ''}</span>\`;
951
- } else if (p.findingsCount > 0) {
952
- statusBadge = \`<span class="neighbor-rel" style="background:#d97706; color:#fff;">🟡 \${p.findingsCount} Open Finding\${p.findingsCount > 1 ? 's' : ''}</span>\`;
953
- } else {
954
- statusBadge = '<span class="neighbor-rel badge-covered">🟢 0 Open Findings</span>';
955
- }
956
- } else if (lens === 'lens3') {
957
- // A score requires running the suite once per mutant; this endpoint only generates them
958
- // statically, so it reports the mutant count and says the score is unmeasured.
959
- const mutantCount = p.mutantCount || 0;
960
- statusBadge = \`<span class="neighbor-rel badge-unmeasured" title="Static generation only — run mutation testing to obtain a score">🧪 \${mutantCount} Mutant\${mutantCount === 1 ? '' : 's'} · Score not measured</span>\`;
961
- }
962
-
963
- html += \`
964
- <tr class="pipeline-row">
965
- \${stageCell('stage-req', '🟣', p.reqId, p.reqId, false)}
966
- \${stageCell('stage-hspec', '🟣', p.hspecId, p.hspecId, true)}
967
- \${stageCell('stage-aspec', '🔵', p.aspecId, p.aspecId, true)}
968
- \${stageCell('stage-tspec', '🔷', p.tspecId, p.tspecId, true)}
969
- \${stageCell('stage-file', '📄', p.fileId ? (p.fileId.split('/').pop() || p.fileId) : null, p.fileId, true)}
970
- \${stageCell('stage-symbol', '⚡', p.symbolId, p.fileId && p.symbolId ? p.fileId + '#' + p.symbolId : null, true)}
971
- <td>\${statusBadge}</td>
972
- </tr>
973
- \`;
974
- });
975
-
976
- html += \`
977
- </tbody>
978
- </table>
979
- </div>
980
- </div>
981
- \`;
1023
+ </td>
1024
+ \${cells}
1025
+ </tr>\`;
982
1026
  });
983
1027
 
984
- container.innerHTML = html;
1028
+ container.innerHTML = \`
1029
+ <div style="margin-bottom: 10px;">
1030
+ <span class="hm-summary-pill \${summaryCls}">\${fullyTraced} / \${matrix.reqs.length} requirements fully traced</span>
1031
+ <span style="color: var(--ink-2); font-size: 12px; margin-left: 10px;">showing \${rowIdx.length} row\${rowIdx.length === 1 ? '' : 's'}</span>
1032
+ </div>
1033
+ <div class="hm-scroll">
1034
+ <table class="hm-grid">
1035
+ <thead><tr><th class="hm-corner">Requirement</th>\${headCells}</tr></thead>
1036
+ <tbody>\${rows}</tbody>
1037
+ </table>
1038
+ </div>\`;
985
1039
  }
986
1040
 
987
1041
  // World Top-Tier Quantitative REQ-Centric RTM Traceability Matrix Grid Renderer
@@ -0,0 +1,47 @@
1
+ /** The six pipeline stages, in traversal order — the columns of the matrix. */
2
+ export declare const STAGES: readonly ["REQ", "H-SPEC", "A-SPEC", "T-SPEC", "File", "AST Symbol"];
3
+ /** One pipeline chain row from /api/rtm/heatmap (only the fields the matrix reads). */
4
+ export interface PipelineRow {
5
+ reqId: string;
6
+ reqTitle?: string;
7
+ stagesComplete: number;
8
+ }
9
+ export interface StageMatrix {
10
+ reqs: {
11
+ id: string;
12
+ title: string;
13
+ }[];
14
+ stages: string[];
15
+ /** values[r][s] ∈ [0,1] = fraction of REQ r's chains that reached stage s. */
16
+ values: number[][];
17
+ }
18
+ /**
19
+ * @implements A-SPEC-545.1
20
+ * Pure: turn per-chain pipeline rows into a REQ × stage coverage matrix. Rows are REQs (first-seen
21
+ * order), columns the six stages; each cell is the fraction of that REQ's chains that REACHED the
22
+ * stage (stagesComplete >= s+1). This is the overview the 2D heatmap paints — magnitude per cell.
23
+ */
24
+ export declare function buildStageMatrix(pipelines: PipelineRow[]): StageMatrix;
25
+ /**
26
+ * @implements A-SPEC-545.1
27
+ * Pure & deterministic: a row permutation that clusters similar, high-coverage rows together
28
+ * (Behrisch-style seriation). Key: descending row-mean, then the profile vector descending, then the
29
+ * original index (stable). Same input ⇒ same output.
30
+ */
31
+ /**
32
+ * @implements A-SPEC-545.2
33
+ * Pure: map a coverage fraction v∈[0,1] to a sequential-ramp bucket index 0..5. Monotonic
34
+ * non-decreasing, clamped ([0,1]), NaN→0. The dashboard paints cell background = var(--ramp-<bucket>).
35
+ */
36
+ export declare function coverageBucket(v: number): number;
37
+ export interface HeatmapMatrix extends StageMatrix {
38
+ order: number[];
39
+ buckets: number[][];
40
+ }
41
+ /**
42
+ * @implements A-SPEC-545.2
43
+ * Pure server-side payload builder: buildStageMatrix + seriateRows + coverageBucket in one object,
44
+ * so the client render is a thin paint over data.matrix.
45
+ */
46
+ export declare function heatmapMatrix(pipelines: PipelineRow[]): HeatmapMatrix;
47
+ export declare function seriateRows(values: number[][]): number[];
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ // @implements A-SPEC-545.1
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.STAGES = void 0;
5
+ exports.buildStageMatrix = buildStageMatrix;
6
+ exports.coverageBucket = coverageBucket;
7
+ exports.heatmapMatrix = heatmapMatrix;
8
+ exports.seriateRows = seriateRows;
9
+ /** The six pipeline stages, in traversal order — the columns of the matrix. */
10
+ exports.STAGES = ['REQ', 'H-SPEC', 'A-SPEC', 'T-SPEC', 'File', 'AST Symbol'];
11
+ /**
12
+ * @implements A-SPEC-545.1
13
+ * Pure: turn per-chain pipeline rows into a REQ × stage coverage matrix. Rows are REQs (first-seen
14
+ * order), columns the six stages; each cell is the fraction of that REQ's chains that REACHED the
15
+ * stage (stagesComplete >= s+1). This is the overview the 2D heatmap paints — magnitude per cell.
16
+ */
17
+ function buildStageMatrix(pipelines) {
18
+ const order = [];
19
+ const groups = new Map();
20
+ for (const p of pipelines) {
21
+ let g = groups.get(p.reqId);
22
+ if (!g) {
23
+ g = { title: p.reqTitle ?? '', chains: [] };
24
+ groups.set(p.reqId, g);
25
+ order.push(p.reqId);
26
+ }
27
+ g.chains.push(p.stagesComplete);
28
+ }
29
+ const reqs = order.map((id) => ({ id, title: groups.get(id).title }));
30
+ const values = order.map((id) => {
31
+ const chains = groups.get(id).chains;
32
+ return exports.STAGES.map((_, s) => {
33
+ const reached = chains.filter((c) => c >= s + 1).length;
34
+ return Number((reached / chains.length).toFixed(3));
35
+ });
36
+ });
37
+ return { reqs, stages: [...exports.STAGES], values };
38
+ }
39
+ /**
40
+ * @implements A-SPEC-545.1
41
+ * Pure & deterministic: a row permutation that clusters similar, high-coverage rows together
42
+ * (Behrisch-style seriation). Key: descending row-mean, then the profile vector descending, then the
43
+ * original index (stable). Same input ⇒ same output.
44
+ */
45
+ /**
46
+ * @implements A-SPEC-545.2
47
+ * Pure: map a coverage fraction v∈[0,1] to a sequential-ramp bucket index 0..5. Monotonic
48
+ * non-decreasing, clamped ([0,1]), NaN→0. The dashboard paints cell background = var(--ramp-<bucket>).
49
+ */
50
+ function coverageBucket(v) {
51
+ if (!Number.isFinite(v) || v <= 0)
52
+ return 0; // NaN / -Inf / ≤0 → lowest
53
+ if (v >= 1)
54
+ return 5; // ≥1 → highest (upper clamp)
55
+ return Math.min(5, Math.floor(v * 6));
56
+ }
57
+ /**
58
+ * @implements A-SPEC-545.2
59
+ * Pure server-side payload builder: buildStageMatrix + seriateRows + coverageBucket in one object,
60
+ * so the client render is a thin paint over data.matrix.
61
+ */
62
+ function heatmapMatrix(pipelines) {
63
+ const { reqs, stages, values } = buildStageMatrix(pipelines);
64
+ const order = seriateRows(values);
65
+ const buckets = values.map((row) => row.map(coverageBucket));
66
+ return { reqs, stages, values, order, buckets };
67
+ }
68
+ function seriateRows(values) {
69
+ const mean = (row) => row.length ? row.reduce((a, b) => a + b, 0) / row.length : 0;
70
+ return values
71
+ .map((row, i) => ({ i, m: mean(row), row }))
72
+ .sort((a, b) => {
73
+ if (b.m !== a.m)
74
+ return b.m - a.m; // higher coverage first
75
+ for (let k = 0; k < Math.max(a.row.length, b.row.length); k++) {
76
+ const d = (b.row[k] ?? 0) - (a.row[k] ?? 0); // then profile descending
77
+ if (d !== 0)
78
+ return d;
79
+ }
80
+ return a.i - b.i; // stable on ties
81
+ })
82
+ .map((x) => x.i);
83
+ }
@@ -49,6 +49,7 @@ exports.cacheIsStale = cacheIsStale;
49
49
  // major.minor.patch — a full semver library would carry prerelease/build code this never runs), and
50
50
  // the registry query (A-SPEC-531.2) uses Node's built-in https.
51
51
  const path = __importStar(require("node:path"));
52
+ const npx_bin_1 = require("../project/npx-bin");
52
53
  exports.NPM_URL = 'https://www.npmjs.com/package/@holmes-lab/holmes-kit';
53
54
  const DEFAULT_TTL_MS = 24 * 3600_000;
54
55
  /** Parse `major.minor.patch` to a 3-tuple; a non-numeric field becomes 0 (never throws). */
@@ -97,10 +98,11 @@ function readCache(home, readFile) {
97
98
  function installModeGuide(mode, latest, current) {
98
99
  const head = `[Holmes-Kit] Update available: ${latest} (current ${current}).`;
99
100
  switch (mode) {
101
+ // @implements A-SPEC-544.1 — one command installs the latest AND re-pins every wired workspace,
102
+ // so the nudge no longer spells out `npm i` + `init --force`. win32 → npx.cmd (REQ-542).
100
103
  case 'global-npx':
101
- return `${head} Run: npm i -g @holmes-lab/holmes-kit@latest, then holmes-kit init --force (re-pins wiring; requires HOLMES_APPROVAL).`;
102
104
  case 'local-dep':
103
- return `${head} Run: npm i -D @holmes-lab/holmes-kit@latest`;
105
+ return `${head} Run: ${(0, npx_bin_1.npxBin)()} holmes-kit upgrade`;
104
106
  case 'source':
105
107
  return null;
106
108
  }
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.11.0",
4
+ "version": "0.12.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",