@holmes-lab/holmes-kit 0.19.6 → 0.20.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 +96 -0
- package/README.md +8 -1
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve-status.d.ts +24 -0
- package/dist/holmes/cli/approve-status.js +166 -0
- package/dist/holmes/cli/approve-watch.d.ts +6 -0
- package/dist/holmes/cli/approve-watch.js +6 -0
- package/dist/holmes/cli/approve.d.ts +27 -0
- package/dist/holmes/cli/approve.js +68 -7
- package/dist/holmes/cli/cli-execution.d.ts +2 -0
- package/dist/holmes/cli/cli-execution.js +24 -0
- package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
- package/dist/holmes/cli/gitignore-merge.js +6 -2
- package/dist/holmes/cli/index.js +176 -139
- package/dist/holmes/cli/playbook-skills.d.ts +2 -22
- package/dist/holmes/cli/playbook-skills.js +10 -54
- package/dist/holmes/governance/approval-grants.d.ts +22 -0
- package/dist/holmes/governance/approval-grants.js +55 -1
- package/dist/holmes/governance/approval-queue.d.ts +43 -4
- package/dist/holmes/governance/approval-queue.js +86 -12
- package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
- package/dist/holmes/governance/display-id-aliases.js +86 -0
- package/dist/holmes/governance/ledger-store.d.ts +42 -14
- package/dist/holmes/governance/ledger-store.js +49 -3
- package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
- package/dist/holmes/governance/ledger-timeline.js +11 -1
- package/dist/holmes/governance/provenance-chain.d.ts +2 -0
- package/dist/holmes/governance/provenance-chain.js +13 -2
- package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
- package/dist/holmes/governance/provenance-ledger.js +69 -16
- package/dist/holmes/governance/session-context.d.ts +3 -0
- package/dist/holmes/governance/session-context.js +26 -8
- package/dist/holmes/guardrail/risk-classifier.js +2 -2
- package/dist/holmes/guardrail/write-target.js +2 -1
- package/dist/holmes/hooks/pre-tool-use.js +12 -1
- package/dist/holmes/hooks/stop.js +14 -3
- package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
- package/dist/holmes/mcp/handlers/adoption.js +186 -0
- package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
- package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
- package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
- package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
- package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
- package/dist/holmes/mcp/handlers/entity-store.js +212 -0
- package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
- package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
- package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
- package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
- package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
- package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
- package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
- package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
- package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
- package/dist/holmes/mcp/handlers/review-queries.js +79 -0
- package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
- package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +55 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.js +324 -0
- package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
- package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
- package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
- package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
- package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
- package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
- package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
- package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
- package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
- package/dist/holmes/mcp/handlers/test-execution.js +210 -0
- package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
- package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
- package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
- package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
- package/dist/holmes/mcp/handlers.d.ts +653 -424
- package/dist/holmes/mcp/handlers.js +267 -3250
- package/dist/holmes/mcp/server.js +2 -1
- package/dist/holmes/mcp/tool-schemas.js +43 -2
- package/dist/holmes/project/execution-context.d.ts +17 -0
- package/dist/holmes/project/execution-context.js +120 -0
- package/dist/holmes/project/installer-markers.d.ts +34 -0
- package/dist/holmes/project/installer-markers.js +65 -0
- package/dist/holmes/project/root.d.ts +3 -1
- package/dist/holmes/project/root.js +4 -3
- package/dist/holmes/project/workspace-identity.d.ts +29 -0
- package/dist/holmes/project/workspace-identity.js +181 -0
- package/dist/holmes/review/test-outcomes.d.ts +6 -1
- package/dist/holmes/review/test-outcomes.js +23 -5
- package/dist/holmes/review/test-runner.d.ts +18 -0
- package/dist/holmes/review/test-runner.js +132 -4
- package/dist/holmes/rtm/graph-store.d.ts +50 -1
- package/dist/holmes/rtm/graph-store.js +244 -3
- package/dist/holmes/rtm/incremental.d.ts +1 -0
- package/dist/holmes/rtm/incremental.js +12 -3
- package/dist/holmes/rtm/localize.js +7 -0
- package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
- package/dist/holmes/rtm/rtm-graph.js +13 -0
- package/dist/holmes/semantic/vector-cache.d.ts +16 -1
- package/dist/holmes/semantic/vector-cache.js +155 -19
- package/dist/holmes/spec/approval-status.d.ts +10 -0
- package/dist/holmes/spec/approval-status.js +7 -3
- package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
- package/dist/holmes/spec/entity-git-snapshot.js +276 -0
- package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
- package/dist/holmes/spec/entity-integration-discharge.js +233 -0
- package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
- package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
- package/dist/holmes/spec/entity-integration.d.ts +207 -0
- package/dist/holmes/spec/entity-integration.js +747 -0
- package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
- package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
- package/dist/holmes/spec/entity-renumber.d.ts +32 -0
- package/dist/holmes/spec/entity-renumber.js +156 -0
- package/dist/holmes/spec/entity-store.d.ts +135 -0
- package/dist/holmes/spec/entity-store.js +1051 -0
- package/dist/holmes/spec/entity-transaction.d.ts +89 -0
- package/dist/holmes/spec/entity-transaction.js +701 -0
- package/dist/holmes/spec/renumber.d.ts +58 -0
- package/dist/holmes/spec/renumber.js +200 -1
- package/dist/holmes/spec/spec-store.d.ts +3 -2
- package/dist/holmes/spec/spec-store.js +23 -2
- package/dist/holmes/spec/transition-policy.d.ts +36 -0
- package/dist/holmes/spec/transition-policy.js +135 -0
- package/package.json +1 -1
|
@@ -1,3 +1,61 @@
|
|
|
1
|
+
export interface SourceRenumberInventory {
|
|
2
|
+
files: Array<{
|
|
3
|
+
file: string;
|
|
4
|
+
before: string;
|
|
5
|
+
after: string;
|
|
6
|
+
version: string;
|
|
7
|
+
anchorIds: string[];
|
|
8
|
+
}>;
|
|
9
|
+
inventory: Array<{
|
|
10
|
+
file: string;
|
|
11
|
+
version: string;
|
|
12
|
+
}>;
|
|
13
|
+
diagnostics: Array<{
|
|
14
|
+
file: string;
|
|
15
|
+
reason: string;
|
|
16
|
+
}>;
|
|
17
|
+
proseCandidates: Array<{
|
|
18
|
+
file: string;
|
|
19
|
+
line: number;
|
|
20
|
+
text: string;
|
|
21
|
+
}>;
|
|
22
|
+
ignoreVersion: string | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* @implements A-SPEC-646
|
|
26
|
+
* Installer-owned text, blanked to spaces (length and line count preserved, so AST offsets and prose line
|
|
27
|
+
* numbers still address the real source). Measured 2026-09-14 in a workspace wired by `holmes-kit init`:
|
|
28
|
+
* the `.gitattributes` marker block carries `# @implements A-SPEC-256.1` (holmes-kit's OWN spec) and the
|
|
29
|
+
* installed skills mention `@implements`; both were "reference-bearing files with no supported comment
|
|
30
|
+
* grammar" and every integration in every init-wired workspace was blocked by the product's own files.
|
|
31
|
+
* Ownership, not the id, is the distinction: a workspace file anchoring an id no entity carries stays an
|
|
32
|
+
* explicit conflict (A-SPEC-629). Marker recognition reuses the installer's own definitions.
|
|
33
|
+
*/
|
|
34
|
+
export declare function maskInstallerOwned(source: string): string;
|
|
35
|
+
/** Plan directly from immutable bytes; filesystem discovery and Git object loading remain outside. */
|
|
36
|
+
export declare function planSourceTextRenumber(file: string, bytes: Buffer, pairs: Array<{
|
|
37
|
+
oldId: string;
|
|
38
|
+
newId: string;
|
|
39
|
+
}>, options?: {
|
|
40
|
+
inspectAllAnchors?: boolean;
|
|
41
|
+
}): Promise<Omit<SourceRenumberInventory, 'ignoreVersion'>>;
|
|
42
|
+
/**
|
|
43
|
+
* @implements A-SPEC-648
|
|
44
|
+
* Is this unfollowed symlink a VIEW of directories the walk already reaches, rather than a source?
|
|
45
|
+
* Measured 2026-09-14 in external workspaces on macOS and Linux: `holmes-kit init --agent all` writes
|
|
46
|
+
* `.agents/skills -> ../.claude/skills` (the Antigravity view of the one skills directory) and the walk
|
|
47
|
+
* diagnosed it like an aliased source, so `entity_renumber` was never executable in any three-harness
|
|
48
|
+
* workspace. The Git snapshot walk already treats an unchanged non-regular entry as untouched (A-SPEC-637).
|
|
49
|
+
* A directory link whose real path stays inside the workspace and is not the store or the entity control
|
|
50
|
+
* state contributes nothing — its files are reached (or deliberately skipped) through the real directory.
|
|
51
|
+
* A file link, a dangling link, a link leaving the workspace or one onto the store keeps the diagnostic.
|
|
52
|
+
* Nothing is followed either way.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isWorkspaceViewLink(root: string, abs: string, storeLocator: string): boolean;
|
|
55
|
+
export declare function planSourceRenumber(root: string, pairs: Array<{
|
|
56
|
+
oldId: string;
|
|
57
|
+
newId: string;
|
|
58
|
+
}>, storeLocator: string, originalSources?: ReadonlyMap<string, string>): Promise<SourceRenumberInventory>;
|
|
1
59
|
export interface RenumberPlan {
|
|
2
60
|
moves: {
|
|
3
61
|
from: string;
|
|
@@ -33,12 +33,17 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.maskInstallerOwned = maskInstallerOwned;
|
|
37
|
+
exports.planSourceTextRenumber = planSourceTextRenumber;
|
|
38
|
+
exports.isWorkspaceViewLink = isWorkspaceViewLink;
|
|
39
|
+
exports.planSourceRenumber = planSourceRenumber;
|
|
36
40
|
exports.rewriteAnchors = rewriteAnchors;
|
|
37
41
|
exports.planRenumber = planRenumber;
|
|
38
42
|
exports.applyRenumber = applyRenumber;
|
|
39
43
|
exports.readSourcesForRenumber = readSourcesForRenumber;
|
|
40
44
|
exports.readSpecsForRenumber = readSpecsForRenumber;
|
|
41
|
-
// @implements A-SPEC-255
|
|
45
|
+
// @implements A-SPEC-629.1, A-SPEC-628, A-SPEC-255
|
|
46
|
+
// @implements A-SPEC-640
|
|
42
47
|
/**
|
|
43
48
|
* Renumbering a spec, as a contract instead of a memory exercise.
|
|
44
49
|
*
|
|
@@ -65,6 +70,200 @@ exports.readSpecsForRenumber = readSpecsForRenumber;
|
|
|
65
70
|
const fs = __importStar(require("node:fs"));
|
|
66
71
|
const path = __importStar(require("node:path"));
|
|
67
72
|
const spec_id_1 = require("./spec-id");
|
|
73
|
+
const node_crypto_1 = require("node:crypto");
|
|
74
|
+
const cpg_scanner_1 = require("../cpg/cpg-scanner");
|
|
75
|
+
const ast_store_1 = require("../cpg/foundation/ast-store");
|
|
76
|
+
const ignore_1 = require("../project/ignore");
|
|
77
|
+
const change_source_1 = require("../project/change-source");
|
|
78
|
+
const anchor_ids_1 = require("../rtm/anchor-ids");
|
|
79
|
+
const entity_transaction_1 = require("./entity-transaction");
|
|
80
|
+
const installer_markers_1 = require("../project/installer-markers");
|
|
81
|
+
/**
|
|
82
|
+
* @implements A-SPEC-646
|
|
83
|
+
* Installer-owned text, blanked to spaces (length and line count preserved, so AST offsets and prose line
|
|
84
|
+
* numbers still address the real source). Measured 2026-09-14 in a workspace wired by `holmes-kit init`:
|
|
85
|
+
* the `.gitattributes` marker block carries `# @implements A-SPEC-256.1` (holmes-kit's OWN spec) and the
|
|
86
|
+
* installed skills mention `@implements`; both were "reference-bearing files with no supported comment
|
|
87
|
+
* grammar" and every integration in every init-wired workspace was blocked by the product's own files.
|
|
88
|
+
* Ownership, not the id, is the distinction: a workspace file anchoring an id no entity carries stays an
|
|
89
|
+
* explicit conflict (A-SPEC-629). Marker recognition reuses the installer's own definitions.
|
|
90
|
+
*/
|
|
91
|
+
function maskInstallerOwned(source) {
|
|
92
|
+
const blank = (text) => text.replace(/[^\n]/g, ' ');
|
|
93
|
+
if ((0, installer_markers_1.carriesMarker)(source))
|
|
94
|
+
return blank(source);
|
|
95
|
+
const lines = source.split('\n');
|
|
96
|
+
let inside = false;
|
|
97
|
+
for (let i = 0; i < lines.length; i++) {
|
|
98
|
+
const trimmed = lines[i].trim();
|
|
99
|
+
if (!inside && trimmed === installer_markers_1.HOLMES_BLOCK_BEGIN)
|
|
100
|
+
inside = true;
|
|
101
|
+
if (inside) {
|
|
102
|
+
const closing = trimmed === installer_markers_1.HOLMES_BLOCK_END;
|
|
103
|
+
lines[i] = blank(lines[i]);
|
|
104
|
+
if (closing)
|
|
105
|
+
inside = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
/** Plan directly from immutable bytes; filesystem discovery and Git object loading remain outside. */
|
|
111
|
+
async function planSourceTextRenumber(file, bytes, pairs, options = {}) {
|
|
112
|
+
const result = { files: [], inventory: [], diagnostics: [], proseCandidates: [] };
|
|
113
|
+
const ids = new Set(pairs.map(pair => pair.oldId));
|
|
114
|
+
const mentions = (text) => [...text.matchAll(new RegExp(ID_RE, 'g'))].some(match => options.inspectAllAnchors || ids.has(match[0]));
|
|
115
|
+
const source = bytes.toString('utf8'), version = 'sha256:' + (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex');
|
|
116
|
+
const visible = maskInstallerOwned(source); // A-SPEC-646 — judged text; rewriting stays on `source`
|
|
117
|
+
if (path.posix.basename(file) === '.gitignore') {
|
|
118
|
+
result.diagnostics.push({ file, reason: 'Nested ignore rules are not interpreted.' });
|
|
119
|
+
result.inventory.push({ file, version });
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
const supported = cpg_scanner_1.SCANNABLE_EXTENSIONS.includes(path.extname(file));
|
|
123
|
+
if (!supported && !mentions(visible))
|
|
124
|
+
return result;
|
|
125
|
+
result.inventory.push({ file, version });
|
|
126
|
+
if (!mentions(visible))
|
|
127
|
+
return result;
|
|
128
|
+
if (!Buffer.from(source).equals(bytes) || source.includes('\0')) {
|
|
129
|
+
result.diagnostics.push({ file, reason: 'Reference-bearing file is not UTF-8 text.' });
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
let after = source, prose = visible;
|
|
133
|
+
const anchorIds = new Set();
|
|
134
|
+
if (supported && (0, ast_store_1.languageFor)(file)) {
|
|
135
|
+
try {
|
|
136
|
+
const ast = await (0, ast_store_1.parseAst)(source, file);
|
|
137
|
+
if (!ast || ast.errorCount) {
|
|
138
|
+
result.diagnostics.push({ file, reason: 'Source syntax could not be parsed completely.' });
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const comments = ast.nodes.filter(node => /(?:^|_)comment$/.test(node.type)).sort((a, b) => a.start - b.start || b.end - a.end);
|
|
142
|
+
const outer = comments.filter((node, i) => !comments.slice(0, i).some(parent => parent.start <= node.start && parent.end >= node.end));
|
|
143
|
+
for (const node of outer.reverse()) {
|
|
144
|
+
const comment = source.slice(node.start, node.end), visibleComment = visible.slice(node.start, node.end);
|
|
145
|
+
for (const id of (0, anchor_ids_1.implementsIdLists)(visibleComment).flat())
|
|
146
|
+
if (options.inspectAllAnchors || ids.has(id))
|
|
147
|
+
anchorIds.add(id);
|
|
148
|
+
after = after.slice(0, node.start) + rewriteAnchors(comment, pairs) + after.slice(node.end);
|
|
149
|
+
prose = prose.slice(0, node.start) + visibleComment.replace(ANCHOR_LIST_RE, marker => ' '.repeat(marker.length)) + prose.slice(node.end);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
result.diagnostics.push({ file, reason: 'Source grammar is unavailable.' });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (visible.includes('@implements'))
|
|
158
|
+
result.diagnostics.push({ file, reason: 'Reference-bearing file has no supported comment grammar.' });
|
|
159
|
+
const originalLines = source.split('\n');
|
|
160
|
+
prose.split('\n').forEach((line, index) => { if (mentions(line))
|
|
161
|
+
result.proseCandidates.push({ file, line: index + 1, text: originalLines[index] }); });
|
|
162
|
+
if (after !== source || (options.inspectAllAnchors && anchorIds.size))
|
|
163
|
+
result.files.push({ file, before: source, after, version, anchorIds: [...anchorIds].sort() });
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* @implements A-SPEC-648
|
|
168
|
+
* Is this unfollowed symlink a VIEW of directories the walk already reaches, rather than a source?
|
|
169
|
+
* Measured 2026-09-14 in external workspaces on macOS and Linux: `holmes-kit init --agent all` writes
|
|
170
|
+
* `.agents/skills -> ../.claude/skills` (the Antigravity view of the one skills directory) and the walk
|
|
171
|
+
* diagnosed it like an aliased source, so `entity_renumber` was never executable in any three-harness
|
|
172
|
+
* workspace. The Git snapshot walk already treats an unchanged non-regular entry as untouched (A-SPEC-637).
|
|
173
|
+
* A directory link whose real path stays inside the workspace and is not the store or the entity control
|
|
174
|
+
* state contributes nothing — its files are reached (or deliberately skipped) through the real directory.
|
|
175
|
+
* A file link, a dangling link, a link leaving the workspace or one onto the store keeps the diagnostic.
|
|
176
|
+
* Nothing is followed either way.
|
|
177
|
+
*/
|
|
178
|
+
function isWorkspaceViewLink(root, abs, storeLocator) {
|
|
179
|
+
let real;
|
|
180
|
+
try {
|
|
181
|
+
real = fs.realpathSync(abs);
|
|
182
|
+
if (!fs.statSync(real).isDirectory())
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
const base = fs.realpathSync(root);
|
|
189
|
+
if (real !== base && !real.startsWith(base + path.sep))
|
|
190
|
+
return false;
|
|
191
|
+
const rel = path.relative(base, real).split(path.sep).join('/');
|
|
192
|
+
const under = (prefix) => rel === prefix || rel.startsWith(prefix + '/');
|
|
193
|
+
return !(under(storeLocator) || under('.ax/entities') || under('.ax/approvals'));
|
|
194
|
+
}
|
|
195
|
+
async function planSourceRenumber(root, pairs, storeLocator, originalSources = new Map()) {
|
|
196
|
+
const result = { files: [], inventory: [], diagnostics: [], proseCandidates: [], ignoreVersion: null };
|
|
197
|
+
const digest = (bytes) => 'sha256:' + (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex');
|
|
198
|
+
let ignoreText = '';
|
|
199
|
+
try {
|
|
200
|
+
const file = path.join(root, '.gitignore');
|
|
201
|
+
if (fs.lstatSync(file).isSymbolicLink())
|
|
202
|
+
throw new Error('Symlinked ignore policy');
|
|
203
|
+
const bytes = fs.readFileSync(file);
|
|
204
|
+
ignoreText = bytes.toString('utf8');
|
|
205
|
+
result.ignoreVersion = digest(bytes);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (error.code !== 'ENOENT')
|
|
209
|
+
result.diagnostics.push({ file: '.gitignore', reason: 'Ignore policy could not be read safely.' });
|
|
210
|
+
}
|
|
211
|
+
const ignore = (0, ignore_1.parseIgnore)(ignoreText);
|
|
212
|
+
for (const pattern of ignore.unsupportedPatterns())
|
|
213
|
+
result.diagnostics.push({ file: '.gitignore', reason: 'Unsupported ignore pattern: ' + pattern });
|
|
214
|
+
const walkSources = async (relative) => {
|
|
215
|
+
let entries;
|
|
216
|
+
try {
|
|
217
|
+
entries = fs.readdirSync(path.join(root, relative), { withFileTypes: true });
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
result.diagnostics.push({ file: relative || '.', reason: 'Unreadable source directory.' });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
224
|
+
const file = relative ? relative + '/' + entry.name : entry.name, abs = path.join(root, file);
|
|
225
|
+
const controlState = file === '.ax/entities' || file === '.ax/approvals' || (0, change_source_1.isDerivedState)(file + (entry.isDirectory() ? '/' : ''));
|
|
226
|
+
if (controlState || file === storeLocator || file.startsWith(storeLocator + '/') || (entry.isDirectory() && cpg_scanner_1.SKIP.has(entry.name)) || ignore.isIgnored(file))
|
|
227
|
+
continue;
|
|
228
|
+
// @implements A-SPEC-640 — the publication primitive's transients (temp, aside) are never sources.
|
|
229
|
+
if ((0, entity_transaction_1.isEntityTransientName)(entry.name))
|
|
230
|
+
continue;
|
|
231
|
+
if (entry.isSymbolicLink()) {
|
|
232
|
+
if (isWorkspaceViewLink(root, abs, storeLocator))
|
|
233
|
+
continue; // A-SPEC-648 — a view, not a source
|
|
234
|
+
result.diagnostics.push({ file, reason: 'Symlinked source is not followed.' });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (entry.isDirectory()) {
|
|
238
|
+
if (fs.existsSync(path.join(abs, 'pyvenv.cfg')))
|
|
239
|
+
continue;
|
|
240
|
+
await walkSources(file);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (!entry.isFile()) {
|
|
244
|
+
result.diagnostics.push({ file, reason: 'Unsupported source file type.' });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (file === '.gitignore')
|
|
248
|
+
continue;
|
|
249
|
+
let bytes;
|
|
250
|
+
try {
|
|
251
|
+
bytes = originalSources.has(file) ? Buffer.from(originalSources.get(file)) : fs.readFileSync(abs);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
result.diagnostics.push({ file, reason: 'Unreadable source file.' });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const planned = await planSourceTextRenumber(file, bytes, pairs);
|
|
258
|
+
result.files.push(...planned.files);
|
|
259
|
+
result.inventory.push(...planned.inventory);
|
|
260
|
+
result.diagnostics.push(...planned.diagnostics);
|
|
261
|
+
result.proseCandidates.push(...planned.proseCandidates);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
await walkSources('');
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
68
267
|
/** The anchor grammar, shared with `rtm/anchor-ids.ts` so the two judgments cannot drift apart. */
|
|
69
268
|
const ID_RE = String.raw `(?:REQ|A-SPEC|H-SPEC|C-SPEC|T-SPEC)-\d{3,}(?:\.\d+)?`;
|
|
70
269
|
const ANCHOR_LIST_RE = new RegExp(String.raw `(@implements[ \t]+)(${ID_RE}(?:[ \t]*,[ \t]*${ID_RE})*)`, 'g');
|
|
@@ -96,8 +96,8 @@ export declare class LocalMarkdownRepository implements SpecStore {
|
|
|
96
96
|
* Writes are SERIALIZED per store instance. The expectedVersion check and the file write are
|
|
97
97
|
* separated by an `await this.list()` (the orphan sweep), so two concurrent write() calls could
|
|
98
98
|
* both pass the check before either wrote — a check-then-act hole round-3 review demonstrated.
|
|
99
|
-
* The
|
|
100
|
-
*
|
|
99
|
+
* The promise chain preserves same-instance ordering. Holmes workspace writes also hold the
|
|
100
|
+
* entity-store lock across asynchronous lookups, coordinating with explicit adoption.
|
|
101
101
|
*/
|
|
102
102
|
private writeChain;
|
|
103
103
|
write(spec: Spec, opts?: {
|
|
@@ -106,6 +106,7 @@ export declare class LocalMarkdownRepository implements SpecStore {
|
|
|
106
106
|
version: string;
|
|
107
107
|
}>;
|
|
108
108
|
private writeSerialized;
|
|
109
|
+
private writeLegacy;
|
|
109
110
|
private findFilePath;
|
|
110
111
|
list(): Promise<Spec[]>;
|
|
111
112
|
read(id: string): Promise<{
|
|
@@ -36,7 +36,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.MemorySpecStore = exports.LocalMarkdownRepository = exports.TargetPathOccupiedError = exports.SpecVersionConflictError = void 0;
|
|
37
37
|
exports.unreadableSpecFiles = unreadableSpecFiles;
|
|
38
38
|
exports.notFoundReason = notFoundReason;
|
|
39
|
+
// @implements A-SPEC-627, A-SPEC-151, A-SPEC-100.1
|
|
39
40
|
const node_crypto_1 = require("node:crypto");
|
|
41
|
+
const entity_store_1 = require("./entity-store");
|
|
40
42
|
const fs = __importStar(require("node:fs"));
|
|
41
43
|
const path = __importStar(require("node:path"));
|
|
42
44
|
const spec_parser_1 = require("./spec-parser");
|
|
@@ -186,8 +188,8 @@ class LocalMarkdownRepository {
|
|
|
186
188
|
* Writes are SERIALIZED per store instance. The expectedVersion check and the file write are
|
|
187
189
|
* separated by an `await this.list()` (the orphan sweep), so two concurrent write() calls could
|
|
188
190
|
* both pass the check before either wrote — a check-then-act hole round-3 review demonstrated.
|
|
189
|
-
* The
|
|
190
|
-
*
|
|
191
|
+
* The promise chain preserves same-instance ordering. Holmes workspace writes also hold the
|
|
192
|
+
* entity-store lock across asynchronous lookups, coordinating with explicit adoption.
|
|
191
193
|
*/
|
|
192
194
|
writeChain = Promise.resolve();
|
|
193
195
|
async write(spec, opts) {
|
|
@@ -197,6 +199,19 @@ class LocalMarkdownRepository {
|
|
|
197
199
|
return p;
|
|
198
200
|
}
|
|
199
201
|
async writeSerialized(spec, opts) {
|
|
202
|
+
const newPath = this.targetPathFor(spec);
|
|
203
|
+
try {
|
|
204
|
+
return await (0, entity_store_1.writeEntityRepository)(this.root, spec, newPath, opts?.expectedVersion, () => this.writeLegacy(spec, opts));
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
if (e instanceof entity_store_1.EntityStoreError && e.code === 'version-conflict')
|
|
208
|
+
throw new SpecVersionConflictError(e.message);
|
|
209
|
+
if (e instanceof entity_store_1.EntityStoreError && e.code === 'target-occupied')
|
|
210
|
+
throw new TargetPathOccupiedError(e.message, newPath);
|
|
211
|
+
throw e;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async writeLegacy(spec, opts) {
|
|
200
215
|
const newPath = this.targetPathFor(spec);
|
|
201
216
|
// @implements A-SPEC-151
|
|
202
217
|
// Checked BEFORE any filesystem change. A refusal that half-wrote would be the worst outcome:
|
|
@@ -264,6 +279,9 @@ class LocalMarkdownRepository {
|
|
|
264
279
|
return result;
|
|
265
280
|
}
|
|
266
281
|
async list() {
|
|
282
|
+
const entitySnapshot = (0, entity_store_1.readEntityRepositorySnapshot)(this.root);
|
|
283
|
+
if (entitySnapshot !== undefined)
|
|
284
|
+
return entitySnapshot.map(record => record.spec);
|
|
267
285
|
const out = [];
|
|
268
286
|
const walk = (d) => {
|
|
269
287
|
if (!fs.existsSync(d))
|
|
@@ -292,6 +310,9 @@ class LocalMarkdownRepository {
|
|
|
292
310
|
return out;
|
|
293
311
|
}
|
|
294
312
|
async read(id) {
|
|
313
|
+
const entitySnapshot = (0, entity_store_1.readEntityRepositorySnapshot)(this.root);
|
|
314
|
+
if (entitySnapshot !== undefined)
|
|
315
|
+
return entitySnapshot.find(record => record.spec.id === id) ?? null;
|
|
295
316
|
const file = await this.findFilePath(id);
|
|
296
317
|
if (file === null)
|
|
297
318
|
return null;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Spec } from './spec-parser';
|
|
2
|
+
/** Withdrawal policy is shared; callers retain authority and idempotent no-op handling. */
|
|
3
|
+
export declare function prepareLifecycleSpec(spec: Spec, all: Spec[], operation: 'unseal' | 'retire', reason?: string): {
|
|
4
|
+
ok: false;
|
|
5
|
+
reason: string;
|
|
6
|
+
candidate?: undefined;
|
|
7
|
+
dependents?: undefined;
|
|
8
|
+
} | {
|
|
9
|
+
ok: true;
|
|
10
|
+
candidate: Spec;
|
|
11
|
+
dependents: Spec[];
|
|
12
|
+
reason?: undefined;
|
|
13
|
+
};
|
|
14
|
+
/** Act-time policy only: callers own authority, version checks, persistence and audit. */
|
|
15
|
+
export declare function prepareApprovedSpec(spec: Spec, specs: Spec[], resolver: (specs: Spec[]) => (id: string) => Spec | null, readFile: (relative: string) => string | null): {
|
|
16
|
+
ok: false;
|
|
17
|
+
reason: string;
|
|
18
|
+
findings?: undefined;
|
|
19
|
+
candidate?: undefined;
|
|
20
|
+
digest?: undefined;
|
|
21
|
+
parentDigests?: undefined;
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
reason: string;
|
|
25
|
+
findings: import("./validator").Finding[];
|
|
26
|
+
candidate?: undefined;
|
|
27
|
+
digest?: undefined;
|
|
28
|
+
parentDigests?: undefined;
|
|
29
|
+
} | {
|
|
30
|
+
ok: true;
|
|
31
|
+
candidate: Spec;
|
|
32
|
+
digest: string;
|
|
33
|
+
parentDigests: Record<string, string>;
|
|
34
|
+
reason?: undefined;
|
|
35
|
+
findings?: undefined;
|
|
36
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.prepareLifecycleSpec = prepareLifecycleSpec;
|
|
4
|
+
exports.prepareApprovedSpec = prepareApprovedSpec;
|
|
5
|
+
const validator_1 = require("./validator");
|
|
6
|
+
const spec_digest_1 = require("./spec-digest");
|
|
7
|
+
const breaking_change_1 = require("./breaking-change");
|
|
8
|
+
const compat_impact_1 = require("./compat-impact");
|
|
9
|
+
const approval_blockers_1 = require("./approval-blockers");
|
|
10
|
+
/** Withdrawal policy is shared; callers retain authority and idempotent no-op handling. */
|
|
11
|
+
function prepareLifecycleSpec(spec, all, operation, reason) {
|
|
12
|
+
const dependents = all.filter(s => s.id !== spec.id && s.dependsOn.includes(spec.id));
|
|
13
|
+
const blocking = dependents.filter(s => s.status === 'approved').map(s => s.id);
|
|
14
|
+
if (blocking.length > 0)
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
reason: operation === 'retire'
|
|
18
|
+
? `${spec.id}을(를) 폐기하면 approved 문서 ${blocking.join(', ')}의 사슬이 끊깁니다 — 지금 무언가를 지탱하고 있는 사슬이므로 거부합니다.` + ' 해당 문서를 먼저 폐기하거나 부모를 다른 문서로 옮기십시오.'
|
|
19
|
+
: `${spec.id}의 봉인을 해제하면 approved 문서 ${blocking.join(', ')}의 parent_digests 가 stale 이 됩니다 — 지금 그 사슬을 지탱하고 있으므로 거부합니다.` + ' 해당 문서를 먼저 해제/폐기하거나 부모를 다른 문서로 옮기십시오.',
|
|
20
|
+
};
|
|
21
|
+
const frontmatter = { ...spec.frontmatter };
|
|
22
|
+
if (operation === 'unseal') {
|
|
23
|
+
delete frontmatter.approved_digest;
|
|
24
|
+
delete frontmatter.parent_digests;
|
|
25
|
+
}
|
|
26
|
+
else if (typeof reason === 'string' && reason.trim() && frontmatter.superseded_by === undefined) {
|
|
27
|
+
frontmatter.retired_reason = reason.trim();
|
|
28
|
+
}
|
|
29
|
+
const candidate = { ...spec, status: operation === 'unseal' ? 'draft' : 'outdated', frontmatter };
|
|
30
|
+
return { ok: true, candidate, dependents };
|
|
31
|
+
}
|
|
32
|
+
/** Act-time policy only: callers own authority, version checks, persistence and audit. */
|
|
33
|
+
function prepareApprovedSpec(spec, specs, resolver, readFile) {
|
|
34
|
+
// @implements A-SPEC-188 — duplicates make the id ambiguous, for the SPEC and for its
|
|
35
|
+
// PARENTS alike. Round-3 probed the parent half: with a stray duplicate of the parent
|
|
36
|
+
// walking first, the child's parent_digests sealed the stray's (forged) digest — the very
|
|
37
|
+
// snapshot drift detection compares against — while the refusal principle stated one field
|
|
38
|
+
// away was "the tool does not pick a copy". Refuse for every ambiguous id in the act.
|
|
39
|
+
const ambiguous = [spec.id, ...spec.dependsOn].filter((id) => specs.filter((s) => s.id === id).length > 1);
|
|
40
|
+
if (ambiguous.length > 0) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
reason: `${[...new Set(ambiguous)].join(', ')}이(가) 스토어에 두 번 이상 존재합니다 — 어느 사본이 진본인지 도구가 고를 수 없어 봉인 전에 거부합니다.`
|
|
44
|
+
+ ' doctor로 중복 파일을 확인해 하나로 정리한 뒤 다시 승인하십시오.',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// @implements A-SPEC-168
|
|
48
|
+
// A child may not be approved under a parent that is not. The chain's meaning is that each
|
|
49
|
+
// layer is justified by the one above it, and measured 2026-08-08 that failed in four places —
|
|
50
|
+
// including A-SPEC-100.1/100.2, the core MCP guardrail, whose design H-SPEC-100 nobody ever
|
|
51
|
+
// approved. The obligation is on the ACT: existing approvals are untouched, because making it
|
|
52
|
+
// a state rule is what REQ-146 measured turning 302 specs into violations at once.
|
|
53
|
+
// @implements A-SPEC-182
|
|
54
|
+
// Shared with the gate rather than restated. Adversarial review found this check living ONLY
|
|
55
|
+
// here: the gate reported "nothing blocks" for a draft under a draft parent — the modal state
|
|
56
|
+
// of top-down authoring — and the author learned otherwise only by attempting the approval.
|
|
57
|
+
const parentIssues = (0, approval_blockers_1.parentBlockers)(spec, resolver(specs));
|
|
58
|
+
if (parentIssues.length > 0)
|
|
59
|
+
return { ok: false, reason: parentIssues[0] };
|
|
60
|
+
// Validate AS IT WILL BE — approved. Approval-gated checks (placeholder citations, 4-quadrant
|
|
61
|
+
// GWT) must fire NOW, not one turn after the seal exists.
|
|
62
|
+
const candidate = { ...spec, status: 'approved', frontmatter: { ...spec.frontmatter } };
|
|
63
|
+
delete candidate.frontmatter.approved_digest; // seals are recomputed by this act, never inherited
|
|
64
|
+
delete candidate.frontmatter.parent_digests;
|
|
65
|
+
const prevalidation = (0, validator_1.validateSpec)(candidate, resolver(specs));
|
|
66
|
+
const blocking = prevalidation.findings.filter((f) => f.level === 'error' && !approval_blockers_1.SUPPLIED_BY_APPROVAL.has(f.code) // this act supplies exactly those
|
|
67
|
+
);
|
|
68
|
+
// @implements A-SPEC-641 — the reason names the findings: measured 2026-09-14, the bare sentence
|
|
69
|
+
// reached renumber and integration operators as "validation errors block approval" with nothing
|
|
70
|
+
// to act on. The multi-document acts prefix the document id.
|
|
71
|
+
if (blocking.length > 0)
|
|
72
|
+
return { ok: false, reason: 'validation errors block approval: ' + blocking.map((f) => f.message).join('; '), findings: blocking };
|
|
73
|
+
// @implements A-SPEC-146
|
|
74
|
+
// ADR-013's duty is owed by the ACT, not by the document's static validity. Measured: putting
|
|
75
|
+
// this in `validateSpec`'s requiredFields produced 38 ART-3 violations and a Stop hook that
|
|
76
|
+
// blocked every turn, because all 38 governed A-SPECs are already approved. Here it constrains
|
|
77
|
+
// the future without invalidating the past — and a re-approval, which by definition means the
|
|
78
|
+
// content changed, is exactly when the question is due.
|
|
79
|
+
const breakingIssue = (0, breaking_change_1.checkBreakingChangeDeclared)(candidate);
|
|
80
|
+
if (breakingIssue)
|
|
81
|
+
return { ok: false, reason: breakingIssue };
|
|
82
|
+
// @implements A-SPEC-565.1 — the compat declaration duty rides the SAME act (REQ-565): sealing
|
|
83
|
+
// is when "did you consider the three harnesses and the three OSes" is due, and act-time is
|
|
84
|
+
// what keeps 512 already-approved specs out of retroactive violation (the 38-violation incident
|
|
85
|
+
// above). The bound reader feeds the OS cross-check from this root's working tree.
|
|
86
|
+
const compatIssue = (0, compat_impact_1.checkCompatDeclared)(candidate, {
|
|
87
|
+
// No root → no working tree to read: the OS cross-check skips file-by-file (fail-open),
|
|
88
|
+
// while the declaration syntax itself is still enforced — the duty never depends on `root`.
|
|
89
|
+
readFile,
|
|
90
|
+
});
|
|
91
|
+
if (compatIssue)
|
|
92
|
+
return { ok: false, reason: compatIssue };
|
|
93
|
+
// @implements A-SPEC-182
|
|
94
|
+
// A document whose prose is still the generator's placeholder must not be sealed. Measured
|
|
95
|
+
// 2026-08-13 on a brownfield adoption: H-SPEC-100 took `status: approved` and an
|
|
96
|
+
// `approved_digest` with all seven prose sections reading "TODO — a human writes this" —
|
|
97
|
+
// while its own first line said approving is the point a description becomes normative.
|
|
98
|
+
// Placed on the ACT, not in `validateSpec`, for the reason A-SPEC-146 measured: a static
|
|
99
|
+
// predicate turns every already-approved spec into a violation and bricks the harness.
|
|
100
|
+
const stubs = (0, approval_blockers_1.placeholderSections)(candidate);
|
|
101
|
+
if (stubs.length > 0) {
|
|
102
|
+
return { ok: false, reason: (0, approval_blockers_1.placeholderMessage)(stubs) };
|
|
103
|
+
}
|
|
104
|
+
// @implements A-SPEC-505.1 — acceptance substance, judged at the ACT on the same candidate
|
|
105
|
+
// the seal would freeze, like the placeholder gate above (154/428 approved REQs here are
|
|
106
|
+
// non-stated legacy; a validateSpec predicate would brick them all). Judged HERE and not
|
|
107
|
+
// before the approval channels: an earlier extra read shifts the optimistic-concurrency
|
|
108
|
+
// window and lets a mid-approval edit get sealed (store-integrity contract: edits win,
|
|
109
|
+
// approvals lose). The post-grant refusal it costs is the standing property of every
|
|
110
|
+
// act-time blocker in this block (breaking_change, placeholder) — one ordering, one truth.
|
|
111
|
+
const unactionable = (0, approval_blockers_1.unactionableCriteriaBlocker)(candidate);
|
|
112
|
+
if (unactionable)
|
|
113
|
+
return { ok: false, reason: unactionable };
|
|
114
|
+
// Parents-first: a sealed child snapshotting an unsealed parent would pin nothing. This loop
|
|
115
|
+
// exists to COLLECT the digests; the refusal inside it is now a backstop, because
|
|
116
|
+
// `parentBlockers` above already returns for the same condition with the same sentence. Kept
|
|
117
|
+
// rather than deleted so a future change to that function cannot silently let an unsealed
|
|
118
|
+
// parent through — but note the wording lives there, and only there, if it is ever edited.
|
|
119
|
+
const parentDigests = {};
|
|
120
|
+
for (const pid of spec.dependsOn) {
|
|
121
|
+
const parent = specs.find((s) => s.id === pid);
|
|
122
|
+
if (parent && parent.status === 'approved') {
|
|
123
|
+
const parentSeal = (0, spec_digest_1.sealOf)(parent).approvedDigest;
|
|
124
|
+
if (!parentSeal)
|
|
125
|
+
return { ok: false, reason: (0, approval_blockers_1.unsealedParentMessage)(pid) };
|
|
126
|
+
parentDigests[pid] = parentSeal;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// @implements A-SPEC-135
|
|
130
|
+
// The caller compares the returned digest with the prior seal to record seal movement.
|
|
131
|
+
const digest = (0, spec_digest_1.specDigest)(candidate);
|
|
132
|
+
candidate.frontmatter.approved_digest = digest;
|
|
133
|
+
candidate.frontmatter.parent_digests = parentDigests;
|
|
134
|
+
return { ok: true, candidate, digest, parentDigests };
|
|
135
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "@implements A-SPEC-209",
|
|
3
3
|
"name": "@holmes-lab/holmes-kit",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.20.0",
|
|
5
5
|
"description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
|
|
6
6
|
"main": "dist/holmes/mcp/server.js",
|
|
7
7
|
"types": "dist/holmes/mcp/server.d.ts",
|