@holmes-lab/holmes-kit 0.10.1 → 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.
@@ -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,10 @@
1
+ /**
2
+ * @implements A-SPEC-542.1
3
+ * The npx binary an OPERATOR can actually run on the platform this process runs on — which is the
4
+ * platform of the machine whose terminal reads the emitted hint. Field-measured 2026-09-05 (codex on
5
+ * Windows): a locked-down PowerShell execution policy blocks the `npx` call because the `.ps1` shim
6
+ * is resolved first (PSSecurityException); `npx.cmd` runs. Everywhere else `npx` is correct, so every
7
+ * non-win32 emission stays byte-identical. Only the exact 'win32' token switches — a typo or unknown
8
+ * platform must never flip a POSIX hint to a Windows-only form.
9
+ */
10
+ export declare function npxBin(platform?: string): string;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ // @implements A-SPEC-542.1
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.npxBin = npxBin;
5
+ /**
6
+ * @implements A-SPEC-542.1
7
+ * The npx binary an OPERATOR can actually run on the platform this process runs on — which is the
8
+ * platform of the machine whose terminal reads the emitted hint. Field-measured 2026-09-05 (codex on
9
+ * Windows): a locked-down PowerShell execution policy blocks the `npx` call because the `.ps1` shim
10
+ * is resolved first (PSSecurityException); `npx.cmd` runs. Everywhere else `npx` is correct, so every
11
+ * non-win32 emission stays byte-identical. Only the exact 'win32' token switches — a typo or unknown
12
+ * platform must never flip a POSIX hint to a Windows-only form.
13
+ */
14
+ function npxBin(platform = process.platform) {
15
+ return platform === 'win32' ? 'npx.cmd' : 'npx';
16
+ }
@@ -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
+ }