@holmes-lab/holmes-kit 0.11.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 +62 -0
- package/README.md +2 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/doctor.js +3 -1
- package/dist/holmes/mcp/handlers.d.ts +16 -0
- package/dist/holmes/mcp/handlers.js +42 -10
- package/dist/holmes/mcp/tool-schemas.js +10 -0
- package/dist/holmes/server/cfg-view.d.ts +69 -0
- package/dist/holmes/server/cfg-view.js +107 -0
- package/dist/holmes/server/dashboard-launcher.d.ts +40 -0
- package/dist/holmes/server/dashboard-launcher.js +54 -0
- package/dist/holmes/server/dashboard.js +294 -152
- package/dist/holmes/server/rtm-matrix.d.ts +47 -0
- package/dist/holmes/server/rtm-matrix.js +83 -0
- package/dist/holmes/spec/adr-refs.d.ts +7 -0
- package/dist/holmes/spec/adr-refs.js +17 -0
- package/dist/holmes/update/update-notice.js +4 -2
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,68 @@ 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
|
+
|
|
34
|
+
## [0.12.0] - 2026-09-05
|
|
35
|
+
|
|
36
|
+
World-top-tier RTM dashboard, in flight. The `holmes-kit serve` dashboard stops being a table of
|
|
37
|
+
accordions and becomes a real coverage matrix, gains a one-call launcher, and learns to serve CFG/PDG
|
|
38
|
+
for a function. Additive and backward-compatible — every existing `/api/*` payload keeps its shape and
|
|
39
|
+
values; these are new fields, a new endpoint, a new tool, and a rebuilt heatmap view.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **2D coverage matrix** (REQ-545): the heatmap tab is now a single **requirement × pipeline-stage
|
|
44
|
+
grid** — rows are requirements ordered by completeness (seriation), each cell a sequential-ramp
|
|
45
|
+
colour with the coverage percent printed in it (so magnitude never rides on colour alone). Replaces
|
|
46
|
+
the per-REQ accordion-of-tables. `/api/rtm/heatmap` gains an additive `matrix` field
|
|
47
|
+
(`{reqs, stages, values, order, buckets}`) built by pure, unit-tested `buildStageMatrix` +
|
|
48
|
+
`seriateRows` + `coverageBucket`.
|
|
49
|
+
- **`rtm_dashboard` MCP tool** (REQ-545): ask to *see* the RTM heatmap / coverage matrix and the
|
|
50
|
+
server launches (idempotently — a second call reuses the live one) and returns `{ url, running,
|
|
51
|
+
census }`, where `census` is an honesty summary (`reqCount, pipelineCount, coveredCount,
|
|
52
|
+
coveragePct, retiredCount, findingsScanned`). No more starting `serve` by hand.
|
|
53
|
+
- **`/api/cfg` — control-flow + program-dependence for one function** (REQ-545): `?file=&symbol=`
|
|
54
|
+
returns that function's CFG (blocks with source line/label, control-flow edges) and PDG (block-level
|
|
55
|
+
control deps + reaching-def data deps), consumed from the same `cfgOf`/`pdgOf` the taint lane uses.
|
|
56
|
+
A non-CFG language is named as `unsupported`, never faked as an empty graph. (Graph-view rendering
|
|
57
|
+
of this lands next.)
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- **Dashboard design tokens** (REQ-545): the palette moved off the Tailwind-default + emoji-marker
|
|
62
|
+
look to a tokenised system — a single-hue sequential coverage ramp, reserved status colours, a
|
|
63
|
+
UI/mono type pairing (data in tabular-nums), surfaces and ink, with a light and a dark palette via
|
|
64
|
+
`prefers-color-scheme`. Applied to the new matrix view and the tab bar; structure emoji removed
|
|
65
|
+
(meaning markers only). The rest of the dashboard tokenises incrementally in the follow-on slice.
|
|
66
|
+
- **Upgrade nudges point at one command** (REQ-544): the session banner's drift line and doctor's
|
|
67
|
+
version-drift fix hint now name `holmes-kit upgrade` (the right binary per platform — `npx.cmd` on
|
|
68
|
+
win32) instead of spelling out a manual `npm i` + `init --force`.
|
|
69
|
+
|
|
8
70
|
## [0.11.0] - 2026-09-05
|
|
9
71
|
|
|
10
72
|
Zero-config upgrades — preparation is automatic, execution is your explicit choice. Additive and
|
package/README.md
CHANGED
|
@@ -20,6 +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–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+`).
|
|
23
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.
|
|
24
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`.
|
|
25
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
|
-
|
|
1
|
+
3b23a9b-mtnx2lfo
|
|
@@ -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} 입니다 — 재배선 전까지 옛 서버가 계속 뜹니다`,
|
|
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;
|
|
@@ -225,6 +241,7 @@ const ignore_1 = require("../project/ignore");
|
|
|
225
241
|
// @implements A-SPEC-126
|
|
226
242
|
const scan_1 = require("../reverse/scan");
|
|
227
243
|
const draft_1 = require("../reverse/draft");
|
|
244
|
+
const adr_refs_1 = require("../spec/adr-refs");
|
|
228
245
|
const spec_id_guard_1 = require("./spec-id-guard");
|
|
229
246
|
const anchor_1 = require("../reverse/anchor");
|
|
230
247
|
// @implements A-SPEC-128
|
|
@@ -677,10 +694,10 @@ function collectDecisions(root, scanned, specs) {
|
|
|
677
694
|
return { decisions: [], citations: [] };
|
|
678
695
|
}
|
|
679
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.
|
|
680
698
|
const cite = (text, from, out) => {
|
|
681
|
-
for (const
|
|
682
|
-
|
|
683
|
-
out.push({ from, adr: m[0] });
|
|
699
|
+
for (const adr of (0, adr_refs_1.adrRefsIn)(text, ids))
|
|
700
|
+
out.push({ from, adr });
|
|
684
701
|
};
|
|
685
702
|
const citations = [];
|
|
686
703
|
for (const f of scanned) {
|
|
@@ -1322,6 +1339,24 @@ function makeRawHandlers(store, opts) {
|
|
|
1322
1339
|
const events = fs.existsSync(dir) ? new ledger_store_1.FileLedgerStore(dir).loadAll() : [];
|
|
1323
1340
|
return { ok: true, events: (0, ledger_timeline_1.timelineFrom)(events, a.id) };
|
|
1324
1341
|
},
|
|
1342
|
+
// @implements A-SPEC-545.3 — "show me the RTM dashboard" launches (idempotently) the server the
|
|
1343
|
+
// agent would otherwise start by hand, and returns its URL plus an honesty census of what it shows.
|
|
1344
|
+
async rtm_dashboard(a) {
|
|
1345
|
+
const dest = resolveLedgerRoot(a.root);
|
|
1346
|
+
if (!dest.ok)
|
|
1347
|
+
return { ok: false, reason: dest.reason };
|
|
1348
|
+
try {
|
|
1349
|
+
const { startDashboardServer } = await Promise.resolve().then(() => __importStar(require('../server/dashboard')));
|
|
1350
|
+
const { ensureDashboard, dashboardCensus } = await Promise.resolve().then(() => __importStar(require('../server/dashboard-launcher')));
|
|
1351
|
+
const launch = await ensureDashboard(dest.root, a.port, (opts) => startDashboardServer(opts));
|
|
1352
|
+
const rtm = await fetchJson(`${launch.url}/api/rtm`);
|
|
1353
|
+
const heatmap = await fetchJson(`${launch.url}/api/rtm/heatmap`);
|
|
1354
|
+
return { ok: true, url: launch.url, running: launch.running, census: dashboardCensus(rtm, heatmap) };
|
|
1355
|
+
}
|
|
1356
|
+
catch (err) {
|
|
1357
|
+
return { ok: false, reason: `대시보드 기동 실패: ${err?.message ?? String(err)}` };
|
|
1358
|
+
}
|
|
1359
|
+
},
|
|
1325
1360
|
async spec_approve(a) {
|
|
1326
1361
|
const approvalRaw = process.env.HOLMES_APPROVAL;
|
|
1327
1362
|
let approval;
|
|
@@ -3214,13 +3249,10 @@ function makeRawHandlers(store, opts) {
|
|
|
3214
3249
|
const root = a.root ? (0, root_2.resolveProjectRoot)(a.root).root : process.cwd();
|
|
3215
3250
|
const specsDir = path.join(root, '.ax', 'specs');
|
|
3216
3251
|
const existing = await store.list();
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
maxId = num;
|
|
3222
|
-
}
|
|
3223
|
-
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));
|
|
3224
3256
|
const reqId = `REQ-${nextId}`;
|
|
3225
3257
|
const hspecId = `H-SPEC-${nextId}`;
|
|
3226
3258
|
const aspecId = `A-SPEC-${nextId}`;
|
|
@@ -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,69 @@
|
|
|
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
|
+
};
|
|
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;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CFG_ROW_H = exports.CFG_COL_W = void 0;
|
|
4
|
+
exports.lineAtByte = lineAtByte;
|
|
5
|
+
exports.buildCfgView = buildCfgView;
|
|
6
|
+
exports.layoutCfg = layoutCfg;
|
|
7
|
+
/**
|
|
8
|
+
* @implements A-SPEC-545.4
|
|
9
|
+
* Pure: 1-based line number of a byte offset (count of newlines in [0, offset) + 1). Clamped.
|
|
10
|
+
*/
|
|
11
|
+
function lineAtByte(source, offset) {
|
|
12
|
+
const at = Math.max(0, Math.min(offset, source.length));
|
|
13
|
+
let line = 1;
|
|
14
|
+
for (let i = 0; i < at; i++)
|
|
15
|
+
if (source.charCodeAt(i) === 10)
|
|
16
|
+
line++;
|
|
17
|
+
return line;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* @implements A-SPEC-545.4
|
|
21
|
+
* Pure: fold a foundation Cfg + Pdg into a display-ready graph — block labels/lines, control-flow
|
|
22
|
+
* edges, block-level control deps, and reaching defs projected to block pairs (deduped, no self-edge).
|
|
23
|
+
*/
|
|
24
|
+
function buildCfgView(cfg, pdg, ast, source) {
|
|
25
|
+
if ('unsupported' in cfg)
|
|
26
|
+
return cfg;
|
|
27
|
+
if ('unsupported' in pdg)
|
|
28
|
+
return pdg;
|
|
29
|
+
// stmt node index → owning block id
|
|
30
|
+
const blockOf = new Map();
|
|
31
|
+
for (const b of cfg.blocks)
|
|
32
|
+
for (const s of b.stmts)
|
|
33
|
+
blockOf.set(s, b.id);
|
|
34
|
+
const unreachable = new Set(cfg.unreachable);
|
|
35
|
+
const blocks = cfg.blocks.map((b) => {
|
|
36
|
+
const first = b.stmts.length ? b.stmts[0] : -1;
|
|
37
|
+
const node = first >= 0 ? ast.nodes[first] : undefined;
|
|
38
|
+
const line = node ? lineAtByte(source, node.start) : 0;
|
|
39
|
+
const raw = node ? source.slice(node.start, node.end) : '';
|
|
40
|
+
const label = raw.split('\n', 1)[0].slice(0, 80);
|
|
41
|
+
return { id: b.id, line, label, unreachable: unreachable.has(b.id) };
|
|
42
|
+
});
|
|
43
|
+
const edges = cfg.edges.map((e) => ({ from: e.from, to: e.to, kind: e.kind }));
|
|
44
|
+
const ctrlDeps = pdg.controlDeps.map((c) => ({ ctrl: c.ctrl, dep: c.dep }));
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const dataDeps = [];
|
|
47
|
+
for (const d of pdg.reachingDefs) {
|
|
48
|
+
const from = blockOf.get(d.defStmt);
|
|
49
|
+
const to = blockOf.get(d.useStmt);
|
|
50
|
+
if (from === undefined || to === undefined || from === to)
|
|
51
|
+
continue; // no self-edge / unresolved
|
|
52
|
+
const key = `${from}>${to}:${d.name}`;
|
|
53
|
+
if (seen.has(key))
|
|
54
|
+
continue;
|
|
55
|
+
seen.add(key);
|
|
56
|
+
dataDeps.push({ from, to, name: d.name });
|
|
57
|
+
}
|
|
58
|
+
return { entry: cfg.entry, exit: cfg.exit, blocks, edges, ctrlDeps, dataDeps };
|
|
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
|
+
}
|
|
@@ -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,80 @@ 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
|
+
// @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).
|
|
276
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
277
|
+
res.end(JSON.stringify({ ok: true, file, symbol, lang, cfg: view, layout: (0, cfg_view_1.layoutCfg)(view) }));
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
if (!res.headersSent) {
|
|
281
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
282
|
+
res.end(JSON.stringify({ ok: false, error: 'Internal Server Error' }));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
205
287
|
if (parsedUrl === '/api/cpg') {
|
|
206
288
|
try {
|
|
207
289
|
const files = scanner.scan(root);
|
|
@@ -388,7 +470,57 @@ function renderDashboardHtml() {
|
|
|
388
470
|
<title>Holmes-Kit World Top-Tier Quantitative RTM Matrix & AST/CPG Graph Canvas</title>
|
|
389
471
|
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
|
390
472
|
<style>
|
|
391
|
-
|
|
473
|
+
/* @implements A-SPEC-545.2 — design tokens: sequential coverage ramp, status, surfaces, 2 fonts.
|
|
474
|
+
Dark-first; light palette under prefers-color-scheme. The green ramp reads "traced". */
|
|
475
|
+
:root {
|
|
476
|
+
--surface-0: #0d1417; --surface-1: #131e22; --surface-2: #1b2b30; --border: #26383d;
|
|
477
|
+
--ink-0: #eaf2f0; --ink-1: #9fb2b0; --ink-2: #6b807e;
|
|
478
|
+
--ramp-0: #17211d; --ramp-1: #1f3a2c; --ramp-2: #26543a; --ramp-3: #2c7449; --ramp-4: #33a35f; --ramp-5: #46d97e;
|
|
479
|
+
--ramp-ink-lo: #8ba39c; --ramp-ink-hi: #06120c;
|
|
480
|
+
--ok: #46d97e; --warn: #e0a83c; --bad: #e5595c; --accent: #46b0d9;
|
|
481
|
+
--font-ui: ui-sans-serif, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
482
|
+
--font-mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;
|
|
483
|
+
}
|
|
484
|
+
@media (prefers-color-scheme: light) {
|
|
485
|
+
:root {
|
|
486
|
+
--surface-0: #f4f7f6; --surface-1: #ffffff; --surface-2: #eaf0ee; --border: #d3ddda;
|
|
487
|
+
--ink-0: #12201c; --ink-1: #4a5c58; --ink-2: #7b8d89;
|
|
488
|
+
--ramp-0: #e7efe9; --ramp-1: #c3e0cd; --ramp-2: #8fc9a5; --ramp-3: #54ac77; --ramp-4: #2f8a54; --ramp-5: #166b3c;
|
|
489
|
+
--ramp-ink-lo: #5b6f69; --ramp-ink-hi: #f4faf6;
|
|
490
|
+
--ok: #166b3c; --warn: #b7791f; --bad: #c53539; --accent: #1f7fa6;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
body { font-family: var(--font-ui); background-color: var(--surface-0); color: var(--ink-0); margin: 0; padding: 24px; user-select: none; }
|
|
494
|
+
|
|
495
|
+
/* Seriated REQ × stage coverage matrix (2D grid) */
|
|
496
|
+
.hm-toolbar { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; margin-bottom: 12px; color: var(--ink-1); font-size: 12px; }
|
|
497
|
+
.hm-legend { display: inline-flex; align-items: center; gap: 6px; }
|
|
498
|
+
.hm-legend-swatch { width: 16px; height: 12px; border-radius: 2px; display: inline-block; border: 1px solid var(--border); }
|
|
499
|
+
.hm-scroll { max-height: 74vh; overflow: auto; border: 1px solid var(--border); border-radius: 12px; background: var(--surface-1); }
|
|
500
|
+
table.hm-grid { border-collapse: separate; border-spacing: 0; width: 100%; font-family: var(--font-mono); }
|
|
501
|
+
table.hm-grid th, table.hm-grid td { border-bottom: 1px solid var(--surface-0); border-right: 1px solid var(--surface-0); }
|
|
502
|
+
table.hm-grid thead th { position: sticky; top: 0; z-index: 3; background: var(--surface-2); color: var(--ink-1);
|
|
503
|
+
font-family: var(--font-ui); font-size: 10px; font-weight: 700; letter-spacing: 0.6px; text-transform: uppercase;
|
|
504
|
+
padding: 10px 8px; text-align: center; white-space: nowrap; }
|
|
505
|
+
table.hm-grid thead th.hm-corner { left: 0; z-index: 4; text-align: left; }
|
|
506
|
+
table.hm-grid td.hm-rowlabel, table.hm-grid th.hm-corner { position: sticky; left: 0; z-index: 2; background: var(--surface-1);
|
|
507
|
+
min-width: 260px; max-width: 320px; padding: 7px 12px; border-right: 1px solid var(--border); }
|
|
508
|
+
.hm-req-id { font-family: var(--font-mono); font-weight: 700; color: var(--ink-0); font-size: 12px; }
|
|
509
|
+
.hm-req-title { font-family: var(--font-ui); color: var(--ink-2); font-size: 11px; margin-left: 8px;
|
|
510
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
511
|
+
.hm-rowlabel-flex { display: flex; align-items: baseline; overflow: hidden; }
|
|
512
|
+
table.hm-grid td.hm-cell { text-align: center; font-variant-numeric: tabular-nums; font-size: 11px; padding: 7px 6px;
|
|
513
|
+
color: var(--ramp-ink-hi); cursor: default; min-width: 62px; }
|
|
514
|
+
table.hm-grid tbody tr:hover td.hm-cell { outline: 1px solid var(--accent); outline-offset: -2px; }
|
|
515
|
+
.hm-b0 { background: var(--ramp-0); color: var(--ramp-ink-lo); }
|
|
516
|
+
.hm-b1 { background: var(--ramp-1); color: var(--ramp-ink-lo); }
|
|
517
|
+
.hm-b2 { background: var(--ramp-2); color: var(--ramp-ink-hi); }
|
|
518
|
+
.hm-b3 { background: var(--ramp-3); color: var(--ramp-ink-hi); }
|
|
519
|
+
.hm-b4 { background: var(--ramp-4); color: var(--ramp-ink-hi); }
|
|
520
|
+
.hm-b5 { background: var(--ramp-5); color: var(--ramp-ink-hi); }
|
|
521
|
+
.hm-summary-pill { font-family: var(--font-ui); font-weight: 700; font-size: 11px; padding: 2px 8px; border-radius: 999px; }
|
|
522
|
+
.hm-pill-ok { background: color-mix(in srgb, var(--ok) 22%, transparent); color: var(--ok); border: 1px solid var(--ok); }
|
|
523
|
+
.hm-pill-warn { background: color-mix(in srgb, var(--warn) 22%, transparent); color: var(--warn); border: 1px solid var(--warn); }
|
|
392
524
|
.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
525
|
.nav-tabs { display: flex; gap: 12px; margin-bottom: 20px; }
|
|
394
526
|
.tab-btn { background: #1e293b; color: #94a3b8; border: 1px solid #334155; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-weight: bold; }
|
|
@@ -524,6 +656,20 @@ function renderDashboardHtml() {
|
|
|
524
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; }
|
|
525
657
|
.tool-btn:hover { background: #3b82f6; border-color: #3b82f6; color: #fff; transform: scale(1.05); }
|
|
526
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; }
|
|
527
673
|
</style>
|
|
528
674
|
</head>
|
|
529
675
|
<body>
|
|
@@ -541,10 +687,10 @@ function renderDashboardHtml() {
|
|
|
541
687
|
</div>
|
|
542
688
|
|
|
543
689
|
<div class="nav-tabs">
|
|
544
|
-
<button id="tabRtmBtn" class="tab-btn active" onclick="switchTab('rtm')"
|
|
545
|
-
<button id="tabHeatmapBtn" class="tab-btn" onclick="switchTab('heatmap')"
|
|
546
|
-
<button id="tabGraphBtn" class="tab-btn" onclick="switchTab('graph')"
|
|
547
|
-
<button id="tabLegacyBtn" class="tab-btn" onclick="switchTab('legacy')"
|
|
690
|
+
<button id="tabRtmBtn" class="tab-btn active" onclick="switchTab('rtm')">Quantitative REQ RTM</button>
|
|
691
|
+
<button id="tabHeatmapBtn" class="tab-btn" onclick="switchTab('heatmap')">Coverage Matrix</button>
|
|
692
|
+
<button id="tabGraphBtn" class="tab-btn" onclick="switchTab('graph')">Pipeline Canvas</button>
|
|
693
|
+
<button id="tabLegacyBtn" class="tab-btn" onclick="switchTab('legacy')">Unmapped Specs</button>
|
|
548
694
|
</div>
|
|
549
695
|
|
|
550
696
|
<div id="rtmTab">
|
|
@@ -570,30 +716,28 @@ function renderDashboardHtml() {
|
|
|
570
716
|
</div>
|
|
571
717
|
|
|
572
718
|
<div id="heatmapTab" style="display: none;">
|
|
573
|
-
<div class="
|
|
574
|
-
<
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
<
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
</
|
|
593
|
-
</div>
|
|
594
|
-
<div class="heatmap-wrapper">
|
|
595
|
-
<div id="heatmapContainer">Loading REQ-grouped 6-stage pipeline heatmap...</div>
|
|
719
|
+
<div class="hm-toolbar">
|
|
720
|
+
<span style="font-family: var(--font-ui); font-weight: 700; color: var(--ink-0); font-size: 13px;">Requirement × Pipeline-Stage Coverage</span>
|
|
721
|
+
<span style="color: var(--ink-2);">rows seriated by completeness · cell = share of a REQ's chains reaching that stage</span>
|
|
722
|
+
<span style="display: inline-flex; align-items: center; gap: 8px;">
|
|
723
|
+
<label for="pipelineFilterSelect" style="color: var(--ink-1);">Rows</label>
|
|
724
|
+
<select id="pipelineFilterSelect" class="select-box" onchange="changeHeatmapLens()">
|
|
725
|
+
<option value="all" selected>All requirements</option>
|
|
726
|
+
<option value="uncovered">Incomplete only</option>
|
|
727
|
+
</select>
|
|
728
|
+
</span>
|
|
729
|
+
<span class="hm-legend">
|
|
730
|
+
<span style="color: var(--ink-2);">0%</span>
|
|
731
|
+
<span class="hm-legend-swatch" style="background: var(--ramp-0);"></span>
|
|
732
|
+
<span class="hm-legend-swatch" style="background: var(--ramp-1);"></span>
|
|
733
|
+
<span class="hm-legend-swatch" style="background: var(--ramp-2);"></span>
|
|
734
|
+
<span class="hm-legend-swatch" style="background: var(--ramp-3);"></span>
|
|
735
|
+
<span class="hm-legend-swatch" style="background: var(--ramp-4);"></span>
|
|
736
|
+
<span class="hm-legend-swatch" style="background: var(--ramp-5);"></span>
|
|
737
|
+
<span style="color: var(--ink-2);">100%</span>
|
|
738
|
+
</span>
|
|
596
739
|
</div>
|
|
740
|
+
<div id="heatmapContainer">Loading coverage matrix…</div>
|
|
597
741
|
</div>
|
|
598
742
|
|
|
599
743
|
<div id="graphTab" style="display: none;">
|
|
@@ -844,144 +988,70 @@ function renderDashboardHtml() {
|
|
|
844
988
|
}
|
|
845
989
|
}
|
|
846
990
|
|
|
847
|
-
//
|
|
848
|
-
//
|
|
849
|
-
|
|
850
|
-
|
|
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
|
|
991
|
+
// @implements A-SPEC-545.2 — seriated REQ × stage coverage matrix (2D grid), painted from the
|
|
992
|
+
// server-built data.matrix. Rows are ordered by matrix.order (completeness), each cell coloured
|
|
993
|
+
// by its ramp bucket with the coverage percent shown inline (tabular-nums), so magnitude never
|
|
994
|
+
// relies on colour alone.
|
|
858
995
|
function render6StagePipelineMatrix(data) {
|
|
859
996
|
const container = document.getElementById('heatmapContainer');
|
|
860
|
-
const
|
|
861
|
-
const
|
|
862
|
-
const statusFilter =
|
|
997
|
+
const matrix = data.matrix || { reqs: [], stages: [], values: [], order: [], buckets: [] };
|
|
998
|
+
const filterEl = document.getElementById('pipelineFilterSelect');
|
|
999
|
+
const statusFilter = filterEl ? (filterEl.value || 'all') : 'all';
|
|
863
1000
|
|
|
864
|
-
if (
|
|
865
|
-
container.innerHTML = '<div style="color
|
|
1001
|
+
if (!matrix.reqs || matrix.reqs.length === 0) {
|
|
1002
|
+
container.innerHTML = '<div style="color: var(--ink-1); font-size: 14px; padding: 16px;">No pipeline trace chains found.</div>';
|
|
866
1003
|
return;
|
|
867
1004
|
}
|
|
868
1005
|
|
|
869
|
-
|
|
870
|
-
let
|
|
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
|
-
}
|
|
1006
|
+
const isRowComplete = (i) => (matrix.values[i] || []).every(v => v >= 1);
|
|
1007
|
+
let rowIdx = (matrix.order && matrix.order.length ? matrix.order : matrix.reqs.map((_, i) => i)).slice();
|
|
1008
|
+
if (statusFilter === 'uncovered') rowIdx = rowIdx.filter(i => !isRowComplete(i));
|
|
876
1009
|
|
|
877
|
-
|
|
878
|
-
const
|
|
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
|
-
});
|
|
1010
|
+
const fullyTraced = matrix.reqs.filter((_, i) => isRowComplete(i)).length;
|
|
1011
|
+
const summaryCls = fullyTraced === matrix.reqs.length ? 'hm-pill-ok' : 'hm-pill-warn';
|
|
885
1012
|
|
|
886
|
-
if (
|
|
887
|
-
|
|
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>';
|
|
1013
|
+
if (rowIdx.length === 0) {
|
|
1014
|
+
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
1015
|
return;
|
|
892
1016
|
}
|
|
893
1017
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
const
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
<span class="req-id-badge">\${escapeHtml(reqId)}</span>
|
|
915
|
-
<span class="req-title">\${escapeHtml(group.reqTitle || 'Business Requirement Pipeline')}</span>
|
|
916
|
-
\${reqStatusBadge}
|
|
1018
|
+
const pct = (v) => Math.round((v || 0) * 100);
|
|
1019
|
+
const headCells = matrix.stages.map(s => \`<th>\${escapeHtml(s)}</th>\`).join('');
|
|
1020
|
+
|
|
1021
|
+
let rows = '';
|
|
1022
|
+
rowIdx.forEach(i => {
|
|
1023
|
+
const req = matrix.reqs[i];
|
|
1024
|
+
const vals = matrix.values[i] || [];
|
|
1025
|
+
const bkts = matrix.buckets[i] || [];
|
|
1026
|
+
const cells = matrix.stages.map((stage, s) => {
|
|
1027
|
+
const v = vals[s] || 0;
|
|
1028
|
+
const b = bkts[s] || 0;
|
|
1029
|
+
const title = escapeHtml(req.id) + ' · ' + escapeHtml(stage) + ': ' + pct(v) + '% of chains reached';
|
|
1030
|
+
return \`<td class="hm-cell hm-b\${b}" title="\${title}">\${pct(v)}</td>\`;
|
|
1031
|
+
}).join('');
|
|
1032
|
+
rows += \`
|
|
1033
|
+
<tr>
|
|
1034
|
+
<td class="hm-rowlabel" onclick="focusSpecInGraph('\${escapeHtml(req.id)}')" title="\${escapeHtml(req.title || '')}">
|
|
1035
|
+
<div class="hm-rowlabel-flex">
|
|
1036
|
+
<span class="hm-req-id">\${escapeHtml(req.id)}</span>
|
|
1037
|
+
<span class="hm-req-title">\${escapeHtml(req.title || '')}</span>
|
|
917
1038
|
</div>
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
</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
|
-
\`;
|
|
1039
|
+
</td>
|
|
1040
|
+
\${cells}
|
|
1041
|
+
</tr>\`;
|
|
982
1042
|
});
|
|
983
1043
|
|
|
984
|
-
container.innerHTML =
|
|
1044
|
+
container.innerHTML = \`
|
|
1045
|
+
<div style="margin-bottom: 10px;">
|
|
1046
|
+
<span class="hm-summary-pill \${summaryCls}">\${fullyTraced} / \${matrix.reqs.length} requirements fully traced</span>
|
|
1047
|
+
<span style="color: var(--ink-2); font-size: 12px; margin-left: 10px;">showing \${rowIdx.length} row\${rowIdx.length === 1 ? '' : 's'}</span>
|
|
1048
|
+
</div>
|
|
1049
|
+
<div class="hm-scroll">
|
|
1050
|
+
<table class="hm-grid">
|
|
1051
|
+
<thead><tr><th class="hm-corner">Requirement</th>\${headCells}</tr></thead>
|
|
1052
|
+
<tbody>\${rows}</tbody>
|
|
1053
|
+
</table>
|
|
1054
|
+
</div>\`;
|
|
985
1055
|
}
|
|
986
1056
|
|
|
987
1057
|
// World Top-Tier Quantitative REQ-Centric RTM Traceability Matrix Grid Renderer
|
|
@@ -1114,12 +1184,84 @@ function renderDashboardHtml() {
|
|
|
1114
1184
|
|
|
1115
1185
|
async function focusSpecInGraph(specId) {
|
|
1116
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
|
+
}
|
|
1117
1201
|
if (!graphData) {
|
|
1118
1202
|
await loadGraphCanvas();
|
|
1119
1203
|
}
|
|
1120
1204
|
selectAndTraverseNode(specId);
|
|
1121
1205
|
}
|
|
1122
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
|
+
|
|
1123
1265
|
document.getElementById('searchInput').addEventListener('input', (e) => {
|
|
1124
1266
|
const q = e.target.value.toLowerCase();
|
|
1125
1267
|
const filtered = allSpecs.filter(s => s.id.toLowerCase().includes(q) || (s.title && s.title.toLowerCase().includes(q)));
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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:
|
|
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.
|
|
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"
|