@holmes-lab/holmes-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/bin/holmes-hook-antigravity.js +31 -0
- package/bin/holmes-kit.js +23 -0
- package/bin/holmes-mcp.js +34 -0
- package/bin/holmes-stop-antigravity.js +29 -0
- package/dist/.build-id +1 -0
- package/dist/holmes/cli/agents.js +168 -0
- package/dist/holmes/cli/doctor.js +625 -0
- package/dist/holmes/cli/gitignore-merge.js +84 -0
- package/dist/holmes/cli/governed-precondition.js +157 -0
- package/dist/holmes/cli/index.js +384 -0
- package/dist/holmes/cli/init.js +462 -0
- package/dist/holmes/cli/playbook-skills.js +711 -0
- package/dist/holmes/cli/roles-readme.js +134 -0
- package/dist/holmes/cli/settings-merge.js +122 -0
- package/dist/holmes/config/config.js +70 -0
- package/dist/holmes/context/bundler.js +114 -0
- package/dist/holmes/context/render.js +29 -0
- package/dist/holmes/context/tiers.js +110 -0
- package/dist/holmes/context/tokens.js +8 -0
- package/dist/holmes/cpg/cpg-scanner.js +213 -0
- package/dist/holmes/cpg/hash-cache.js +86 -0
- package/dist/holmes/cpg/language-parser-walk.js +917 -0
- package/dist/holmes/cpg/language-parser-worker.js +81 -0
- package/dist/holmes/cpg/language-parser.js +234 -0
- package/dist/holmes/cpg/scan-cache.js +108 -0
- package/dist/holmes/cpg/source-path.js +44 -0
- package/dist/holmes/cpg/test-files.js +84 -0
- package/dist/holmes/governance/constitution-debt.js +73 -0
- package/dist/holmes/governance/constitution-report.js +25 -0
- package/dist/holmes/governance/constitution.js +129 -0
- package/dist/holmes/governance/identity.js +30 -0
- package/dist/holmes/governance/ledger-lock.js +165 -0
- package/dist/holmes/governance/ledger-store.conformance.js +90 -0
- package/dist/holmes/governance/ledger-store.js +106 -0
- package/dist/holmes/governance/progress-ledger.js +83 -0
- package/dist/holmes/governance/provenance-chain.js +365 -0
- package/dist/holmes/governance/provenance-ledger.js +0 -0
- package/dist/holmes/governance/provenance-schema.js +47 -0
- package/dist/holmes/governance/replica-id.js +106 -0
- package/dist/holmes/governance/role-policy.js +137 -0
- package/dist/holmes/governance/trust-score.js +43 -0
- package/dist/holmes/guardrail/anchors.js +31 -0
- package/dist/holmes/guardrail/blind-spots.js +38 -0
- package/dist/holmes/guardrail/decision-ledger.js +107 -0
- package/dist/holmes/guardrail/executable-artifact.js +129 -0
- package/dist/holmes/guardrail/governance-history.js +101 -0
- package/dist/holmes/guardrail/phase.js +169 -0
- package/dist/holmes/guardrail/risk-classifier.js +450 -0
- package/dist/holmes/guardrail/risk-gate.js +160 -0
- package/dist/holmes/guardrail/risk-types.js +6 -0
- package/dist/holmes/guardrail/tspec-state.js +392 -0
- package/dist/holmes/guardrail/write-target.js +224 -0
- package/dist/holmes/hooks/adapters/antigravity.js +194 -0
- package/dist/holmes/hooks/pre-tool-use.js +1262 -0
- package/dist/holmes/hooks/stop.js +416 -0
- package/dist/holmes/mcp/basis.js +162 -0
- package/dist/holmes/mcp/handlers.js +1831 -0
- package/dist/holmes/mcp/server.js +71 -0
- package/dist/holmes/mcp/stdio-client.js +165 -0
- package/dist/holmes/mcp/supervisor.js +178 -0
- package/dist/holmes/mcp/tool-schemas.js +394 -0
- package/dist/holmes/mcp/validate-args.js +281 -0
- package/dist/holmes/messages/registry.js +50 -0
- package/dist/holmes/project/baseline.js +210 -0
- package/dist/holmes/project/change-source.js +233 -0
- package/dist/holmes/project/ignore.js +145 -0
- package/dist/holmes/project/root.js +113 -0
- package/dist/holmes/reverse/anchor.js +162 -0
- package/dist/holmes/reverse/cluster.js +187 -0
- package/dist/holmes/reverse/draft.js +151 -0
- package/dist/holmes/reverse/dynamic-wiring.js +47 -0
- package/dist/holmes/reverse/scan.js +194 -0
- package/dist/holmes/reverse/surface.js +154 -0
- package/dist/holmes/reverse/test-map.js +263 -0
- package/dist/holmes/review/coverage.js +33 -0
- package/dist/holmes/review/findings.js +123 -0
- package/dist/holmes/review/package.js +40 -0
- package/dist/holmes/review/review-targets.js +92 -0
- package/dist/holmes/review/scope.js +57 -0
- package/dist/holmes/review/test-evidence.js +77 -0
- package/dist/holmes/review/test-runner.js +572 -0
- package/dist/holmes/rtm/dataflow-taint.js +262 -0
- package/dist/holmes/rtm/gap-analyzer.js +27 -0
- package/dist/holmes/rtm/git-changes.js +72 -0
- package/dist/holmes/rtm/incremental.js +45 -0
- package/dist/holmes/rtm/localize.js +100 -0
- package/dist/holmes/rtm/rtm-builder.js +191 -0
- package/dist/holmes/rtm/rtm-check.js +89 -0
- package/dist/holmes/rtm/rtm-graph.js +232 -0
- package/dist/holmes/rtm/taint.js +92 -0
- package/dist/holmes/rtm/test-scope.js +336 -0
- package/dist/holmes/spec/approval-blockers.js +204 -0
- package/dist/holmes/spec/breaking-change.js +89 -0
- package/dist/holmes/spec/legacy-format.js +87 -0
- package/dist/holmes/spec/spec-digest.js +71 -0
- package/dist/holmes/spec/spec-parser.js +106 -0
- package/dist/holmes/spec/spec-store.conformance.js +118 -0
- package/dist/holmes/spec/spec-store.js +331 -0
- package/dist/holmes/spec/spec-types.js +177 -0
- package/dist/holmes/spec/validator.js +280 -0
- package/package.json +76 -0
- package/playbooks/adopt/PLAYBOOK.md +125 -0
- package/playbooks/author-slice/PLAYBOOK.md +119 -0
- package/playbooks/promote-slice/PLAYBOOK.md +134 -0
|
@@ -0,0 +1,233 @@
|
|
|
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.EMPTY_CHANGE_SET = exports.GitChangeSource = exports.SnapshotChangeSource = exports.hashOf = exports.isDerivedState = void 0;
|
|
37
|
+
exports.hashTree = hashTree;
|
|
38
|
+
exports.diffHashes = diffHashes;
|
|
39
|
+
// @implements A-SPEC-128
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
42
|
+
const node_crypto_1 = require("node:crypto");
|
|
43
|
+
const cpg_scanner_1 = require("../cpg/cpg-scanner");
|
|
44
|
+
const git_changes_1 = require("../rtm/git-changes");
|
|
45
|
+
const baseline_1 = require("./baseline");
|
|
46
|
+
const root_1 = require("./root");
|
|
47
|
+
const EMPTY = { added: [], modified: [], deleted: [], renamed: [] };
|
|
48
|
+
exports.EMPTY_CHANGE_SET = EMPTY;
|
|
49
|
+
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
50
|
+
/**
|
|
51
|
+
* Derived state under `.ax/` — bookkeeping the harness writes about the project, not project content.
|
|
52
|
+
*
|
|
53
|
+
* Both adapters exclude exactly this set, which is what makes "the two sources agree" a property
|
|
54
|
+
* rather than a coincidence. Found by the agreement test: the snapshot source skipped all of `.ax/`
|
|
55
|
+
* while git reported a freshly written baseline file as an addition, so the same edits produced
|
|
56
|
+
* different change sets depending on which adapter answered.
|
|
57
|
+
*
|
|
58
|
+
* `.ax/specs/**` is deliberately NOT here. A spec is project content — a reviewer who changed a
|
|
59
|
+
* requirement should see that in the change set, and hiding it would make the two halves of the chain
|
|
60
|
+
* visible at different times. The line is derived-vs-authored, not "anything under .ax".
|
|
61
|
+
*/
|
|
62
|
+
const DERIVED_STATE_RE = /^\.ax\/(state|cpg_cache|ledger|reports)\/|^\.ax\/[^/]*\.sqlite/;
|
|
63
|
+
const isDerivedState = (relPath) => DERIVED_STATE_RE.test(relPath);
|
|
64
|
+
exports.isDerivedState = isDerivedState;
|
|
65
|
+
/** Same digest the scan caches use, so a baseline and a scan cache never disagree about a file. */
|
|
66
|
+
const hashOf = (content) => (0, node_crypto_1.createHash)('sha256').update(content, 'utf8').digest('hex');
|
|
67
|
+
exports.hashOf = hashOf;
|
|
68
|
+
/**
|
|
69
|
+
* Content hashes for every file in the tree, keyed by project-relative POSIX path.
|
|
70
|
+
*
|
|
71
|
+
* Derived state under `.ax/` is excluded (see `isDerivedState`) along with the scanner's vendor/cache
|
|
72
|
+
* skips, so a run never reports its own bookkeeping as project churn. Authored specs under
|
|
73
|
+
* `.ax/specs/**` ARE included — they are project content.
|
|
74
|
+
*
|
|
75
|
+
* Files that cannot be read are skipped rather than failing the walk — an unreadable file is not
|
|
76
|
+
* evidence of a change.
|
|
77
|
+
*/
|
|
78
|
+
function hashTree(root, opts = {}) {
|
|
79
|
+
const out = {};
|
|
80
|
+
const walk = (dir) => {
|
|
81
|
+
let entries;
|
|
82
|
+
try {
|
|
83
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
for (const e of entries) {
|
|
89
|
+
if (cpg_scanner_1.SKIP.has(e.name))
|
|
90
|
+
continue;
|
|
91
|
+
const abs = path.join(dir, e.name);
|
|
92
|
+
const rel = path.relative(root, abs).split(path.sep).join('/');
|
|
93
|
+
if ((0, exports.isDerivedState)(`${rel}${e.isDirectory() ? '/' : ''}`) || opts.isIgnored?.(rel))
|
|
94
|
+
continue;
|
|
95
|
+
if (e.isDirectory()) {
|
|
96
|
+
walk(abs);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!e.isFile())
|
|
100
|
+
continue; // symlinks and devices are not project content
|
|
101
|
+
try {
|
|
102
|
+
out[rel] = (0, exports.hashOf)(fs.readFileSync(abs, 'utf8'));
|
|
103
|
+
}
|
|
104
|
+
catch { /* unreadable — not a change */ }
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
walk(root);
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
/** Compare a baseline's hashes against the tree's. Pure — the adapters supply the two maps. */
|
|
111
|
+
function diffHashes(before, after) {
|
|
112
|
+
const added = [];
|
|
113
|
+
const modified = [];
|
|
114
|
+
const deleted = [];
|
|
115
|
+
for (const [p, h] of Object.entries(after)) {
|
|
116
|
+
if (!(p in before))
|
|
117
|
+
added.push(p);
|
|
118
|
+
else if (before[p] !== h)
|
|
119
|
+
modified.push(p);
|
|
120
|
+
}
|
|
121
|
+
for (const p of Object.keys(before))
|
|
122
|
+
if (!(p in after))
|
|
123
|
+
deleted.push(p);
|
|
124
|
+
return { added: added.sort(cmp), modified: modified.sort(cmp), deleted: deleted.sort(cmp), renamed: [] };
|
|
125
|
+
}
|
|
126
|
+
/** Changes since a labelled baseline. Available on any directory, with or without version control. */
|
|
127
|
+
class SnapshotChangeSource {
|
|
128
|
+
root;
|
|
129
|
+
label;
|
|
130
|
+
opts;
|
|
131
|
+
constructor(root, label,
|
|
132
|
+
// @implements A-SPEC-134 — `verify` is the provenance ledger path. When set, a baseline whose
|
|
133
|
+
// digest is not anchored on the chain (tampered, or written unanchored) is treated as ABSENT:
|
|
134
|
+
// a forged narrowing must never be honored. Omitted (pure unit tests) → today's behavior.
|
|
135
|
+
opts = {}) {
|
|
136
|
+
this.root = root;
|
|
137
|
+
this.label = label;
|
|
138
|
+
this.opts = opts;
|
|
139
|
+
}
|
|
140
|
+
/** Trusted iff a baseline exists AND (no verify requested OR its digest is anchored on the chain). */
|
|
141
|
+
trustedBaseline() {
|
|
142
|
+
const baseline = (0, baseline_1.readBaseline)(this.root, this.label);
|
|
143
|
+
if (!baseline)
|
|
144
|
+
return null;
|
|
145
|
+
if (this.opts.verify && !(0, baseline_1.isBaselineTrusted)(this.root, this.label, this.opts.verify))
|
|
146
|
+
return null;
|
|
147
|
+
return baseline;
|
|
148
|
+
}
|
|
149
|
+
changes() {
|
|
150
|
+
const baseline = this.trustedBaseline();
|
|
151
|
+
if (!baseline)
|
|
152
|
+
return null;
|
|
153
|
+
return diffHashes(baseline.files, hashTree(this.root, this.opts));
|
|
154
|
+
}
|
|
155
|
+
describe() {
|
|
156
|
+
const raw = (0, baseline_1.readBaseline)(this.root, this.label);
|
|
157
|
+
const trusted = this.trustedBaseline();
|
|
158
|
+
const tampered = raw !== null && trusted === null; // exists but failed the integrity check
|
|
159
|
+
return {
|
|
160
|
+
kind: 'snapshot',
|
|
161
|
+
baseline: this.label,
|
|
162
|
+
detectsRenames: false,
|
|
163
|
+
available: trusted !== null,
|
|
164
|
+
...(trusted ? {} : {
|
|
165
|
+
reason: tampered
|
|
166
|
+
? `baseline "${this.label}" failed its integrity check (not anchored on the provenance chain) — treating as absent, full scope`
|
|
167
|
+
: `no baseline named "${this.label}" — run a passing test_run, or take one explicitly`,
|
|
168
|
+
}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
exports.SnapshotChangeSource = SnapshotChangeSource;
|
|
173
|
+
/**
|
|
174
|
+
* Changes across a git range.
|
|
175
|
+
*
|
|
176
|
+
* THE TOP-LEVEL GUARD LIVES HERE, and only here. `gitChangedFiles` yields paths relative to the git
|
|
177
|
+
* top-level while `scan(root, root)` tags them relative to `root`; when those disagree the keys
|
|
178
|
+
* silently diverge and impact sets come back wrong. That hazard belongs to this adapter — it was
|
|
179
|
+
* previously imposed on every tool, including ones that never look at a diff, which is what made the
|
|
180
|
+
* whole harness demand a repository.
|
|
181
|
+
*/
|
|
182
|
+
class GitChangeSource {
|
|
183
|
+
root;
|
|
184
|
+
base;
|
|
185
|
+
head;
|
|
186
|
+
topLevel;
|
|
187
|
+
constructor(root, base, head) {
|
|
188
|
+
this.root = root;
|
|
189
|
+
this.base = base;
|
|
190
|
+
this.head = head;
|
|
191
|
+
this.topLevel = (0, root_1.resolveProjectRoot)(root).git?.topLevel;
|
|
192
|
+
if (this.topLevel && fs.realpathSync(path.resolve(root)) !== this.topLevel) {
|
|
193
|
+
throw new Error(`GitChangeSource: root must be the git top-level (got ${root}, top-level is ${this.topLevel})`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
changes() {
|
|
197
|
+
if (!this.topLevel)
|
|
198
|
+
return null;
|
|
199
|
+
try {
|
|
200
|
+
const c = (0, git_changes_1.gitChangedFiles)(this.root, this.base, this.head);
|
|
201
|
+
// The same derived-state exclusion the snapshot source applies. A project that committed its
|
|
202
|
+
// `.ax/state` before the ignore block existed would otherwise get different answers from the
|
|
203
|
+
// two adapters for identical edits.
|
|
204
|
+
const keep = (p) => !(0, exports.isDerivedState)(p);
|
|
205
|
+
return {
|
|
206
|
+
added: c.added.filter(keep).sort(cmp),
|
|
207
|
+
modified: c.modified.filter(keep).sort(cmp),
|
|
208
|
+
deleted: c.deleted.filter(keep).sort(cmp),
|
|
209
|
+
renamed: c.renamed.filter((r) => keep(r.to)).sort((a, b) => cmp(a.to, b.to)),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// An unreachable ref (a shallow clone, a range that does not exist yet) is an unavailable
|
|
214
|
+
// source, not a crash — the caller widens to full scope exactly as it does with no baseline.
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
describe() {
|
|
219
|
+
const range = `${this.base}..${this.head}`;
|
|
220
|
+
if (!this.topLevel) {
|
|
221
|
+
return { kind: 'git', baseline: range, detectsRenames: true, available: false, reason: 'not a git working tree' };
|
|
222
|
+
}
|
|
223
|
+
const available = this.changes() !== null;
|
|
224
|
+
return {
|
|
225
|
+
kind: 'git',
|
|
226
|
+
baseline: range,
|
|
227
|
+
detectsRenames: true,
|
|
228
|
+
available,
|
|
229
|
+
...(available ? {} : { reason: `git range ${range} could not be resolved` }),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
exports.GitChangeSource = GitChangeSource;
|
|
@@ -0,0 +1,145 @@
|
|
|
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.parseIgnore = parseIgnore;
|
|
37
|
+
exports.loadIgnore = loadIgnore;
|
|
38
|
+
// @implements A-SPEC-128
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const LIMITATIONS = [
|
|
42
|
+
'only the project-root .gitignore is read — nested per-directory .gitignore files are not',
|
|
43
|
+
'backslash escapes are not interpreted',
|
|
44
|
+
'.git/info/exclude and the global core.excludesFile are not read',
|
|
45
|
+
];
|
|
46
|
+
/** Constructs the subset deliberately does not implement: escapes, and an unterminated class. */
|
|
47
|
+
const UNSUPPORTED_RE = /\\/;
|
|
48
|
+
const UNTERMINATED_CLASS_RE = /\[[^\]]*$/;
|
|
49
|
+
/**
|
|
50
|
+
* Translate one supported pattern into an anchored regex over project-relative FILE paths.
|
|
51
|
+
*
|
|
52
|
+
* `dirOnly` is expressed in the tail rather than as a second check at match time: a directory rule
|
|
53
|
+
* must have something UNDER it (`dist/` excludes `dist/bundle.js`, never a file named `dist`). An
|
|
54
|
+
* earlier version re-assembled the compiled regex's source at match time to express that, which
|
|
55
|
+
* passed its tests and was still the wrong shape — unreadable, and correct only for the cases tried.
|
|
56
|
+
*/
|
|
57
|
+
function compile(pattern, dirOnly) {
|
|
58
|
+
const anchored = pattern.startsWith('/');
|
|
59
|
+
let body = anchored ? pattern.slice(1) : pattern;
|
|
60
|
+
// A pattern containing a slash anywhere is anchored to the root; a bare name matches at any depth.
|
|
61
|
+
const rooted = anchored || body.includes('/');
|
|
62
|
+
let out = '';
|
|
63
|
+
for (let i = 0; i < body.length; i++) {
|
|
64
|
+
const c = body[i];
|
|
65
|
+
if (c === '*') {
|
|
66
|
+
if (body[i + 1] === '*') {
|
|
67
|
+
// `**/` spans zero or more directories; a bare `**` spans anything including separators.
|
|
68
|
+
if (body[i + 2] === '/') {
|
|
69
|
+
out += '(?:[^/]+/)*';
|
|
70
|
+
i += 2;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
out += '.*';
|
|
74
|
+
i += 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
out += '[^/]*';
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (c === '?') {
|
|
83
|
+
out += '[^/]';
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (c === '[') {
|
|
87
|
+
// A character class, copied through with git's `!` negation translated to a regex `^`. `/` is
|
|
88
|
+
// excluded so a class can never cross a path separator.
|
|
89
|
+
const end = body.indexOf(']', i + 1);
|
|
90
|
+
const inner = body.slice(i + 1, end);
|
|
91
|
+
const negated = inner.startsWith('!') || inner.startsWith('^');
|
|
92
|
+
out += `[${negated ? '^' : ''}${(negated ? inner.slice(1) : inner).replace(/[\\\]]/g, '')}]`;
|
|
93
|
+
i = end;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
out += c.replace(/[.+^${}()|]/g, '\\$&');
|
|
97
|
+
}
|
|
98
|
+
// Matching a FILE path: an ordinary rule matches the file itself or anything beneath it; a
|
|
99
|
+
// directory-only rule matches ONLY what is beneath it.
|
|
100
|
+
const tail = dirOnly ? '/.+$' : '(?:/.*)?$';
|
|
101
|
+
return new RegExp(rooted ? `^${out}${tail}` : `^(?:.*/)?${out}${tail}`);
|
|
102
|
+
}
|
|
103
|
+
function parseIgnore(content) {
|
|
104
|
+
const rules = [];
|
|
105
|
+
const unsupported = [];
|
|
106
|
+
for (const raw of content.split('\n')) {
|
|
107
|
+
const line = raw.replace(/\s+$/, ''); // trailing whitespace is not part of a pattern
|
|
108
|
+
if (line.trim() === '' || line.trimStart().startsWith('#'))
|
|
109
|
+
continue;
|
|
110
|
+
if (UNSUPPORTED_RE.test(line) || UNTERMINATED_CLASS_RE.test(line)) {
|
|
111
|
+
unsupported.push(line);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const negated = line.startsWith('!');
|
|
115
|
+
let body = negated ? line.slice(1) : line;
|
|
116
|
+
const dirOnly = body.endsWith('/');
|
|
117
|
+
if (dirOnly)
|
|
118
|
+
body = body.slice(0, -1);
|
|
119
|
+
if (body === '')
|
|
120
|
+
continue;
|
|
121
|
+
rules.push({ negated, dirOnly, re: compile(body, dirOnly), source: line });
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
isIgnored(relPath) {
|
|
125
|
+
const p = relPath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
126
|
+
let verdict = false;
|
|
127
|
+
// LAST match wins, which is what makes negation work: `*.log` then `!keep.log` re-includes.
|
|
128
|
+
for (const r of rules)
|
|
129
|
+
if (r.re.test(p))
|
|
130
|
+
verdict = !r.negated;
|
|
131
|
+
return verdict;
|
|
132
|
+
},
|
|
133
|
+
unsupportedPatterns: () => [...unsupported],
|
|
134
|
+
limitations: () => [...LIMITATIONS],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Load the project-root `.gitignore`. A missing file ignores nothing. */
|
|
138
|
+
function loadIgnore(root) {
|
|
139
|
+
try {
|
|
140
|
+
return parseIgnore(fs.readFileSync(path.join(root, '.gitignore'), 'utf8'));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return parseIgnore('');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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.MARKER = void 0;
|
|
37
|
+
exports.cleanSubprocessEnv = cleanSubprocessEnv;
|
|
38
|
+
exports.resolveProjectRoot = resolveProjectRoot;
|
|
39
|
+
// @implements A-SPEC-128
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
42
|
+
const node_child_process_1 = require("node:child_process");
|
|
43
|
+
// @implements A-SPEC-194
|
|
44
|
+
// @implements A-SPEC-196
|
|
45
|
+
function cleanSubprocessEnv(env = process.env) {
|
|
46
|
+
const cleaned = { ...env };
|
|
47
|
+
const SCRUB_KEYS = new Set(['HOLMES_APPROVAL', 'HOLMES_LEDGER_KEY']);
|
|
48
|
+
for (const k of Object.keys(cleaned)) {
|
|
49
|
+
if (SCRUB_KEYS.has(k.toUpperCase())) {
|
|
50
|
+
delete cleaned[k];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return cleaned;
|
|
54
|
+
}
|
|
55
|
+
/** The marker directory that makes a directory a Holmes-Kit project. */
|
|
56
|
+
exports.MARKER = '.ax';
|
|
57
|
+
function gitTopLevel(dir) {
|
|
58
|
+
try {
|
|
59
|
+
// stderr silenced: "not a git repository" is an expected branch here, not a fault worth printing.
|
|
60
|
+
const out = (0, node_child_process_1.execFileSync)('git', ['-C', dir, 'rev-parse', '--show-toplevel'], {
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
63
|
+
env: cleanSubprocessEnv(),
|
|
64
|
+
}).trim();
|
|
65
|
+
// git resolves symlinks in its output; realpath ours too so comparisons are like-for-like
|
|
66
|
+
// (macOS /var -> /private/var otherwise makes an identical directory look like a mismatch).
|
|
67
|
+
return out ? fs.realpathSync(out) : undefined;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function resolveProjectRoot(dir) {
|
|
74
|
+
let start;
|
|
75
|
+
try {
|
|
76
|
+
start = fs.realpathSync(path.resolve(dir));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// @implements A-SPEC-189 §7 (round 11) — this is a POINTED refusal about the caller's own
|
|
80
|
+
// argument ('that path does not exist'), not a fault. Without the marker the MCP boundary
|
|
81
|
+
// rethrew it and 11 tools answered the commonest user mistake with a raw -32603 — the class
|
|
82
|
+
// this REQ exists to remove — while phase_status and spec_approve, which catch it themselves,
|
|
83
|
+
// answered with {ok:false, reason}. One mistake, two shapes.
|
|
84
|
+
throw Object.assign(new Error(`resolveProjectRoot: ${dir} does not exist`), { holmesRefusal: true });
|
|
85
|
+
}
|
|
86
|
+
if (!fs.statSync(start).isDirectory()) {
|
|
87
|
+
throw Object.assign(new Error(`resolveProjectRoot: ${dir} is not a directory`), { holmesRefusal: true });
|
|
88
|
+
}
|
|
89
|
+
// Walk up for the marker. The loop terminates at the filesystem root, where dirname(p) === p.
|
|
90
|
+
//
|
|
91
|
+
// @implements A-SPEC-191 §13 — a candidate INSIDE a `.ax` directory is never a project root.
|
|
92
|
+
// Round-9 measured the attack this closes: one ordinary `review_record({root: <P>/.ax/specs})`
|
|
93
|
+
// call — no approval, no shell, no gate — creates `<P>/.ax/specs/.ax/ledger/`, and the very next
|
|
94
|
+
// derivation walks up from the spec store and stops at that planted marker. Every consumed nonce
|
|
95
|
+
// then reopened, the audit lines moved to a shadow ledger the project never reads, and
|
|
96
|
+
// spec_approve refused the CORRECT root forever after. `.ax` holds a project's records; a project
|
|
97
|
+
// does not live inside its own records.
|
|
98
|
+
// round-10: the exclusion compared SPELLING. On a case-insensitive filesystem (macOS default) a
|
|
99
|
+
// store written as `.AX/specs` slipped past it, and the planted-marker hijack came straight back.
|
|
100
|
+
const insideAx = (p) => p.split(path.sep).some((seg) => seg.toLowerCase() === exports.MARKER.toLowerCase());
|
|
101
|
+
let found;
|
|
102
|
+
for (let cur = start;; cur = path.dirname(cur)) {
|
|
103
|
+
if (!insideAx(cur) && fs.existsSync(path.join(cur, exports.MARKER))) {
|
|
104
|
+
found = cur;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
if (path.dirname(cur) === cur)
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
const root = found ?? start;
|
|
111
|
+
const topLevel = gitTopLevel(root);
|
|
112
|
+
return { root, marker: found ? exports.MARKER : 'given', ...(topLevel ? { git: { topLevel } } : {}) };
|
|
113
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
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.planAnchors = planAnchors;
|
|
37
|
+
exports.applyAnchors = applyAnchors;
|
|
38
|
+
// @implements A-SPEC-126
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
/**
|
|
42
|
+
* Comment prefix per language, covering every extension in `SCANNABLE_EXTENSIONS`.
|
|
43
|
+
* An anchor written with the wrong prefix is a syntax error in the target file, so this table is
|
|
44
|
+
* matched explicitly rather than defaulted.
|
|
45
|
+
*/
|
|
46
|
+
const COMMENT_PREFIX = [
|
|
47
|
+
[/\.(py|rb|sh|yml|yaml|toml)$/, '#'],
|
|
48
|
+
[/\.(ts|mts|cts|tsx|jsx|js|mjs|cjs|java|cs|go|rs|cpp|cc|cxx|hpp|hh|h)$/, '//'],
|
|
49
|
+
];
|
|
50
|
+
const prefixFor = (file) => (COMMENT_PREFIX.find(([re]) => re.test(file)) ?? [null, '//'])[1];
|
|
51
|
+
/**
|
|
52
|
+
* Compute the anchor edits for a mapping, refusing anything that must not be written.
|
|
53
|
+
*
|
|
54
|
+
* `approvedAspecs` is the caller's list of APPROVED A-SPEC ids — passed in rather than read here so
|
|
55
|
+
* the function stays pure with respect to the spec store and can be tested without one.
|
|
56
|
+
*/
|
|
57
|
+
function planAnchors(root, mapping, approvedAspecs) {
|
|
58
|
+
const edits = [];
|
|
59
|
+
const refused = [];
|
|
60
|
+
const approved = new Set(approvedAspecs);
|
|
61
|
+
const rootReal = path.resolve(root);
|
|
62
|
+
// The disk is read at PLAN time only, so a repeated pair would produce two identical edits and
|
|
63
|
+
// apply would insert the same anchor twice. Dedupe here rather than trusting the caller's mapping.
|
|
64
|
+
const planned = new Set();
|
|
65
|
+
// JSON-encoded pair, not a hand-picked separator: a filename may contain any character, and a
|
|
66
|
+
// separator that can occur in a path would silently merge two different mappings into one key.
|
|
67
|
+
const keyOf = (file, aspec) => JSON.stringify([file, aspec]);
|
|
68
|
+
for (const { file, aspec } of mapping) {
|
|
69
|
+
if (planned.has(keyOf(file, aspec))) {
|
|
70
|
+
refused.push({ file, aspec, reason: 'duplicate mapping entry' });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (!approved.has(aspec)) {
|
|
74
|
+
refused.push({ file, aspec, reason: `target A-SPEC ${aspec} is not approved` });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// The mapping arrives as tool input, and applying an edit is a file write: a traversing path
|
|
78
|
+
// would let an anchor land anywhere on the host. Containment is checked before the read.
|
|
79
|
+
// @implements A-SPEC-189 §11 (round 12) — `path.resolve` folds `..` and nothing else, so a
|
|
80
|
+
// symlink INSIDE the root (a directory link or a file link) carried the write past this check
|
|
81
|
+
// while the returned plan still showed a root-relative path. The plan/apply split exists so a
|
|
82
|
+
// human can inspect the destinations before anything is written; naming the wrong destination
|
|
83
|
+
// defeats the split itself. Judge where the write LANDS, and say so when it lands outside.
|
|
84
|
+
const abs = path.resolve(rootReal, file);
|
|
85
|
+
const landing = (() => {
|
|
86
|
+
let head = abs;
|
|
87
|
+
const tail = [];
|
|
88
|
+
for (let hop = 0; hop < 64; hop++) {
|
|
89
|
+
try {
|
|
90
|
+
return [fs.realpathSync(head), ...tail].join(path.sep);
|
|
91
|
+
}
|
|
92
|
+
catch { /* walk up */ }
|
|
93
|
+
const parent = path.dirname(head);
|
|
94
|
+
if (parent === head)
|
|
95
|
+
return abs;
|
|
96
|
+
tail.unshift(path.basename(head));
|
|
97
|
+
head = parent;
|
|
98
|
+
}
|
|
99
|
+
return abs;
|
|
100
|
+
})();
|
|
101
|
+
// 뿌리도 같은 방식으로 해소해야 비교가 성립한다 — `/var` → `/private/var` 처럼 뿌리만
|
|
102
|
+
// 링크를 지나면 제 파일이 전부 '밖'이 된다(§17b 가 훅에서 배운 것과 같은 함정).
|
|
103
|
+
const rootLanding = (() => { try {
|
|
104
|
+
return fs.realpathSync(rootReal);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return rootReal;
|
|
108
|
+
} })();
|
|
109
|
+
const contained = (x, base) => x === base || x.startsWith(base + path.sep);
|
|
110
|
+
if (!contained(abs, rootReal) || !contained(landing, rootLanding)) {
|
|
111
|
+
refused.push({ file, aspec, reason: 'path resolves outside the root' });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
let code;
|
|
115
|
+
try {
|
|
116
|
+
code = fs.readFileSync(abs, 'utf8');
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
refused.push({ file, aspec, reason: 'unreadable' });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (new RegExp(`@implements\\s+${aspec.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(code)) {
|
|
123
|
+
refused.push({ file, aspec, reason: 'already anchored' });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
planned.add(keyOf(file, aspec));
|
|
127
|
+
edits.push({
|
|
128
|
+
file, aspec,
|
|
129
|
+
insert: `${prefixFor(file)} @implements ${aspec}\n`,
|
|
130
|
+
// A shebang must stay executable: line 1 is reserved for it.
|
|
131
|
+
atLine: /^#!/.test(code) ? 1 : 0,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return { edits, refused };
|
|
135
|
+
}
|
|
136
|
+
/** Apply an anchor plan. DRY-RUN BY DEFAULT: writing requires an explicit `dryRun: false`. */
|
|
137
|
+
function applyAnchors(root, edits, opts = {}) {
|
|
138
|
+
const dryRun = opts.dryRun !== false;
|
|
139
|
+
if (dryRun)
|
|
140
|
+
return { applied: 0, dryRun: true, would: edits.length, failed: [] };
|
|
141
|
+
let applied = 0;
|
|
142
|
+
const failed = [];
|
|
143
|
+
for (const e of edits) {
|
|
144
|
+
try {
|
|
145
|
+
const abs = path.join(root, e.file);
|
|
146
|
+
const code = fs.readFileSync(abs, 'utf8');
|
|
147
|
+
// Match the file's own line ending. Splicing an LF line into a CRLF file leaves one stray
|
|
148
|
+
// ending, which reads as a whole-file change in some tooling and a lint error in others —
|
|
149
|
+
// noise attributable to the anchor rather than to the code. Brownfield trees have both.
|
|
150
|
+
const eol = code.includes('\r\n') ? '\r\n' : '\n';
|
|
151
|
+
const lines = code.split(eol);
|
|
152
|
+
lines.splice(e.atLine, 0, e.insert.replace(/\n$/, ''));
|
|
153
|
+
fs.writeFileSync(abs, lines.join(eol));
|
|
154
|
+
applied++;
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
// Fail-safe: one unwritable file yields fewer anchors, never a corrupted tree or a lost report.
|
|
158
|
+
failed.push({ file: e.file, aspec: e.aspec, reason: err instanceof Error ? err.message : String(err) });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { applied, dryRun: false, would: edits.length, failed };
|
|
162
|
+
}
|