mason-context 0.20.2 → 0.22.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 +25 -0
- package/README.md +24 -5
- package/dist/mason-audit.js +1755 -698
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-drift.js +662 -193
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +862 -198
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +8713 -8484
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +1088 -380
- package/dist/mason-review.js.map +1 -1
- package/dist/mason-runtime.js +297 -148
- package/dist/mason-runtime.js.map +1 -1
- package/dist/mason.js +7566 -5905
- package/dist/mason.js.map +1 -1
- package/package.json +11 -4
package/dist/mason-hook.js
CHANGED
|
@@ -1,40 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
if (!import.meta.url.endsWith("/mason-runtime.js")) await import("./mason-runtime.js").then(m => m.registerRuntime());
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __esm = (fn, res) => function __init() {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
};
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
3
12
|
|
|
4
|
-
// src/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
// src/audit/types.ts
|
|
14
|
+
var types_exports = {};
|
|
15
|
+
__export(types_exports, {
|
|
16
|
+
ALL_CHECKS: () => ALL_CHECKS
|
|
17
|
+
});
|
|
18
|
+
var ALL_CHECKS;
|
|
19
|
+
var init_types = __esm({
|
|
20
|
+
"src/audit/types.ts"() {
|
|
21
|
+
"use strict";
|
|
22
|
+
ALL_CHECKS = [
|
|
23
|
+
"deleted-reference",
|
|
24
|
+
"new-module",
|
|
25
|
+
"stale-count",
|
|
26
|
+
"dead-command",
|
|
27
|
+
"deps-changed",
|
|
28
|
+
"decision-anchor-drift"
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
});
|
|
14
32
|
|
|
15
|
-
// src/
|
|
16
|
-
import
|
|
17
|
-
import path3 from "path";
|
|
18
|
-
import { randomUUID } from "crypto";
|
|
33
|
+
// src/hook/hook.ts
|
|
34
|
+
import { z as z6 } from "zod";
|
|
19
35
|
|
|
20
|
-
// src/utils/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const slash = value.replace(/\\/g, "/");
|
|
24
|
-
if (!slash || slash.includes("\0") || path.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash)) return null;
|
|
25
|
-
if (slash.split("/").includes("..")) return null;
|
|
26
|
-
const normalized = path.posix.normalize(slash).replace(/\/$/, "");
|
|
27
|
-
return normalized === "." ? null : normalized;
|
|
28
|
-
}
|
|
29
|
-
function anchorMatches(anchor, file) {
|
|
30
|
-
const a = normalizeRepoPath(anchor);
|
|
31
|
-
const f = normalizeRepoPath(file);
|
|
32
|
-
return a !== null && f !== null && (a === f || f.startsWith(`${a}/`));
|
|
36
|
+
// src/utils/validation.ts
|
|
37
|
+
function isRecord(value) {
|
|
38
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
33
39
|
}
|
|
34
|
-
function
|
|
35
|
-
return
|
|
40
|
+
function isStringArray(value) {
|
|
41
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
36
42
|
}
|
|
37
43
|
|
|
44
|
+
// src/hook/hook.ts
|
|
45
|
+
import fs7 from "fs/promises";
|
|
46
|
+
import path9 from "path";
|
|
47
|
+
import os2 from "os";
|
|
48
|
+
|
|
49
|
+
// src/audit/inspection.ts
|
|
50
|
+
import fg2 from "fast-glob";
|
|
51
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
52
|
+
import path3 from "path";
|
|
53
|
+
|
|
38
54
|
// src/utils/git-read.ts
|
|
39
55
|
import { execFile } from "child_process";
|
|
40
56
|
import { promisify } from "util";
|
|
@@ -57,7 +73,19 @@ async function profilePhase(label, run) {
|
|
|
57
73
|
|
|
58
74
|
// src/utils/git-read.ts
|
|
59
75
|
var execute = promisify(execFile);
|
|
60
|
-
var commands = /* @__PURE__ */ new Set([
|
|
76
|
+
var commands = /* @__PURE__ */ new Set([
|
|
77
|
+
"rev-parse",
|
|
78
|
+
"symbolic-ref",
|
|
79
|
+
"for-each-ref",
|
|
80
|
+
"ls-files",
|
|
81
|
+
"check-ignore",
|
|
82
|
+
"status",
|
|
83
|
+
"log",
|
|
84
|
+
"diff",
|
|
85
|
+
"ls-tree",
|
|
86
|
+
"show",
|
|
87
|
+
"merge-base"
|
|
88
|
+
]);
|
|
61
89
|
function execGit(args, options) {
|
|
62
90
|
const command = args.find((arg) => !arg.startsWith("-")) ?? "other";
|
|
63
91
|
return profilePhase("git." + (commands.has(command) ? command : "other"), async () => {
|
|
@@ -77,11 +105,127 @@ import fs from "fs/promises";
|
|
|
77
105
|
import { constants } from "fs";
|
|
78
106
|
import path2 from "path";
|
|
79
107
|
import fg from "fast-glob";
|
|
80
|
-
|
|
108
|
+
|
|
109
|
+
// src/utils/paths.ts
|
|
110
|
+
import path from "path";
|
|
111
|
+
function normalizeRepoPath(value) {
|
|
112
|
+
const slash = value.replace(/\\/g, "/");
|
|
113
|
+
if (!slash || slash.includes("\0") || path.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash))
|
|
114
|
+
return null;
|
|
115
|
+
if (slash.split("/").includes("..")) return null;
|
|
116
|
+
const normalized = path.posix.normalize(slash).replace(/\/$/, "");
|
|
117
|
+
return normalized === "." ? null : normalized;
|
|
118
|
+
}
|
|
119
|
+
function isWithinRoot(root, candidate) {
|
|
120
|
+
const relative = path.relative(root, candidate);
|
|
121
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
122
|
+
}
|
|
123
|
+
function anchorMatches(anchor, file) {
|
|
124
|
+
const a = normalizeRepoPath(anchor);
|
|
125
|
+
const f = normalizeRepoPath(file);
|
|
126
|
+
return a !== null && f !== null && (a === f || f.startsWith(`${a}/`));
|
|
127
|
+
}
|
|
128
|
+
function matchingPaths(anchors, files) {
|
|
129
|
+
return [...new Set(files)].filter(
|
|
130
|
+
(file) => anchors.some((anchor) => anchorMatches(anchor, file))
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
function indexMatchingPaths(scopes, files) {
|
|
134
|
+
const node = () => ({ children: /* @__PURE__ */ new Map(), scopes: /* @__PURE__ */ new Set() });
|
|
135
|
+
const root = node();
|
|
136
|
+
const matches = scopes.map(() => []);
|
|
137
|
+
scopes.forEach((anchors, index) => {
|
|
138
|
+
for (const anchor of anchors) {
|
|
139
|
+
const normalized = normalizeRepoPath(anchor);
|
|
140
|
+
if (!normalized) continue;
|
|
141
|
+
let current = root;
|
|
142
|
+
for (const part of normalized.split("/")) {
|
|
143
|
+
if (!current.children.has(part)) current.children.set(part, node());
|
|
144
|
+
current = current.children.get(part);
|
|
145
|
+
}
|
|
146
|
+
current.scopes.add(index);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
for (const file of new Set(files)) {
|
|
150
|
+
const normalized = normalizeRepoPath(file);
|
|
151
|
+
if (!normalized) continue;
|
|
152
|
+
const selected = /* @__PURE__ */ new Set();
|
|
153
|
+
let current = root;
|
|
154
|
+
for (const part of normalized.split("/")) {
|
|
155
|
+
current = current.children.get(part);
|
|
156
|
+
if (!current) break;
|
|
157
|
+
for (const index of current.scopes) selected.add(index);
|
|
158
|
+
}
|
|
159
|
+
for (const index of selected) matches[index].push(file);
|
|
160
|
+
}
|
|
161
|
+
return matches;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/utils/files.ts
|
|
165
|
+
var SOURCE_EXTENSIONS = [
|
|
166
|
+
"ts",
|
|
167
|
+
"tsx",
|
|
168
|
+
"js",
|
|
169
|
+
"jsx",
|
|
170
|
+
"mts",
|
|
171
|
+
"cts",
|
|
172
|
+
"mjs",
|
|
173
|
+
"cjs",
|
|
174
|
+
"vue",
|
|
175
|
+
"svelte",
|
|
176
|
+
"kt",
|
|
177
|
+
"kts",
|
|
178
|
+
"java",
|
|
179
|
+
"py",
|
|
180
|
+
"go",
|
|
181
|
+
"rs",
|
|
182
|
+
"swift",
|
|
183
|
+
"rb",
|
|
184
|
+
"cs",
|
|
185
|
+
"cpp",
|
|
186
|
+
"c",
|
|
187
|
+
"h",
|
|
188
|
+
"hpp",
|
|
189
|
+
"dart",
|
|
190
|
+
"php"
|
|
191
|
+
];
|
|
81
192
|
var SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
|
|
193
|
+
var SOURCE_IGNORE = [
|
|
194
|
+
"**/node_modules/**",
|
|
195
|
+
"**/dist/**",
|
|
196
|
+
"**/build/**",
|
|
197
|
+
"**/.gradle/**",
|
|
198
|
+
"**/target/**",
|
|
199
|
+
"**/.git/**",
|
|
200
|
+
"**/.mason/**",
|
|
201
|
+
"**/vendor/**",
|
|
202
|
+
"**/__pycache__/**",
|
|
203
|
+
"**/venv/**",
|
|
204
|
+
"**/.venv/**",
|
|
205
|
+
"**/*.min.*",
|
|
206
|
+
"**/*.map",
|
|
207
|
+
"**/*.lock",
|
|
208
|
+
"**/generated/**",
|
|
209
|
+
"**/*.generated.*",
|
|
210
|
+
"**/R.java",
|
|
211
|
+
"**/BuildConfig.java",
|
|
212
|
+
"**/package-lock.json",
|
|
213
|
+
"**/yarn.lock",
|
|
214
|
+
"**/pnpm-lock.yaml"
|
|
215
|
+
];
|
|
82
216
|
var MAX_SOURCE_BYTES = 1024 * 1024;
|
|
217
|
+
function isSensitiveFile(file) {
|
|
218
|
+
return file.split(/[\\/]/).some(
|
|
219
|
+
(part) => /^(?:\.env(?:\..*)?|id_rsa.*|id_ed25519.*)$|\.(?:pem|key|p12|pfx|jks|keystore)$|credentials\.|secret|^local\.properties$/i.test(
|
|
220
|
+
part
|
|
221
|
+
)
|
|
222
|
+
);
|
|
223
|
+
}
|
|
83
224
|
async function readBoundedFile(file, maxBytes) {
|
|
84
|
-
const handle = await fs.open(
|
|
225
|
+
const handle = await fs.open(
|
|
226
|
+
file,
|
|
227
|
+
constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW
|
|
228
|
+
);
|
|
85
229
|
try {
|
|
86
230
|
const stat = await handle.stat();
|
|
87
231
|
if (!stat.isFile() || stat.size > maxBytes) return null;
|
|
@@ -97,15 +241,168 @@ async function readBoundedFile(file, maxBytes) {
|
|
|
97
241
|
await handle.close();
|
|
98
242
|
}
|
|
99
243
|
}
|
|
244
|
+
async function loadProjectConfig(root) {
|
|
245
|
+
try {
|
|
246
|
+
const canonicalRoot = await fs.realpath(root);
|
|
247
|
+
const configPath = await fs.realpath(path2.join(root, ".mason/config.json"));
|
|
248
|
+
if (!isWithinRoot(canonicalRoot, configPath))
|
|
249
|
+
throw new Error("Project configuration resolves outside the repository");
|
|
250
|
+
const raw = await readBoundedFile(configPath, 64 * 1024);
|
|
251
|
+
if (raw === null)
|
|
252
|
+
throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
|
|
253
|
+
const value = JSON.parse(raw);
|
|
254
|
+
if (!isRecord(value)) throw new Error("Expected a configuration object");
|
|
255
|
+
const config = {};
|
|
256
|
+
for (const key of ["patterns", "alwaysInclude", "ignore"]) {
|
|
257
|
+
if (value[key] === void 0) continue;
|
|
258
|
+
if (!isStringArray(value[key])) {
|
|
259
|
+
throw new Error(`Configuration ${key} must be an array of strings`);
|
|
260
|
+
}
|
|
261
|
+
config[key] = value[key];
|
|
262
|
+
}
|
|
263
|
+
if (value.audit !== void 0) {
|
|
264
|
+
const audit = value.audit;
|
|
265
|
+
if (!isRecord(audit)) throw new Error("Configuration audit must be an object");
|
|
266
|
+
const patterns = (raw2) => {
|
|
267
|
+
if (!isStringArray(raw2) || raw2.length > 100 || !raw2.every(
|
|
268
|
+
(s) => typeof s === "string" && normalizeRepoPath(s) === s && !s.startsWith("!") && !/[\x00-\x1f]/.test(s)
|
|
269
|
+
)) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
"Audit patterns must be at most 100 repository-relative globs without traversal or negation"
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return raw2;
|
|
275
|
+
};
|
|
276
|
+
config.audit = {};
|
|
277
|
+
if (audit.include !== void 0) config.audit.include = patterns(audit.include);
|
|
278
|
+
if (audit.exclude !== void 0) {
|
|
279
|
+
if (!isRecord(audit.exclude))
|
|
280
|
+
throw new Error("audit.exclude must map check names to patterns");
|
|
281
|
+
const { ALL_CHECKS: ALL_CHECKS2 } = await Promise.resolve().then(() => (init_types(), types_exports));
|
|
282
|
+
config.audit.exclude = {};
|
|
283
|
+
for (const [check, globs] of Object.entries(audit.exclude)) {
|
|
284
|
+
if (!ALL_CHECKS2.includes(check))
|
|
285
|
+
throw new Error("Unknown audit check: " + check);
|
|
286
|
+
config.audit.exclude[check] = patterns(globs);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return config;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error.code === "ENOENT") return {};
|
|
293
|
+
throw new Error(
|
|
294
|
+
`Cannot apply project file policy: ${error instanceof Error ? error.message : String(error)}`,
|
|
295
|
+
{ cause: error }
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function createFileAccess(rootDir) {
|
|
300
|
+
const root = path2.resolve(rootDir);
|
|
301
|
+
const canonicalRoot = await fs.realpath(root).catch(() => root);
|
|
302
|
+
const config = await loadProjectConfig(root);
|
|
303
|
+
const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
|
|
304
|
+
let gitFiles = null;
|
|
305
|
+
try {
|
|
306
|
+
const { stdout } = await execGit(
|
|
307
|
+
["ls-files", "-z", "--cached", "--others", "--exclude-standard"],
|
|
308
|
+
{ cwd: root, maxBuffer: 50 * 1024 * 1024 }
|
|
309
|
+
);
|
|
310
|
+
gitFiles = new Set(stdout.split("\0").filter(Boolean));
|
|
311
|
+
} catch {
|
|
312
|
+
let inGit = false;
|
|
313
|
+
try {
|
|
314
|
+
await execGit(["rev-parse", "--git-dir"], { cwd: root });
|
|
315
|
+
inGit = true;
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
if (inGit) throw new Error("Cannot enumerate Git files safely");
|
|
319
|
+
}
|
|
320
|
+
async function resolve(file) {
|
|
321
|
+
const relative = normalizeRepoPath(file);
|
|
322
|
+
if (!relative || isSensitiveFile(relative) || gitFiles && !gitFiles.has(relative))
|
|
323
|
+
return null;
|
|
324
|
+
const candidate = path2.join(root, relative);
|
|
325
|
+
try {
|
|
326
|
+
const real = await fs.realpath(candidate);
|
|
327
|
+
if (!isWithinRoot(canonicalRoot, real) || isSensitiveFile(path2.relative(canonicalRoot, real)))
|
|
328
|
+
return null;
|
|
329
|
+
const stat = await fs.stat(real);
|
|
330
|
+
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) return null;
|
|
331
|
+
if (gitFiles && !gitFiles.has(path2.relative(canonicalRoot, real).split(path2.sep).join("/")))
|
|
332
|
+
return null;
|
|
333
|
+
return real;
|
|
334
|
+
} catch {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function list(patterns = SOURCE_GLOB, options = {}) {
|
|
339
|
+
const found = await fg(patterns, { cwd: root, ignore, followSymbolicLinks: false, ...options });
|
|
340
|
+
const safe = await Promise.all(found.map(async (f) => await resolve(f) ? f : null));
|
|
341
|
+
return safe.filter((f) => f !== null).sort();
|
|
342
|
+
}
|
|
343
|
+
async function read(file) {
|
|
344
|
+
const relative = normalizeRepoPath(file);
|
|
345
|
+
if (!relative) return null;
|
|
346
|
+
const real = await resolve(relative);
|
|
347
|
+
if (!real) return null;
|
|
348
|
+
for (const rel of /* @__PURE__ */ new Set([
|
|
349
|
+
relative,
|
|
350
|
+
path2.relative(canonicalRoot, real).split(path2.sep).join("/")
|
|
351
|
+
])) {
|
|
352
|
+
if (!(await fg(fg.escapePath(rel), { cwd: root, ignore, dot: true })).length) return null;
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
const content = await readBoundedFile(real, MAX_SOURCE_BYTES);
|
|
356
|
+
return content === null ? null : { path: relative, content, totalLines: content.split("\n").length };
|
|
357
|
+
} catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return { root, config, list, read, inventory: () => gitFiles ? [...gitFiles].sort() : null };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/audit/inspection.ts
|
|
365
|
+
var inspections = new AsyncLocalStorage2();
|
|
366
|
+
function inspectionRead(root, key, read) {
|
|
367
|
+
const inspection = inspections.getStore();
|
|
368
|
+
if (!inspection || inspection.root !== path3.resolve(root)) return read();
|
|
369
|
+
if (!inspection.reads.has(key)) {
|
|
370
|
+
const pending = read();
|
|
371
|
+
inspection.reads.set(key, pending);
|
|
372
|
+
void pending.catch(() => {
|
|
373
|
+
if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return inspection.reads.get(key);
|
|
377
|
+
}
|
|
378
|
+
function inspectionGit(args, options) {
|
|
379
|
+
return inspectionRead(
|
|
380
|
+
options.cwd,
|
|
381
|
+
"git:" + JSON.stringify([args, options]),
|
|
382
|
+
() => execGit(args, options)
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
var sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
|
|
386
|
+
var sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
|
|
387
|
+
|
|
388
|
+
// src/decisions/anchors.ts
|
|
389
|
+
import { createHash } from "crypto";
|
|
390
|
+
import fs3 from "fs/promises";
|
|
391
|
+
import { constants as constants2 } from "fs";
|
|
392
|
+
import { z } from "zod";
|
|
393
|
+
import fg3 from "fast-glob";
|
|
100
394
|
|
|
101
395
|
// src/utils/storage.ts
|
|
396
|
+
import fs2 from "fs/promises";
|
|
397
|
+
import path4 from "path";
|
|
398
|
+
import { randomUUID } from "crypto";
|
|
102
399
|
async function storePath(root, relative, createParents = false) {
|
|
103
400
|
const normalized = normalizeRepoPath(relative);
|
|
104
401
|
if (!normalized) throw new Error(`Invalid store path: ${relative}`);
|
|
105
402
|
let current = await fs2.realpath(root);
|
|
106
403
|
const parts = normalized.split("/");
|
|
107
404
|
for (let i = 0; i < parts.length; i++) {
|
|
108
|
-
current =
|
|
405
|
+
current = path4.join(current, parts[i]);
|
|
109
406
|
let stat;
|
|
110
407
|
try {
|
|
111
408
|
stat = await fs2.lstat(current);
|
|
@@ -124,56 +421,166 @@ async function storePath(root, relative, createParents = false) {
|
|
|
124
421
|
}
|
|
125
422
|
return current;
|
|
126
423
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
424
|
+
|
|
425
|
+
// src/decisions/anchors.ts
|
|
426
|
+
var repoPath = z.string().refine((p) => normalizeRepoPath(p) === p);
|
|
427
|
+
var anchorCaptureSchema = z.object({
|
|
428
|
+
anchors: z.array(repoPath),
|
|
429
|
+
files: z.record(repoPath, z.string().regex(/^[a-f0-9]{64}$/)),
|
|
430
|
+
complete: z.boolean()
|
|
431
|
+
}).refine(
|
|
432
|
+
(capture) => JSON.stringify(capture.anchors) === JSON.stringify([...new Set(capture.anchors)].sort()) && matchingPaths(capture.anchors, Object.keys(capture.files)).length === Object.keys(capture.files).length,
|
|
433
|
+
"Capture paths must belong to its normalized, sorted anchor scope"
|
|
434
|
+
);
|
|
435
|
+
async function captureAnchorScopes(root, scopes) {
|
|
436
|
+
const union = [...new Set(scopes.flat())].sort();
|
|
437
|
+
let context;
|
|
438
|
+
const prepareContext = async () => {
|
|
439
|
+
const access = await inspectionRead(
|
|
440
|
+
root,
|
|
441
|
+
"decision-anchor-access",
|
|
442
|
+
() => createFileAccess(root)
|
|
443
|
+
);
|
|
444
|
+
const inventory = access.inventory();
|
|
445
|
+
if (!inventory) throw new Error("Git anchor inventory unavailable");
|
|
446
|
+
const ignored = await inspectionGit(
|
|
447
|
+
[
|
|
448
|
+
"ls-files",
|
|
449
|
+
"-z",
|
|
450
|
+
"--others",
|
|
451
|
+
"--ignored",
|
|
452
|
+
"--exclude-standard",
|
|
453
|
+
"--",
|
|
454
|
+
...union.map((anchor) => `:(literal)${anchor}`)
|
|
455
|
+
],
|
|
456
|
+
{ cwd: root, maxBuffer: 8 * 1024 * 1024 }
|
|
457
|
+
);
|
|
458
|
+
const files = [
|
|
459
|
+
.../* @__PURE__ */ new Set([...inventory, ...ignored.stdout.split("\0").filter(Boolean)])
|
|
460
|
+
].sort();
|
|
461
|
+
const patterns = await Promise.all(
|
|
462
|
+
union.map(async (anchor) => {
|
|
463
|
+
const literal = fg3.escapePath(anchor);
|
|
464
|
+
try {
|
|
465
|
+
return (await fs3.lstat(await storePath(root, anchor))).isDirectory() ? `${literal}/**/*` : literal;
|
|
466
|
+
} catch {
|
|
467
|
+
return literal;
|
|
468
|
+
}
|
|
469
|
+
})
|
|
470
|
+
);
|
|
471
|
+
const eligible = new Set(await access.list(patterns, { dot: true }));
|
|
472
|
+
const indexed = indexMatchingPaths(scopes, files);
|
|
473
|
+
const filesByScope = new Map(
|
|
474
|
+
scopes.map((scope, index) => [JSON.stringify([...new Set(scope)].sort()), indexed[index]])
|
|
475
|
+
);
|
|
476
|
+
return { filesByScope, eligible };
|
|
477
|
+
};
|
|
478
|
+
return Promise.all(
|
|
479
|
+
scopes.map((scope) => {
|
|
480
|
+
const anchors = [...new Set(scope)].sort();
|
|
481
|
+
return inspectionRead(
|
|
482
|
+
root,
|
|
483
|
+
"decision-anchor-capture:" + JSON.stringify(anchors),
|
|
484
|
+
async () => {
|
|
485
|
+
const capture = { anchors, files: {}, complete: true };
|
|
486
|
+
if (!anchors.length) return capture;
|
|
487
|
+
try {
|
|
488
|
+
const { eligible, filesByScope } = await (context ??= prepareContext());
|
|
489
|
+
const files = filesByScope.get(JSON.stringify(anchors));
|
|
490
|
+
if (files.length > 2e3) return { ...capture, complete: false };
|
|
491
|
+
for (const file of files) {
|
|
492
|
+
const evidence = await inspectionRead(
|
|
493
|
+
root,
|
|
494
|
+
"decision-anchor-file:" + file,
|
|
495
|
+
async () => {
|
|
496
|
+
try {
|
|
497
|
+
const absolute = await storePath(root, file);
|
|
498
|
+
await fs3.lstat(absolute);
|
|
499
|
+
if (!eligible.has(file)) return { complete: false };
|
|
500
|
+
const handle = await fs3.open(
|
|
501
|
+
absolute,
|
|
502
|
+
constants2.O_RDONLY | constants2.O_NONBLOCK | constants2.O_NOFOLLOW
|
|
503
|
+
);
|
|
504
|
+
try {
|
|
505
|
+
const stat = await handle.stat();
|
|
506
|
+
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES)
|
|
507
|
+
return { complete: false };
|
|
508
|
+
const bytes = Buffer.alloc(Math.min(stat.size + 1, MAX_SOURCE_BYTES + 1));
|
|
509
|
+
let size = 0;
|
|
510
|
+
while (size < bytes.length) {
|
|
511
|
+
const read = await handle.read(bytes, size, bytes.length - size, null);
|
|
512
|
+
if (!read.bytesRead) break;
|
|
513
|
+
size += read.bytesRead;
|
|
514
|
+
}
|
|
515
|
+
if (size === bytes.length) return { complete: false };
|
|
516
|
+
return {
|
|
517
|
+
complete: true,
|
|
518
|
+
hash: createHash("sha256").update(bytes.subarray(0, size)).digest("hex")
|
|
519
|
+
};
|
|
520
|
+
} finally {
|
|
521
|
+
await handle.close();
|
|
522
|
+
}
|
|
523
|
+
} catch (error) {
|
|
524
|
+
return { complete: error.code === "ENOENT" };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
);
|
|
528
|
+
if (!evidence.complete) capture.complete = false;
|
|
529
|
+
if (evidence.hash) capture.files[file] = evidence.hash;
|
|
530
|
+
}
|
|
531
|
+
} catch {
|
|
532
|
+
capture.complete = false;
|
|
533
|
+
}
|
|
534
|
+
return capture;
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
})
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
function changedCaptureFiles(before, after) {
|
|
541
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(before.files), ...Object.keys(after.files)])].filter((file) => before.files[file] !== after.files[file]).sort();
|
|
139
542
|
}
|
|
140
543
|
|
|
544
|
+
// src/decisions/decisions.ts
|
|
545
|
+
import fs5 from "fs/promises";
|
|
546
|
+
import path6 from "path";
|
|
547
|
+
import { createHash as createHash3 } from "crypto";
|
|
548
|
+
import { z as z5 } from "zod";
|
|
549
|
+
|
|
141
550
|
// src/snapshot/snapshot.ts
|
|
142
|
-
import
|
|
143
|
-
import { z } from "zod";
|
|
551
|
+
import { z as z3 } from "zod";
|
|
144
552
|
|
|
145
553
|
// src/utils/store-lock.ts
|
|
146
|
-
import
|
|
554
|
+
import { z as z2 } from "zod";
|
|
555
|
+
import fs4 from "fs/promises";
|
|
147
556
|
import os from "os";
|
|
148
|
-
|
|
149
|
-
// src/test-map.ts
|
|
150
|
-
import path4 from "path";
|
|
557
|
+
var lockOwnerSchema = z2.object({ host: z2.string(), pid: z2.number().int().positive() });
|
|
151
558
|
|
|
152
559
|
// src/snapshot/snapshot.ts
|
|
153
|
-
var
|
|
560
|
+
var repoPath2 = z3.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
154
561
|
var verificationFields = {
|
|
155
|
-
refreshedHash:
|
|
156
|
-
verifiedAt:
|
|
157
|
-
verifiedHash:
|
|
158
|
-
verificationToken:
|
|
159
|
-
verificationFailed:
|
|
160
|
-
verificationNote:
|
|
562
|
+
refreshedHash: z3.string().optional(),
|
|
563
|
+
verifiedAt: z3.string().optional(),
|
|
564
|
+
verifiedHash: z3.string().optional(),
|
|
565
|
+
verificationToken: z3.string().optional(),
|
|
566
|
+
verificationFailed: z3.boolean().optional(),
|
|
567
|
+
verificationNote: z3.string().optional()
|
|
161
568
|
};
|
|
162
|
-
var featureSchema =
|
|
163
|
-
description:
|
|
164
|
-
files:
|
|
165
|
-
tests:
|
|
166
|
-
type:
|
|
569
|
+
var featureSchema = z3.object({
|
|
570
|
+
description: z3.string(),
|
|
571
|
+
files: z3.array(repoPath2),
|
|
572
|
+
tests: z3.array(repoPath2).optional(),
|
|
573
|
+
type: z3.enum(["capability", "infrastructure"]).optional(),
|
|
167
574
|
...verificationFields
|
|
168
575
|
}).passthrough();
|
|
169
|
-
var flowSchema =
|
|
170
|
-
var snapshotSchema =
|
|
171
|
-
version:
|
|
172
|
-
createdAt:
|
|
173
|
-
updatedAt:
|
|
174
|
-
gitHash:
|
|
175
|
-
features:
|
|
176
|
-
flows:
|
|
576
|
+
var flowSchema = z3.object({ description: z3.string(), chain: z3.array(repoPath2), ...verificationFields }).passthrough();
|
|
577
|
+
var snapshotSchema = z3.object({
|
|
578
|
+
version: z3.literal(2),
|
|
579
|
+
createdAt: z3.string(),
|
|
580
|
+
updatedAt: z3.string(),
|
|
581
|
+
gitHash: z3.string(),
|
|
582
|
+
features: z3.record(featureSchema),
|
|
583
|
+
flows: z3.record(flowSchema)
|
|
177
584
|
}).passthrough();
|
|
178
585
|
async function getCurrentGitHash(rootDir) {
|
|
179
586
|
try {
|
|
@@ -187,114 +594,168 @@ async function getCurrentGitHash(rootDir) {
|
|
|
187
594
|
}
|
|
188
595
|
|
|
189
596
|
// src/context/lexical.ts
|
|
190
|
-
import
|
|
597
|
+
import path5 from "path";
|
|
191
598
|
|
|
192
599
|
// src/decisions/provenance.ts
|
|
193
|
-
import { z as
|
|
600
|
+
import { z as z4 } from "zod";
|
|
601
|
+
import { createHash as createHash2 } from "crypto";
|
|
194
602
|
|
|
195
603
|
// src/context/trust.ts
|
|
196
604
|
function assessTrust(entry, freshness) {
|
|
197
605
|
const verification = entry.verificationFailed ? "failed" : entry.verifiedAt ? "passed" : "unverified";
|
|
198
606
|
const reasons = [];
|
|
199
|
-
if (freshness === "unknown")
|
|
200
|
-
|
|
201
|
-
|
|
607
|
+
if (freshness === "unknown")
|
|
608
|
+
reasons.push(
|
|
609
|
+
"Anchors, history, or working-tree evidence are unavailable; verify before relying on this entry."
|
|
610
|
+
);
|
|
611
|
+
if (freshness === "changed")
|
|
612
|
+
reasons.push(
|
|
613
|
+
"Anchored files changed; verify against current code before relying on this entry."
|
|
614
|
+
);
|
|
615
|
+
if (verification === "failed")
|
|
616
|
+
reasons.push(
|
|
617
|
+
`Verification failed: ${entry.verificationNote ?? "re-map this entry before relying on it"}`
|
|
618
|
+
);
|
|
202
619
|
if (verification === "unverified") reasons.push("No correctness verification has been recorded.");
|
|
203
|
-
return {
|
|
620
|
+
return {
|
|
621
|
+
freshness,
|
|
622
|
+
verification,
|
|
623
|
+
verifiedAt: entry.verifiedAt,
|
|
624
|
+
verifiedHash: entry.verifiedHash,
|
|
625
|
+
reasons
|
|
626
|
+
};
|
|
204
627
|
}
|
|
205
628
|
|
|
206
629
|
// src/decisions/provenance.ts
|
|
207
|
-
var text = (max) =>
|
|
208
|
-
var decisionSourceSchema =
|
|
209
|
-
kind:
|
|
630
|
+
var text = (max) => z4.string().trim().min(1).max(max);
|
|
631
|
+
var decisionSourceSchema = z4.object({
|
|
632
|
+
kind: z4.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
|
|
210
633
|
reference: text(1e3),
|
|
211
634
|
note: text(500).optional()
|
|
212
635
|
}).strict();
|
|
213
|
-
var attributionSchema =
|
|
636
|
+
var attributionSchema = z4.object({
|
|
214
637
|
owner: text(200).nullable().optional(),
|
|
215
|
-
sources:
|
|
638
|
+
sources: z4.array(decisionSourceSchema).max(20).optional(),
|
|
216
639
|
actor: text(200).optional()
|
|
217
640
|
});
|
|
218
|
-
var contentSchema =
|
|
219
|
-
title:
|
|
220
|
-
body:
|
|
221
|
-
category:
|
|
222
|
-
files:
|
|
641
|
+
var contentSchema = z4.object({
|
|
642
|
+
title: z4.string().min(1),
|
|
643
|
+
body: z4.string().min(1),
|
|
644
|
+
category: z4.enum(["decision", "gotcha", "deprecation", "convention"]),
|
|
645
|
+
files: z4.array(z4.string().refine((f) => normalizeRepoPath(f) !== null)),
|
|
223
646
|
owner: text(200).optional(),
|
|
224
|
-
sources:
|
|
647
|
+
sources: z4.array(decisionSourceSchema).max(20)
|
|
225
648
|
});
|
|
226
|
-
var approvalSchema =
|
|
227
|
-
var statusSchema =
|
|
228
|
-
var reviewEvidenceSchema =
|
|
229
|
-
baseHash:
|
|
230
|
-
headHash:
|
|
231
|
-
historyAvailable:
|
|
232
|
-
changedFiles:
|
|
233
|
-
localChanges:
|
|
649
|
+
var approvalSchema = z4.enum(["unreviewed", "proposed", "accepted"]);
|
|
650
|
+
var statusSchema = z4.enum(["active", "superseded", "retired"]);
|
|
651
|
+
var reviewEvidenceSchema = z4.object({
|
|
652
|
+
baseHash: z4.string(),
|
|
653
|
+
headHash: z4.string(),
|
|
654
|
+
historyAvailable: z4.boolean(),
|
|
655
|
+
changedFiles: z4.array(z4.string()),
|
|
656
|
+
localChanges: z4.array(z4.string())
|
|
234
657
|
});
|
|
235
|
-
var eventSchema =
|
|
236
|
-
kind:
|
|
237
|
-
|
|
658
|
+
var eventSchema = z4.object({
|
|
659
|
+
kind: z4.enum([
|
|
660
|
+
"imported",
|
|
661
|
+
"created",
|
|
662
|
+
"revised",
|
|
663
|
+
"accepted",
|
|
664
|
+
"reaffirmed",
|
|
665
|
+
"retired",
|
|
666
|
+
"superseded"
|
|
667
|
+
]),
|
|
668
|
+
at: z4.string().datetime(),
|
|
238
669
|
actor: text(200).optional(),
|
|
239
670
|
note: text(1500).optional(),
|
|
240
|
-
revision:
|
|
671
|
+
revision: z4.number().int().positive(),
|
|
241
672
|
content: contentSchema,
|
|
242
673
|
approval: approvalSchema,
|
|
243
674
|
status: statusSchema,
|
|
244
|
-
refreshedHash:
|
|
245
|
-
evidence: reviewEvidenceSchema.optional()
|
|
675
|
+
refreshedHash: z4.string(),
|
|
676
|
+
evidence: reviewEvidenceSchema.optional(),
|
|
677
|
+
capture: anchorCaptureSchema.optional()
|
|
246
678
|
});
|
|
247
|
-
var legacySchema =
|
|
248
|
-
version:
|
|
249
|
-
id:
|
|
250
|
-
title:
|
|
251
|
-
body:
|
|
679
|
+
var legacySchema = z4.object({
|
|
680
|
+
version: z4.literal(1),
|
|
681
|
+
id: z4.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
682
|
+
title: z4.string().min(1),
|
|
683
|
+
body: z4.string().min(1),
|
|
252
684
|
category: contentSchema.shape.category,
|
|
253
685
|
files: contentSchema.shape.files,
|
|
254
|
-
createdAt:
|
|
255
|
-
updatedAt:
|
|
256
|
-
refreshedHash:
|
|
257
|
-
status:
|
|
258
|
-
supersededBy:
|
|
686
|
+
createdAt: z4.string(),
|
|
687
|
+
updatedAt: z4.string(),
|
|
688
|
+
refreshedHash: z4.string(),
|
|
689
|
+
status: z4.enum(["active", "superseded"]),
|
|
690
|
+
supersededBy: z4.string().optional()
|
|
259
691
|
}).passthrough();
|
|
260
692
|
var currentSchema = legacySchema.extend({
|
|
261
|
-
version:
|
|
693
|
+
version: z4.literal(2),
|
|
262
694
|
status: statusSchema,
|
|
263
695
|
approval: approvalSchema,
|
|
264
|
-
revision:
|
|
696
|
+
revision: z4.number().int().positive(),
|
|
265
697
|
owner: text(200).optional(),
|
|
266
|
-
sources:
|
|
267
|
-
history:
|
|
698
|
+
sources: z4.array(decisionSourceSchema).max(20),
|
|
699
|
+
history: z4.array(eventSchema).min(1),
|
|
700
|
+
capture: anchorCaptureSchema.optional(),
|
|
701
|
+
inspections: z4.array(
|
|
702
|
+
z4.object({
|
|
703
|
+
at: z4.string().datetime(),
|
|
704
|
+
inspector: text(200),
|
|
705
|
+
note: text(1500),
|
|
706
|
+
outcome: z4.literal("no_contradiction_found"),
|
|
707
|
+
scope: z4.literal("operative").optional(),
|
|
708
|
+
decisionDigest: z4.string().regex(/^[a-f0-9]{64}$/),
|
|
709
|
+
headHash: z4.string(),
|
|
710
|
+
capture: anchorCaptureSchema
|
|
711
|
+
})
|
|
712
|
+
).max(200).optional()
|
|
268
713
|
}).superRefine((record, ctx) => {
|
|
269
714
|
const invalid = (message) => ctx.addIssue({ code: "custom", message });
|
|
270
715
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
271
716
|
let previous;
|
|
272
717
|
for (const event of record.history) {
|
|
273
718
|
if (!previous) {
|
|
274
|
-
if (!["created", "imported"].includes(event.kind) || event.revision !== 1)
|
|
275
|
-
|
|
719
|
+
if (!["created", "imported"].includes(event.kind) || event.revision !== 1)
|
|
720
|
+
invalid("History must begin with creation or legacy import at revision 1");
|
|
721
|
+
if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed"))
|
|
722
|
+
invalid("Initial records cannot claim acceptance");
|
|
276
723
|
} else {
|
|
277
724
|
if (["created", "imported"].includes(event.kind)) invalid("History cannot restart");
|
|
278
725
|
if (previous.status !== "active") invalid("Archived decisions cannot be changed");
|
|
279
|
-
if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0))
|
|
280
|
-
|
|
281
|
-
if (event.kind
|
|
282
|
-
|
|
726
|
+
if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0))
|
|
727
|
+
invalid("Invalid revision sequence");
|
|
728
|
+
if (event.kind !== "revised" && !same(event.content, previous.content))
|
|
729
|
+
invalid("A review cannot silently revise decision content");
|
|
730
|
+
if (event.kind === "reaffirmed" && previous.approval !== "accepted")
|
|
731
|
+
invalid("Only accepted decisions can be reaffirmed");
|
|
732
|
+
if (event.kind === "accepted" && previous.approval === "accepted")
|
|
733
|
+
invalid("Use reaffirmation for an accepted decision");
|
|
283
734
|
const approval = event.kind === "revised" ? "proposed" : ["accepted", "reaffirmed"].includes(event.kind) ? "accepted" : previous.approval;
|
|
284
735
|
if (event.approval !== approval) invalid("Approval disagrees with review history");
|
|
285
|
-
if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash)
|
|
736
|
+
if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash)
|
|
737
|
+
invalid("Only a review can refresh the evidence baseline");
|
|
286
738
|
}
|
|
287
|
-
if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active"))
|
|
288
|
-
|
|
739
|
+
if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active"))
|
|
740
|
+
invalid("Lifecycle disagrees with history");
|
|
741
|
+
if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence))
|
|
742
|
+
invalid("Reviews require a named reviewer, reason, and code evidence");
|
|
289
743
|
if (["accepted", "reaffirmed"].includes(event.kind)) {
|
|
290
|
-
if (!event.content.owner || !event.content.sources.length)
|
|
291
|
-
|
|
744
|
+
if (!event.content.owner || !event.content.sources.length)
|
|
745
|
+
invalid("Accepted decisions require an owner and source");
|
|
746
|
+
if (!event.evidence || !/^[a-f0-9]{40,64}$/.test(event.evidence.headHash) || event.refreshedHash !== event.evidence.headHash || event.evidence.localChanges.length)
|
|
747
|
+
invalid("Acceptance requires a committed evidence baseline");
|
|
292
748
|
}
|
|
749
|
+
if (event.capture && !same([...event.content.files].sort(), event.capture.anchors))
|
|
750
|
+
invalid("Capture scope disagrees with decision anchors");
|
|
293
751
|
previous = event;
|
|
294
752
|
}
|
|
295
|
-
if (
|
|
753
|
+
if (previous && !same(previous.capture, record.capture))
|
|
754
|
+
invalid("Capture does not match the final history event");
|
|
755
|
+
if (!previous || !same(previous.content, decisionContent(record)) || previous.approval !== record.approval || previous.status !== record.status || previous.revision !== record.revision || previous.refreshedHash !== record.refreshedHash)
|
|
756
|
+
invalid("Decision does not match the final history event");
|
|
296
757
|
});
|
|
297
|
-
var decisionSchema =
|
|
758
|
+
var decisionSchema = z4.union([legacySchema, currentSchema]);
|
|
298
759
|
function decisionContent(record) {
|
|
299
760
|
return {
|
|
300
761
|
title: record.title,
|
|
@@ -309,7 +770,8 @@ function decisionApproval(record) {
|
|
|
309
770
|
return record.version === 1 ? "unreviewed" : record.approval;
|
|
310
771
|
}
|
|
311
772
|
function effectiveDecision(record) {
|
|
312
|
-
if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed")
|
|
773
|
+
if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed")
|
|
774
|
+
return record;
|
|
313
775
|
let index = record.history.length - 1;
|
|
314
776
|
while (index >= 0 && !["accepted", "reaffirmed"].includes(record.history[index].kind)) index--;
|
|
315
777
|
if (index < 0) return record;
|
|
@@ -321,6 +783,7 @@ function effectiveDecision(record) {
|
|
|
321
783
|
approval: "accepted",
|
|
322
784
|
revision: event.revision,
|
|
323
785
|
refreshedHash: event.refreshedHash,
|
|
786
|
+
capture: event.capture,
|
|
324
787
|
updatedAt: event.at,
|
|
325
788
|
history: record.history.slice(0, index + 1)
|
|
326
789
|
};
|
|
@@ -330,7 +793,9 @@ function decisionAnchors(record) {
|
|
|
330
793
|
}
|
|
331
794
|
function decisionProvenance(record, freshness = "unknown") {
|
|
332
795
|
const approval = decisionApproval(record);
|
|
333
|
-
const review = record.version === 2 ? [...record.history].reverse().find(
|
|
796
|
+
const review = record.version === 2 ? [...record.history].reverse().find(
|
|
797
|
+
(e) => ["accepted", "reaffirmed"].includes(e.kind) && e.revision === record.revision
|
|
798
|
+
) : void 0;
|
|
334
799
|
return {
|
|
335
800
|
approval,
|
|
336
801
|
revision: record.version === 2 ? record.revision : 0,
|
|
@@ -338,71 +803,171 @@ function decisionProvenance(record, freshness = "unknown") {
|
|
|
338
803
|
sources: record.version === 2 ? record.sources : [],
|
|
339
804
|
guidance: record.status !== "active" ? "historical" : approval === "accepted" ? "constraint" : approval === "proposed" ? "proposal" : "unreviewed",
|
|
340
805
|
reviewRequired: record.status === "active" && (approval !== "accepted" || freshness !== "current"),
|
|
341
|
-
lastReview: review ? {
|
|
806
|
+
lastReview: review ? {
|
|
807
|
+
reviewer: review.actor,
|
|
808
|
+
at: review.at,
|
|
809
|
+
note: review.note,
|
|
810
|
+
gitHash: review.refreshedHash
|
|
811
|
+
} : null
|
|
342
812
|
};
|
|
343
813
|
}
|
|
344
814
|
function decisionTrust(record, freshness) {
|
|
345
815
|
const review = decisionProvenance(record, freshness).lastReview;
|
|
346
|
-
return assessTrust(
|
|
816
|
+
return assessTrust(
|
|
817
|
+
review ? { verifiedAt: review.at, verifiedHash: review.gitHash } : {},
|
|
818
|
+
freshness
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
function decisionInspectionDigest(record, scope = "all") {
|
|
822
|
+
const effective = effectiveDecision(record);
|
|
823
|
+
if (scope === "operative")
|
|
824
|
+
return createHash2("sha256").update(
|
|
825
|
+
JSON.stringify([
|
|
826
|
+
"operative",
|
|
827
|
+
record.status,
|
|
828
|
+
decisionApproval(effective),
|
|
829
|
+
effective.version === 2 ? effective.revision : 0,
|
|
830
|
+
decisionContent(effective),
|
|
831
|
+
effective.refreshedHash
|
|
832
|
+
])
|
|
833
|
+
).digest("hex");
|
|
834
|
+
return createHash2("sha256").update(
|
|
835
|
+
JSON.stringify([
|
|
836
|
+
record.status,
|
|
837
|
+
decisionApproval(record),
|
|
838
|
+
record.version === 2 ? record.revision : 0,
|
|
839
|
+
decisionContent(record),
|
|
840
|
+
decisionContent(effective),
|
|
841
|
+
effective.refreshedHash
|
|
842
|
+
])
|
|
843
|
+
).digest("hex");
|
|
844
|
+
}
|
|
845
|
+
function latestDecisionInspection(record, scope = "all") {
|
|
846
|
+
return record.version === 2 ? record.inspections?.filter(
|
|
847
|
+
(i) => (scope === "operative" || i.scope !== "operative") && i.decisionDigest === decisionInspectionDigest(record, i.scope ?? "all")
|
|
848
|
+
).at(-1) : void 0;
|
|
347
849
|
}
|
|
348
850
|
function revisionKnowledge(record, freshness) {
|
|
349
|
-
return {
|
|
851
|
+
return {
|
|
852
|
+
...decisionContent(record),
|
|
853
|
+
...decisionProvenance(record, freshness),
|
|
854
|
+
trust: decisionTrust(record, freshness)
|
|
855
|
+
};
|
|
350
856
|
}
|
|
351
857
|
function decisionKnowledge(record, freshness = "unknown", proposalFreshness = "unknown") {
|
|
352
858
|
const effective = effectiveDecision(record);
|
|
859
|
+
const inspection = latestDecisionInspection(
|
|
860
|
+
record,
|
|
861
|
+
decisionApproval(effective) === "accepted" ? "operative" : "all"
|
|
862
|
+
);
|
|
353
863
|
return {
|
|
354
864
|
...revisionKnowledge(effective, freshness),
|
|
355
|
-
...
|
|
865
|
+
...inspection ? {
|
|
866
|
+
inspection: {
|
|
867
|
+
freshness,
|
|
868
|
+
at: inspection.at,
|
|
869
|
+
inspector: inspection.inspector,
|
|
870
|
+
note: inspection.note,
|
|
871
|
+
outcome: inspection.outcome,
|
|
872
|
+
scope: inspection.scope ?? "all",
|
|
873
|
+
headHash: inspection.headHash
|
|
874
|
+
}
|
|
875
|
+
} : {},
|
|
876
|
+
...effective !== record ? {
|
|
877
|
+
pendingProposal: {
|
|
878
|
+
...revisionKnowledge(record, proposalFreshness),
|
|
879
|
+
assessment: latestDecisionInspection(record) ? "no_contradiction_found" : "not_assessed"
|
|
880
|
+
}
|
|
881
|
+
} : {}
|
|
356
882
|
};
|
|
357
883
|
}
|
|
358
|
-
var DECISION_GUIDANCE = "Accepted decisions are recorded team constraints, subject to freshness checks. A pendingProposal is an unaccepted replacement; the accepted revision remains operative until explicit acceptance or retirement. Proposals are suggestions; legacy unreviewed records need confirmation. Use review_decision to
|
|
884
|
+
var DECISION_GUIDANCE = "Accepted decisions are recorded team constraints, subject to freshness checks. A pendingProposal is an unaccepted replacement; the accepted revision remains operative until explicit acceptance or retirement. Proposals are suggestions; legacy unreviewed records need confirmation. Use review_decision to prepare evidence and record a separate source inspection without human approval, or an authorized human review; identities and sources are recorded assertions, not authenticated proof.";
|
|
359
885
|
|
|
360
886
|
// src/decisions/decisions.ts
|
|
361
887
|
var TITLE_MAX_CHARS = 80;
|
|
362
888
|
var BODY_RECOMMENDED_CHARS = 1500;
|
|
363
889
|
var BODY_MAX_CHARS = 2500;
|
|
364
|
-
var boundedText = (field, max, hint) =>
|
|
365
|
-
errorMap: (issue, ctx) => ({
|
|
890
|
+
var boundedText = (field, max, hint) => z5.string({
|
|
891
|
+
errorMap: (issue, ctx) => ({
|
|
892
|
+
message: issue.code === "too_big" && typeof ctx.data === "string" ? `${field} has ${ctx.data.length} characters; maximum is ${max} (${ctx.data.length - max} over). ${hint}` : ctx.defaultError
|
|
893
|
+
})
|
|
366
894
|
}).trim().min(1, `${field} must be non-empty`).max(max);
|
|
367
|
-
var decisionTitleSchema = boundedText(
|
|
895
|
+
var decisionTitleSchema = boundedText(
|
|
896
|
+
"title",
|
|
897
|
+
TITLE_MAX_CHARS,
|
|
898
|
+
"Tighten it to a specific headline."
|
|
899
|
+
);
|
|
368
900
|
var decisionBodySchema = boundedText(
|
|
369
901
|
"body",
|
|
370
902
|
BODY_MAX_CHARS,
|
|
371
903
|
`Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer. Preserve the rule, reason, and exceptions; put supporting references in sources.`
|
|
372
904
|
);
|
|
905
|
+
async function readDecisionInputs(rootDir) {
|
|
906
|
+
return structuredClone(
|
|
907
|
+
await inspectionRead(rootDir, "decision-store:inputs", async () => {
|
|
908
|
+
const inputs = [];
|
|
909
|
+
const diagnostics = [];
|
|
910
|
+
let entries;
|
|
911
|
+
try {
|
|
912
|
+
entries = await fs5.readdir(await storePath(rootDir, ".mason/decisions"));
|
|
913
|
+
} catch (error) {
|
|
914
|
+
if (error.code !== "ENOENT")
|
|
915
|
+
diagnostics.push({ path: ".mason/decisions", message: String(error) });
|
|
916
|
+
return { inputs, diagnostics };
|
|
917
|
+
}
|
|
918
|
+
for (const entry of entries.sort()) {
|
|
919
|
+
if (!entry.endsWith(".json")) continue;
|
|
920
|
+
const relative = `.mason/decisions/${entry}`;
|
|
921
|
+
try {
|
|
922
|
+
const raw = await readBoundedFile(await storePath(rootDir, relative), 10 * 1024 * 1024);
|
|
923
|
+
if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
|
|
924
|
+
inputs.push([relative, raw]);
|
|
925
|
+
} catch (error) {
|
|
926
|
+
if (error.code === "ENOENT") inputs.push([relative, null]);
|
|
927
|
+
else
|
|
928
|
+
diagnostics.push({
|
|
929
|
+
path: relative,
|
|
930
|
+
message: error instanceof Error ? error.message : String(error)
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return { inputs, diagnostics };
|
|
935
|
+
})
|
|
936
|
+
);
|
|
937
|
+
}
|
|
373
938
|
async function loadDecisionStore(rootDir) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
939
|
+
return structuredClone(
|
|
940
|
+
await inspectionRead(rootDir, "decision-store:parsed", async () => {
|
|
941
|
+
const raw = await readDecisionInputs(rootDir);
|
|
942
|
+
const records = [];
|
|
943
|
+
const diagnostics = [...raw.diagnostics];
|
|
944
|
+
for (const [relative, content] of raw.inputs) {
|
|
945
|
+
try {
|
|
946
|
+
const record = decisionSchema.parse(content === null ? null : JSON.parse(content));
|
|
947
|
+
if (relative !== `.mason/decisions/${record.id}.json`)
|
|
948
|
+
throw new Error("Record id does not match its filename");
|
|
949
|
+
records.push(record);
|
|
950
|
+
} catch (error) {
|
|
951
|
+
diagnostics.push({
|
|
952
|
+
path: relative,
|
|
953
|
+
message: error instanceof Error ? error.message : String(error)
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return { records, diagnostics };
|
|
958
|
+
})
|
|
959
|
+
);
|
|
395
960
|
}
|
|
396
961
|
|
|
397
962
|
// src/hook/hook.ts
|
|
398
|
-
import { createHash as
|
|
963
|
+
import { createHash as createHash4 } from "crypto";
|
|
399
964
|
|
|
400
965
|
// src/decisions/drift.ts
|
|
401
|
-
import
|
|
966
|
+
import path8 from "path";
|
|
402
967
|
|
|
403
968
|
// src/drift/drift.ts
|
|
404
|
-
import
|
|
405
|
-
import
|
|
969
|
+
import fs6 from "fs/promises";
|
|
970
|
+
import path7 from "path";
|
|
406
971
|
function parseChanges(output) {
|
|
407
972
|
const fields = output.split("\0");
|
|
408
973
|
const changes = [];
|
|
@@ -412,18 +977,25 @@ function parseChanges(output) {
|
|
|
412
977
|
if (!first) break;
|
|
413
978
|
const second = /^[RC]/.test(code) ? fields[i++] : void 0;
|
|
414
979
|
const change = second ? code.startsWith("R") ? { status: "renamed", path: second, previousPath: first } : { status: "added", path: second } : { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", path: first };
|
|
415
|
-
if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/")))
|
|
980
|
+
if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/")))
|
|
981
|
+
continue;
|
|
416
982
|
changes.push(change);
|
|
417
983
|
}
|
|
418
984
|
return changes;
|
|
419
985
|
}
|
|
420
986
|
function touchedPaths(changes) {
|
|
421
|
-
return [
|
|
987
|
+
return [
|
|
988
|
+
...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))
|
|
989
|
+
].sort();
|
|
422
990
|
}
|
|
423
991
|
async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
|
|
424
|
-
if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-"))
|
|
992
|
+
if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-"))
|
|
993
|
+
return null;
|
|
425
994
|
try {
|
|
426
|
-
const { stdout } = await execGit(
|
|
995
|
+
const { stdout } = await execGit(
|
|
996
|
+
["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"],
|
|
997
|
+
{ cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
998
|
+
);
|
|
427
999
|
return parseChanges(stdout);
|
|
428
1000
|
} catch {
|
|
429
1001
|
return null;
|
|
@@ -432,11 +1004,23 @@ async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
|
|
|
432
1004
|
async function getWorkingTree(resolvedRoot) {
|
|
433
1005
|
try {
|
|
434
1006
|
const [diff, untracked] = await Promise.all([
|
|
435
|
-
execGit(["diff", "--name-status", "-z", "-M", "HEAD", "--"], {
|
|
436
|
-
|
|
1007
|
+
execGit(["diff", "--name-status", "-z", "-M", "HEAD", "--"], {
|
|
1008
|
+
cwd: resolvedRoot,
|
|
1009
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1010
|
+
}),
|
|
1011
|
+
execGit(["ls-files", "-z", "--others", "--exclude-standard"], {
|
|
1012
|
+
cwd: resolvedRoot,
|
|
1013
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1014
|
+
})
|
|
437
1015
|
]);
|
|
438
1016
|
const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
|
|
439
|
-
return {
|
|
1017
|
+
return {
|
|
1018
|
+
available: true,
|
|
1019
|
+
changedFiles: [
|
|
1020
|
+
.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])
|
|
1021
|
+
].sort(),
|
|
1022
|
+
untrackedFiles
|
|
1023
|
+
};
|
|
440
1024
|
} catch {
|
|
441
1025
|
return { available: false, changedFiles: [], untrackedFiles: [] };
|
|
442
1026
|
}
|
|
@@ -444,13 +1028,51 @@ async function getWorkingTree(resolvedRoot) {
|
|
|
444
1028
|
|
|
445
1029
|
// src/decisions/drift.ts
|
|
446
1030
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
447
|
-
const resolvedRoot =
|
|
1031
|
+
const resolvedRoot = path8.resolve(rootDir);
|
|
448
1032
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
449
|
-
const report = {
|
|
450
|
-
|
|
1033
|
+
const report = {
|
|
1034
|
+
historyAvailable: true,
|
|
1035
|
+
totalDecisions: store.records.length,
|
|
1036
|
+
staleDecisions: {},
|
|
1037
|
+
freshness: {},
|
|
1038
|
+
diagnostics: store.diagnostics
|
|
1039
|
+
};
|
|
1040
|
+
const [head, workingTree] = await Promise.all([
|
|
1041
|
+
getCurrentGitHash(resolvedRoot),
|
|
1042
|
+
getWorkingTree(resolvedRoot)
|
|
1043
|
+
]);
|
|
451
1044
|
const changesByHash = /* @__PURE__ */ new Map();
|
|
452
|
-
const
|
|
1045
|
+
const baselines = store.records.filter((record) => record.status === "active").flatMap((record) => {
|
|
1046
|
+
const effective = effectiveDecision(record);
|
|
1047
|
+
return [.../* @__PURE__ */ new Set([effective, record])].flatMap((candidate) => {
|
|
1048
|
+
const inspection = latestDecisionInspection(
|
|
1049
|
+
record,
|
|
1050
|
+
candidate === effective && decisionApproval(effective) === "accepted" ? "operative" : "all"
|
|
1051
|
+
);
|
|
1052
|
+
const capture = inspection?.capture ?? (candidate.version === 2 ? candidate.capture : void 0);
|
|
1053
|
+
return capture ? [capture] : [];
|
|
1054
|
+
});
|
|
1055
|
+
});
|
|
1056
|
+
const captures = await captureAnchorScopes(
|
|
1057
|
+
resolvedRoot,
|
|
1058
|
+
baselines.map((baseline) => baseline.anchors)
|
|
1059
|
+
);
|
|
1060
|
+
const currentCaptures = new Map(
|
|
1061
|
+
baselines.map((baseline, index) => [JSON.stringify(baseline.anchors), captures[index]])
|
|
1062
|
+
);
|
|
1063
|
+
const inspect = async (record, inspectionCapture) => {
|
|
453
1064
|
if (record.files.length === 0) return { freshness: "unknown", changedFiles: [] };
|
|
1065
|
+
const baseline = inspectionCapture ?? (record.version === 2 ? record.capture : void 0);
|
|
1066
|
+
if (baseline) {
|
|
1067
|
+
const current = currentCaptures.get(JSON.stringify(baseline.anchors));
|
|
1068
|
+
const hits2 = changedCaptureFiles(baseline, current).filter(
|
|
1069
|
+
(file) => matchingPaths(record.files, [file]).length
|
|
1070
|
+
);
|
|
1071
|
+
return {
|
|
1072
|
+
freshness: !baseline.complete || !current.complete ? "unknown" : hits2.length ? "changed" : "current",
|
|
1073
|
+
changedFiles: hits2
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
454
1076
|
let touched = changesByHash.get(record.refreshedHash);
|
|
455
1077
|
if (touched === void 0) {
|
|
456
1078
|
const changes = record.refreshedHash === head && head !== "unknown" ? [] : await getChangesWithStatus(resolvedRoot, record.refreshedHash);
|
|
@@ -460,15 +1082,26 @@ async function computeDecisionDrift(rootDir, decisions) {
|
|
|
460
1082
|
if (touched === null) report.historyAvailable = false;
|
|
461
1083
|
const hits = touched ? matchingPaths(record.files, touched) : [];
|
|
462
1084
|
const localHits = matchingPaths(record.files, workingTree.changedFiles);
|
|
463
|
-
return {
|
|
1085
|
+
return {
|
|
1086
|
+
freshness: touched === null || !workingTree.available ? "unknown" : hits.length || localHits.length ? "changed" : "current",
|
|
1087
|
+
changedFiles: [.../* @__PURE__ */ new Set([...hits, ...localHits])].sort()
|
|
1088
|
+
};
|
|
464
1089
|
};
|
|
465
1090
|
for (const record of store.records) {
|
|
466
1091
|
if (record.status !== "active") continue;
|
|
467
1092
|
const effective = effectiveDecision(record);
|
|
468
|
-
const
|
|
1093
|
+
const inspection = latestDecisionInspection(
|
|
1094
|
+
record,
|
|
1095
|
+
decisionApproval(effective) === "accepted" ? "operative" : "all"
|
|
1096
|
+
);
|
|
1097
|
+
const state = await inspect(effective, inspection?.capture);
|
|
469
1098
|
report.freshness[record.id] = state.freshness;
|
|
470
1099
|
if (state.changedFiles.length) report.staleDecisions[record.id] = state.changedFiles;
|
|
471
|
-
if (effective !== record)
|
|
1100
|
+
if (effective !== record)
|
|
1101
|
+
(report.pendingProposals ??= {})[record.id] = await inspect(
|
|
1102
|
+
record,
|
|
1103
|
+
latestDecisionInspection(record)?.capture
|
|
1104
|
+
);
|
|
472
1105
|
}
|
|
473
1106
|
return report;
|
|
474
1107
|
}
|
|
@@ -477,9 +1110,17 @@ async function computeDecisionDrift(rootDir, decisions) {
|
|
|
477
1110
|
var MAX_INJECTED_DECISIONS = 3;
|
|
478
1111
|
var MAX_WALK_UP = 30;
|
|
479
1112
|
var SUPPORTED_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write"]);
|
|
1113
|
+
var hookStdinSchema = z6.object({
|
|
1114
|
+
session_id: z6.string().optional(),
|
|
1115
|
+
agent_id: z6.string().optional(),
|
|
1116
|
+
cwd: z6.string().optional(),
|
|
1117
|
+
hook_event_name: z6.string().optional(),
|
|
1118
|
+
tool_name: z6.string().optional(),
|
|
1119
|
+
tool_input: z6.object({ file_path: z6.string().optional() }).optional()
|
|
1120
|
+
});
|
|
480
1121
|
async function exists(p) {
|
|
481
1122
|
try {
|
|
482
|
-
await
|
|
1123
|
+
await fs7.access(p);
|
|
483
1124
|
return true;
|
|
484
1125
|
} catch {
|
|
485
1126
|
return false;
|
|
@@ -488,9 +1129,9 @@ async function exists(p) {
|
|
|
488
1129
|
async function findMasonRoot(startDir) {
|
|
489
1130
|
let dir = startDir;
|
|
490
1131
|
for (let i = 0; i < MAX_WALK_UP; i++) {
|
|
491
|
-
if (await exists(
|
|
492
|
-
if (await exists(
|
|
493
|
-
const parent =
|
|
1132
|
+
if (await exists(path9.join(dir, ".mason", "decisions"))) return dir;
|
|
1133
|
+
if (await exists(path9.join(dir, ".git"))) return null;
|
|
1134
|
+
const parent = path9.dirname(dir);
|
|
494
1135
|
if (parent === dir) return null;
|
|
495
1136
|
dir = parent;
|
|
496
1137
|
}
|
|
@@ -508,8 +1149,8 @@ function stateKey(input) {
|
|
|
508
1149
|
}
|
|
509
1150
|
async function loadInjected(stateFile) {
|
|
510
1151
|
try {
|
|
511
|
-
const parsed = JSON.parse(await
|
|
512
|
-
return new Set(
|
|
1152
|
+
const parsed = JSON.parse(await fs7.readFile(stateFile, "utf-8"));
|
|
1153
|
+
return new Set(isStringArray(parsed) ? parsed : []);
|
|
513
1154
|
} catch {
|
|
514
1155
|
return /* @__PURE__ */ new Set();
|
|
515
1156
|
}
|
|
@@ -517,7 +1158,7 @@ async function loadInjected(stateFile) {
|
|
|
517
1158
|
function formatContext(relPath, records, drift) {
|
|
518
1159
|
const lines = [];
|
|
519
1160
|
lines.push(
|
|
520
|
-
`Mason: decision knowledge relevant to ${relPath} or updated since this session saw it. This replaces earlier guidance for the same decision id. ${DECISION_GUIDANCE} Retired or superseded records are no longer active.
|
|
1161
|
+
`Mason: decision knowledge relevant to ${relPath} or updated since this session saw it. This replaces earlier guidance for the same decision id. ${DECISION_GUIDANCE} Retired or superseded records are no longer active. Use Mason tools to record decision changes and source inspections; do not edit the JSON in .mason/decisions/ directly.`
|
|
521
1162
|
);
|
|
522
1163
|
const append = (id, knowledge, label, freshness) => {
|
|
523
1164
|
const stale = freshness === "current" ? "" : freshness === "changed" ? " [recorded against changed files \u2013 verify against current code before relying on it]" : " [freshness unknown \u2013 verify against current code before relying on it]";
|
|
@@ -526,16 +1167,31 @@ function formatContext(relPath, records, drift) {
|
|
|
526
1167
|
);
|
|
527
1168
|
};
|
|
528
1169
|
for (const record of records) {
|
|
529
|
-
const knowledge = decisionKnowledge(
|
|
530
|
-
|
|
531
|
-
|
|
1170
|
+
const knowledge = decisionKnowledge(
|
|
1171
|
+
record,
|
|
1172
|
+
drift.freshness?.[record.id] ?? "unknown",
|
|
1173
|
+
drift.pendingProposals?.[record.id]?.freshness ?? "unknown"
|
|
1174
|
+
);
|
|
1175
|
+
append(
|
|
1176
|
+
record.id,
|
|
1177
|
+
knowledge,
|
|
1178
|
+
record.status === "active" ? knowledge.approval : record.status,
|
|
1179
|
+
knowledge.trust.freshness
|
|
1180
|
+
);
|
|
1181
|
+
if (knowledge.pendingProposal)
|
|
1182
|
+
append(
|
|
1183
|
+
record.id,
|
|
1184
|
+
knowledge.pendingProposal,
|
|
1185
|
+
"proposed",
|
|
1186
|
+
knowledge.pendingProposal.trust.freshness
|
|
1187
|
+
);
|
|
532
1188
|
}
|
|
533
1189
|
return lines.join("\n");
|
|
534
1190
|
}
|
|
535
1191
|
async function runHook(stdinText, env = {}) {
|
|
536
1192
|
let input;
|
|
537
1193
|
try {
|
|
538
|
-
input = JSON.parse(stdinText);
|
|
1194
|
+
input = hookStdinSchema.parse(JSON.parse(stdinText));
|
|
539
1195
|
} catch {
|
|
540
1196
|
return null;
|
|
541
1197
|
}
|
|
@@ -543,18 +1199,24 @@ async function runHook(stdinText, env = {}) {
|
|
|
543
1199
|
if (input.tool_name && !SUPPORTED_TOOLS.has(input.tool_name)) return null;
|
|
544
1200
|
const filePath = input.tool_input?.file_path;
|
|
545
1201
|
if (!filePath || typeof filePath !== "string") return null;
|
|
546
|
-
const absPath =
|
|
547
|
-
const root = await findMasonRoot(
|
|
1202
|
+
const absPath = path9.isAbsolute(filePath) ? filePath : path9.resolve(input.cwd ?? process.cwd(), filePath);
|
|
1203
|
+
const root = await findMasonRoot(path9.dirname(absPath));
|
|
548
1204
|
if (!root) return null;
|
|
549
|
-
|
|
1205
|
+
if (env.rootDir && await fs7.realpath(root) !== await fs7.realpath(env.rootDir)) return null;
|
|
1206
|
+
const relPath = path9.relative(root, absPath).split(path9.sep).join("/");
|
|
550
1207
|
if (relPath.startsWith("..")) return null;
|
|
551
1208
|
const { records } = await loadDecisionStore(root);
|
|
552
1209
|
const stateDir = env.stateDir ?? os2.tmpdir();
|
|
553
|
-
const stateFile =
|
|
1210
|
+
const stateFile = path9.join(
|
|
1211
|
+
stateDir,
|
|
1212
|
+
`mason-hook-${createHash4("sha256").update(root).digest("hex").slice(0, 12)}-${stateKey(input)}.json`
|
|
1213
|
+
);
|
|
554
1214
|
const injected = await loadInjected(stateFile);
|
|
555
|
-
const recordKey = (record) => `${record.id}:${
|
|
1215
|
+
const recordKey = (record) => `${record.id}:${createHash4("sha256").update(JSON.stringify(record)).digest("hex")}`;
|
|
556
1216
|
const previouslyInjected = (record) => [...injected].some((key) => key === record.id || key.startsWith(`${record.id}:`));
|
|
557
|
-
const fresh = records.filter(
|
|
1217
|
+
const fresh = records.filter(
|
|
1218
|
+
(r) => !injected.has(recordKey(r)) && (previouslyInjected(r) || r.status === "active" && anchorsCover(r, relPath))
|
|
1219
|
+
);
|
|
558
1220
|
if (fresh.length === 0) return null;
|
|
559
1221
|
fresh.sort((a, b) => {
|
|
560
1222
|
const withdrawn = Number(b.status !== "active") - Number(a.status !== "active");
|
|
@@ -567,12 +1229,12 @@ async function runHook(stdinText, env = {}) {
|
|
|
567
1229
|
const drift = await computeDecisionDrift(root, selected);
|
|
568
1230
|
for (const record of selected) injected.add(recordKey(record));
|
|
569
1231
|
try {
|
|
570
|
-
await
|
|
1232
|
+
await fs7.writeFile(stateFile, JSON.stringify([...injected]), "utf-8");
|
|
571
1233
|
} catch {
|
|
572
1234
|
}
|
|
573
1235
|
return JSON.stringify({
|
|
574
1236
|
hookSpecificOutput: {
|
|
575
|
-
hookEventName: "PostToolUse",
|
|
1237
|
+
hookEventName: env.hookEventName ?? "PostToolUse",
|
|
576
1238
|
additionalContext: formatContext(relPath, selected, drift)
|
|
577
1239
|
}
|
|
578
1240
|
});
|
|
@@ -635,7 +1297,9 @@ async function readStdin() {
|
|
|
635
1297
|
if (process.stdin.isTTY) return "";
|
|
636
1298
|
const chunks = [];
|
|
637
1299
|
for await (const chunk of process.stdin) {
|
|
638
|
-
|
|
1300
|
+
const value = chunk;
|
|
1301
|
+
if (typeof value === "string" || value instanceof Uint8Array) chunks.push(Buffer.from(value));
|
|
1302
|
+
else throw new Error("Unsupported stdin chunk.");
|
|
639
1303
|
}
|
|
640
1304
|
return Buffer.concat(chunks).toString("utf-8");
|
|
641
1305
|
}
|