@holmes-lab/holmes-kit 0.1.4 → 0.1.6
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 +18 -0
- package/README.md +40 -1
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/ci-gate.js +103 -0
- package/dist/holmes/cli/doctor.js +19 -1
- package/dist/holmes/cli/index.js +35 -0
- package/dist/holmes/cpg/cpg-scanner.js +3 -1
- package/dist/holmes/cpg/scan-cache.js +40 -8
- package/dist/holmes/governance/provenance-ledger.js +15 -14
- package/dist/holmes/rtm/heatmap.js +69 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
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
|
+
<!-- @implements A-SPEC-209 -->
|
|
8
|
+
## [0.1.6] - 2026-08-19
|
|
9
|
+
|
|
10
|
+
### Documentation
|
|
11
|
+
- **Current Production Features vs. Vision & Roadmap Distinction**: Explicitly separated currently supported production features in `v0.1.x` from future roadmap goals (Jira/Linear tool connectors, multi-repo polyrepo governance `REQ-211`, external SAST bridges) in `README.md` to avoid feature expectation confusion.
|
|
12
|
+
|
|
13
|
+
## [0.1.5] - 2026-08-19
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **Subprocess Secret Isolation (REQ-205)**: Standardized `cleanSubprocessEnv` to scrub sensitive environment variables (`HOLMES_ROLE`, `HOLMES_ENV_CHANNEL`, `HOLMES_APPROVAL`, `HOLMES_LEDGER_KEY`) across all child process executions.
|
|
17
|
+
- **Multi-Ledger Dynamic Indexing Engine (REQ-206)**: High-performance mtime/size-based indexing cache with immutable deep cloning (`cloneEvents`) and fail-closed error handling in `ProvenanceLedger`.
|
|
18
|
+
- **Doctor Self-Healing Engine (REQ-207)**: Integrated `runDoctor({ autoFix: true })` auto-remediation loop and `Check.remediate()` callbacks for system diagnostic repair.
|
|
19
|
+
- **CPG Incremental Indexer (REQ-208)**: SHA-256 content-hash AST parsing skip in `ScanFileCache` & `CpgScanner` with scoped directory mark-and-sweep pruning and deep `DataFlowFacts` cloning.
|
|
20
|
+
- **Automated RTM & Security Taint Heatmap Generator (REQ-212)**: Self-contained interactive HTML/SVG report builder (`generateRtmHeatmap`) for spec chain coverage and security taint reachability.
|
|
21
|
+
- **CI/CD Governance Gate Runner (REQ-213)**: Non-interactive headless CI runner (`holmes-kit ci`) for GitHub Actions / GitLab CI with path traversal protection and gitignore filtering.
|
|
22
|
+
|
|
23
|
+
### Documentation
|
|
24
|
+
- **Sherlock Holmes Vision & Philosophy**: Updated `README.md` to detail the Sherlock Holmes deduction, defect analysis, full SDLC coverage, enterprise tool harness vision, and supported AI coding tools matrix.
|
|
7
25
|
|
|
8
26
|
## [0.1.4] - 2026-08-18
|
|
9
27
|
|
package/README.md
CHANGED
|
@@ -6,7 +6,46 @@
|
|
|
6
6
|
|
|
7
7
|
> **"No Spec, No Code"** — Deterministic Agentic Software Engineering (ASE) harness with causal traceability.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 🕵️ Philosophy & Vision: Beyond Code Generation
|
|
12
|
+
|
|
13
|
+
**Holmes-Kit** is not merely an AI code generator. Inspired by **Sherlock Holmes**, Holmes-Kit is built to **deduce, track down, and analyze code defects, structural drift, and security vulnerabilities** across the full Software Development Life Cycle (**SDLC**).
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
### 🛡️ Currently Supported Features (v0.1.x Production Features)
|
|
18
|
+
|
|
19
|
+
- 📋 **Requirements & Specification Governance**: Strict **"No Spec, No Code"** enforcement with 4-tier spec chain traceability (`REQ ➔ H-SPEC ➔ A-SPEC ➔ T-SPEC`) and `// @implements A-SPEC-XXX` line 1 code anchors.
|
|
20
|
+
- 🐞 **Causal Defect Localization & CPG**: AST Code Property Graph (CPG) & Dataflow Taint reachability analysis across 7 languages (TS/JS, Python, Go, Rust, Java, C/C++, C#).
|
|
21
|
+
- 🧪 **Self-Healing & Diagnostic Doctor**: Automated integrity checks and self-healing auto-fix remediation (`holmes-kit doctor --fix` & `spec_remediate`).
|
|
22
|
+
- 🚦 **CI/CD Governance Gate Runner**: Non-interactive headless CI/CD build gate (`holmes-kit ci`) for GitHub Actions and GitLab CI pipelines.
|
|
23
|
+
- 📊 **Automated RTM & Taint Heatmap**: Interactive standalone HTML/SVG report generation (`generateRtmHeatmap`) for spec coverage and security dataflow reachability.
|
|
24
|
+
- 🤖 **CLI-First AI Harness Matrix**: Native process hook gating for Claude Code, Antigravity CLI (AGY), Codex CLI, and Google Antigravity SDK.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
### 🚀 Full SDLC Vision & Roadmap (Future Goals)
|
|
29
|
+
|
|
30
|
+
Holmes-Kit strives to expand into a unified enterprise SDLC harness bridging external development tools:
|
|
31
|
+
- 🔌 **Enterprise Issue Tracker Connectors**: Flexible synchronization with Jira, GitHub Issues, Linear, and Notion.
|
|
32
|
+
- 🌐 **Multi-Repository Polyrepo Governance**: Multi-repo spec chain synchronization and cross-repository dependency impact analysis (`REQ-211`).
|
|
33
|
+
- 🛡️ **External SAST & Linter Bridge**: Deep integration with SonarQube, Semgrep, and ESLint findings.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 🤖 Supported AI Coding Tools & Environments (CLI-First Focus)
|
|
38
|
+
|
|
39
|
+
Holmes-Kit prioritizes **CLI-based AI Coding Agents** where OS-level process hooks, subshell isolation, and deterministic control plane enforcement are natively guaranteed:
|
|
40
|
+
|
|
41
|
+
| AI Coding Agent / Framework | Harness Tier | Integration & Enforcement Mechanisms |
|
|
42
|
+
| :--- | :--- | :--- |
|
|
43
|
+
| 🤖 **Claude Code CLI** | 🥇 Tier 1 (Native) | OS PreToolUse & Stop hooks (`.claude/settings.local.json`), MCP server (`.mcp.json`) |
|
|
44
|
+
| 🚀 **Antigravity CLI (AGY)** | 🥇 Tier 1 (Native) | AGY Hooks (`hooks.json`), MCP config (`.agents/mcp_config.json`), Governance Skills |
|
|
45
|
+
| 💻 **Codex CLI / Agentic Shell** | 🥇 Tier 1 (Native) | Codex MCP integration (`.codex/mcp_config.json`), Subshell Isolation Gates |
|
|
46
|
+
| 🧩 **Google Antigravity SDK** | 🥇 Tier 1 (Native) | Autonomous Agent SDK bindings and cryptographic provenance verification |
|
|
47
|
+
|
|
48
|
+
> **Note**: Holmes-Kit focuses strictly on CLI-based autonomous agents to guarantee 100% deterministic OS hook gating (`deny` enforcement) before file modifications occur.
|
|
10
49
|
|
|
11
50
|
---
|
|
12
51
|
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
9337c9a-msytbtde
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runCiGate = runCiGate;
|
|
37
|
+
// @implements A-SPEC-213
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const cpg_scanner_1 = require("../cpg/cpg-scanner");
|
|
41
|
+
const pre_tool_use_1 = require("../hooks/pre-tool-use");
|
|
42
|
+
const ignore_1 = require("../project/ignore");
|
|
43
|
+
/**
|
|
44
|
+
* Headless CI/CD Governance Gate Runner for GitHub Actions / GitLab CI.
|
|
45
|
+
*
|
|
46
|
+
* @implements A-SPEC-213
|
|
47
|
+
*/
|
|
48
|
+
function runCiGate(targetDir, options) {
|
|
49
|
+
const root = path.resolve(targetDir);
|
|
50
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
violations: [{ reason: `Target directory does not exist or is not a directory: ${root}` }],
|
|
54
|
+
summary: `[Holmes-Kit CI Gate Refusal] Target directory is invalid — ${root}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const specsDirName = options?.specsDir ?? (0, pre_tool_use_1.wiredSpecsDir)(process.argv, process.env);
|
|
58
|
+
const specsDir = path.resolve(root, specsDirName);
|
|
59
|
+
if (!(0, pre_tool_use_1.isGovernedProject)(specsDir, process.env)) {
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
violations: [],
|
|
63
|
+
summary: `[Holmes-Kit CI Gate] Project is not governed (${specsDirName} absent) — skipping CI gate checks.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const ignore = (0, ignore_1.loadIgnore)(root);
|
|
67
|
+
const scanner = new cpg_scanner_1.CpgScanner();
|
|
68
|
+
const scannedFiles = scanner.scan(root, root);
|
|
69
|
+
const violations = [];
|
|
70
|
+
for (const sf of scannedFiles) {
|
|
71
|
+
const rel = path.relative(root, sf.path).split(path.sep).join('/');
|
|
72
|
+
if (ignore.isIgnored(rel))
|
|
73
|
+
continue;
|
|
74
|
+
// Check if scannable source file has no approved implements anchor
|
|
75
|
+
if (sf.implementsSpecs.length === 0) {
|
|
76
|
+
violations.push({
|
|
77
|
+
file: rel,
|
|
78
|
+
reason: `Missing line 1 // @implements anchor comment`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (sf.unanchoredImplements.length > 0) {
|
|
82
|
+
for (const wrongKind of sf.unanchoredImplements) {
|
|
83
|
+
violations.push({
|
|
84
|
+
file: rel,
|
|
85
|
+
specId: wrongKind,
|
|
86
|
+
reason: `Invalid anchor kind @implements ${wrongKind} — only A-SPEC anchors are valid code implementation anchors`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (violations.length > 0) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
violations,
|
|
95
|
+
summary: `[Holmes-Kit CI Gate Refusal] Governance Violations Detected — ${violations.length} un-anchored file(s) or invalid spec anchors found in PR commit range.`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
ok: true,
|
|
100
|
+
violations: [],
|
|
101
|
+
summary: `[Holmes-Kit CI Gate Success] 100% Governance Conformance — All scanned files carry valid approved spec anchors.`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -37,6 +37,7 @@ exports.STRIPPED_FOR_PROBE = void 0;
|
|
|
37
37
|
exports.probeEnv = probeEnv;
|
|
38
38
|
exports.runDoctor = runDoctor;
|
|
39
39
|
exports.formatChecks = formatChecks;
|
|
40
|
+
// @implements A-SPEC-207
|
|
40
41
|
// @implements A-SPEC-100.2
|
|
41
42
|
const fs = __importStar(require("node:fs"));
|
|
42
43
|
const path = __importStar(require("node:path"));
|
|
@@ -95,7 +96,7 @@ function wiredSettingsPath(target) {
|
|
|
95
96
|
return (0, init_1.settingsPathOf)(target, 'project');
|
|
96
97
|
return (0, init_1.settingsPathOf)(target, fs.existsSync((0, init_1.settingsPathOf)(target, 'local')) ? 'local' : 'project');
|
|
97
98
|
}
|
|
98
|
-
async function runDoctor(packageRoot, target, opts) {
|
|
99
|
+
async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
99
100
|
const checks = [];
|
|
100
101
|
const add = (name, level, detail, fix) => checks.push({ name, level, detail, fix });
|
|
101
102
|
// 1. Build output — also the "prepare didn't run" detector.
|
|
@@ -451,6 +452,23 @@ async function runDoctor(packageRoot, target, opts) {
|
|
|
451
452
|
}
|
|
452
453
|
add('gate blind spots', 'PASS', (0, blind_spots_1.blindSpotSummary)());
|
|
453
454
|
add('environment', 'PASS', present);
|
|
455
|
+
if (extraChecks) {
|
|
456
|
+
checks.push(...extraChecks);
|
|
457
|
+
}
|
|
458
|
+
if (opts?.autoFix) {
|
|
459
|
+
for (const c of checks) {
|
|
460
|
+
if ((c.level === 'FAIL' || c.level === 'WARN') && c.remediate) {
|
|
461
|
+
try {
|
|
462
|
+
const ok = c.remediate();
|
|
463
|
+
if (ok) {
|
|
464
|
+
c.level = 'PASS';
|
|
465
|
+
c.detail = `[REPAIRED] ${c.detail}`;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch { /* remediation failed */ }
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
454
472
|
return checks;
|
|
455
473
|
}
|
|
456
474
|
/**
|
package/dist/holmes/cli/index.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.packageRoot = packageRoot;
|
|
37
37
|
exports.main = main;
|
|
38
38
|
// @implements A-SPEC-100.2
|
|
39
|
+
// @implements A-SPEC-213
|
|
39
40
|
const fs = __importStar(require("node:fs"));
|
|
40
41
|
const path = __importStar(require("node:path"));
|
|
41
42
|
const init_1 = require("./init");
|
|
@@ -69,6 +70,7 @@ const KNOWN_FLAGS = {
|
|
|
69
70
|
init: ['help', 'target', 'mode', 'specs-dir', 'settings', 'matcher', 'dry-run', 'no-mcp', 'remove', 'force', 'agent'],
|
|
70
71
|
doctor: ['help', 'target', 'json'],
|
|
71
72
|
skills: ['help', 'target'],
|
|
73
|
+
ci: ['help', 'target', 'specs-dir', 'json'],
|
|
72
74
|
};
|
|
73
75
|
class UnknownFlagError extends Error {
|
|
74
76
|
}
|
|
@@ -290,6 +292,39 @@ async function main(argv) {
|
|
|
290
292
|
process.stderr.write(`Could not refresh recovery skill ${skillLabel(f.name)}: ${f.reason}\n`);
|
|
291
293
|
return r.failed.length > 0 ? 1 : 0;
|
|
292
294
|
}
|
|
295
|
+
// @implements A-SPEC-213
|
|
296
|
+
if (cmd === 'ci') {
|
|
297
|
+
if (typeof flags.target === 'string') {
|
|
298
|
+
const t = path.resolve(flags.target);
|
|
299
|
+
if (!fs.existsSync(t) || !fs.statSync(t).isDirectory()) {
|
|
300
|
+
process.stderr.write(`--target ${t} is not an existing directory\n\n${USAGE}`);
|
|
301
|
+
return 2;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const specsDir = typeof flags['specs-dir'] === 'string' ? flags['specs-dir'] : undefined;
|
|
305
|
+
if (specsDir !== undefined) {
|
|
306
|
+
if (path.isAbsolute(specsDir) || specsDir.split(/[\\/]/).includes('..') || specsDir.startsWith('~')
|
|
307
|
+
|| /["'`$\\;&|<>\n\r]/.test(specsDir)) {
|
|
308
|
+
process.stderr.write(`--specs-dir ${specsDir} must be a plain relative path inside the target (no '..', no '~', no quotes or shell metacharacters)\n\n${USAGE}`);
|
|
309
|
+
return 2;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const target = typeof flags.target === 'string' ? flags.target : process.cwd();
|
|
313
|
+
const { runCiGate } = require('./ci-gate');
|
|
314
|
+
const res = runCiGate(target, { specsDir });
|
|
315
|
+
if (flags.json === true) {
|
|
316
|
+
process.stdout.write(JSON.stringify(res, null, 2) + '\n');
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
process.stdout.write(res.summary + '\n');
|
|
320
|
+
for (const v of res.violations) {
|
|
321
|
+
const fileLabel = v.file ? ` [${v.file}]` : '';
|
|
322
|
+
const specLabel = v.specId ? ` (${v.specId})` : '';
|
|
323
|
+
process.stderr.write(` - ${fileLabel}${specLabel}: ${v.reason}\n`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return res.ok ? 0 : 1;
|
|
327
|
+
}
|
|
293
328
|
if (cmd === 'init') {
|
|
294
329
|
const mode = (typeof flags.mode === 'string' ? flags.mode : 'guardrail');
|
|
295
330
|
// 형제 열거 오류와 같은 모양(round 7): --settings·--target 은 USAGE 와 함께 exit 2 인데
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.CpgScanner = exports.isTestFile = exports.TEST_FILE_PATTERNS = exports.SCANNABLE_EXTENSIONS = exports.SKIP = void 0;
|
|
37
|
+
// @implements A-SPEC-208
|
|
37
38
|
const fs = __importStar(require("node:fs"));
|
|
38
39
|
const path = __importStar(require("node:path"));
|
|
39
40
|
const language_parser_1 = require("./language-parser");
|
|
@@ -206,7 +207,8 @@ class CpgScanner {
|
|
|
206
207
|
}
|
|
207
208
|
};
|
|
208
209
|
walk(rootDir);
|
|
209
|
-
|
|
210
|
+
const scopeRel = (0, source_path_1.toSourcePath)(repoRoot, rootDir);
|
|
211
|
+
this.cache?.save(scopeRel === '.' ? undefined : scopeRel); // batched persist + scoped mark-and-sweep prune
|
|
210
212
|
return out;
|
|
211
213
|
}
|
|
212
214
|
}
|
|
@@ -34,10 +34,26 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.ScanFileCache = exports.SCAN_CACHE_VERSION = void 0;
|
|
37
|
+
// @implements A-SPEC-208
|
|
37
38
|
// @implements A-SPEC-113.1
|
|
38
39
|
const fs = __importStar(require("node:fs"));
|
|
39
40
|
const path = __importStar(require("node:path"));
|
|
40
41
|
const node_crypto_1 = require("node:crypto");
|
|
42
|
+
function cloneExpr(e) {
|
|
43
|
+
if (!e)
|
|
44
|
+
return { text: '', refs: [], callees: [] };
|
|
45
|
+
return { text: e.text, refs: [...(e.refs ?? [])], callees: [...(e.callees ?? [])] };
|
|
46
|
+
}
|
|
47
|
+
function cloneDataFlow(df) {
|
|
48
|
+
if (!df)
|
|
49
|
+
return undefined;
|
|
50
|
+
return {
|
|
51
|
+
defs: (df.defs ?? []).map((d) => ({ ...d, expr: cloneExpr(d.expr) })),
|
|
52
|
+
calls: (df.calls ?? []).map((c) => ({ ...c, args: (c.args ?? []).map(cloneExpr) })),
|
|
53
|
+
params: (df.params ?? []).map((p) => ({ ...p })),
|
|
54
|
+
returns: (df.returns ?? []).map((r) => ({ ...r, expr: cloneExpr(r.expr) })),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
41
57
|
// Bump on ANY change to symbol extraction, grammar versions, or the ScannedFile schema (review D3:
|
|
42
58
|
// without a version stamp, cached entries kept serving OLD-parser symbols indefinitely after an
|
|
43
59
|
// upgrade — content hashes match, so nothing else invalidates them). A mismatch drops the whole
|
|
@@ -75,7 +91,14 @@ class ScanFileCache {
|
|
|
75
91
|
this.hits++;
|
|
76
92
|
// Default array fields so an entry from a slightly older writer can never yield `undefined`
|
|
77
93
|
// where a fresh scan yields [] (review D3b — downstream iterates these).
|
|
78
|
-
return {
|
|
94
|
+
return {
|
|
95
|
+
...e.file,
|
|
96
|
+
symbols: (e.file.symbols ?? []).map((s) => ({ ...s })),
|
|
97
|
+
implementsSpecs: [...(e.file.implementsSpecs ?? [])],
|
|
98
|
+
unanchoredImplements: [...(e.file.unanchoredImplements ?? [])],
|
|
99
|
+
edges: e.file.edges ? e.file.edges.map((edge) => ({ ...edge })) : undefined,
|
|
100
|
+
dataflow: cloneDataFlow(e.file.dataflow),
|
|
101
|
+
};
|
|
79
102
|
}
|
|
80
103
|
this.misses++;
|
|
81
104
|
return undefined;
|
|
@@ -89,20 +112,29 @@ class ScanFileCache {
|
|
|
89
112
|
* Persist once per scan. Mark-and-sweep: entries whose path was NOT seen this scan belong to
|
|
90
113
|
* deleted/renamed files — pruned so the cache cannot grow unboundedly. No-op when nothing changed.
|
|
91
114
|
*/
|
|
92
|
-
save() {
|
|
93
|
-
const
|
|
115
|
+
save(scannedScopeDir) {
|
|
116
|
+
const scopePrefix = scannedScopeDir ? (scannedScopeDir.endsWith('/') ? scannedScopeDir : scannedScopeDir + '/') : undefined;
|
|
117
|
+
const stale = Object.keys(this.entries).filter((p) => {
|
|
118
|
+
if (this.seen.has(p))
|
|
119
|
+
return false;
|
|
120
|
+
// If a scoped directory was scanned, only prune stale entries within that directory
|
|
121
|
+
if (scopePrefix && p !== scannedScopeDir && !p.startsWith(scopePrefix))
|
|
122
|
+
return false;
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
94
125
|
for (const p of stale) {
|
|
95
126
|
delete this.entries[p];
|
|
96
127
|
this.dirty = true;
|
|
97
128
|
}
|
|
98
|
-
if (!this.dirty)
|
|
99
|
-
return;
|
|
100
129
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
130
|
+
if (this.dirty) {
|
|
131
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
132
|
+
fs.writeFileSync(this.file, JSON.stringify({ v: exports.SCAN_CACHE_VERSION, entries: this.entries }), 'utf8');
|
|
133
|
+
this.dirty = false;
|
|
134
|
+
}
|
|
104
135
|
}
|
|
105
136
|
catch { /* cache persistence is best-effort — next scan just runs colder */ }
|
|
137
|
+
this.seen.clear();
|
|
106
138
|
}
|
|
107
139
|
}
|
|
108
140
|
exports.ScanFileCache = ScanFileCache;
|
|
@@ -105,8 +105,10 @@ class ProvenanceLedger {
|
|
|
105
105
|
try {
|
|
106
106
|
names = fs.readdirSync(this.dir);
|
|
107
107
|
}
|
|
108
|
-
catch {
|
|
109
|
-
|
|
108
|
+
catch (err) {
|
|
109
|
+
if (err && err.code === 'ENOENT')
|
|
110
|
+
return [];
|
|
111
|
+
throw err;
|
|
110
112
|
}
|
|
111
113
|
const out = [];
|
|
112
114
|
if (names.includes(LEGACY))
|
|
@@ -133,6 +135,12 @@ class ProvenanceLedger {
|
|
|
133
135
|
}
|
|
134
136
|
}).join('|');
|
|
135
137
|
}
|
|
138
|
+
cloneEvents(events) {
|
|
139
|
+
return events.map((e) => ({
|
|
140
|
+
...e,
|
|
141
|
+
inputs: e.inputs ? [...e.inputs] : undefined,
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
136
144
|
/**
|
|
137
145
|
* Every event across every chain, ordered by `(ts, replicaId, seq)`.
|
|
138
146
|
*
|
|
@@ -144,25 +152,18 @@ class ProvenanceLedger {
|
|
|
144
152
|
const ch = this.chains();
|
|
145
153
|
const key = this.computeMtimeKey(ch);
|
|
146
154
|
if (this.indexCache && this.indexCache.key === key) {
|
|
147
|
-
return this.indexCache.events;
|
|
155
|
+
return this.cloneEvents(this.indexCache.events);
|
|
148
156
|
}
|
|
149
157
|
const out = [];
|
|
150
|
-
let fullyConsistent = true;
|
|
151
158
|
for (const c of ch) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
out.push({ ...e, replicaId: c.replicaId });
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
fullyConsistent = false;
|
|
159
|
+
for (const e of new provenance_chain_1.ProvenanceChain(c.file).load()) {
|
|
160
|
+
out.push({ ...e, replicaId: c.replicaId });
|
|
158
161
|
}
|
|
159
162
|
}
|
|
160
163
|
const sortKey = (e) => `${e.ts} ${e.replicaId ?? ''} ${String(e.seq).padStart(12, '0')}`;
|
|
161
164
|
out.sort((a, b) => (sortKey(a) < sortKey(b) ? -1 : sortKey(a) > sortKey(b) ? 1 : 0));
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
return out;
|
|
165
|
+
this.indexCache = { key, events: out };
|
|
166
|
+
return this.cloneEvents(out);
|
|
166
167
|
}
|
|
167
168
|
/** Each chain verified on its own; one bad chain fails the whole ledger, and is named. */
|
|
168
169
|
verifyAll() {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateRtmHeatmap = generateRtmHeatmap;
|
|
4
|
+
/**
|
|
5
|
+
* Generate a standalone interactive HTML/SVG Heatmap Report for RTM Traceability & Taint Coverage.
|
|
6
|
+
*
|
|
7
|
+
* @implements A-SPEC-212
|
|
8
|
+
*/
|
|
9
|
+
function generateRtmHeatmap(specs, options) {
|
|
10
|
+
const safeSpecs = Array.isArray(specs) ? specs : [];
|
|
11
|
+
const totalSpecs = safeSpecs.length;
|
|
12
|
+
const coveredSpecs = safeSpecs.filter((s) => s && s.covered === true).length;
|
|
13
|
+
const coveragePct = totalSpecs > 0 ? Math.round((coveredSpecs / totalSpecs) * 100) : 0;
|
|
14
|
+
const title = options?.title ?? 'Holmes-Kit RTM & Security Taint Heatmap';
|
|
15
|
+
const isDark = options?.theme !== 'light';
|
|
16
|
+
const bgColor = isDark ? '#0f172a' : '#f8fafc';
|
|
17
|
+
const cardBg = isDark ? '#1e293b' : '#ffffff';
|
|
18
|
+
const textColor = isDark ? '#f8fafc' : '#0f172a';
|
|
19
|
+
const coveredBg = isDark ? '#065f46' : '#d1fae5';
|
|
20
|
+
const coveredText = isDark ? '#34d399' : '#065f46';
|
|
21
|
+
const uncoveredBg = isDark ? '#991b1b' : '#fee2e2';
|
|
22
|
+
const uncoveredText = isDark ? '#f87171' : '#991b1b';
|
|
23
|
+
const cellsHtml = safeSpecs
|
|
24
|
+
.map((s) => {
|
|
25
|
+
const isCovered = s && s.covered === true;
|
|
26
|
+
const bg = isCovered ? coveredBg : uncoveredBg;
|
|
27
|
+
const fg = isCovered ? coveredText : uncoveredText;
|
|
28
|
+
const cls = isCovered ? 'badge-covered' : 'badge-uncovered';
|
|
29
|
+
const statusLabel = isCovered ? 'COVERED' : 'UNCOVERED';
|
|
30
|
+
return `
|
|
31
|
+
<div class="card ${cls}" style="background-color: ${bg}; color: ${fg}; padding: 12px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1);">
|
|
32
|
+
<div style="font-weight: bold; font-size: 14px;">${escapeHtml(s?.id)}</div>
|
|
33
|
+
<div style="font-size: 12px; margin-top: 4px; opacity: 0.9;">${escapeHtml(s?.title)}</div>
|
|
34
|
+
<span style="display: inline-block; margin-top: 8px; font-size: 10px; font-weight: 800; text-transform: uppercase;">${statusLabel}</span>
|
|
35
|
+
</div>`;
|
|
36
|
+
})
|
|
37
|
+
.join('\n');
|
|
38
|
+
const html = `<!DOCTYPE html>
|
|
39
|
+
<html lang="en">
|
|
40
|
+
<head>
|
|
41
|
+
<meta charset="UTF-8">
|
|
42
|
+
<title>${escapeHtml(title)}</title>
|
|
43
|
+
<style>
|
|
44
|
+
body { font-family: system-ui, -apple-system, sans-serif; background-color: ${bgColor}; color: ${textColor}; margin: 0; padding: 24px; }
|
|
45
|
+
.header { margin-bottom: 24px; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 16px; }
|
|
46
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
|
|
47
|
+
.stat-badge { font-size: 32px; font-weight: 800; color: ${coveragePct >= 80 ? '#10b981' : coveragePct >= 50 ? '#f59e0b' : '#ef4444'}; }
|
|
48
|
+
</style>
|
|
49
|
+
</head>
|
|
50
|
+
<body>
|
|
51
|
+
<div class="header">
|
|
52
|
+
<h1>🔍 ${escapeHtml(title)}</h1>
|
|
53
|
+
<div>Specification Coverage: <span class="stat-badge">${coveragePct}%</span> (${coveredSpecs} / ${totalSpecs} Specs Covered)</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="grid">
|
|
56
|
+
${cellsHtml}
|
|
57
|
+
</div>
|
|
58
|
+
</body>
|
|
59
|
+
</html>`;
|
|
60
|
+
return { html, coveragePct, totalSpecs, coveredSpecs };
|
|
61
|
+
}
|
|
62
|
+
function escapeHtml(val) {
|
|
63
|
+
return String(val ?? '')
|
|
64
|
+
.replace(/&/g, '&')
|
|
65
|
+
.replace(/</g, '<')
|
|
66
|
+
.replace(/>/g, '>')
|
|
67
|
+
.replace(/"/g, '"')
|
|
68
|
+
.replace(/'/g, ''');
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
+
"//": "@implements A-SPEC-209",
|
|
2
3
|
"name": "@holmes-lab/holmes-kit",
|
|
3
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.6",
|
|
4
5
|
"description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
|
|
5
6
|
"main": "dist/holmes/mcp/server.js",
|
|
6
7
|
"bin": {
|