@holmes-lab/holmes-kit 0.1.3 → 0.1.5
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 +53 -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 +0 -0
- package/dist/holmes/hooks/stop.js +2 -1
- package/dist/holmes/mcp/handlers.js +8 -7
- package/dist/holmes/project/root.js +3 -1
- 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.5] - 2026-08-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **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.
|
|
12
|
+
- **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`.
|
|
13
|
+
- **Doctor Self-Healing Engine (REQ-207)**: Integrated `runDoctor({ autoFix: true })` auto-remediation loop and `Check.remediate()` callbacks for system diagnostic repair.
|
|
14
|
+
- **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.
|
|
15
|
+
- **Automated RTM & Security Taint Heatmap Generator (REQ-212)**: Self-contained interactive HTML/SVG report builder (`generateRtmHeatmap`) for spec chain coverage and security taint reachability.
|
|
16
|
+
- **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.
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
- **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.
|
|
20
|
+
|
|
21
|
+
## [0.1.4] - 2026-08-18
|
|
22
|
+
|
|
23
|
+
### Documentation
|
|
24
|
+
- **Supported Languages & Environments Matrix**: Added comprehensive compatibility tables in `README.md` covering 7 AST/CPG supported languages (TypeScript/JS, Python, Go, Rust, Java, C/C++, C#) and tier-based OS environments (macOS, Linux, Windows/WSL2).
|
|
7
25
|
|
|
8
26
|
## [0.1.3] - 2026-08-18
|
|
9
27
|
|
package/README.md
CHANGED
|
@@ -6,7 +6,32 @@
|
|
|
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
|
+
While most AI tools focus solely on generating code snippets, Holmes-Kit acts as a **harmonious, flexible governance harness** that bridges:
|
|
16
|
+
- 📋 **Requirements & Specification Management** (`REQ → H-SPEC → A-SPEC → T-SPEC`)
|
|
17
|
+
- 🐞 **Issue Tracking & Root Cause Localization** (Tree-sitter Code Property Graph + Recursive-CTE RTM)
|
|
18
|
+
- 🧪 **Empirical Verification & Self-Healing** (`holmes-kit doctor --fix`)
|
|
19
|
+
- 🔌 **Enterprise SDLC Tool Integration** (Harmonious integration with Jira, GitHub Issues, and external linters/SAST)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 🤖 Supported AI Coding Tools & Environments (CLI-First Focus)
|
|
24
|
+
|
|
25
|
+
Holmes-Kit prioritizes **CLI-based AI Coding Agents** where OS-level process hooks, subshell isolation, and deterministic control plane enforcement are natively guaranteed:
|
|
26
|
+
|
|
27
|
+
| AI Coding Agent / Framework | Harness Tier | Integration & Enforcement Mechanisms |
|
|
28
|
+
| :--- | :--- | :--- |
|
|
29
|
+
| 🤖 **Claude Code CLI** | 🥇 Tier 1 (Native) | OS PreToolUse & Stop hooks (`.claude/settings.local.json`), MCP server (`.mcp.json`) |
|
|
30
|
+
| 🚀 **Antigravity CLI (AGY)** | 🥇 Tier 1 (Native) | AGY Hooks (`hooks.json`), MCP config (`.agents/mcp_config.json`), Governance Skills |
|
|
31
|
+
| 💻 **Codex CLI / Agentic Shell** | 🥇 Tier 1 (Native) | Codex MCP integration (`.codex/mcp_config.json`), Subshell Isolation Gates |
|
|
32
|
+
| 🧩 **Google Antigravity SDK** | 🥇 Tier 1 (Native) | Autonomous Agent SDK bindings and cryptographic provenance verification |
|
|
33
|
+
|
|
34
|
+
> **Note**: Holmes-Kit focuses strictly on CLI-based autonomous agents to guarantee 100% deterministic OS hook gating (`deny` enforcement) before file modifications occur.
|
|
10
35
|
|
|
11
36
|
---
|
|
12
37
|
|
|
@@ -88,6 +113,33 @@ If you prefer to configure MCP manually or integrate with other IDEs, add the fo
|
|
|
88
113
|
|
|
89
114
|
---
|
|
90
115
|
|
|
116
|
+
## 🌐 Supported Languages & Environments
|
|
117
|
+
|
|
118
|
+
Holmes-Kit embeds native AST & Code Property Graph (D-CPG) analyzers to track causal relationships and anchor implementations across diverse technology stacks.
|
|
119
|
+
|
|
120
|
+
### 💻 Supported Programming Languages
|
|
121
|
+
| Language | CPG Parser | Anchor Syntax | Dataflow Taint & RTM |
|
|
122
|
+
| :--- | :--- | :--- | :---: |
|
|
123
|
+
| **TypeScript / JavaScript** | Tree-sitter TypeScript/JS | `// @implements A-SPEC-XXX` | ✅ Full Support |
|
|
124
|
+
| **Python** | Tree-sitter Python | `# @implements A-SPEC-XXX` | ✅ Full Support |
|
|
125
|
+
| **Go** | Tree-sitter Go | `// @implements A-SPEC-XXX` | ✅ Full Support |
|
|
126
|
+
| **Rust** | Tree-sitter Rust | `// @implements A-SPEC-XXX` | ✅ Full Support |
|
|
127
|
+
| **Java** | Tree-sitter Java | `// @implements A-SPEC-XXX` | ✅ Full Support |
|
|
128
|
+
| **C / C++** | Tree-sitter C/C++ | `// @implements A-SPEC-XXX` | ✅ Full Support |
|
|
129
|
+
| **C# (.NET)** | Tree-sitter C# | `// @implements A-SPEC-XXX` | ✅ Full Support |
|
|
130
|
+
|
|
131
|
+
### 🖥️ Supported Operating Systems & Runtimes
|
|
132
|
+
| OS / Platform | Architecture | Status | Notes |
|
|
133
|
+
| :--- | :--- | :---: | :--- |
|
|
134
|
+
| **macOS** | Apple Silicon (arm64) / Intel (x64) | ✅ Tier 1 | macOS 12+ (Full hook enforcement) |
|
|
135
|
+
| **Linux** | x86_64 / arm64 | ✅ Tier 1 | Ubuntu, Debian, Fedora, Arch, RHEL |
|
|
136
|
+
| **Windows (WSL2)** | x86_64 | ✅ Tier 1 | WSL2 Ubuntu/Debian recommended |
|
|
137
|
+
| **Windows Native** | x86_64 | ✅ Tier 2 | Windows 10/11 (Node.js 20+ with C++ build tools) |
|
|
138
|
+
|
|
139
|
+
> **Runtime Requirement**: Node.js `>= 20.0.0` (LTS recommended)
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
91
143
|
## 🏛️ Key Capabilities
|
|
92
144
|
|
|
93
145
|
- **Deterministic Hook Enforcement**: Gating is enforced via OS-level PreToolUse & Stop hooks, not easily bypassed prompts.
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
c34e494-msyt6an1
|
|
@@ -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;
|
|
Binary file
|
|
@@ -51,6 +51,7 @@ const test_evidence_1 = require("../review/test-evidence");
|
|
|
51
51
|
const pre_tool_use_1 = require("./pre-tool-use");
|
|
52
52
|
const governance_history_1 = require("../guardrail/governance-history");
|
|
53
53
|
const constitution_debt_1 = require("../governance/constitution-debt");
|
|
54
|
+
const root_1 = require("../project/root");
|
|
54
55
|
/**
|
|
55
56
|
* @implements A-SPEC-175
|
|
56
57
|
* The turn-boundary answer to "this project is governed but its specs are gone".
|
|
@@ -339,7 +340,7 @@ if (require.main === module) {
|
|
|
339
340
|
// stale record can never vouch for changed code. Absent/stale -> syntactic fallback.
|
|
340
341
|
let executedByAspec;
|
|
341
342
|
try {
|
|
342
|
-
const head = (0, node_child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: stopProjectRoot(), stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
343
|
+
const head = (0, node_child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: stopProjectRoot(), stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)() }).toString().trim();
|
|
343
344
|
const ev = (0, test_evidence_1.readTestEvidence)(stopProjectRoot());
|
|
344
345
|
if ((0, test_evidence_1.isFresh)(ev, head))
|
|
345
346
|
executedByAspec = ev.executedByAspec;
|
|
@@ -53,6 +53,7 @@ const spec_types_1 = require("../spec/spec-types");
|
|
|
53
53
|
const legacy_format_1 = require("../spec/legacy-format");
|
|
54
54
|
const cpg_scanner_1 = require("../cpg/cpg-scanner");
|
|
55
55
|
const scan_cache_1 = require("../cpg/scan-cache");
|
|
56
|
+
const root_1 = require("../project/root");
|
|
56
57
|
// N7: every whole-tree scan goes through the file-key incremental cache under the PROJECT root's
|
|
57
58
|
// .ax/cpg_cache — unchanged files (content-hash hit) skip the tree-sitter parse entirely.
|
|
58
59
|
// The cache lives under the RESOLVED project root, not the raw scan root: scanning a SUBDIRECTORY
|
|
@@ -65,7 +66,7 @@ const scan_cache_1 = require("../cpg/scan-cache");
|
|
|
65
66
|
// left open on the scan path. When the store is bound, the cache is the bound project's; otherwise
|
|
66
67
|
// only an anchored answer may be written to, and an unanchored one falls back to a temp cache.
|
|
67
68
|
const cacheDirFor = (root) => {
|
|
68
|
-
const r = (0,
|
|
69
|
+
const r = (0, root_2.resolveProjectRoot)(root);
|
|
69
70
|
if (r.marker !== 'given')
|
|
70
71
|
return path.join(r.root, '.ax', 'cpg_cache');
|
|
71
72
|
if (fs.existsSync(path.join(r.root, '.ax')))
|
|
@@ -126,7 +127,7 @@ function isHandlerRefusal(e) {
|
|
|
126
127
|
return !!e && typeof e === 'object' && e.holmesRefusal === true;
|
|
127
128
|
}
|
|
128
129
|
// @implements A-SPEC-128
|
|
129
|
-
const
|
|
130
|
+
const root_2 = require("../project/root");
|
|
130
131
|
const change_source_1 = require("../project/change-source");
|
|
131
132
|
const baseline_1 = require("../project/baseline");
|
|
132
133
|
const ignore_1 = require("../project/ignore");
|
|
@@ -146,7 +147,7 @@ const anchor_1 = require("../reverse/anchor");
|
|
|
146
147
|
// yields top-level-relative paths while scan(root, root) tags root-relative ones, so a disagreement
|
|
147
148
|
// silently produces wrong impact sets. `GitChangeSource` now enforces exactly that, and only there.
|
|
148
149
|
function projectRootOf(root) {
|
|
149
|
-
return (0,
|
|
150
|
+
return (0, root_2.resolveProjectRoot)(root).root;
|
|
150
151
|
}
|
|
151
152
|
// @implements A-SPEC-191 §10 — WHERE a single-use approval is spent must not be a caller's choice.
|
|
152
153
|
// r8 measured all three escapes at once: a second `root`, a `root` naming a subdirectory, and an
|
|
@@ -1190,7 +1191,7 @@ function makeRawHandlers(store) {
|
|
|
1190
1191
|
const executedByAspec = (0, test_evidence_1.computeExecutedByAspec)(result.executedByFile ?? {}, anchors);
|
|
1191
1192
|
let head = '';
|
|
1192
1193
|
try {
|
|
1193
|
-
head = (0, node_child_process_1.execFileSync)('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
1194
|
+
head = (0, node_child_process_1.execFileSync)('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8', env: (0, root_1.cleanSubprocessEnv)() }).trim();
|
|
1194
1195
|
}
|
|
1195
1196
|
catch { /* non-git */ }
|
|
1196
1197
|
// Record ONLY an actually-executed, GREEN run (review C4/C7): a red suite must not stand as
|
|
@@ -1828,7 +1829,7 @@ function makeRawHandlers(store) {
|
|
|
1828
1829
|
return { ...plan, ...applied, blockers };
|
|
1829
1830
|
},
|
|
1830
1831
|
async spec_slice_init(a) {
|
|
1831
|
-
const root = a.root ? (0,
|
|
1832
|
+
const root = a.root ? (0, root_2.resolveProjectRoot)(a.root).root : process.cwd();
|
|
1832
1833
|
const specsDir = path.join(root, '.ax', 'specs');
|
|
1833
1834
|
const existing = await store.list();
|
|
1834
1835
|
let maxId = 200;
|
|
@@ -1990,7 +1991,7 @@ depends_on:
|
|
|
1990
1991
|
return { ok: true, specsCreated: [reqId, hspecId, aspecId, tspecId] };
|
|
1991
1992
|
},
|
|
1992
1993
|
async spec_slice_approve(a) {
|
|
1993
|
-
const root = a.root ? (0,
|
|
1994
|
+
const root = a.root ? (0, root_2.resolveProjectRoot)(a.root).root : process.cwd();
|
|
1994
1995
|
const rawStore = new spec_store_1.LocalMarkdownRepository(path.join(root, '.ax', 'specs'));
|
|
1995
1996
|
const specs = await rawStore.list();
|
|
1996
1997
|
const inSlice = specs.filter((s) => {
|
|
@@ -2030,7 +2031,7 @@ depends_on:
|
|
|
2030
2031
|
return { ok: true, approvedSpecs };
|
|
2031
2032
|
},
|
|
2032
2033
|
async spec_remediate(a) {
|
|
2033
|
-
const root = a.root ? (0,
|
|
2034
|
+
const root = a.root ? (0, root_2.resolveProjectRoot)(a.root).root : process.cwd();
|
|
2034
2035
|
const actionsTaken = [];
|
|
2035
2036
|
if (a.targetFile && a.aspecId) {
|
|
2036
2037
|
const fullPath = path.isAbsolute(a.targetFile) ? a.targetFile : path.join(root, a.targetFile);
|
|
@@ -36,15 +36,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.MARKER = void 0;
|
|
37
37
|
exports.cleanSubprocessEnv = cleanSubprocessEnv;
|
|
38
38
|
exports.resolveProjectRoot = resolveProjectRoot;
|
|
39
|
+
// @implements A-SPEC-205
|
|
39
40
|
// @implements A-SPEC-128
|
|
40
41
|
const fs = __importStar(require("node:fs"));
|
|
41
42
|
const path = __importStar(require("node:path"));
|
|
42
43
|
const node_child_process_1 = require("node:child_process");
|
|
43
44
|
// @implements A-SPEC-194
|
|
44
45
|
// @implements A-SPEC-196
|
|
46
|
+
// @implements A-SPEC-205
|
|
45
47
|
function cleanSubprocessEnv(env = process.env) {
|
|
46
48
|
const cleaned = { ...env };
|
|
47
|
-
const SCRUB_KEYS = new Set(['HOLMES_APPROVAL', 'HOLMES_LEDGER_KEY']);
|
|
49
|
+
const SCRUB_KEYS = new Set(['HOLMES_APPROVAL', 'HOLMES_LEDGER_KEY', 'HOLMES_ROLE', 'HOLMES_ENV_CHANNEL']);
|
|
48
50
|
for (const k of Object.keys(cleaned)) {
|
|
49
51
|
if (SCRUB_KEYS.has(k.toUpperCase())) {
|
|
50
52
|
delete cleaned[k];
|
|
@@ -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.5",
|
|
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": {
|