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-audit.js
CHANGED
|
@@ -10,6 +10,19 @@ var __export = (target, all) => {
|
|
|
10
10
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
// src/utils/validation.ts
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function isStringArray(value) {
|
|
18
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
19
|
+
}
|
|
20
|
+
var init_validation = __esm({
|
|
21
|
+
"src/utils/validation.ts"() {
|
|
22
|
+
"use strict";
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
13
26
|
// src/utils/profile.ts
|
|
14
27
|
import { AsyncLocalStorage } from "async_hooks";
|
|
15
28
|
async function profilePhase(label, run) {
|
|
@@ -54,7 +67,19 @@ var init_git_read = __esm({
|
|
|
54
67
|
"use strict";
|
|
55
68
|
init_profile();
|
|
56
69
|
execute = promisify(execFile);
|
|
57
|
-
commands = /* @__PURE__ */ new Set([
|
|
70
|
+
commands = /* @__PURE__ */ new Set([
|
|
71
|
+
"rev-parse",
|
|
72
|
+
"symbolic-ref",
|
|
73
|
+
"for-each-ref",
|
|
74
|
+
"ls-files",
|
|
75
|
+
"check-ignore",
|
|
76
|
+
"status",
|
|
77
|
+
"log",
|
|
78
|
+
"diff",
|
|
79
|
+
"ls-tree",
|
|
80
|
+
"show",
|
|
81
|
+
"merge-base"
|
|
82
|
+
]);
|
|
58
83
|
}
|
|
59
84
|
});
|
|
60
85
|
|
|
@@ -62,7 +87,8 @@ var init_git_read = __esm({
|
|
|
62
87
|
import path from "path";
|
|
63
88
|
function normalizeRepoPath(value) {
|
|
64
89
|
const slash = value.replace(/\\/g, "/");
|
|
65
|
-
if (!slash || slash.includes("\0") || path.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash))
|
|
90
|
+
if (!slash || slash.includes("\0") || path.posix.isAbsolute(slash) || /^[A-Za-z]:/.test(slash))
|
|
91
|
+
return null;
|
|
66
92
|
if (slash.split("/").includes("..")) return null;
|
|
67
93
|
const normalized = path.posix.normalize(slash).replace(/\/$/, "");
|
|
68
94
|
return normalized === "." ? null : normalized;
|
|
@@ -77,7 +103,39 @@ function anchorMatches(anchor, file) {
|
|
|
77
103
|
return a !== null && f !== null && (a === f || f.startsWith(`${a}/`));
|
|
78
104
|
}
|
|
79
105
|
function matchingPaths(anchors, files) {
|
|
80
|
-
return [...new Set(files)].filter(
|
|
106
|
+
return [...new Set(files)].filter(
|
|
107
|
+
(file) => anchors.some((anchor) => anchorMatches(anchor, file))
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
function indexMatchingPaths(scopes2, files) {
|
|
111
|
+
const node = () => ({ children: /* @__PURE__ */ new Map(), scopes: /* @__PURE__ */ new Set() });
|
|
112
|
+
const root = node();
|
|
113
|
+
const matches = scopes2.map(() => []);
|
|
114
|
+
scopes2.forEach((anchors, index) => {
|
|
115
|
+
for (const anchor of anchors) {
|
|
116
|
+
const normalized = normalizeRepoPath(anchor);
|
|
117
|
+
if (!normalized) continue;
|
|
118
|
+
let current = root;
|
|
119
|
+
for (const part of normalized.split("/")) {
|
|
120
|
+
if (!current.children.has(part)) current.children.set(part, node());
|
|
121
|
+
current = current.children.get(part);
|
|
122
|
+
}
|
|
123
|
+
current.scopes.add(index);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
for (const file of new Set(files)) {
|
|
127
|
+
const normalized = normalizeRepoPath(file);
|
|
128
|
+
if (!normalized) continue;
|
|
129
|
+
const selected = /* @__PURE__ */ new Set();
|
|
130
|
+
let current = root;
|
|
131
|
+
for (const part of normalized.split("/")) {
|
|
132
|
+
current = current.children.get(part);
|
|
133
|
+
if (!current) break;
|
|
134
|
+
for (const index of current.scopes) selected.add(index);
|
|
135
|
+
}
|
|
136
|
+
for (const index of selected) matches[index].push(file);
|
|
137
|
+
}
|
|
138
|
+
return matches;
|
|
81
139
|
}
|
|
82
140
|
var init_paths = __esm({
|
|
83
141
|
"src/utils/paths.ts"() {
|
|
@@ -110,8 +168,18 @@ import fs from "fs/promises";
|
|
|
110
168
|
import { constants } from "fs";
|
|
111
169
|
import path2 from "path";
|
|
112
170
|
import fg from "fast-glob";
|
|
171
|
+
function isSensitiveFile(file) {
|
|
172
|
+
return file.split(/[\\/]/).some(
|
|
173
|
+
(part) => /^(?:\.env(?:\..*)?|id_rsa.*|id_ed25519.*)$|\.(?:pem|key|p12|pfx|jks|keystore)$|credentials\.|secret|^local\.properties$/i.test(
|
|
174
|
+
part
|
|
175
|
+
)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
113
178
|
async function readBoundedFile(file, maxBytes) {
|
|
114
|
-
const handle = await fs.open(
|
|
179
|
+
const handle = await fs.open(
|
|
180
|
+
file,
|
|
181
|
+
constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW
|
|
182
|
+
);
|
|
115
183
|
try {
|
|
116
184
|
const stat = await handle.stat();
|
|
117
185
|
if (!stat.isFile() || stat.size > maxBytes) return null;
|
|
@@ -131,36 +199,44 @@ async function loadProjectConfig(root) {
|
|
|
131
199
|
try {
|
|
132
200
|
const canonicalRoot = await fs.realpath(root);
|
|
133
201
|
const configPath = await fs.realpath(path2.join(root, ".mason/config.json"));
|
|
134
|
-
if (!isWithinRoot(canonicalRoot, configPath))
|
|
202
|
+
if (!isWithinRoot(canonicalRoot, configPath))
|
|
203
|
+
throw new Error("Project configuration resolves outside the repository");
|
|
135
204
|
const raw = await readBoundedFile(configPath, 64 * 1024);
|
|
136
|
-
if (raw === null)
|
|
205
|
+
if (raw === null)
|
|
206
|
+
throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
|
|
137
207
|
const value = JSON.parse(raw);
|
|
138
|
-
if (!
|
|
208
|
+
if (!isRecord(value)) throw new Error("Expected a configuration object");
|
|
139
209
|
const config = {};
|
|
140
210
|
for (const key of ["patterns", "alwaysInclude", "ignore"]) {
|
|
141
211
|
if (value[key] === void 0) continue;
|
|
142
|
-
if (!
|
|
212
|
+
if (!isStringArray(value[key])) {
|
|
143
213
|
throw new Error(`Configuration ${key} must be an array of strings`);
|
|
144
214
|
}
|
|
145
215
|
config[key] = value[key];
|
|
146
216
|
}
|
|
147
217
|
if (value.audit !== void 0) {
|
|
148
218
|
const audit = value.audit;
|
|
149
|
-
if (!
|
|
219
|
+
if (!isRecord(audit)) throw new Error("Configuration audit must be an object");
|
|
150
220
|
const patterns = (raw2) => {
|
|
151
|
-
if (!
|
|
152
|
-
|
|
221
|
+
if (!isStringArray(raw2) || raw2.length > 100 || !raw2.every(
|
|
222
|
+
(s) => typeof s === "string" && normalizeRepoPath(s) === s && !s.startsWith("!") && !/[\x00-\x1f]/.test(s)
|
|
223
|
+
)) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
"Audit patterns must be at most 100 repository-relative globs without traversal or negation"
|
|
226
|
+
);
|
|
153
227
|
}
|
|
154
228
|
return raw2;
|
|
155
229
|
};
|
|
156
230
|
config.audit = {};
|
|
157
231
|
if (audit.include !== void 0) config.audit.include = patterns(audit.include);
|
|
158
232
|
if (audit.exclude !== void 0) {
|
|
159
|
-
if (!
|
|
233
|
+
if (!isRecord(audit.exclude))
|
|
234
|
+
throw new Error("audit.exclude must map check names to patterns");
|
|
160
235
|
const { ALL_CHECKS: ALL_CHECKS2 } = await Promise.resolve().then(() => (init_types(), types_exports));
|
|
161
236
|
config.audit.exclude = {};
|
|
162
237
|
for (const [check, globs] of Object.entries(audit.exclude)) {
|
|
163
|
-
if (!ALL_CHECKS2.includes(check))
|
|
238
|
+
if (!ALL_CHECKS2.includes(check))
|
|
239
|
+
throw new Error("Unknown audit check: " + check);
|
|
164
240
|
config.audit.exclude[check] = patterns(globs);
|
|
165
241
|
}
|
|
166
242
|
}
|
|
@@ -168,16 +244,110 @@ async function loadProjectConfig(root) {
|
|
|
168
244
|
return config;
|
|
169
245
|
} catch (error) {
|
|
170
246
|
if (error.code === "ENOENT") return {};
|
|
171
|
-
throw new Error(
|
|
247
|
+
throw new Error(
|
|
248
|
+
`Cannot apply project file policy: ${error instanceof Error ? error.message : String(error)}`,
|
|
249
|
+
{ cause: error }
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function createFileAccess(rootDir) {
|
|
254
|
+
const root = path2.resolve(rootDir);
|
|
255
|
+
const canonicalRoot = await fs.realpath(root).catch(() => root);
|
|
256
|
+
const config = await loadProjectConfig(root);
|
|
257
|
+
const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
|
|
258
|
+
let gitFiles = null;
|
|
259
|
+
try {
|
|
260
|
+
const { stdout } = await execGit(
|
|
261
|
+
["ls-files", "-z", "--cached", "--others", "--exclude-standard"],
|
|
262
|
+
{ cwd: root, maxBuffer: 50 * 1024 * 1024 }
|
|
263
|
+
);
|
|
264
|
+
gitFiles = new Set(stdout.split("\0").filter(Boolean));
|
|
265
|
+
} catch {
|
|
266
|
+
let inGit = false;
|
|
267
|
+
try {
|
|
268
|
+
await execGit(["rev-parse", "--git-dir"], { cwd: root });
|
|
269
|
+
inGit = true;
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
if (inGit) throw new Error("Cannot enumerate Git files safely");
|
|
273
|
+
}
|
|
274
|
+
async function resolve(file) {
|
|
275
|
+
const relative = normalizeRepoPath(file);
|
|
276
|
+
if (!relative || isSensitiveFile(relative) || gitFiles && !gitFiles.has(relative))
|
|
277
|
+
return null;
|
|
278
|
+
const candidate = path2.join(root, relative);
|
|
279
|
+
try {
|
|
280
|
+
const real = await fs.realpath(candidate);
|
|
281
|
+
if (!isWithinRoot(canonicalRoot, real) || isSensitiveFile(path2.relative(canonicalRoot, real)))
|
|
282
|
+
return null;
|
|
283
|
+
const stat = await fs.stat(real);
|
|
284
|
+
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) return null;
|
|
285
|
+
if (gitFiles && !gitFiles.has(path2.relative(canonicalRoot, real).split(path2.sep).join("/")))
|
|
286
|
+
return null;
|
|
287
|
+
return real;
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function list(patterns = SOURCE_GLOB, options = {}) {
|
|
293
|
+
const found = await fg(patterns, { cwd: root, ignore, followSymbolicLinks: false, ...options });
|
|
294
|
+
const safe = await Promise.all(found.map(async (f) => await resolve(f) ? f : null));
|
|
295
|
+
return safe.filter((f) => f !== null).sort();
|
|
296
|
+
}
|
|
297
|
+
async function read(file) {
|
|
298
|
+
const relative = normalizeRepoPath(file);
|
|
299
|
+
if (!relative) return null;
|
|
300
|
+
const real = await resolve(relative);
|
|
301
|
+
if (!real) return null;
|
|
302
|
+
for (const rel of /* @__PURE__ */ new Set([
|
|
303
|
+
relative,
|
|
304
|
+
path2.relative(canonicalRoot, real).split(path2.sep).join("/")
|
|
305
|
+
])) {
|
|
306
|
+
if (!(await fg(fg.escapePath(rel), { cwd: root, ignore, dot: true })).length) return null;
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
const content = await readBoundedFile(real, MAX_SOURCE_BYTES);
|
|
310
|
+
return content === null ? null : { path: relative, content, totalLines: content.split("\n").length };
|
|
311
|
+
} catch {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
172
314
|
}
|
|
315
|
+
return { root, config, list, read, inventory: () => gitFiles ? [...gitFiles].sort() : null };
|
|
173
316
|
}
|
|
174
317
|
var SOURCE_EXTENSIONS, SOURCE_GLOB, SOURCE_IGNORE, MAX_SOURCE_BYTES;
|
|
175
318
|
var init_files = __esm({
|
|
176
319
|
"src/utils/files.ts"() {
|
|
177
320
|
"use strict";
|
|
321
|
+
init_validation();
|
|
178
322
|
init_git_read();
|
|
179
323
|
init_paths();
|
|
180
|
-
SOURCE_EXTENSIONS = [
|
|
324
|
+
SOURCE_EXTENSIONS = [
|
|
325
|
+
"ts",
|
|
326
|
+
"tsx",
|
|
327
|
+
"js",
|
|
328
|
+
"jsx",
|
|
329
|
+
"mts",
|
|
330
|
+
"cts",
|
|
331
|
+
"mjs",
|
|
332
|
+
"cjs",
|
|
333
|
+
"vue",
|
|
334
|
+
"svelte",
|
|
335
|
+
"kt",
|
|
336
|
+
"kts",
|
|
337
|
+
"java",
|
|
338
|
+
"py",
|
|
339
|
+
"go",
|
|
340
|
+
"rs",
|
|
341
|
+
"swift",
|
|
342
|
+
"rb",
|
|
343
|
+
"cs",
|
|
344
|
+
"cpp",
|
|
345
|
+
"c",
|
|
346
|
+
"h",
|
|
347
|
+
"hpp",
|
|
348
|
+
"dart",
|
|
349
|
+
"php"
|
|
350
|
+
];
|
|
181
351
|
SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
|
|
182
352
|
SOURCE_IGNORE = [
|
|
183
353
|
"**/node_modules/**",
|
|
@@ -245,7 +415,10 @@ async function readStoreJson(root, relative) {
|
|
|
245
415
|
return parsed;
|
|
246
416
|
} catch (error) {
|
|
247
417
|
if (error.code === "ENOENT") return null;
|
|
248
|
-
throw new Error(
|
|
418
|
+
throw new Error(
|
|
419
|
+
`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`,
|
|
420
|
+
{ cause: error }
|
|
421
|
+
);
|
|
249
422
|
}
|
|
250
423
|
}
|
|
251
424
|
async function writeStoreJson(root, relative, value) {
|
|
@@ -293,13 +466,18 @@ function inspectionRead(root, key, read) {
|
|
|
293
466
|
return inspection.reads.get(key);
|
|
294
467
|
}
|
|
295
468
|
function inspectionGit(args, options) {
|
|
296
|
-
return inspectionRead(
|
|
469
|
+
return inspectionRead(
|
|
470
|
+
options.cwd,
|
|
471
|
+
"git:" + JSON.stringify([args, options]),
|
|
472
|
+
() => execGit(args, options)
|
|
473
|
+
);
|
|
297
474
|
}
|
|
298
475
|
async function auditGitPaths(root, kind) {
|
|
299
476
|
const custom = (await loadProjectConfig(root)).audit?.include ?? [];
|
|
300
477
|
const shared = inspections.getStore()?.root === path4.resolve(root) && !custom.length;
|
|
301
478
|
const expanded = fg2.generateTasks(custom).flatMap((task) => task.positive);
|
|
302
|
-
if (expanded.length > 1e3)
|
|
479
|
+
if (expanded.length > 1e3)
|
|
480
|
+
throw new Error("Audit include patterns expand to more than 1,000 globs");
|
|
303
481
|
const documentSpecs = [...docSpecs, ...expanded.map((pattern) => `:(glob)${pattern}`)];
|
|
304
482
|
const specs = shared ? [...documentSpecs, ...sourceSpecs] : kind === "documents" ? documentSpecs : sourceSpecs;
|
|
305
483
|
const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
|
|
@@ -307,8 +485,14 @@ async function auditGitPaths(root, kind) {
|
|
|
307
485
|
["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
|
|
308
486
|
{ cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
|
|
309
487
|
);
|
|
310
|
-
const paths = [...new Set(stdout.split("\0").filter(Boolean))].filter(
|
|
311
|
-
|
|
488
|
+
const paths = [...new Set(stdout.split("\0").filter(Boolean))].filter(
|
|
489
|
+
(file) => kind === "documents" ? custom.length > 0 || /^(readme|agents|claude)\.md$/i.test(path4.posix.basename(file)) : (
|
|
490
|
+
// Git's *.ts also matches a file named .ts, unlike path.extname().
|
|
491
|
+
sourceSuffixes.has(file.slice(file.lastIndexOf(".")))
|
|
492
|
+
)
|
|
493
|
+
);
|
|
494
|
+
if (Buffer.byteLength(paths.join("\0")) > limit)
|
|
495
|
+
throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
|
|
312
496
|
return paths.sort();
|
|
313
497
|
}
|
|
314
498
|
var inspections, sourceSpecs, docSpecs, sourceSuffixes;
|
|
@@ -319,7 +503,11 @@ var init_inspection = __esm({
|
|
|
319
503
|
init_files();
|
|
320
504
|
inspections = new AsyncLocalStorage2();
|
|
321
505
|
sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
|
|
322
|
-
docSpecs = [
|
|
506
|
+
docSpecs = [
|
|
507
|
+
":(icase,glob)**/readme.md",
|
|
508
|
+
":(icase,glob)**/agents.md",
|
|
509
|
+
":(icase,glob)**/claude.md"
|
|
510
|
+
];
|
|
323
511
|
sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
|
|
324
512
|
}
|
|
325
513
|
});
|
|
@@ -335,7 +523,10 @@ async function auditInputPath(root, file) {
|
|
|
335
523
|
return await storePath(root, file);
|
|
336
524
|
} catch (error) {
|
|
337
525
|
if (error instanceof Error && error.message.startsWith("Symlink")) {
|
|
338
|
-
throw new Error(
|
|
526
|
+
throw new Error(
|
|
527
|
+
`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`,
|
|
528
|
+
{ cause: error }
|
|
529
|
+
);
|
|
339
530
|
}
|
|
340
531
|
throw error;
|
|
341
532
|
}
|
|
@@ -357,11 +548,19 @@ async function auditGlob(root, patterns, options) {
|
|
|
357
548
|
const relative = path5.relative(root, directory).split(path5.sep).join("/") || ".";
|
|
358
549
|
directories.add(relative);
|
|
359
550
|
if (directories.size > MAX_INPUTS) {
|
|
360
|
-
callback(
|
|
551
|
+
callback(
|
|
552
|
+
new Error(
|
|
553
|
+
`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`
|
|
554
|
+
)
|
|
555
|
+
);
|
|
361
556
|
return;
|
|
362
557
|
}
|
|
363
558
|
auditInputPath(root, relative).then(() => {
|
|
364
|
-
nativeFs.readdir(
|
|
559
|
+
nativeFs.readdir(
|
|
560
|
+
directory,
|
|
561
|
+
opts,
|
|
562
|
+
callback
|
|
563
|
+
);
|
|
365
564
|
}, callback);
|
|
366
565
|
});
|
|
367
566
|
const lstat2 = ((file, callback) => {
|
|
@@ -388,12 +587,16 @@ async function auditGlob(root, patterns, options) {
|
|
|
388
587
|
if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
|
|
389
588
|
matches.push(file);
|
|
390
589
|
if (matches.length > MAX_INPUTS) {
|
|
391
|
-
const largest = [
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
590
|
+
const largest = [
|
|
591
|
+
...matches.reduce((counts, item) => {
|
|
592
|
+
const dir = item.split("/")[0];
|
|
593
|
+
counts.set(dir, (counts.get(dir) ?? 0) + 1);
|
|
594
|
+
return counts;
|
|
595
|
+
}, /* @__PURE__ */ new Map())
|
|
596
|
+
].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
597
|
+
throw new Error(
|
|
598
|
+
`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count3]) => `${dir} (${count3})`).join(", ")}. Narrow this check's inputs.`
|
|
599
|
+
);
|
|
397
600
|
}
|
|
398
601
|
}
|
|
399
602
|
} finally {
|
|
@@ -440,11 +643,16 @@ async function includedCheckPaths(root, paths, exclude) {
|
|
|
440
643
|
if (!exclude.length) return new Set(paths);
|
|
441
644
|
const included = /* @__PURE__ */ new Set();
|
|
442
645
|
for (let offset = 0; offset < paths.length; offset += 256) {
|
|
443
|
-
for (const file of await auditGlob(
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
646
|
+
for (const file of await auditGlob(
|
|
647
|
+
root,
|
|
648
|
+
paths.slice(offset, offset + 256).map((file2) => fg4.escapePath(file2)),
|
|
649
|
+
{
|
|
650
|
+
ignore: exclude,
|
|
651
|
+
followSymbolicLinks: false,
|
|
652
|
+
label: "Audit check exclusions"
|
|
653
|
+
}
|
|
654
|
+
))
|
|
655
|
+
included.add(file);
|
|
448
656
|
}
|
|
449
657
|
return included;
|
|
450
658
|
}
|
|
@@ -456,28 +664,32 @@ var init_policy = __esm({
|
|
|
456
664
|
});
|
|
457
665
|
|
|
458
666
|
// src/utils/store-lock.ts
|
|
667
|
+
import { z } from "zod";
|
|
459
668
|
import fs4 from "fs/promises";
|
|
460
669
|
import os from "os";
|
|
461
670
|
async function lockDiagnostic(file) {
|
|
462
671
|
let reason;
|
|
463
672
|
try {
|
|
464
|
-
const owner = JSON.parse(await fs4.readFile(file, "utf8"));
|
|
465
|
-
if (!owner
|
|
673
|
+
const owner = lockOwnerSchema.safeParse(JSON.parse(await fs4.readFile(file, "utf8")));
|
|
674
|
+
if (!owner.success) {
|
|
466
675
|
reason = "Lock owner data is incomplete or malformed.";
|
|
467
|
-
} else if (owner.host !== os.hostname()) {
|
|
676
|
+
} else if (owner.data.host !== os.hostname()) {
|
|
468
677
|
reason = "The lock belongs to another host; its process cannot be checked locally.";
|
|
469
678
|
} else {
|
|
470
679
|
try {
|
|
471
|
-
process.kill(owner.pid, 0);
|
|
472
|
-
reason = `Local owner PID ${owner.pid} is still running.`;
|
|
680
|
+
process.kill(owner.data.pid, 0);
|
|
681
|
+
reason = `Local owner PID ${owner.data.pid} is still running.`;
|
|
473
682
|
} catch (error) {
|
|
474
|
-
reason = error.code === "ESRCH" ? `Local owner PID ${owner.pid} has exited, but lock recovery has not completed.` : `Local owner PID ${owner.pid} could not be checked.`;
|
|
683
|
+
reason = error.code === "ESRCH" ? `Local owner PID ${owner.data.pid} has exited, but lock recovery has not completed.` : `Local owner PID ${owner.data.pid} could not be checked.`;
|
|
475
684
|
}
|
|
476
685
|
}
|
|
477
686
|
} catch (error) {
|
|
478
687
|
reason = error instanceof SyntaxError ? "Lock owner data is incomplete or malformed." : "Lock owner data could not be read; the lock may have changed.";
|
|
479
688
|
}
|
|
480
|
-
if (await fs4.lstat(file + ".reclaim").then(
|
|
689
|
+
if (await fs4.lstat(file + ".reclaim").then(
|
|
690
|
+
() => true,
|
|
691
|
+
() => false
|
|
692
|
+
)) {
|
|
481
693
|
reason += " A recovery guard also needs inspection: " + file + ".reclaim.";
|
|
482
694
|
}
|
|
483
695
|
return reason + " Retry if a writer is active. For manual recovery, stop all Mason writers for this checkout, confirm they have exited (including other hosts), then remove only confirmed abandoned lock and lock.reclaim files. See docs/reference.md#snapshot-repairs-and-verification.";
|
|
@@ -497,7 +709,7 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
|
|
|
497
709
|
}
|
|
498
710
|
if (error.code !== "EEXIST") throw error;
|
|
499
711
|
try {
|
|
500
|
-
const owner = JSON.parse(await fs4.readFile(file, "utf8"));
|
|
712
|
+
const owner = lockOwnerSchema.parse(JSON.parse(await fs4.readFile(file, "utf8")));
|
|
501
713
|
if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
502
714
|
try {
|
|
503
715
|
process.kill(owner.pid, 0);
|
|
@@ -507,7 +719,7 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
|
|
|
507
719
|
let guard;
|
|
508
720
|
try {
|
|
509
721
|
guard = await fs4.open(reclaim, "wx", 384);
|
|
510
|
-
const current = JSON.parse(await fs4.readFile(file, "utf8"));
|
|
722
|
+
const current = lockOwnerSchema.parse(JSON.parse(await fs4.readFile(file, "utf8")));
|
|
511
723
|
if (current.pid === owner.pid && current.host === owner.host) await fs4.unlink(file);
|
|
512
724
|
} finally {
|
|
513
725
|
if (guard) {
|
|
@@ -520,7 +732,11 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
|
|
|
520
732
|
}
|
|
521
733
|
} catch {
|
|
522
734
|
}
|
|
523
|
-
if (Date.now() >= deadline)
|
|
735
|
+
if (Date.now() >= deadline)
|
|
736
|
+
throw new Error(
|
|
737
|
+
label + " is busy or its lock needs inspection: " + file + ". " + await lockDiagnostic(file),
|
|
738
|
+
{ cause: error }
|
|
739
|
+
);
|
|
524
740
|
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
525
741
|
}
|
|
526
742
|
if (handle) {
|
|
@@ -542,10 +758,12 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
|
|
|
542
758
|
await fs4.unlink(file);
|
|
543
759
|
}
|
|
544
760
|
}
|
|
761
|
+
var lockOwnerSchema;
|
|
545
762
|
var init_store_lock = __esm({
|
|
546
763
|
"src/utils/store-lock.ts"() {
|
|
547
764
|
"use strict";
|
|
548
765
|
init_storage();
|
|
766
|
+
lockOwnerSchema = z.object({ host: z.string(), pid: z.number().int().positive() });
|
|
549
767
|
}
|
|
550
768
|
});
|
|
551
769
|
|
|
@@ -557,18 +775,8 @@ var init_lock = __esm({
|
|
|
557
775
|
}
|
|
558
776
|
});
|
|
559
777
|
|
|
560
|
-
// src/test-map.ts
|
|
561
|
-
import path6 from "path";
|
|
562
|
-
var init_test_map = __esm({
|
|
563
|
-
"src/test-map.ts"() {
|
|
564
|
-
"use strict";
|
|
565
|
-
init_files();
|
|
566
|
-
}
|
|
567
|
-
});
|
|
568
|
-
|
|
569
778
|
// src/snapshot/snapshot.ts
|
|
570
|
-
import
|
|
571
|
-
import { z } from "zod";
|
|
779
|
+
import { z as z2 } from "zod";
|
|
572
780
|
async function getCurrentGitHash(rootDir) {
|
|
573
781
|
try {
|
|
574
782
|
const { stdout } = await execGit(["rev-parse", "HEAD"], {
|
|
@@ -585,42 +793,40 @@ var init_snapshot = __esm({
|
|
|
585
793
|
"use strict";
|
|
586
794
|
init_git_read();
|
|
587
795
|
init_files();
|
|
588
|
-
init_files();
|
|
589
796
|
init_storage();
|
|
590
797
|
init_lock();
|
|
591
798
|
init_paths();
|
|
592
|
-
|
|
593
|
-
repoPath = z.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
799
|
+
repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
594
800
|
verificationFields = {
|
|
595
|
-
refreshedHash:
|
|
596
|
-
verifiedAt:
|
|
597
|
-
verifiedHash:
|
|
598
|
-
verificationToken:
|
|
599
|
-
verificationFailed:
|
|
600
|
-
verificationNote:
|
|
801
|
+
refreshedHash: z2.string().optional(),
|
|
802
|
+
verifiedAt: z2.string().optional(),
|
|
803
|
+
verifiedHash: z2.string().optional(),
|
|
804
|
+
verificationToken: z2.string().optional(),
|
|
805
|
+
verificationFailed: z2.boolean().optional(),
|
|
806
|
+
verificationNote: z2.string().optional()
|
|
601
807
|
};
|
|
602
|
-
featureSchema =
|
|
603
|
-
description:
|
|
604
|
-
files:
|
|
605
|
-
tests:
|
|
606
|
-
type:
|
|
808
|
+
featureSchema = z2.object({
|
|
809
|
+
description: z2.string(),
|
|
810
|
+
files: z2.array(repoPath),
|
|
811
|
+
tests: z2.array(repoPath).optional(),
|
|
812
|
+
type: z2.enum(["capability", "infrastructure"]).optional(),
|
|
607
813
|
...verificationFields
|
|
608
814
|
}).passthrough();
|
|
609
|
-
flowSchema =
|
|
610
|
-
snapshotSchema =
|
|
611
|
-
version:
|
|
612
|
-
createdAt:
|
|
613
|
-
updatedAt:
|
|
614
|
-
gitHash:
|
|
615
|
-
features:
|
|
616
|
-
flows:
|
|
815
|
+
flowSchema = z2.object({ description: z2.string(), chain: z2.array(repoPath), ...verificationFields }).passthrough();
|
|
816
|
+
snapshotSchema = z2.object({
|
|
817
|
+
version: z2.literal(2),
|
|
818
|
+
createdAt: z2.string(),
|
|
819
|
+
updatedAt: z2.string(),
|
|
820
|
+
gitHash: z2.string(),
|
|
821
|
+
features: z2.record(featureSchema),
|
|
822
|
+
flows: z2.record(flowSchema)
|
|
617
823
|
}).passthrough();
|
|
618
824
|
}
|
|
619
825
|
});
|
|
620
826
|
|
|
621
827
|
// src/drift/drift.ts
|
|
622
828
|
import fs5 from "fs/promises";
|
|
623
|
-
import
|
|
829
|
+
import path6 from "path";
|
|
624
830
|
function parseChanges(output) {
|
|
625
831
|
const fields = output.split("\0");
|
|
626
832
|
const changes = [];
|
|
@@ -630,18 +836,25 @@ function parseChanges(output) {
|
|
|
630
836
|
if (!first) break;
|
|
631
837
|
const second = /^[RC]/.test(code) ? fields[i++] : void 0;
|
|
632
838
|
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 };
|
|
633
|
-
if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/")))
|
|
839
|
+
if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/")))
|
|
840
|
+
continue;
|
|
634
841
|
changes.push(change);
|
|
635
842
|
}
|
|
636
843
|
return changes;
|
|
637
844
|
}
|
|
638
845
|
function touchedPaths(changes) {
|
|
639
|
-
return [
|
|
846
|
+
return [
|
|
847
|
+
...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))
|
|
848
|
+
].sort();
|
|
640
849
|
}
|
|
641
850
|
async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
|
|
642
|
-
if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-"))
|
|
851
|
+
if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-"))
|
|
852
|
+
return null;
|
|
643
853
|
try {
|
|
644
|
-
const { stdout } = await execGit(
|
|
854
|
+
const { stdout } = await execGit(
|
|
855
|
+
["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"],
|
|
856
|
+
{ cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
857
|
+
);
|
|
645
858
|
return parseChanges(stdout);
|
|
646
859
|
} catch {
|
|
647
860
|
return null;
|
|
@@ -650,11 +863,23 @@ async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
|
|
|
650
863
|
async function getWorkingTree(resolvedRoot) {
|
|
651
864
|
try {
|
|
652
865
|
const [diff, untracked] = await Promise.all([
|
|
653
|
-
execGit(["diff", "--name-status", "-z", "-M", "HEAD", "--"], {
|
|
654
|
-
|
|
866
|
+
execGit(["diff", "--name-status", "-z", "-M", "HEAD", "--"], {
|
|
867
|
+
cwd: resolvedRoot,
|
|
868
|
+
maxBuffer: 10 * 1024 * 1024
|
|
869
|
+
}),
|
|
870
|
+
execGit(["ls-files", "-z", "--others", "--exclude-standard"], {
|
|
871
|
+
cwd: resolvedRoot,
|
|
872
|
+
maxBuffer: 10 * 1024 * 1024
|
|
873
|
+
})
|
|
655
874
|
]);
|
|
656
875
|
const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
|
|
657
|
-
return {
|
|
876
|
+
return {
|
|
877
|
+
available: true,
|
|
878
|
+
changedFiles: [
|
|
879
|
+
.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])
|
|
880
|
+
].sort(),
|
|
881
|
+
untrackedFiles
|
|
882
|
+
};
|
|
658
883
|
} catch {
|
|
659
884
|
return { available: false, changedFiles: [], untrackedFiles: [] };
|
|
660
885
|
}
|
|
@@ -669,7 +894,7 @@ var init_drift = __esm({
|
|
|
669
894
|
});
|
|
670
895
|
|
|
671
896
|
// src/audit/commands.ts
|
|
672
|
-
import
|
|
897
|
+
import path7 from "path";
|
|
673
898
|
function splitCommands(line) {
|
|
674
899
|
const parts = [];
|
|
675
900
|
let quote = "", start = 0;
|
|
@@ -705,7 +930,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
705
930
|
const cd = text2.match(/^cd\s+(.+)$/);
|
|
706
931
|
if (cd) {
|
|
707
932
|
const directory = literal(cd[1]);
|
|
708
|
-
cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null :
|
|
933
|
+
cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path7.posix.join(cwd ?? ".", directory);
|
|
709
934
|
continue;
|
|
710
935
|
}
|
|
711
936
|
for (const match of text2.matchAll(COMMAND)) {
|
|
@@ -713,13 +938,16 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
713
938
|
const selector = flags.match(/^(?:--prefix|--dir|-C)(?:\s+|=)(.+)$/);
|
|
714
939
|
let directory = cwd;
|
|
715
940
|
let scopeUnknown;
|
|
716
|
-
if (shell && match.index !== 0 || match[3].startsWith("-"))
|
|
941
|
+
if (shell && match.index !== 0 || match[3].startsWith("-"))
|
|
942
|
+
scopeUnknown = "Unsupported shell wrapper or options before the script.";
|
|
717
943
|
if (selector) {
|
|
718
944
|
const selected = literal(selector[1]);
|
|
719
|
-
directory = selected === null || cwd === null ? null :
|
|
945
|
+
directory = selected === null || cwd === null ? null : path7.posix.join(cwd ?? ".", selected);
|
|
720
946
|
} else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
|
|
721
947
|
const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
|
|
722
|
-
if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(
|
|
948
|
+
if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(
|
|
949
|
+
tail
|
|
950
|
+
)) {
|
|
723
951
|
scopeUnknown = "Package selection or conditional options after the script need review.";
|
|
724
952
|
}
|
|
725
953
|
if (/[|()]/.test(text2) || /\b(?:pushd|popd|if|for|while)\b/.test(text2) || directory === null) {
|
|
@@ -743,10 +971,13 @@ var init_commands = __esm({
|
|
|
743
971
|
"src/audit/commands.ts"() {
|
|
744
972
|
"use strict";
|
|
745
973
|
TOKEN = `(?:"[^"\\n]*"|'[^'\\n]*'|[^\\s\`;&|]+)`;
|
|
746
|
-
COMMAND = new RegExp(
|
|
974
|
+
COMMAND = new RegExp(
|
|
975
|
+
`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`,
|
|
976
|
+
"g"
|
|
977
|
+
);
|
|
747
978
|
literal = (value) => {
|
|
748
979
|
const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
|
|
749
|
-
return /[
|
|
980
|
+
return /[$`*?[\]{}~\\\x00-\x1f]/.test(unquoted) || path7.posix.isAbsolute(unquoted) ? null : unquoted;
|
|
750
981
|
};
|
|
751
982
|
}
|
|
752
983
|
});
|
|
@@ -806,11 +1037,7 @@ function extractTreeClaims(blockLines, blockStartLine) {
|
|
|
806
1037
|
}
|
|
807
1038
|
const isDir = name.endsWith("/");
|
|
808
1039
|
const cleanName = name.replace(/\/+$/, "");
|
|
809
|
-
const segments = [
|
|
810
|
-
...rootPrefix ? [rootPrefix] : [],
|
|
811
|
-
...stack.map((s) => s.name),
|
|
812
|
-
cleanName
|
|
813
|
-
];
|
|
1040
|
+
const segments = [...rootPrefix ? [rootPrefix] : [], ...stack.map((s) => s.name), cleanName];
|
|
814
1041
|
claims.push({
|
|
815
1042
|
path: segments.join("/"),
|
|
816
1043
|
line: blockStartLine + i,
|
|
@@ -907,7 +1134,7 @@ function extractClaims(content) {
|
|
|
907
1134
|
let line = token.map[0] + 1;
|
|
908
1135
|
for (const child of token.children ?? []) {
|
|
909
1136
|
const target = child.type === "link_open" ? child.attrGet("href") : child.type === "image" ? child.attrGet("src") : null;
|
|
910
|
-
if (target && !/^(?:[a-z][a-z0-9+.-]*:|[
|
|
1137
|
+
if (target && !/^(?:[a-z][a-z0-9+.-]*:|[/#~])/i.test(target)) {
|
|
911
1138
|
let pathname = null;
|
|
912
1139
|
try {
|
|
913
1140
|
pathname = decodeURIComponent(target.split(/[?#]/)[0]);
|
|
@@ -993,9 +1220,11 @@ function extractClaims(content) {
|
|
|
993
1220
|
});
|
|
994
1221
|
}
|
|
995
1222
|
for (const match of line.matchAll(/`([^`]+)`/g)) {
|
|
996
|
-
for (const claim of commandClaims(match[1], lineNo, void 0, true).claims)
|
|
1223
|
+
for (const claim of commandClaims(match[1], lineNo, void 0, true).claims)
|
|
1224
|
+
addCommand(claim);
|
|
997
1225
|
}
|
|
998
|
-
for (const claim of commandClaims(line.replace(/`[^`]+`/g, ""), lineNo).claims)
|
|
1226
|
+
for (const claim of commandClaims(line.replace(/`[^`]+`/g, ""), lineNo).claims)
|
|
1227
|
+
addCommand(claim);
|
|
999
1228
|
}
|
|
1000
1229
|
if (inFence) processBlock();
|
|
1001
1230
|
return {
|
|
@@ -1072,14 +1301,7 @@ async function lastCommitOf(resolvedRoot, relPath) {
|
|
|
1072
1301
|
async function deletingCommitOf(resolvedRoot, relPath) {
|
|
1073
1302
|
try {
|
|
1074
1303
|
const { stdout } = await inspectionGit(
|
|
1075
|
-
[
|
|
1076
|
-
"log",
|
|
1077
|
-
"-1",
|
|
1078
|
-
"--diff-filter=D",
|
|
1079
|
-
`--format=${COMMIT_FORMAT}`,
|
|
1080
|
-
"--",
|
|
1081
|
-
`:(literal)${relPath}`
|
|
1082
|
-
],
|
|
1304
|
+
["log", "-1", "--diff-filter=D", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
|
|
1083
1305
|
{ cwd: resolvedRoot }
|
|
1084
1306
|
);
|
|
1085
1307
|
const line = stdout.trim().split("\n")[0];
|
|
@@ -1138,7 +1360,7 @@ var init_git = __esm({
|
|
|
1138
1360
|
|
|
1139
1361
|
// src/audit/docs.ts
|
|
1140
1362
|
import fs6 from "fs/promises";
|
|
1141
|
-
import
|
|
1363
|
+
import path8 from "path";
|
|
1142
1364
|
import fg5 from "fast-glob";
|
|
1143
1365
|
async function dirtyDocs(resolvedRoot, files) {
|
|
1144
1366
|
if (!files.length) return /* @__PURE__ */ new Set();
|
|
@@ -1146,7 +1368,14 @@ async function dirtyDocs(resolvedRoot, files) {
|
|
|
1146
1368
|
const dirty = /* @__PURE__ */ new Set();
|
|
1147
1369
|
for (let offset = 0; offset < files.length; offset += 256) {
|
|
1148
1370
|
const { stdout } = await execGit(
|
|
1149
|
-
[
|
|
1371
|
+
[
|
|
1372
|
+
"status",
|
|
1373
|
+
"--porcelain=v1",
|
|
1374
|
+
"-z",
|
|
1375
|
+
"--untracked-files=all",
|
|
1376
|
+
"--",
|
|
1377
|
+
...files.slice(offset, offset + 256).map((file) => `:(literal)${file}`)
|
|
1378
|
+
],
|
|
1150
1379
|
{ cwd: resolvedRoot, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 }
|
|
1151
1380
|
);
|
|
1152
1381
|
const entries = stdout.split("\0");
|
|
@@ -1163,7 +1392,7 @@ async function dirtyDocs(resolvedRoot, files) {
|
|
|
1163
1392
|
}
|
|
1164
1393
|
}
|
|
1165
1394
|
async function discoverDocPaths(root) {
|
|
1166
|
-
let candidates
|
|
1395
|
+
let candidates;
|
|
1167
1396
|
try {
|
|
1168
1397
|
candidates = await auditGitPaths(root, "documents");
|
|
1169
1398
|
} catch (error) {
|
|
@@ -1191,15 +1420,26 @@ async function discoverDocPaths(root) {
|
|
|
1191
1420
|
const paths = [];
|
|
1192
1421
|
const unique = [...new Set(candidates)].sort();
|
|
1193
1422
|
for (let offset = 0; offset < unique.length; offset += 256) {
|
|
1194
|
-
paths.push(
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1423
|
+
paths.push(
|
|
1424
|
+
...await auditGlob(
|
|
1425
|
+
root,
|
|
1426
|
+
unique.slice(offset, offset + 256).map((p) => fg5.escapePath(p)),
|
|
1427
|
+
{
|
|
1428
|
+
ignore: excluded,
|
|
1429
|
+
followSymbolicLinks: false,
|
|
1430
|
+
label: "Documentation discovery"
|
|
1431
|
+
}
|
|
1432
|
+
)
|
|
1433
|
+
);
|
|
1434
|
+
if (paths.length > 1e4)
|
|
1435
|
+
throw new Error(
|
|
1436
|
+
"Documentation discovery exceeds 10,000 relevant files; narrow .mason/config.json ignore patterns."
|
|
1437
|
+
);
|
|
1200
1438
|
}
|
|
1201
1439
|
const priority = (file) => {
|
|
1202
|
-
const index = DOC_CANDIDATES.findIndex(
|
|
1440
|
+
const index = DOC_CANDIDATES.findIndex(
|
|
1441
|
+
(p) => p.toLowerCase() === file.toLowerCase()
|
|
1442
|
+
);
|
|
1203
1443
|
return index < 0 ? DOC_CANDIDATES.length : index;
|
|
1204
1444
|
};
|
|
1205
1445
|
return paths.sort((a, b) => priority(a) - priority(b) || a.localeCompare(b));
|
|
@@ -1214,16 +1454,17 @@ async function localDocPaths(root) {
|
|
|
1214
1454
|
if (error.code === "ENOENT") continue;
|
|
1215
1455
|
throw error;
|
|
1216
1456
|
}
|
|
1217
|
-
for (const entry of entries)
|
|
1218
|
-
|
|
1219
|
-
|
|
1457
|
+
for (const entry of entries)
|
|
1458
|
+
if (/^(?:agents|claude|readme)\.md$/i.test(entry.name)) {
|
|
1459
|
+
paths.push(path8.posix.join(dir, entry.name));
|
|
1460
|
+
}
|
|
1220
1461
|
}
|
|
1221
1462
|
return paths;
|
|
1222
1463
|
}
|
|
1223
1464
|
function documentScope(file) {
|
|
1224
|
-
if (!/^(readme|agents|claude)\.md$/i.test(
|
|
1225
|
-
const parent =
|
|
1226
|
-
return
|
|
1465
|
+
if (!/^(readme|agents|claude)\.md$/i.test(path8.posix.basename(file))) return ".";
|
|
1466
|
+
const parent = path8.posix.dirname(file);
|
|
1467
|
+
return path8.posix.basename(parent).toLowerCase() === ".claude" ? path8.posix.dirname(parent) : parent;
|
|
1227
1468
|
}
|
|
1228
1469
|
async function discoverDocs(resolvedRoot) {
|
|
1229
1470
|
return profilePhase("audit.documents", () => readDocs(resolvedRoot));
|
|
@@ -1233,23 +1474,28 @@ async function readDocs(resolvedRoot) {
|
|
|
1233
1474
|
const files = await discoverDocPaths(resolvedRoot);
|
|
1234
1475
|
const dirty = await dirtyDocs(resolvedRoot, files);
|
|
1235
1476
|
for (let offset = 0; offset < files.length; offset += 4) {
|
|
1236
|
-
docs.push(
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1477
|
+
docs.push(
|
|
1478
|
+
...await Promise.all(
|
|
1479
|
+
files.slice(offset, offset + 4).map(async (candidate) => {
|
|
1480
|
+
const [content, lastCommit] = await Promise.all([
|
|
1481
|
+
readAuditInput(resolvedRoot, candidate),
|
|
1482
|
+
profilePhase("audit.document-history", () => lastCommitOf(resolvedRoot, candidate))
|
|
1483
|
+
]);
|
|
1484
|
+
if (content === null)
|
|
1485
|
+
throw new Error("Document disappeared during discovery: " + candidate);
|
|
1486
|
+
return {
|
|
1487
|
+
path: candidate,
|
|
1488
|
+
content,
|
|
1489
|
+
scope: documentScope(candidate),
|
|
1490
|
+
kind: /^readme\.md$/i.test(path8.posix.basename(candidate)) ? "readme" : /^(agents|claude)\.md$/i.test(path8.posix.basename(candidate)) ? "instructions" : "guide",
|
|
1491
|
+
lineCount: content.split("\n").length,
|
|
1492
|
+
lastCommit,
|
|
1493
|
+
dirty: dirty.has(candidate),
|
|
1494
|
+
claims: extractClaims(content)
|
|
1495
|
+
};
|
|
1496
|
+
})
|
|
1497
|
+
)
|
|
1498
|
+
);
|
|
1253
1499
|
}
|
|
1254
1500
|
return docs;
|
|
1255
1501
|
}
|
|
@@ -1264,24 +1510,21 @@ var init_docs = __esm({
|
|
|
1264
1510
|
init_inputs();
|
|
1265
1511
|
init_files();
|
|
1266
1512
|
init_inspection();
|
|
1267
|
-
DOC_CANDIDATES = [
|
|
1268
|
-
"AGENTS.md",
|
|
1269
|
-
"CLAUDE.md",
|
|
1270
|
-
".claude/CLAUDE.md"
|
|
1271
|
-
];
|
|
1513
|
+
DOC_CANDIDATES = ["AGENTS.md", "CLAUDE.md", ".claude/CLAUDE.md"];
|
|
1272
1514
|
}
|
|
1273
1515
|
});
|
|
1274
1516
|
|
|
1275
1517
|
// src/audit/scope.ts
|
|
1276
1518
|
import fs7 from "fs/promises";
|
|
1277
|
-
import
|
|
1519
|
+
import path9 from "path";
|
|
1278
1520
|
function scopedPath(directory, value) {
|
|
1279
|
-
if (
|
|
1280
|
-
|
|
1521
|
+
if (path9.posix.isAbsolute(value) || value.includes("\\") || /[\x00-\x1f]/.test(value))
|
|
1522
|
+
return null;
|
|
1523
|
+
const resolved = path9.posix.normalize(path9.posix.join(directory, value));
|
|
1281
1524
|
return resolved === ".." || resolved.startsWith("../") ? null : resolved;
|
|
1282
1525
|
}
|
|
1283
1526
|
function pathClaimScope(doc, claim) {
|
|
1284
|
-
const directory = claim.relativeTo === "document" ?
|
|
1527
|
+
const directory = claim.relativeTo === "document" ? path9.posix.dirname(doc) : documentScope(doc);
|
|
1285
1528
|
const local = scopedPath(directory, claim.path);
|
|
1286
1529
|
if (!local) return null;
|
|
1287
1530
|
const candidates = claim.relativeTo === "document" || directory === "." ? [local] : [...new Set([local, scopedPath(".", claim.path)].filter((p) => p !== null))];
|
|
@@ -1315,12 +1558,14 @@ var init_scope = __esm({
|
|
|
1315
1558
|
});
|
|
1316
1559
|
|
|
1317
1560
|
// src/audit/checks/deleted-reference.ts
|
|
1318
|
-
import
|
|
1561
|
+
import path10 from "path";
|
|
1319
1562
|
async function checkDeletedReferences(ctx) {
|
|
1320
1563
|
const result = emptyResult();
|
|
1321
1564
|
for (const doc of ctx.docs) {
|
|
1322
1565
|
const changes = ctx.changesSinceDoc.get(doc.path);
|
|
1323
|
-
const renames = new Map(
|
|
1566
|
+
const renames = new Map(
|
|
1567
|
+
(changes ?? []).filter((c) => c.status === "renamed" && c.previousPath).map((c) => [c.previousPath, c.path])
|
|
1568
|
+
);
|
|
1324
1569
|
for (const claim of doc.claims.paths) {
|
|
1325
1570
|
const scope = pathClaimScope(doc.path, claim);
|
|
1326
1571
|
if (!scope) {
|
|
@@ -1340,12 +1585,17 @@ async function checkDeletedReferences(ctx) {
|
|
|
1340
1585
|
});
|
|
1341
1586
|
continue;
|
|
1342
1587
|
}
|
|
1343
|
-
if ((await Promise.all(scope.candidates.map((file) => pathExists(ctx.root, file)))).some(
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1588
|
+
if ((await Promise.all(scope.candidates.map((file) => pathExists(ctx.root, file)))).some(
|
|
1589
|
+
Boolean
|
|
1590
|
+
))
|
|
1591
|
+
continue;
|
|
1592
|
+
const candidates = await Promise.all(
|
|
1593
|
+
scope.candidates.map(async (file) => ({
|
|
1594
|
+
file,
|
|
1595
|
+
tracked: await lastCommitOf(ctx.root, file),
|
|
1596
|
+
parent: await pathExists(ctx.root, path10.posix.dirname(file))
|
|
1597
|
+
}))
|
|
1598
|
+
);
|
|
1349
1599
|
const candidate = candidates.find((c) => c.tracked || renames.has(c.file)) ?? candidates.find((c) => c.parent);
|
|
1350
1600
|
if (!candidate && claim.relativeTo !== "document") continue;
|
|
1351
1601
|
const resolvedPath = candidate?.file ?? scope.candidates[0];
|
|
@@ -1364,9 +1614,21 @@ async function checkDeletedReferences(ctx) {
|
|
|
1364
1614
|
const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
|
|
1365
1615
|
const message = renamedTo ? `\`${resolvedPath}\` was renamed to \`${renamedTo}\`` : `\`${resolvedPath}\` does not exist` + (deletedInCommit ? ` \u2013 deleted in ${deletedInCommit.hash.slice(0, 7)} "${deletedInCommit.subject}" (${deletedInCommit.date.slice(0, 10)})` : "");
|
|
1366
1616
|
if (scope.basis !== "document" && (renamedTo || candidate?.tracked)) {
|
|
1367
|
-
result.issues.push({
|
|
1617
|
+
result.issues.push({
|
|
1618
|
+
type: "deleted-reference",
|
|
1619
|
+
message,
|
|
1620
|
+
anchor,
|
|
1621
|
+
confidence: "certain",
|
|
1622
|
+
evidence
|
|
1623
|
+
});
|
|
1368
1624
|
} else {
|
|
1369
|
-
result.advisories.push({
|
|
1625
|
+
result.advisories.push({
|
|
1626
|
+
resolution: "recheck",
|
|
1627
|
+
type: "deleted-reference",
|
|
1628
|
+
message: message + (scope.basis === "document" ? "; the bare path's document/repository scope needs review." : "; no tracked history establishes it as a required file. Review for a typo, generated output, or example."),
|
|
1629
|
+
anchor,
|
|
1630
|
+
evidence
|
|
1631
|
+
});
|
|
1370
1632
|
}
|
|
1371
1633
|
}
|
|
1372
1634
|
}
|
|
@@ -1384,15 +1646,12 @@ var init_deleted_reference = __esm({
|
|
|
1384
1646
|
// src/audit/checks/new-module.ts
|
|
1385
1647
|
import fg6 from "fast-glob";
|
|
1386
1648
|
import fs8 from "fs/promises";
|
|
1387
|
-
import
|
|
1649
|
+
import path11 from "path";
|
|
1388
1650
|
function escapeRegExp(text2) {
|
|
1389
1651
|
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1390
1652
|
}
|
|
1391
1653
|
function isMentioned(combinedDocs, name) {
|
|
1392
|
-
const re = new RegExp(
|
|
1393
|
-
`(^|[^A-Za-z0-9_-])${escapeRegExp(name)}(/|[^A-Za-z0-9_-]|$)`,
|
|
1394
|
-
"im"
|
|
1395
|
-
);
|
|
1654
|
+
const re = new RegExp(`(^|[^A-Za-z0-9_-])${escapeRegExp(name)}(/|[^A-Za-z0-9_-]|$)`, "im");
|
|
1396
1655
|
return re.test(combinedDocs);
|
|
1397
1656
|
}
|
|
1398
1657
|
async function checkNewModules(ctx) {
|
|
@@ -1422,34 +1681,57 @@ async function checkNewModules(ctx) {
|
|
|
1422
1681
|
return result;
|
|
1423
1682
|
}
|
|
1424
1683
|
function moduleDocumentation(docs) {
|
|
1425
|
-
return docs.map(
|
|
1684
|
+
return docs.map(
|
|
1685
|
+
(doc) => doc.content + "\n" + (path11.posix.dirname(doc.path) === "." ? "" : path11.posix.dirname(doc.path) + "/")
|
|
1686
|
+
).join("\n");
|
|
1426
1687
|
}
|
|
1427
1688
|
function moduleCandidates(root, combinedDocs) {
|
|
1428
|
-
return inspectionRead(
|
|
1689
|
+
return inspectionRead(
|
|
1690
|
+
root,
|
|
1691
|
+
"modules:" + combinedDocs,
|
|
1692
|
+
() => collectModuleCandidates(root, combinedDocs)
|
|
1693
|
+
);
|
|
1429
1694
|
}
|
|
1430
1695
|
async function collectModuleCandidates(root, combinedDocs) {
|
|
1431
1696
|
const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
|
|
1432
1697
|
const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
|
|
1433
|
-
const sources = sourcePaths.filter(
|
|
1698
|
+
const sources = sourcePaths.filter(
|
|
1699
|
+
(file) => !file.split("/").some((part) => part.startsWith("."))
|
|
1700
|
+
);
|
|
1434
1701
|
const listSubdirs = async (parents) => {
|
|
1435
1702
|
const byParent = /* @__PURE__ */ new Map();
|
|
1436
1703
|
for (let offset = 0; offset < parents.length; offset += 16) {
|
|
1437
|
-
await Promise.all(
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1704
|
+
await Promise.all(
|
|
1705
|
+
parents.slice(offset, offset + 16).map(async (dir) => {
|
|
1706
|
+
const entries = await fs8.readdir(await auditInputPath(root, dir), {
|
|
1707
|
+
withFileTypes: true
|
|
1708
|
+
});
|
|
1709
|
+
const dirs = entries.filter(
|
|
1710
|
+
(entry) => (entry.isDirectory() || entry.isSymbolicLink() && sources.some(
|
|
1711
|
+
(file) => file.startsWith(path11.posix.join(dir, entry.name) + "/")
|
|
1712
|
+
)) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)
|
|
1713
|
+
).map((entry) => path11.posix.join(dir, entry.name));
|
|
1714
|
+
byParent.set(dir, dirs);
|
|
1715
|
+
})
|
|
1716
|
+
);
|
|
1442
1717
|
}
|
|
1443
1718
|
const ignored = await gitIgnoredPaths(root, [...byParent.values()].flat());
|
|
1444
1719
|
const result = /* @__PURE__ */ new Map();
|
|
1445
1720
|
for (const dir of parents) {
|
|
1446
|
-
const visible = new Set(
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1721
|
+
const visible = new Set(
|
|
1722
|
+
byParent.get(dir).filter(
|
|
1723
|
+
(candidate) => !ignored.has(candidate) || sources.some((file) => file.startsWith(candidate + "/"))
|
|
1724
|
+
)
|
|
1725
|
+
);
|
|
1726
|
+
result.set(
|
|
1727
|
+
dir,
|
|
1728
|
+
(await auditGlob(root, dir === "." ? "*" : `${fg6.escapePath(dir)}/*`, {
|
|
1729
|
+
ignore,
|
|
1730
|
+
onlyDirectories: true,
|
|
1731
|
+
label: "Module directory discovery",
|
|
1732
|
+
select: (file) => visible.has(file)
|
|
1733
|
+
})).map((file) => path11.posix.basename(file))
|
|
1734
|
+
);
|
|
1453
1735
|
}
|
|
1454
1736
|
return result;
|
|
1455
1737
|
};
|
|
@@ -1457,12 +1739,19 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1457
1739
|
const files = sources.filter((file) => file.startsWith(dir + "/"));
|
|
1458
1740
|
let count3 = 0;
|
|
1459
1741
|
for (let offset = 0; offset < files.length; offset += 256) {
|
|
1460
|
-
count3 += (await auditGlob(
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1742
|
+
count3 += (await auditGlob(
|
|
1743
|
+
root,
|
|
1744
|
+
files.slice(offset, offset + 256).map((file) => fg6.escapePath(file)),
|
|
1745
|
+
{
|
|
1746
|
+
ignore,
|
|
1747
|
+
followSymbolicLinks: false,
|
|
1748
|
+
label: `Source discovery in ${dir}`
|
|
1749
|
+
}
|
|
1750
|
+
)).length;
|
|
1751
|
+
if (count3 > 1e5)
|
|
1752
|
+
throw new Error(
|
|
1753
|
+
`Source discovery in ${dir} exceeds 100,000 relevant files. Exclude generated source with .mason/config.json ignore patterns or narrow this audit's checks.`
|
|
1754
|
+
);
|
|
1466
1755
|
}
|
|
1467
1756
|
return count3;
|
|
1468
1757
|
};
|
|
@@ -1523,7 +1812,7 @@ var init_new_module = __esm({
|
|
|
1523
1812
|
});
|
|
1524
1813
|
|
|
1525
1814
|
// src/audit/checks/stale-count.ts
|
|
1526
|
-
import
|
|
1815
|
+
import path12 from "path";
|
|
1527
1816
|
async function countGradleModules(root) {
|
|
1528
1817
|
for (const name of ["settings.gradle.kts", "settings.gradle"]) {
|
|
1529
1818
|
const content = await readAuditInput(root, name);
|
|
@@ -1545,7 +1834,10 @@ async function countNpmWorkspaces(root) {
|
|
|
1545
1834
|
let globs = [];
|
|
1546
1835
|
try {
|
|
1547
1836
|
const pkg = JSON.parse(pkgRaw);
|
|
1548
|
-
|
|
1837
|
+
if (!isRecord(pkg)) return null;
|
|
1838
|
+
const workspaces = isRecord(pkg.workspaces) ? pkg.workspaces.packages : pkg.workspaces;
|
|
1839
|
+
if (workspaces !== void 0 && !isStringArray(workspaces)) return null;
|
|
1840
|
+
globs = workspaces ?? [];
|
|
1549
1841
|
} catch {
|
|
1550
1842
|
}
|
|
1551
1843
|
if (globs.length > 0) {
|
|
@@ -1557,7 +1849,7 @@ async function countNpmWorkspaces(root) {
|
|
|
1557
1849
|
return {
|
|
1558
1850
|
actual: matched.length,
|
|
1559
1851
|
countedFrom: "package.json workspaces",
|
|
1560
|
-
members: matched.map((m) =>
|
|
1852
|
+
members: matched.map((m) => path12.dirname(m)).sort()
|
|
1561
1853
|
};
|
|
1562
1854
|
}
|
|
1563
1855
|
}
|
|
@@ -1588,7 +1880,7 @@ async function countNpmWorkspaces(root) {
|
|
|
1588
1880
|
return {
|
|
1589
1881
|
actual: matched.length,
|
|
1590
1882
|
countedFrom: "pnpm-workspace.yaml",
|
|
1591
|
-
members: matched.map((m) =>
|
|
1883
|
+
members: matched.map((m) => path12.dirname(m)).sort()
|
|
1592
1884
|
};
|
|
1593
1885
|
}
|
|
1594
1886
|
}
|
|
@@ -1599,9 +1891,7 @@ async function countCargoCrates(root) {
|
|
|
1599
1891
|
if (content === null) return null;
|
|
1600
1892
|
const membersBlock = content.match(/members\s*=\s*\[([\s\S]*?)\]/);
|
|
1601
1893
|
if (!membersBlock) return null;
|
|
1602
|
-
const entries = [...membersBlock[1].matchAll(/["']([^"']+)["']/g)].map(
|
|
1603
|
-
(m) => m[1]
|
|
1604
|
-
);
|
|
1894
|
+
const entries = [...membersBlock[1].matchAll(/["']([^"']+)["']/g)].map((m) => m[1]);
|
|
1605
1895
|
if (entries.length === 0) return null;
|
|
1606
1896
|
const members = /* @__PURE__ */ new Set();
|
|
1607
1897
|
for (const entry of entries) {
|
|
@@ -1610,8 +1900,8 @@ async function countCargoCrates(root) {
|
|
|
1610
1900
|
ignore: ["**/target/**"],
|
|
1611
1901
|
label: "Cargo workspace discovery"
|
|
1612
1902
|
});
|
|
1613
|
-
for (const m of matched) members.add(
|
|
1614
|
-
} else if (await readAuditInput(root,
|
|
1903
|
+
for (const m of matched) members.add(path12.dirname(m));
|
|
1904
|
+
} else if (await readAuditInput(root, path12.posix.join(entry, "Cargo.toml")) !== null) {
|
|
1615
1905
|
members.add(entry);
|
|
1616
1906
|
}
|
|
1617
1907
|
}
|
|
@@ -1631,7 +1921,7 @@ async function resolveCountSource(root, claim) {
|
|
|
1631
1921
|
}
|
|
1632
1922
|
async function resolveDocCountSource(root, doc, claim) {
|
|
1633
1923
|
const scope = documentScope(doc);
|
|
1634
|
-
const source = await resolveCountSource(
|
|
1924
|
+
const source = await resolveCountSource(path12.join(root, scope), claim);
|
|
1635
1925
|
if (!source || scope === ".") return source;
|
|
1636
1926
|
return {
|
|
1637
1927
|
...source,
|
|
@@ -1675,6 +1965,7 @@ var MEMBERS_CAP;
|
|
|
1675
1965
|
var init_stale_count = __esm({
|
|
1676
1966
|
"src/audit/checks/stale-count.ts"() {
|
|
1677
1967
|
"use strict";
|
|
1968
|
+
init_validation();
|
|
1678
1969
|
init_docs();
|
|
1679
1970
|
init_inputs();
|
|
1680
1971
|
init_checks();
|
|
@@ -1683,13 +1974,13 @@ var init_stale_count = __esm({
|
|
|
1683
1974
|
});
|
|
1684
1975
|
|
|
1685
1976
|
// src/audit/checks/dead-command.ts
|
|
1686
|
-
import
|
|
1977
|
+
import path13 from "path";
|
|
1687
1978
|
function scriptsOf(raw) {
|
|
1688
1979
|
try {
|
|
1689
1980
|
const pkg = JSON.parse(raw);
|
|
1690
|
-
if (!
|
|
1981
|
+
if (!isRecord(pkg)) return null;
|
|
1691
1982
|
if (pkg.scripts === void 0) return [];
|
|
1692
|
-
return
|
|
1983
|
+
return isRecord(pkg.scripts) && Object.values(pkg.scripts).every((value) => typeof value === "string") ? Object.keys(pkg.scripts) : null;
|
|
1693
1984
|
} catch {
|
|
1694
1985
|
return null;
|
|
1695
1986
|
}
|
|
@@ -1703,50 +1994,60 @@ async function commandInputs(root, docs) {
|
|
|
1703
1994
|
await read("package.json");
|
|
1704
1995
|
let inventory;
|
|
1705
1996
|
const resolutions = [];
|
|
1706
|
-
for (const doc of docs)
|
|
1707
|
-
const
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1997
|
+
for (const doc of docs)
|
|
1998
|
+
for (const claim of doc.claims.commands) {
|
|
1999
|
+
const base = documentScope(doc.path);
|
|
2000
|
+
const directory = scopedPath(base, claim.directory ?? ".");
|
|
2001
|
+
const scope = {
|
|
2002
|
+
basis: claim.directory === void 0 ? "document" : "explicit",
|
|
2003
|
+
directory: directory ?? base,
|
|
2004
|
+
candidates: []
|
|
2005
|
+
};
|
|
2006
|
+
const resolution = {
|
|
2007
|
+
doc: doc.path,
|
|
2008
|
+
claim,
|
|
2009
|
+
scope,
|
|
2010
|
+
manifest: null,
|
|
2011
|
+
scripts: [],
|
|
2012
|
+
elsewhere: []
|
|
2013
|
+
};
|
|
2014
|
+
resolutions.push(resolution);
|
|
2015
|
+
if (claim.scopeUnknown || directory === null) {
|
|
2016
|
+
resolution.problem = claim.scopeUnknown ?? "The command's directory leaves the repository.";
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
let cursor = directory;
|
|
2020
|
+
while (true) {
|
|
2021
|
+
const manifest = path13.posix.join(cursor, "package.json");
|
|
2022
|
+
scope.candidates.push(manifest);
|
|
2023
|
+
const raw = await read(manifest);
|
|
2024
|
+
if (raw !== null) {
|
|
2025
|
+
resolution.manifest = manifest;
|
|
2026
|
+
const scripts = scriptsOf(raw);
|
|
2027
|
+
if (scripts === null) resolution.problem = "Invalid package manifest: " + manifest;
|
|
2028
|
+
else resolution.scripts = scripts;
|
|
2029
|
+
break;
|
|
2030
|
+
}
|
|
2031
|
+
if (claim.directory !== void 0 || cursor === ".") break;
|
|
2032
|
+
cursor = path13.posix.dirname(cursor);
|
|
2033
|
+
}
|
|
2034
|
+
if (!resolution.manifest && !resolution.problem) {
|
|
2035
|
+
resolution.problem = `No package.json for the command's scope (${directory}).`;
|
|
2036
|
+
}
|
|
2037
|
+
if (resolution.problem || resolution.scripts.includes(claim.scriptName) || claim.directory !== void 0)
|
|
2038
|
+
continue;
|
|
2039
|
+
inventory ??= await commandManifests(root);
|
|
2040
|
+
for (const manifest of [.../* @__PURE__ */ new Set(["package.json", ...inventory])].sort()) {
|
|
2041
|
+
if (scope.candidates.includes(manifest)) continue;
|
|
2042
|
+
scope.candidates.push(manifest);
|
|
2043
|
+
const raw = await read(manifest);
|
|
2044
|
+
if (raw === null) continue;
|
|
1727
2045
|
const scripts = scriptsOf(raw);
|
|
1728
|
-
if (scripts === null)
|
|
1729
|
-
|
|
1730
|
-
|
|
2046
|
+
if (scripts === null)
|
|
2047
|
+
resolution.problem = "Cannot establish script availability: invalid manifest " + manifest;
|
|
2048
|
+
else if (scripts.includes(claim.scriptName)) resolution.elsewhere.push(manifest);
|
|
1731
2049
|
}
|
|
1732
|
-
if (claim.directory !== void 0 || cursor === ".") break;
|
|
1733
|
-
cursor = path15.posix.dirname(cursor);
|
|
1734
|
-
}
|
|
1735
|
-
if (!resolution.manifest && !resolution.problem) {
|
|
1736
|
-
resolution.problem = `No package.json for the command's scope (${directory}).`;
|
|
1737
|
-
}
|
|
1738
|
-
if (resolution.problem || resolution.scripts.includes(claim.scriptName) || claim.directory !== void 0) continue;
|
|
1739
|
-
inventory ??= await commandManifests(root);
|
|
1740
|
-
for (const manifest of [.../* @__PURE__ */ new Set(["package.json", ...inventory])].sort()) {
|
|
1741
|
-
if (scope.candidates.includes(manifest)) continue;
|
|
1742
|
-
scope.candidates.push(manifest);
|
|
1743
|
-
const raw = await read(manifest);
|
|
1744
|
-
if (raw === null) continue;
|
|
1745
|
-
const scripts = scriptsOf(raw);
|
|
1746
|
-
if (scripts === null) resolution.problem = "Cannot establish script availability: invalid manifest " + manifest;
|
|
1747
|
-
else if (scripts.includes(claim.scriptName)) resolution.elsewhere.push(manifest);
|
|
1748
2050
|
}
|
|
1749
|
-
}
|
|
1750
2051
|
return { manifests: [...contents].sort(([a], [b]) => a.localeCompare(b)), resolutions };
|
|
1751
2052
|
}
|
|
1752
2053
|
async function checkDeadCommands(ctx) {
|
|
@@ -1769,14 +2070,34 @@ async function checkDeadCommands(ctx) {
|
|
|
1769
2070
|
const anchor = { doc, line: claim.line, excerpt: claim.excerpt };
|
|
1770
2071
|
const message = `\`${claim.invocation}\` names script "${claim.scriptName}", which is absent from ${manifest}`;
|
|
1771
2072
|
const certain = documentScope(doc) === "." && (scope.basis === "explicit" || !elsewhere.length);
|
|
1772
|
-
if (certain)
|
|
1773
|
-
|
|
2073
|
+
if (certain)
|
|
2074
|
+
result.issues.push({
|
|
2075
|
+
type: "dead-command",
|
|
2076
|
+
message,
|
|
2077
|
+
anchor,
|
|
2078
|
+
confidence: "certain",
|
|
2079
|
+
evidence
|
|
2080
|
+
});
|
|
2081
|
+
else
|
|
2082
|
+
result.advisories.push({
|
|
2083
|
+
resolution: "recheck",
|
|
2084
|
+
type: "dead-command",
|
|
2085
|
+
message: message + (elsewhere.length ? `; it exists in ${elsewhere.join(", ")}. Clarify the intended working directory.` : "; the intended working directory needs review."),
|
|
2086
|
+
anchor,
|
|
2087
|
+
evidence
|
|
2088
|
+
});
|
|
1774
2089
|
}
|
|
1775
2090
|
return result;
|
|
1776
2091
|
}
|
|
1777
2092
|
function commandManifests(root) {
|
|
1778
2093
|
return auditGlob(root, "**/package.json", {
|
|
1779
|
-
ignore: [
|
|
2094
|
+
ignore: [
|
|
2095
|
+
"**/node_modules/**",
|
|
2096
|
+
"**/dist/**",
|
|
2097
|
+
"**/build/**",
|
|
2098
|
+
".mason/reports/**",
|
|
2099
|
+
"package.json"
|
|
2100
|
+
],
|
|
1780
2101
|
label: "Command manifest discovery"
|
|
1781
2102
|
});
|
|
1782
2103
|
}
|
|
@@ -1784,6 +2105,7 @@ var AVAILABLE_SCRIPTS_CAP;
|
|
|
1784
2105
|
var init_dead_command = __esm({
|
|
1785
2106
|
"src/audit/checks/dead-command.ts"() {
|
|
1786
2107
|
"use strict";
|
|
2108
|
+
init_validation();
|
|
1787
2109
|
init_inputs();
|
|
1788
2110
|
init_docs();
|
|
1789
2111
|
init_scope();
|
|
@@ -1793,36 +2115,63 @@ var init_dead_command = __esm({
|
|
|
1793
2115
|
});
|
|
1794
2116
|
|
|
1795
2117
|
// src/audit/dependency-claims.ts
|
|
1796
|
-
import
|
|
2118
|
+
import path14 from "path";
|
|
1797
2119
|
import { parse as parseToml } from "smol-toml";
|
|
1798
2120
|
function dependencyText(content) {
|
|
1799
|
-
return content.replace(
|
|
2121
|
+
return content.replace(
|
|
2122
|
+
/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g,
|
|
2123
|
+
(text2) => text2.replace(/[^\n]/g, " ")
|
|
2124
|
+
).replace(/<!--[\s\S]*?-->/g, (text2) => text2.replace(/[^\n]/g, " "));
|
|
1800
2125
|
}
|
|
1801
2126
|
function declarations(file, source) {
|
|
1802
2127
|
const result = /* @__PURE__ */ new Map();
|
|
1803
2128
|
if (!source) return result;
|
|
1804
|
-
if (
|
|
2129
|
+
if (path14.posix.basename(file) === "package.json") {
|
|
1805
2130
|
const json = JSON.parse(source);
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
2131
|
+
if (!isRecord(json)) throw new Error("Invalid package manifest");
|
|
2132
|
+
for (const section of [
|
|
2133
|
+
"dependencies",
|
|
2134
|
+
"devDependencies",
|
|
2135
|
+
"peerDependencies",
|
|
2136
|
+
"optionalDependencies",
|
|
2137
|
+
"engines"
|
|
2138
|
+
]) {
|
|
2139
|
+
const entries = json[section];
|
|
2140
|
+
if (entries === void 0) continue;
|
|
2141
|
+
if (!isRecord(entries) || !Object.values(entries).every((value) => typeof value === "string"))
|
|
2142
|
+
throw new Error("Invalid dependency declarations: " + section);
|
|
2143
|
+
for (const [name, value] of Object.entries(entries)) {
|
|
2144
|
+
if (typeof value === "string")
|
|
2145
|
+
result.set(name, [...result.has(name) ? [result.get(name)] : [], value].join("; "));
|
|
1809
2146
|
}
|
|
1810
2147
|
}
|
|
1811
2148
|
} else if (file.endsWith("libs.versions.toml")) {
|
|
1812
2149
|
const catalog = parseToml(source);
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
2150
|
+
const versions = catalog.versions;
|
|
2151
|
+
for (const section of ["libraries", "plugins"]) {
|
|
2152
|
+
const entries = catalog[section];
|
|
2153
|
+
if (entries === void 0) continue;
|
|
2154
|
+
if (!isRecord(entries)) throw new Error("Invalid version catalog: " + section);
|
|
2155
|
+
for (const entry of Object.values(entries)) {
|
|
2156
|
+
if (typeof entry === "string") {
|
|
2157
|
+
const parts = entry.split(":");
|
|
2158
|
+
if (parts.length >= 3) result.set(parts.slice(0, -1).join(":"), parts.at(-1));
|
|
2159
|
+
} else if (isRecord(entry)) {
|
|
2160
|
+
const name = entry.module ?? (typeof entry.group === "string" && typeof entry.name === "string" ? `${entry.group}:${entry.name}` : entry.id);
|
|
2161
|
+
const version = typeof entry.version === "string" ? entry.version : isRecord(entry.version) && typeof entry.version.ref === "string" && isRecord(versions) ? versions[entry.version.ref] : void 0;
|
|
2162
|
+
if (typeof name === "string")
|
|
2163
|
+
result.set(name, typeof version === "string" ? version : "unspecified");
|
|
2164
|
+
else throw new Error("Invalid version catalog entry");
|
|
2165
|
+
} else throw new Error("Invalid version catalog entry");
|
|
1821
2166
|
}
|
|
1822
2167
|
}
|
|
1823
2168
|
} else if (/build\.gradle(?:\.kts)?$/.test(file)) {
|
|
1824
|
-
for (const match of source.matchAll(/["']([\w.-]+:[\w.-]+):([^"'\s]+)["']/g))
|
|
1825
|
-
|
|
2169
|
+
for (const match of source.matchAll(/["']([\w.-]+:[\w.-]+):([^"'\s]+)["']/g))
|
|
2170
|
+
result.set(match[1], match[2]);
|
|
2171
|
+
for (const match of source.matchAll(
|
|
2172
|
+
/\bid\s*\(?\s*["']([\w.-]+)["']\s*\)?\s*version\s*["']([^"']+)["']/g
|
|
2173
|
+
))
|
|
2174
|
+
result.set(match[1], match[2]);
|
|
1826
2175
|
}
|
|
1827
2176
|
return result;
|
|
1828
2177
|
}
|
|
@@ -1837,17 +2186,28 @@ async function matchDependencyChanges(root, from, head, files, content) {
|
|
|
1837
2186
|
}
|
|
1838
2187
|
try {
|
|
1839
2188
|
const read = async (revision) => {
|
|
1840
|
-
const { stdout: entry } = await inspectionGit(
|
|
2189
|
+
const { stdout: entry } = await inspectionGit(
|
|
2190
|
+
["ls-tree", revision, "--", `:(literal)${file}`],
|
|
2191
|
+
{ cwd: root, timeout: 1e4 }
|
|
2192
|
+
);
|
|
1841
2193
|
if (!entry) return "";
|
|
1842
|
-
if (!entry.startsWith("100644 ") && !entry.startsWith("100755 "))
|
|
1843
|
-
|
|
2194
|
+
if (!entry.startsWith("100644 ") && !entry.startsWith("100755 "))
|
|
2195
|
+
throw new Error("Unsupported manifest mode");
|
|
2196
|
+
return (await inspectionGit(["show", `${revision}:${file}`], {
|
|
2197
|
+
cwd: root,
|
|
2198
|
+
maxBuffer: 1024 * 1024,
|
|
2199
|
+
timeout: 1e4
|
|
2200
|
+
})).stdout;
|
|
1844
2201
|
};
|
|
1845
2202
|
const [before, after] = await Promise.all([read(from), read(head)]);
|
|
1846
2203
|
const old = declarations(file, before), next = declarations(file, after);
|
|
1847
2204
|
for (const dependency of /* @__PURE__ */ new Set([...old.keys(), ...next.keys()])) {
|
|
1848
2205
|
if (old.get(dependency) === next.get(dependency)) continue;
|
|
1849
2206
|
const escaped = dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1850
|
-
const mention = new RegExp(
|
|
2207
|
+
const mention = new RegExp(
|
|
2208
|
+
`(^|[^a-zA-Z0-9_@./-])${escaped}(?![a-zA-Z0-9_/-]|\\.[a-zA-Z0-9_])`,
|
|
2209
|
+
"i"
|
|
2210
|
+
);
|
|
1851
2211
|
const line = lines.findIndex((text2) => mention.test(text2));
|
|
1852
2212
|
if (line < 0) continue;
|
|
1853
2213
|
matches.push({
|
|
@@ -1869,6 +2229,7 @@ async function matchDependencyChanges(root, from, head, files, content) {
|
|
|
1869
2229
|
var init_dependency_claims = __esm({
|
|
1870
2230
|
"src/audit/dependency-claims.ts"() {
|
|
1871
2231
|
"use strict";
|
|
2232
|
+
init_validation();
|
|
1872
2233
|
init_inspection();
|
|
1873
2234
|
}
|
|
1874
2235
|
});
|
|
@@ -1878,15 +2239,23 @@ async function git(root, args) {
|
|
|
1878
2239
|
return (await execGit(args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
|
|
1879
2240
|
}
|
|
1880
2241
|
function withoutAndroidReleaseValues(text2) {
|
|
1881
|
-
if (/\/\*|"""|'''/.test(text2) || !/id\s*\(?\s*["']com\.android\.(application|library)["']/.test(text2))
|
|
2242
|
+
if (/\/\*|"""|'''/.test(text2) || !/id\s*\(?\s*["']com\.android\.(application|library)["']/.test(text2))
|
|
2243
|
+
return null;
|
|
1882
2244
|
const scopes2 = [];
|
|
1883
2245
|
const normalized = [];
|
|
1884
2246
|
let assignments = 0;
|
|
1885
2247
|
for (const line of text2.split("\n")) {
|
|
1886
|
-
const code = line.replace(
|
|
1887
|
-
|
|
2248
|
+
const code = line.replace(
|
|
2249
|
+
/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/.*$/g,
|
|
2250
|
+
(token) => token.startsWith("//") ? "" : '""'
|
|
2251
|
+
);
|
|
2252
|
+
const assignment = line.match(
|
|
2253
|
+
/^(\s*)(versionName|versionCode)(\s*(?:=\s*|\s+))("[A-Za-z0-9._+-]+"|'[A-Za-z0-9._+-]+'|\d+)(\s*)$/
|
|
2254
|
+
);
|
|
1888
2255
|
if (assignment && scopes2.join("/") === "android/defaultConfig" && (assignment[2] === "versionCode" ? /^\d+$/.test(assignment[4]) : /^["']/.test(assignment[4]))) {
|
|
1889
|
-
normalized.push(
|
|
2256
|
+
normalized.push(
|
|
2257
|
+
assignment[1] + assignment[2] + assignment[3] + "<release-value>" + assignment[5]
|
|
2258
|
+
);
|
|
1890
2259
|
assignments++;
|
|
1891
2260
|
} else {
|
|
1892
2261
|
if (/\bversion(?:Name|Code)\b/.test(line)) return null;
|
|
@@ -1902,12 +2271,24 @@ function withoutAndroidReleaseValues(text2) {
|
|
|
1902
2271
|
return assignments && !scopes2.length ? normalized.join("\n") : null;
|
|
1903
2272
|
}
|
|
1904
2273
|
async function releaseMetadataOnly(root, commit) {
|
|
1905
|
-
if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file)))
|
|
2274
|
+
if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file)))
|
|
2275
|
+
return false;
|
|
1906
2276
|
try {
|
|
1907
2277
|
const parents = (await git(root, ["rev-list", "--parents", "-n", "1", commit.hash])).trim().split(/\s+/);
|
|
1908
2278
|
if (parents.length !== 2) return false;
|
|
1909
2279
|
for (const file of commit.files) {
|
|
1910
|
-
const raw = await git(root, [
|
|
2280
|
+
const raw = await git(root, [
|
|
2281
|
+
"diff",
|
|
2282
|
+
"--raw",
|
|
2283
|
+
"-z",
|
|
2284
|
+
"--no-renames",
|
|
2285
|
+
"--no-ext-diff",
|
|
2286
|
+
"--no-textconv",
|
|
2287
|
+
parents[1],
|
|
2288
|
+
commit.hash,
|
|
2289
|
+
"--",
|
|
2290
|
+
file
|
|
2291
|
+
]);
|
|
1911
2292
|
if (!/^:(100644|100755) \1 [a-f0-9]+ [a-f0-9]+ M\0/.test(raw)) return false;
|
|
1912
2293
|
const [before, after] = await Promise.all([
|
|
1913
2294
|
git(root, ["show", parents[1] + ":" + file]),
|
|
@@ -1931,13 +2312,21 @@ var init_release_metadata = __esm({
|
|
|
1931
2312
|
// src/audit/checks/deps-changed.ts
|
|
1932
2313
|
function hasDependencyContent(content) {
|
|
1933
2314
|
const text2 = content.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, "").replace(/<!--[\s\S]*?-->/g, "");
|
|
1934
|
-
return /\b(?:dependenc(?:y|ies)|librar(?:y|ies)|frameworks?|tech(?:nology)? stack|prerequisites|requirements)\b/i.test(
|
|
2315
|
+
return /\b(?:dependenc(?:y|ies)|librar(?:y|ies)|frameworks?|tech(?:nology)? stack|prerequisites|requirements)\b/i.test(
|
|
2316
|
+
text2
|
|
2317
|
+
) || /\b(?:package\.json|build\.gradle(?:\.kts)?|settings\.gradle(?:\.kts)?|libs\.versions\.toml|Cargo\.toml|go\.mod|pyproject\.toml|requirements\.txt|Gemfile|composer\.json)\b/i.test(
|
|
2318
|
+
text2
|
|
2319
|
+
) || /\b(?:node(?:\.js)?|npm|pnpm|yarn|bun|deno|python|ruby|rust|go|java|jdk|kotlin|gradle|swift|php|react|vue|angular|compose|ktor|room)\s*(?:version\s*)?[`*:=>~^v\s-]*\d+(?:\.\d+)*\b/i.test(
|
|
2320
|
+
text2
|
|
2321
|
+
) || /\b(?:npm|pnpm|yarn|pip3?|cargo|gem|composer)\s+(?:install|add|require)\b/i.test(text2);
|
|
1935
2322
|
}
|
|
1936
2323
|
function manifestPathspecs(doc) {
|
|
1937
2324
|
const scope = documentScope(doc);
|
|
1938
2325
|
if (scope === ".") return MANIFEST_PATHSPECS;
|
|
1939
|
-
const escaped = scope.replace(/[
|
|
1940
|
-
return MANIFEST_PATHSPECS.map(
|
|
2326
|
+
const escaped = scope.replace(/[\\*?[\]]/g, "\\$&");
|
|
2327
|
+
return MANIFEST_PATHSPECS.map(
|
|
2328
|
+
(spec) => spec.startsWith(":(glob)") ? ":(glob)" + escaped + "/" + spec.slice(":(glob)".length) : ":(literal)" + scope + "/" + spec
|
|
2329
|
+
);
|
|
1941
2330
|
}
|
|
1942
2331
|
async function checkDepsChanged(ctx) {
|
|
1943
2332
|
const result = emptyResult();
|
|
@@ -1955,12 +2344,17 @@ async function checkDepsChanged(ctx) {
|
|
|
1955
2344
|
let content = doc.content;
|
|
1956
2345
|
if (doc.dirty) {
|
|
1957
2346
|
try {
|
|
1958
|
-
content = (await inspectionGit(
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
2347
|
+
content = (await inspectionGit(["show", `${doc.lastCommit.hash}:${doc.path}`], {
|
|
2348
|
+
cwd: ctx.root,
|
|
2349
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
2350
|
+
timeout: 1e4
|
|
2351
|
+
})).stdout;
|
|
1962
2352
|
} catch {
|
|
1963
|
-
result.skipped.push({
|
|
2353
|
+
result.skipped.push({
|
|
2354
|
+
check: "deps-changed",
|
|
2355
|
+
doc: doc.path,
|
|
2356
|
+
reason: `${doc.path}: committed dependency content is unavailable`
|
|
2357
|
+
});
|
|
1964
2358
|
continue;
|
|
1965
2359
|
}
|
|
1966
2360
|
}
|
|
@@ -1996,11 +2390,12 @@ async function checkDepsChanged(ctx) {
|
|
|
1996
2390
|
);
|
|
1997
2391
|
if (!relevance.matches.length && !hasDependencyContent(content)) continue;
|
|
1998
2392
|
const match = relevance.matches[0];
|
|
1999
|
-
if (doc.dirty && match)
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2393
|
+
if (doc.dirty && match)
|
|
2394
|
+
result.skipped.push({
|
|
2395
|
+
check: "deps-changed",
|
|
2396
|
+
doc: doc.path,
|
|
2397
|
+
reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
|
|
2398
|
+
});
|
|
2004
2399
|
(doc.dirty ? result.suppressedAdvisories : result.advisories).push({
|
|
2005
2400
|
type: "deps-changed",
|
|
2006
2401
|
...match ? {} : { resolution: "informational" },
|
|
@@ -2046,8 +2441,145 @@ var init_deps_changed = __esm({
|
|
|
2046
2441
|
}
|
|
2047
2442
|
});
|
|
2048
2443
|
|
|
2444
|
+
// src/decisions/anchors.ts
|
|
2445
|
+
import { createHash } from "crypto";
|
|
2446
|
+
import fs9 from "fs/promises";
|
|
2447
|
+
import { constants as constants2 } from "fs";
|
|
2448
|
+
import { z as z3 } from "zod";
|
|
2449
|
+
import fg7 from "fast-glob";
|
|
2450
|
+
async function captureAnchorScopes(root, scopes2) {
|
|
2451
|
+
const union = [...new Set(scopes2.flat())].sort();
|
|
2452
|
+
let context;
|
|
2453
|
+
const prepareContext = async () => {
|
|
2454
|
+
const access = await inspectionRead(
|
|
2455
|
+
root,
|
|
2456
|
+
"decision-anchor-access",
|
|
2457
|
+
() => createFileAccess(root)
|
|
2458
|
+
);
|
|
2459
|
+
const inventory = access.inventory();
|
|
2460
|
+
if (!inventory) throw new Error("Git anchor inventory unavailable");
|
|
2461
|
+
const ignored = await inspectionGit(
|
|
2462
|
+
[
|
|
2463
|
+
"ls-files",
|
|
2464
|
+
"-z",
|
|
2465
|
+
"--others",
|
|
2466
|
+
"--ignored",
|
|
2467
|
+
"--exclude-standard",
|
|
2468
|
+
"--",
|
|
2469
|
+
...union.map((anchor) => `:(literal)${anchor}`)
|
|
2470
|
+
],
|
|
2471
|
+
{ cwd: root, maxBuffer: 8 * 1024 * 1024 }
|
|
2472
|
+
);
|
|
2473
|
+
const files = [
|
|
2474
|
+
.../* @__PURE__ */ new Set([...inventory, ...ignored.stdout.split("\0").filter(Boolean)])
|
|
2475
|
+
].sort();
|
|
2476
|
+
const patterns = await Promise.all(
|
|
2477
|
+
union.map(async (anchor) => {
|
|
2478
|
+
const literal2 = fg7.escapePath(anchor);
|
|
2479
|
+
try {
|
|
2480
|
+
return (await fs9.lstat(await storePath(root, anchor))).isDirectory() ? `${literal2}/**/*` : literal2;
|
|
2481
|
+
} catch {
|
|
2482
|
+
return literal2;
|
|
2483
|
+
}
|
|
2484
|
+
})
|
|
2485
|
+
);
|
|
2486
|
+
const eligible = new Set(await access.list(patterns, { dot: true }));
|
|
2487
|
+
const indexed = indexMatchingPaths(scopes2, files);
|
|
2488
|
+
const filesByScope = new Map(
|
|
2489
|
+
scopes2.map((scope, index) => [JSON.stringify([...new Set(scope)].sort()), indexed[index]])
|
|
2490
|
+
);
|
|
2491
|
+
return { filesByScope, eligible };
|
|
2492
|
+
};
|
|
2493
|
+
return Promise.all(
|
|
2494
|
+
scopes2.map((scope) => {
|
|
2495
|
+
const anchors = [...new Set(scope)].sort();
|
|
2496
|
+
return inspectionRead(
|
|
2497
|
+
root,
|
|
2498
|
+
"decision-anchor-capture:" + JSON.stringify(anchors),
|
|
2499
|
+
async () => {
|
|
2500
|
+
const capture = { anchors, files: {}, complete: true };
|
|
2501
|
+
if (!anchors.length) return capture;
|
|
2502
|
+
try {
|
|
2503
|
+
const { eligible, filesByScope } = await (context ??= prepareContext());
|
|
2504
|
+
const files = filesByScope.get(JSON.stringify(anchors));
|
|
2505
|
+
if (files.length > 2e3) return { ...capture, complete: false };
|
|
2506
|
+
for (const file of files) {
|
|
2507
|
+
const evidence = await inspectionRead(
|
|
2508
|
+
root,
|
|
2509
|
+
"decision-anchor-file:" + file,
|
|
2510
|
+
async () => {
|
|
2511
|
+
try {
|
|
2512
|
+
const absolute = await storePath(root, file);
|
|
2513
|
+
await fs9.lstat(absolute);
|
|
2514
|
+
if (!eligible.has(file)) return { complete: false };
|
|
2515
|
+
const handle = await fs9.open(
|
|
2516
|
+
absolute,
|
|
2517
|
+
constants2.O_RDONLY | constants2.O_NONBLOCK | constants2.O_NOFOLLOW
|
|
2518
|
+
);
|
|
2519
|
+
try {
|
|
2520
|
+
const stat = await handle.stat();
|
|
2521
|
+
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES)
|
|
2522
|
+
return { complete: false };
|
|
2523
|
+
const bytes = Buffer.alloc(Math.min(stat.size + 1, MAX_SOURCE_BYTES + 1));
|
|
2524
|
+
let size = 0;
|
|
2525
|
+
while (size < bytes.length) {
|
|
2526
|
+
const read = await handle.read(bytes, size, bytes.length - size, null);
|
|
2527
|
+
if (!read.bytesRead) break;
|
|
2528
|
+
size += read.bytesRead;
|
|
2529
|
+
}
|
|
2530
|
+
if (size === bytes.length) return { complete: false };
|
|
2531
|
+
return {
|
|
2532
|
+
complete: true,
|
|
2533
|
+
hash: createHash("sha256").update(bytes.subarray(0, size)).digest("hex")
|
|
2534
|
+
};
|
|
2535
|
+
} finally {
|
|
2536
|
+
await handle.close();
|
|
2537
|
+
}
|
|
2538
|
+
} catch (error) {
|
|
2539
|
+
return { complete: error.code === "ENOENT" };
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
);
|
|
2543
|
+
if (!evidence.complete) capture.complete = false;
|
|
2544
|
+
if (evidence.hash) capture.files[file] = evidence.hash;
|
|
2545
|
+
}
|
|
2546
|
+
} catch {
|
|
2547
|
+
capture.complete = false;
|
|
2548
|
+
}
|
|
2549
|
+
return capture;
|
|
2550
|
+
}
|
|
2551
|
+
);
|
|
2552
|
+
})
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
async function captureAnchors(root, anchors) {
|
|
2556
|
+
return (await captureAnchorScopes(root, [anchors]))[0];
|
|
2557
|
+
}
|
|
2558
|
+
function changedCaptureFiles(before, after) {
|
|
2559
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(before.files), ...Object.keys(after.files)])].filter((file) => before.files[file] !== after.files[file]).sort();
|
|
2560
|
+
}
|
|
2561
|
+
var repoPath2, anchorCaptureSchema;
|
|
2562
|
+
var init_anchors = __esm({
|
|
2563
|
+
"src/decisions/anchors.ts"() {
|
|
2564
|
+
"use strict";
|
|
2565
|
+
init_inspection();
|
|
2566
|
+
init_files();
|
|
2567
|
+
init_paths();
|
|
2568
|
+
init_storage();
|
|
2569
|
+
repoPath2 = z3.string().refine((p) => normalizeRepoPath(p) === p);
|
|
2570
|
+
anchorCaptureSchema = z3.object({
|
|
2571
|
+
anchors: z3.array(repoPath2),
|
|
2572
|
+
files: z3.record(repoPath2, z3.string().regex(/^[a-f0-9]{64}$/)),
|
|
2573
|
+
complete: z3.boolean()
|
|
2574
|
+
}).refine(
|
|
2575
|
+
(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,
|
|
2576
|
+
"Capture paths must belong to its normalized, sorted anchor scope"
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
});
|
|
2580
|
+
|
|
2049
2581
|
// src/context/lexical.ts
|
|
2050
|
-
import
|
|
2582
|
+
import path15 from "path";
|
|
2051
2583
|
var init_lexical = __esm({
|
|
2052
2584
|
"src/context/lexical.ts"() {
|
|
2053
2585
|
"use strict";
|
|
@@ -2062,7 +2594,8 @@ var init_trust = __esm({
|
|
|
2062
2594
|
});
|
|
2063
2595
|
|
|
2064
2596
|
// src/decisions/provenance.ts
|
|
2065
|
-
import { z as
|
|
2597
|
+
import { z as z4 } from "zod";
|
|
2598
|
+
import { createHash as createHash2 } from "crypto";
|
|
2066
2599
|
function decisionContent(record) {
|
|
2067
2600
|
return {
|
|
2068
2601
|
title: record.title,
|
|
@@ -2077,7 +2610,8 @@ function decisionApproval(record) {
|
|
|
2077
2610
|
return record.version === 1 ? "unreviewed" : record.approval;
|
|
2078
2611
|
}
|
|
2079
2612
|
function effectiveDecision(record) {
|
|
2080
|
-
if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed")
|
|
2613
|
+
if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed")
|
|
2614
|
+
return record;
|
|
2081
2615
|
let index = record.history.length - 1;
|
|
2082
2616
|
while (index >= 0 && !["accepted", "reaffirmed"].includes(record.history[index].kind)) index--;
|
|
2083
2617
|
if (index < 0) return record;
|
|
@@ -2089,13 +2623,16 @@ function effectiveDecision(record) {
|
|
|
2089
2623
|
approval: "accepted",
|
|
2090
2624
|
revision: event.revision,
|
|
2091
2625
|
refreshedHash: event.refreshedHash,
|
|
2626
|
+
capture: event.capture,
|
|
2092
2627
|
updatedAt: event.at,
|
|
2093
2628
|
history: record.history.slice(0, index + 1)
|
|
2094
2629
|
};
|
|
2095
2630
|
}
|
|
2096
2631
|
function decisionProvenance(record, freshness = "unknown") {
|
|
2097
2632
|
const approval = decisionApproval(record);
|
|
2098
|
-
const review = record.version === 2 ? [...record.history].reverse().find(
|
|
2633
|
+
const review = record.version === 2 ? [...record.history].reverse().find(
|
|
2634
|
+
(e) => ["accepted", "reaffirmed"].includes(e.kind) && e.revision === record.revision
|
|
2635
|
+
) : void 0;
|
|
2099
2636
|
return {
|
|
2100
2637
|
approval,
|
|
2101
2638
|
revision: record.version === 2 ? record.revision : 0,
|
|
@@ -2103,141 +2640,250 @@ function decisionProvenance(record, freshness = "unknown") {
|
|
|
2103
2640
|
sources: record.version === 2 ? record.sources : [],
|
|
2104
2641
|
guidance: record.status !== "active" ? "historical" : approval === "accepted" ? "constraint" : approval === "proposed" ? "proposal" : "unreviewed",
|
|
2105
2642
|
reviewRequired: record.status === "active" && (approval !== "accepted" || freshness !== "current"),
|
|
2106
|
-
lastReview: review ? {
|
|
2643
|
+
lastReview: review ? {
|
|
2644
|
+
reviewer: review.actor,
|
|
2645
|
+
at: review.at,
|
|
2646
|
+
note: review.note,
|
|
2647
|
+
gitHash: review.refreshedHash
|
|
2648
|
+
} : null
|
|
2107
2649
|
};
|
|
2108
2650
|
}
|
|
2651
|
+
function decisionInspectionDigest(record, scope = "all") {
|
|
2652
|
+
const effective = effectiveDecision(record);
|
|
2653
|
+
if (scope === "operative")
|
|
2654
|
+
return createHash2("sha256").update(
|
|
2655
|
+
JSON.stringify([
|
|
2656
|
+
"operative",
|
|
2657
|
+
record.status,
|
|
2658
|
+
decisionApproval(effective),
|
|
2659
|
+
effective.version === 2 ? effective.revision : 0,
|
|
2660
|
+
decisionContent(effective),
|
|
2661
|
+
effective.refreshedHash
|
|
2662
|
+
])
|
|
2663
|
+
).digest("hex");
|
|
2664
|
+
return createHash2("sha256").update(
|
|
2665
|
+
JSON.stringify([
|
|
2666
|
+
record.status,
|
|
2667
|
+
decisionApproval(record),
|
|
2668
|
+
record.version === 2 ? record.revision : 0,
|
|
2669
|
+
decisionContent(record),
|
|
2670
|
+
decisionContent(effective),
|
|
2671
|
+
effective.refreshedHash
|
|
2672
|
+
])
|
|
2673
|
+
).digest("hex");
|
|
2674
|
+
}
|
|
2675
|
+
function latestDecisionInspection(record, scope = "all") {
|
|
2676
|
+
return record.version === 2 ? record.inspections?.filter(
|
|
2677
|
+
(i) => (scope === "operative" || i.scope !== "operative") && i.decisionDigest === decisionInspectionDigest(record, i.scope ?? "all")
|
|
2678
|
+
).at(-1) : void 0;
|
|
2679
|
+
}
|
|
2109
2680
|
var text, decisionSourceSchema, attributionSchema, contentSchema, approvalSchema, statusSchema, reviewEvidenceSchema, eventSchema, legacySchema, currentSchema, decisionSchema;
|
|
2110
2681
|
var init_provenance = __esm({
|
|
2111
2682
|
"src/decisions/provenance.ts"() {
|
|
2112
2683
|
"use strict";
|
|
2684
|
+
init_anchors();
|
|
2113
2685
|
init_paths();
|
|
2114
2686
|
init_trust();
|
|
2115
|
-
text = (max) =>
|
|
2116
|
-
decisionSourceSchema =
|
|
2117
|
-
kind:
|
|
2687
|
+
text = (max) => z4.string().trim().min(1).max(max);
|
|
2688
|
+
decisionSourceSchema = z4.object({
|
|
2689
|
+
kind: z4.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
|
|
2118
2690
|
reference: text(1e3),
|
|
2119
2691
|
note: text(500).optional()
|
|
2120
2692
|
}).strict();
|
|
2121
|
-
attributionSchema =
|
|
2693
|
+
attributionSchema = z4.object({
|
|
2122
2694
|
owner: text(200).nullable().optional(),
|
|
2123
|
-
sources:
|
|
2695
|
+
sources: z4.array(decisionSourceSchema).max(20).optional(),
|
|
2124
2696
|
actor: text(200).optional()
|
|
2125
2697
|
});
|
|
2126
|
-
contentSchema =
|
|
2127
|
-
title:
|
|
2128
|
-
body:
|
|
2129
|
-
category:
|
|
2130
|
-
files:
|
|
2698
|
+
contentSchema = z4.object({
|
|
2699
|
+
title: z4.string().min(1),
|
|
2700
|
+
body: z4.string().min(1),
|
|
2701
|
+
category: z4.enum(["decision", "gotcha", "deprecation", "convention"]),
|
|
2702
|
+
files: z4.array(z4.string().refine((f) => normalizeRepoPath(f) !== null)),
|
|
2131
2703
|
owner: text(200).optional(),
|
|
2132
|
-
sources:
|
|
2704
|
+
sources: z4.array(decisionSourceSchema).max(20)
|
|
2133
2705
|
});
|
|
2134
|
-
approvalSchema =
|
|
2135
|
-
statusSchema =
|
|
2136
|
-
reviewEvidenceSchema =
|
|
2137
|
-
baseHash:
|
|
2138
|
-
headHash:
|
|
2139
|
-
historyAvailable:
|
|
2140
|
-
changedFiles:
|
|
2141
|
-
localChanges:
|
|
2706
|
+
approvalSchema = z4.enum(["unreviewed", "proposed", "accepted"]);
|
|
2707
|
+
statusSchema = z4.enum(["active", "superseded", "retired"]);
|
|
2708
|
+
reviewEvidenceSchema = z4.object({
|
|
2709
|
+
baseHash: z4.string(),
|
|
2710
|
+
headHash: z4.string(),
|
|
2711
|
+
historyAvailable: z4.boolean(),
|
|
2712
|
+
changedFiles: z4.array(z4.string()),
|
|
2713
|
+
localChanges: z4.array(z4.string())
|
|
2142
2714
|
});
|
|
2143
|
-
eventSchema =
|
|
2144
|
-
kind:
|
|
2145
|
-
|
|
2715
|
+
eventSchema = z4.object({
|
|
2716
|
+
kind: z4.enum([
|
|
2717
|
+
"imported",
|
|
2718
|
+
"created",
|
|
2719
|
+
"revised",
|
|
2720
|
+
"accepted",
|
|
2721
|
+
"reaffirmed",
|
|
2722
|
+
"retired",
|
|
2723
|
+
"superseded"
|
|
2724
|
+
]),
|
|
2725
|
+
at: z4.string().datetime(),
|
|
2146
2726
|
actor: text(200).optional(),
|
|
2147
2727
|
note: text(1500).optional(),
|
|
2148
|
-
revision:
|
|
2728
|
+
revision: z4.number().int().positive(),
|
|
2149
2729
|
content: contentSchema,
|
|
2150
2730
|
approval: approvalSchema,
|
|
2151
2731
|
status: statusSchema,
|
|
2152
|
-
refreshedHash:
|
|
2153
|
-
evidence: reviewEvidenceSchema.optional()
|
|
2732
|
+
refreshedHash: z4.string(),
|
|
2733
|
+
evidence: reviewEvidenceSchema.optional(),
|
|
2734
|
+
capture: anchorCaptureSchema.optional()
|
|
2154
2735
|
});
|
|
2155
|
-
legacySchema =
|
|
2156
|
-
version:
|
|
2157
|
-
id:
|
|
2158
|
-
title:
|
|
2159
|
-
body:
|
|
2736
|
+
legacySchema = z4.object({
|
|
2737
|
+
version: z4.literal(1),
|
|
2738
|
+
id: z4.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
2739
|
+
title: z4.string().min(1),
|
|
2740
|
+
body: z4.string().min(1),
|
|
2160
2741
|
category: contentSchema.shape.category,
|
|
2161
2742
|
files: contentSchema.shape.files,
|
|
2162
|
-
createdAt:
|
|
2163
|
-
updatedAt:
|
|
2164
|
-
refreshedHash:
|
|
2165
|
-
status:
|
|
2166
|
-
supersededBy:
|
|
2743
|
+
createdAt: z4.string(),
|
|
2744
|
+
updatedAt: z4.string(),
|
|
2745
|
+
refreshedHash: z4.string(),
|
|
2746
|
+
status: z4.enum(["active", "superseded"]),
|
|
2747
|
+
supersededBy: z4.string().optional()
|
|
2167
2748
|
}).passthrough();
|
|
2168
2749
|
currentSchema = legacySchema.extend({
|
|
2169
|
-
version:
|
|
2750
|
+
version: z4.literal(2),
|
|
2170
2751
|
status: statusSchema,
|
|
2171
2752
|
approval: approvalSchema,
|
|
2172
|
-
revision:
|
|
2753
|
+
revision: z4.number().int().positive(),
|
|
2173
2754
|
owner: text(200).optional(),
|
|
2174
|
-
sources:
|
|
2175
|
-
history:
|
|
2755
|
+
sources: z4.array(decisionSourceSchema).max(20),
|
|
2756
|
+
history: z4.array(eventSchema).min(1),
|
|
2757
|
+
capture: anchorCaptureSchema.optional(),
|
|
2758
|
+
inspections: z4.array(
|
|
2759
|
+
z4.object({
|
|
2760
|
+
at: z4.string().datetime(),
|
|
2761
|
+
inspector: text(200),
|
|
2762
|
+
note: text(1500),
|
|
2763
|
+
outcome: z4.literal("no_contradiction_found"),
|
|
2764
|
+
scope: z4.literal("operative").optional(),
|
|
2765
|
+
decisionDigest: z4.string().regex(/^[a-f0-9]{64}$/),
|
|
2766
|
+
headHash: z4.string(),
|
|
2767
|
+
capture: anchorCaptureSchema
|
|
2768
|
+
})
|
|
2769
|
+
).max(200).optional()
|
|
2176
2770
|
}).superRefine((record, ctx) => {
|
|
2177
2771
|
const invalid = (message) => ctx.addIssue({ code: "custom", message });
|
|
2178
2772
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
2179
2773
|
let previous;
|
|
2180
2774
|
for (const event of record.history) {
|
|
2181
2775
|
if (!previous) {
|
|
2182
|
-
if (!["created", "imported"].includes(event.kind) || event.revision !== 1)
|
|
2183
|
-
|
|
2776
|
+
if (!["created", "imported"].includes(event.kind) || event.revision !== 1)
|
|
2777
|
+
invalid("History must begin with creation or legacy import at revision 1");
|
|
2778
|
+
if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed"))
|
|
2779
|
+
invalid("Initial records cannot claim acceptance");
|
|
2184
2780
|
} else {
|
|
2185
2781
|
if (["created", "imported"].includes(event.kind)) invalid("History cannot restart");
|
|
2186
2782
|
if (previous.status !== "active") invalid("Archived decisions cannot be changed");
|
|
2187
|
-
if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0))
|
|
2188
|
-
|
|
2189
|
-
if (event.kind
|
|
2190
|
-
|
|
2783
|
+
if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0))
|
|
2784
|
+
invalid("Invalid revision sequence");
|
|
2785
|
+
if (event.kind !== "revised" && !same(event.content, previous.content))
|
|
2786
|
+
invalid("A review cannot silently revise decision content");
|
|
2787
|
+
if (event.kind === "reaffirmed" && previous.approval !== "accepted")
|
|
2788
|
+
invalid("Only accepted decisions can be reaffirmed");
|
|
2789
|
+
if (event.kind === "accepted" && previous.approval === "accepted")
|
|
2790
|
+
invalid("Use reaffirmation for an accepted decision");
|
|
2191
2791
|
const approval = event.kind === "revised" ? "proposed" : ["accepted", "reaffirmed"].includes(event.kind) ? "accepted" : previous.approval;
|
|
2192
2792
|
if (event.approval !== approval) invalid("Approval disagrees with review history");
|
|
2193
|
-
if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash)
|
|
2793
|
+
if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash)
|
|
2794
|
+
invalid("Only a review can refresh the evidence baseline");
|
|
2194
2795
|
}
|
|
2195
|
-
if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active"))
|
|
2196
|
-
|
|
2796
|
+
if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active"))
|
|
2797
|
+
invalid("Lifecycle disagrees with history");
|
|
2798
|
+
if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence))
|
|
2799
|
+
invalid("Reviews require a named reviewer, reason, and code evidence");
|
|
2197
2800
|
if (["accepted", "reaffirmed"].includes(event.kind)) {
|
|
2198
|
-
if (!event.content.owner || !event.content.sources.length)
|
|
2199
|
-
|
|
2801
|
+
if (!event.content.owner || !event.content.sources.length)
|
|
2802
|
+
invalid("Accepted decisions require an owner and source");
|
|
2803
|
+
if (!event.evidence || !/^[a-f0-9]{40,64}$/.test(event.evidence.headHash) || event.refreshedHash !== event.evidence.headHash || event.evidence.localChanges.length)
|
|
2804
|
+
invalid("Acceptance requires a committed evidence baseline");
|
|
2200
2805
|
}
|
|
2806
|
+
if (event.capture && !same([...event.content.files].sort(), event.capture.anchors))
|
|
2807
|
+
invalid("Capture scope disagrees with decision anchors");
|
|
2201
2808
|
previous = event;
|
|
2202
2809
|
}
|
|
2203
|
-
if (
|
|
2810
|
+
if (previous && !same(previous.capture, record.capture))
|
|
2811
|
+
invalid("Capture does not match the final history event");
|
|
2812
|
+
if (!previous || !same(previous.content, decisionContent(record)) || previous.approval !== record.approval || previous.status !== record.status || previous.revision !== record.revision || previous.refreshedHash !== record.refreshedHash)
|
|
2813
|
+
invalid("Decision does not match the final history event");
|
|
2204
2814
|
});
|
|
2205
|
-
decisionSchema =
|
|
2815
|
+
decisionSchema = z4.union([legacySchema, currentSchema]);
|
|
2206
2816
|
}
|
|
2207
2817
|
});
|
|
2208
2818
|
|
|
2209
2819
|
// src/decisions/decisions.ts
|
|
2210
|
-
import
|
|
2211
|
-
import
|
|
2212
|
-
import { createHash } from "crypto";
|
|
2213
|
-
import { z as
|
|
2820
|
+
import fs10 from "fs/promises";
|
|
2821
|
+
import path16 from "path";
|
|
2822
|
+
import { createHash as createHash3 } from "crypto";
|
|
2823
|
+
import { z as z5 } from "zod";
|
|
2824
|
+
async function readDecisionInputs(rootDir) {
|
|
2825
|
+
return structuredClone(
|
|
2826
|
+
await inspectionRead(rootDir, "decision-store:inputs", async () => {
|
|
2827
|
+
const inputs = [];
|
|
2828
|
+
const diagnostics = [];
|
|
2829
|
+
let entries;
|
|
2830
|
+
try {
|
|
2831
|
+
entries = await fs10.readdir(await storePath(rootDir, ".mason/decisions"));
|
|
2832
|
+
} catch (error) {
|
|
2833
|
+
if (error.code !== "ENOENT")
|
|
2834
|
+
diagnostics.push({ path: ".mason/decisions", message: String(error) });
|
|
2835
|
+
return { inputs, diagnostics };
|
|
2836
|
+
}
|
|
2837
|
+
for (const entry of entries.sort()) {
|
|
2838
|
+
if (!entry.endsWith(".json")) continue;
|
|
2839
|
+
const relative = `.mason/decisions/${entry}`;
|
|
2840
|
+
try {
|
|
2841
|
+
const raw = await readBoundedFile(await storePath(rootDir, relative), 10 * 1024 * 1024);
|
|
2842
|
+
if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
|
|
2843
|
+
inputs.push([relative, raw]);
|
|
2844
|
+
} catch (error) {
|
|
2845
|
+
if (error.code === "ENOENT") inputs.push([relative, null]);
|
|
2846
|
+
else
|
|
2847
|
+
diagnostics.push({
|
|
2848
|
+
path: relative,
|
|
2849
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
return { inputs, diagnostics };
|
|
2854
|
+
})
|
|
2855
|
+
);
|
|
2856
|
+
}
|
|
2214
2857
|
async function loadDecisionStore(rootDir) {
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2858
|
+
return structuredClone(
|
|
2859
|
+
await inspectionRead(rootDir, "decision-store:parsed", async () => {
|
|
2860
|
+
const raw = await readDecisionInputs(rootDir);
|
|
2861
|
+
const records = [];
|
|
2862
|
+
const diagnostics = [...raw.diagnostics];
|
|
2863
|
+
for (const [relative, content] of raw.inputs) {
|
|
2864
|
+
try {
|
|
2865
|
+
const record = decisionSchema.parse(content === null ? null : JSON.parse(content));
|
|
2866
|
+
if (relative !== `.mason/decisions/${record.id}.json`)
|
|
2867
|
+
throw new Error("Record id does not match its filename");
|
|
2868
|
+
records.push(record);
|
|
2869
|
+
} catch (error) {
|
|
2870
|
+
diagnostics.push({
|
|
2871
|
+
path: relative,
|
|
2872
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
return { records, diagnostics };
|
|
2877
|
+
})
|
|
2878
|
+
);
|
|
2236
2879
|
}
|
|
2237
2880
|
var TITLE_MAX_CHARS, BODY_RECOMMENDED_CHARS, BODY_MAX_CHARS, boundedText, decisionTitleSchema, decisionBodySchema;
|
|
2238
2881
|
var init_decisions = __esm({
|
|
2239
2882
|
"src/decisions/decisions.ts"() {
|
|
2240
2883
|
"use strict";
|
|
2884
|
+
init_inspection();
|
|
2885
|
+
init_files();
|
|
2886
|
+
init_anchors();
|
|
2241
2887
|
init_storage();
|
|
2242
2888
|
init_paths();
|
|
2243
2889
|
init_snapshot();
|
|
@@ -2246,10 +2892,16 @@ var init_decisions = __esm({
|
|
|
2246
2892
|
TITLE_MAX_CHARS = 80;
|
|
2247
2893
|
BODY_RECOMMENDED_CHARS = 1500;
|
|
2248
2894
|
BODY_MAX_CHARS = 2500;
|
|
2249
|
-
boundedText = (field, max, hint) =>
|
|
2250
|
-
errorMap: (issue, ctx) => ({
|
|
2895
|
+
boundedText = (field, max, hint) => z5.string({
|
|
2896
|
+
errorMap: (issue, ctx) => ({
|
|
2897
|
+
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
|
|
2898
|
+
})
|
|
2251
2899
|
}).trim().min(1, `${field} must be non-empty`).max(max);
|
|
2252
|
-
decisionTitleSchema = boundedText(
|
|
2900
|
+
decisionTitleSchema = boundedText(
|
|
2901
|
+
"title",
|
|
2902
|
+
TITLE_MAX_CHARS,
|
|
2903
|
+
"Tighten it to a specific headline."
|
|
2904
|
+
);
|
|
2253
2905
|
decisionBodySchema = boundedText(
|
|
2254
2906
|
"body",
|
|
2255
2907
|
BODY_MAX_CHARS,
|
|
@@ -2259,15 +2911,53 @@ var init_decisions = __esm({
|
|
|
2259
2911
|
});
|
|
2260
2912
|
|
|
2261
2913
|
// src/decisions/drift.ts
|
|
2262
|
-
import
|
|
2914
|
+
import path17 from "path";
|
|
2263
2915
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
2264
|
-
const resolvedRoot =
|
|
2916
|
+
const resolvedRoot = path17.resolve(rootDir);
|
|
2265
2917
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
2266
|
-
const report = {
|
|
2267
|
-
|
|
2918
|
+
const report = {
|
|
2919
|
+
historyAvailable: true,
|
|
2920
|
+
totalDecisions: store.records.length,
|
|
2921
|
+
staleDecisions: {},
|
|
2922
|
+
freshness: {},
|
|
2923
|
+
diagnostics: store.diagnostics
|
|
2924
|
+
};
|
|
2925
|
+
const [head, workingTree] = await Promise.all([
|
|
2926
|
+
getCurrentGitHash(resolvedRoot),
|
|
2927
|
+
getWorkingTree(resolvedRoot)
|
|
2928
|
+
]);
|
|
2268
2929
|
const changesByHash = /* @__PURE__ */ new Map();
|
|
2269
|
-
const
|
|
2930
|
+
const baselines = store.records.filter((record) => record.status === "active").flatMap((record) => {
|
|
2931
|
+
const effective = effectiveDecision(record);
|
|
2932
|
+
return [.../* @__PURE__ */ new Set([effective, record])].flatMap((candidate) => {
|
|
2933
|
+
const inspection = latestDecisionInspection(
|
|
2934
|
+
record,
|
|
2935
|
+
candidate === effective && decisionApproval(effective) === "accepted" ? "operative" : "all"
|
|
2936
|
+
);
|
|
2937
|
+
const capture = inspection?.capture ?? (candidate.version === 2 ? candidate.capture : void 0);
|
|
2938
|
+
return capture ? [capture] : [];
|
|
2939
|
+
});
|
|
2940
|
+
});
|
|
2941
|
+
const captures = await captureAnchorScopes(
|
|
2942
|
+
resolvedRoot,
|
|
2943
|
+
baselines.map((baseline) => baseline.anchors)
|
|
2944
|
+
);
|
|
2945
|
+
const currentCaptures = new Map(
|
|
2946
|
+
baselines.map((baseline, index) => [JSON.stringify(baseline.anchors), captures[index]])
|
|
2947
|
+
);
|
|
2948
|
+
const inspect = async (record, inspectionCapture) => {
|
|
2270
2949
|
if (record.files.length === 0) return { freshness: "unknown", changedFiles: [] };
|
|
2950
|
+
const baseline = inspectionCapture ?? (record.version === 2 ? record.capture : void 0);
|
|
2951
|
+
if (baseline) {
|
|
2952
|
+
const current = currentCaptures.get(JSON.stringify(baseline.anchors));
|
|
2953
|
+
const hits2 = changedCaptureFiles(baseline, current).filter(
|
|
2954
|
+
(file) => matchingPaths(record.files, [file]).length
|
|
2955
|
+
);
|
|
2956
|
+
return {
|
|
2957
|
+
freshness: !baseline.complete || !current.complete ? "unknown" : hits2.length ? "changed" : "current",
|
|
2958
|
+
changedFiles: hits2
|
|
2959
|
+
};
|
|
2960
|
+
}
|
|
2271
2961
|
let touched = changesByHash.get(record.refreshedHash);
|
|
2272
2962
|
if (touched === void 0) {
|
|
2273
2963
|
const changes = record.refreshedHash === head && head !== "unknown" ? [] : await getChangesWithStatus(resolvedRoot, record.refreshedHash);
|
|
@@ -2277,21 +2967,33 @@ async function computeDecisionDrift(rootDir, decisions) {
|
|
|
2277
2967
|
if (touched === null) report.historyAvailable = false;
|
|
2278
2968
|
const hits = touched ? matchingPaths(record.files, touched) : [];
|
|
2279
2969
|
const localHits = matchingPaths(record.files, workingTree.changedFiles);
|
|
2280
|
-
return {
|
|
2970
|
+
return {
|
|
2971
|
+
freshness: touched === null || !workingTree.available ? "unknown" : hits.length || localHits.length ? "changed" : "current",
|
|
2972
|
+
changedFiles: [.../* @__PURE__ */ new Set([...hits, ...localHits])].sort()
|
|
2973
|
+
};
|
|
2281
2974
|
};
|
|
2282
2975
|
for (const record of store.records) {
|
|
2283
2976
|
if (record.status !== "active") continue;
|
|
2284
2977
|
const effective = effectiveDecision(record);
|
|
2285
|
-
const
|
|
2978
|
+
const inspection = latestDecisionInspection(
|
|
2979
|
+
record,
|
|
2980
|
+
decisionApproval(effective) === "accepted" ? "operative" : "all"
|
|
2981
|
+
);
|
|
2982
|
+
const state = await inspect(effective, inspection?.capture);
|
|
2286
2983
|
report.freshness[record.id] = state.freshness;
|
|
2287
2984
|
if (state.changedFiles.length) report.staleDecisions[record.id] = state.changedFiles;
|
|
2288
|
-
if (effective !== record)
|
|
2985
|
+
if (effective !== record)
|
|
2986
|
+
(report.pendingProposals ??= {})[record.id] = await inspect(
|
|
2987
|
+
record,
|
|
2988
|
+
latestDecisionInspection(record)?.capture
|
|
2989
|
+
);
|
|
2289
2990
|
}
|
|
2290
2991
|
return report;
|
|
2291
2992
|
}
|
|
2292
2993
|
var init_drift2 = __esm({
|
|
2293
2994
|
"src/decisions/drift.ts"() {
|
|
2294
2995
|
"use strict";
|
|
2996
|
+
init_anchors();
|
|
2295
2997
|
init_drift();
|
|
2296
2998
|
init_paths();
|
|
2297
2999
|
init_snapshot();
|
|
@@ -2306,7 +3008,11 @@ async function checkDecisionAnchors(ctx) {
|
|
|
2306
3008
|
if (!ctx.decisionsPresent) return result;
|
|
2307
3009
|
const store = await loadDecisionStore(ctx.root);
|
|
2308
3010
|
const records = store.records;
|
|
2309
|
-
for (const diagnostic of store.diagnostics)
|
|
3011
|
+
for (const diagnostic of store.diagnostics)
|
|
3012
|
+
result.skipped.push({
|
|
3013
|
+
check: "decision-anchor-drift",
|
|
3014
|
+
reason: `${diagnostic.path}: ${diagnostic.message}`
|
|
3015
|
+
});
|
|
2310
3016
|
const drift = await computeDecisionDrift(ctx.root, records);
|
|
2311
3017
|
if (!drift.historyAvailable) {
|
|
2312
3018
|
result.skipped.push({
|
|
@@ -2314,17 +3020,30 @@ async function checkDecisionAnchors(ctx) {
|
|
|
2314
3020
|
reason: "some decision base commits are unreachable (shallow clone?)"
|
|
2315
3021
|
});
|
|
2316
3022
|
}
|
|
2317
|
-
const changed = records.
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
3023
|
+
const changed = records.filter((record) => record.status === "active").flatMap(
|
|
3024
|
+
(record) => [
|
|
3025
|
+
{
|
|
3026
|
+
record: effectiveDecision(record),
|
|
3027
|
+
changedFiles: drift.staleDecisions[record.id] ?? [],
|
|
3028
|
+
freshness: drift.freshness?.[record.id] ?? "unknown"
|
|
3029
|
+
},
|
|
3030
|
+
...drift.pendingProposals?.[record.id] ? [{ record, ...drift.pendingProposals[record.id] }] : []
|
|
3031
|
+
]
|
|
3032
|
+
);
|
|
2321
3033
|
for (const { record, changedFiles, freshness } of changed) {
|
|
3034
|
+
if (freshness === "unknown" && record.files.length) {
|
|
3035
|
+
result.skipped.push({
|
|
3036
|
+
check: "decision-anchor-drift",
|
|
3037
|
+
reason: `Anchor evidence for decision ${record.id} is incomplete or unreadable`
|
|
3038
|
+
});
|
|
3039
|
+
continue;
|
|
3040
|
+
}
|
|
2322
3041
|
if (!changedFiles.length) continue;
|
|
2323
3042
|
const id = record.id;
|
|
2324
3043
|
const provenance = decisionProvenance(record, freshness);
|
|
2325
3044
|
result.advisories.push({
|
|
2326
3045
|
type: "decision-anchor-drift",
|
|
2327
|
-
message: `decision "${record.title}" (${provenance.approval}) has
|
|
3046
|
+
message: `decision "${record.title}" (${provenance.approval}) has anchors changed since capture or review; consistency is unchecked`,
|
|
2328
3047
|
anchor: {
|
|
2329
3048
|
doc: `.mason/decisions/${id}.json`,
|
|
2330
3049
|
line: null,
|
|
@@ -2378,8 +3097,8 @@ var init_checks = __esm({
|
|
|
2378
3097
|
});
|
|
2379
3098
|
|
|
2380
3099
|
// src/audit/findings.ts
|
|
2381
|
-
import { createHash as
|
|
2382
|
-
import { z as
|
|
3100
|
+
import { createHash as createHash4 } from "crypto";
|
|
3101
|
+
import { z as z6 } from "zod";
|
|
2383
3102
|
function findingId(finding) {
|
|
2384
3103
|
const e = finding.evidence;
|
|
2385
3104
|
let key;
|
|
@@ -2409,106 +3128,154 @@ var digest, checkSchema, commitSchema, anchorSchema, count, scopeSchema, evidenc
|
|
|
2409
3128
|
var init_findings = __esm({
|
|
2410
3129
|
"src/audit/findings.ts"() {
|
|
2411
3130
|
"use strict";
|
|
2412
|
-
digest = (value) =>
|
|
2413
|
-
checkSchema =
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
3131
|
+
digest = (value) => createHash4("sha256").update(JSON.stringify(value)).digest("hex");
|
|
3132
|
+
checkSchema = z6.enum([
|
|
3133
|
+
"deleted-reference",
|
|
3134
|
+
"new-module",
|
|
3135
|
+
"stale-count",
|
|
3136
|
+
"dead-command",
|
|
3137
|
+
"deps-changed",
|
|
3138
|
+
"decision-anchor-drift"
|
|
3139
|
+
]);
|
|
3140
|
+
commitSchema = z6.object({
|
|
3141
|
+
hash: z6.string().regex(/^[a-f0-9]{40,64}$/),
|
|
3142
|
+
date: z6.string(),
|
|
3143
|
+
subject: z6.string()
|
|
3144
|
+
});
|
|
3145
|
+
anchorSchema = z6.object({
|
|
3146
|
+
doc: z6.string(),
|
|
3147
|
+
line: z6.number().int().positive().nullable(),
|
|
3148
|
+
excerpt: z6.string().nullable()
|
|
2421
3149
|
});
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
3150
|
+
count = z6.number().int().nonnegative();
|
|
3151
|
+
scopeSchema = z6.object({
|
|
3152
|
+
basis: z6.enum(["repository", "document", "document-link", "explicit"]),
|
|
3153
|
+
directory: z6.string(),
|
|
3154
|
+
candidates: z6.array(z6.string())
|
|
3155
|
+
});
|
|
3156
|
+
evidenceSchema = z6.discriminatedUnion("kind", [
|
|
3157
|
+
z6.object({
|
|
3158
|
+
kind: z6.literal("missing-path"),
|
|
3159
|
+
claimed: z6.string(),
|
|
3160
|
+
renamedTo: z6.string().nullable(),
|
|
2427
3161
|
deletedInCommit: commitSchema.nullable(),
|
|
2428
|
-
everTracked:
|
|
2429
|
-
parentDirExists:
|
|
2430
|
-
resolvedPath:
|
|
3162
|
+
everTracked: z6.boolean(),
|
|
3163
|
+
parentDirExists: z6.boolean(),
|
|
3164
|
+
resolvedPath: z6.string().optional(),
|
|
2431
3165
|
scope: scopeSchema.optional()
|
|
2432
3166
|
}),
|
|
2433
|
-
|
|
2434
|
-
kind:
|
|
2435
|
-
dir:
|
|
3167
|
+
z6.object({
|
|
3168
|
+
kind: z6.literal("unmentioned-dir"),
|
|
3169
|
+
dir: z6.string(),
|
|
2436
3170
|
sourceFileCount: count,
|
|
2437
3171
|
firstCommit: commitSchema.nullable(),
|
|
2438
|
-
checkedDocs:
|
|
3172
|
+
checkedDocs: z6.array(z6.string())
|
|
2439
3173
|
}),
|
|
2440
|
-
|
|
2441
|
-
kind:
|
|
3174
|
+
z6.object({
|
|
3175
|
+
kind: z6.literal("count-mismatch"),
|
|
2442
3176
|
claimed: count,
|
|
2443
3177
|
actual: count,
|
|
2444
|
-
unit:
|
|
2445
|
-
countedFrom:
|
|
2446
|
-
members:
|
|
3178
|
+
unit: z6.string(),
|
|
3179
|
+
countedFrom: z6.string(),
|
|
3180
|
+
members: z6.array(z6.string())
|
|
2447
3181
|
}),
|
|
2448
|
-
|
|
2449
|
-
kind:
|
|
2450
|
-
scriptName:
|
|
2451
|
-
invocation:
|
|
2452
|
-
manifestsChecked:
|
|
2453
|
-
availableScripts:
|
|
3182
|
+
z6.object({
|
|
3183
|
+
kind: z6.literal("missing-script"),
|
|
3184
|
+
scriptName: z6.string(),
|
|
3185
|
+
invocation: z6.string(),
|
|
3186
|
+
manifestsChecked: z6.array(z6.string()),
|
|
3187
|
+
availableScripts: z6.array(z6.string()),
|
|
2454
3188
|
scope: scopeSchema.optional()
|
|
2455
3189
|
}),
|
|
2456
|
-
|
|
2457
|
-
kind:
|
|
3190
|
+
z6.object({
|
|
3191
|
+
kind: z6.literal("doc-behind-manifests"),
|
|
2458
3192
|
docLastCommit: commitSchema,
|
|
2459
|
-
manifestCommits:
|
|
3193
|
+
manifestCommits: z6.array(commitSchema.extend({ files: z6.array(z6.string()) })),
|
|
2460
3194
|
totalCommits: count,
|
|
2461
|
-
matches:
|
|
2462
|
-
|
|
3195
|
+
matches: z6.array(
|
|
3196
|
+
z6.object({
|
|
3197
|
+
dependency: z6.string(),
|
|
3198
|
+
manifest: z6.string(),
|
|
3199
|
+
before: z6.string().nullable(),
|
|
3200
|
+
after: z6.string().nullable(),
|
|
3201
|
+
line: z6.number().int().positive(),
|
|
3202
|
+
excerpt: z6.string()
|
|
3203
|
+
})
|
|
3204
|
+
).optional(),
|
|
3205
|
+
matchingIncomplete: z6.boolean().optional()
|
|
2463
3206
|
}),
|
|
2464
|
-
|
|
2465
|
-
kind:
|
|
2466
|
-
decisionId:
|
|
2467
|
-
title:
|
|
2468
|
-
changedFiles:
|
|
2469
|
-
refreshedHash:
|
|
2470
|
-
provenance:
|
|
3207
|
+
z6.object({
|
|
3208
|
+
kind: z6.literal("decision-anchor"),
|
|
3209
|
+
decisionId: z6.string(),
|
|
3210
|
+
title: z6.string(),
|
|
3211
|
+
changedFiles: z6.array(z6.string()),
|
|
3212
|
+
refreshedHash: z6.string(),
|
|
3213
|
+
provenance: z6.object({}).passthrough().optional()
|
|
2471
3214
|
})
|
|
2472
3215
|
]);
|
|
2473
|
-
findingSchema =
|
|
3216
|
+
findingSchema = z6.object({
|
|
3217
|
+
message: z6.string(),
|
|
3218
|
+
anchor: anchorSchema,
|
|
3219
|
+
evidence: evidenceSchema
|
|
3220
|
+
});
|
|
2474
3221
|
issueSchema = findingSchema.extend({
|
|
2475
|
-
type:
|
|
2476
|
-
confidence:
|
|
3222
|
+
type: z6.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
3223
|
+
confidence: z6.enum(["certain", "likely"])
|
|
3224
|
+
});
|
|
3225
|
+
advisorySchema = findingSchema.extend({
|
|
3226
|
+
type: checkSchema,
|
|
3227
|
+
resolution: z6.enum(["recheck", "informational"]).optional()
|
|
2477
3228
|
});
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
3229
|
+
checkResultSchema = z6.object({
|
|
3230
|
+
issues: z6.array(issueSchema),
|
|
3231
|
+
advisories: z6.array(advisorySchema),
|
|
3232
|
+
suppressedAdvisories: z6.array(advisorySchema).optional(),
|
|
3233
|
+
skipped: z6.array(z6.object({ check: z6.string(), reason: z6.string(), doc: z6.string().optional() }))
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
});
|
|
3237
|
+
|
|
3238
|
+
// src/automation/stop-review.ts
|
|
3239
|
+
import { z as z7 } from "zod";
|
|
3240
|
+
var evidenceSchema2, stopReviewSchema;
|
|
3241
|
+
var init_stop_review = __esm({
|
|
3242
|
+
"src/automation/stop-review.ts"() {
|
|
3243
|
+
"use strict";
|
|
3244
|
+
init_anchors();
|
|
3245
|
+
init_findings();
|
|
3246
|
+
evidenceSchema2 = z7.record(z7.object({ digest: z7.string(), capture: anchorCaptureSchema }));
|
|
3247
|
+
stopReviewSchema = z7.object({
|
|
3248
|
+
pending: z7.record(z7.object({ head: z7.string(), evidence: evidenceSchema2 })),
|
|
3249
|
+
edited: z7.record(z7.record(z7.string().nullable())),
|
|
3250
|
+
delivered: z7.record(z7.object({ digest: z7.string(), files: z7.record(z7.string().nullable()) }))
|
|
2484
3251
|
});
|
|
2485
3252
|
}
|
|
2486
3253
|
});
|
|
2487
3254
|
|
|
2488
3255
|
// src/automation/notifications.ts
|
|
2489
|
-
import { z as
|
|
3256
|
+
import { z as z8 } from "zod";
|
|
2490
3257
|
var conditionSchema, notificationStateSchema;
|
|
2491
3258
|
var init_notifications = __esm({
|
|
2492
3259
|
"src/automation/notifications.ts"() {
|
|
2493
3260
|
"use strict";
|
|
2494
3261
|
init_findings();
|
|
2495
|
-
conditionSchema =
|
|
2496
|
-
level:
|
|
2497
|
-
facts:
|
|
2498
|
-
magnitude:
|
|
3262
|
+
conditionSchema = z8.object({
|
|
3263
|
+
level: z8.number(),
|
|
3264
|
+
facts: z8.array(z8.string()),
|
|
3265
|
+
magnitude: z8.number().optional()
|
|
2499
3266
|
});
|
|
2500
|
-
notificationStateSchema =
|
|
2501
|
-
version:
|
|
2502
|
-
baselineHead:
|
|
2503
|
-
baseline:
|
|
2504
|
-
delivered:
|
|
2505
|
-
cleared:
|
|
3267
|
+
notificationStateSchema = z8.object({
|
|
3268
|
+
version: z8.literal(1),
|
|
3269
|
+
baselineHead: z8.string(),
|
|
3270
|
+
baseline: z8.record(conditionSchema),
|
|
3271
|
+
delivered: z8.record(conditionSchema),
|
|
3272
|
+
cleared: z8.array(z8.string())
|
|
2506
3273
|
});
|
|
2507
3274
|
}
|
|
2508
3275
|
});
|
|
2509
3276
|
|
|
2510
3277
|
// src/automation/store.ts
|
|
2511
|
-
import { z as
|
|
3278
|
+
import { z as z9 } from "zod";
|
|
2512
3279
|
function withLock(root, directory, run, waitMs = 5e3) {
|
|
2513
3280
|
return withStoreLock(root, directory, run, waitMs, "Automation");
|
|
2514
3281
|
}
|
|
@@ -2517,28 +3284,35 @@ var init_store = __esm({
|
|
|
2517
3284
|
"src/automation/store.ts"() {
|
|
2518
3285
|
"use strict";
|
|
2519
3286
|
init_store_lock();
|
|
3287
|
+
init_stop_review();
|
|
2520
3288
|
init_notifications();
|
|
2521
|
-
hostSchema =
|
|
2522
|
-
stateSchema =
|
|
2523
|
-
version:
|
|
2524
|
-
root:
|
|
2525
|
-
gitDir:
|
|
2526
|
-
branch:
|
|
2527
|
-
baselines:
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
3289
|
+
hostSchema = z9.enum(["claude", "codex"]);
|
|
3290
|
+
stateSchema = z9.object({
|
|
3291
|
+
version: z9.literal(1),
|
|
3292
|
+
root: z9.string(),
|
|
3293
|
+
gitDir: z9.string(),
|
|
3294
|
+
branch: z9.string(),
|
|
3295
|
+
baselines: z9.array(
|
|
3296
|
+
z9.object({ path: z9.string(), at: z9.string(), event: z9.string(), fingerprint: z9.string() })
|
|
3297
|
+
).max(128),
|
|
3298
|
+
sessions: z9.record(
|
|
3299
|
+
z9.object({
|
|
3300
|
+
host: hostSchema,
|
|
3301
|
+
lastUsed: z9.string(),
|
|
3302
|
+
notifiedDiagnostics: z9.array(z9.string()).default([]),
|
|
3303
|
+
pending: z9.record(z9.string()),
|
|
3304
|
+
stopReview: stopReviewSchema.optional(),
|
|
3305
|
+
coverageGaps: z9.array(z9.string()),
|
|
3306
|
+
events: z9.record(z9.object({ at: z9.string(), count: z9.number().int().positive() }))
|
|
3307
|
+
})
|
|
3308
|
+
),
|
|
3309
|
+
updatedAt: z9.string(),
|
|
3310
|
+
fingerprint: z9.string().nullable(),
|
|
3311
|
+
latest: z9.string().nullable(),
|
|
3312
|
+
guidance: z9.object({ fingerprint: z9.string(), path: z9.string() }).optional(),
|
|
2539
3313
|
notifications: notificationStateSchema.optional(),
|
|
2540
3314
|
agentNotifications: notificationStateSchema.optional(),
|
|
2541
|
-
analysis:
|
|
3315
|
+
analysis: z9.object({ id: z9.string(), fingerprint: z9.string(), completedAt: z9.number(), path: z9.string() }).optional()
|
|
2542
3316
|
});
|
|
2543
3317
|
}
|
|
2544
3318
|
});
|
|
@@ -2557,10 +3331,10 @@ __export(repair_exports, {
|
|
|
2557
3331
|
verifyRepair: () => verifyRepair,
|
|
2558
3332
|
withRepairSession: () => withRepairSession
|
|
2559
3333
|
});
|
|
2560
|
-
import
|
|
2561
|
-
import
|
|
3334
|
+
import fs11 from "fs/promises";
|
|
3335
|
+
import path18 from "path";
|
|
2562
3336
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2563
|
-
import { z as
|
|
3337
|
+
import { z as z10 } from "zod";
|
|
2564
3338
|
function recheckable(finding) {
|
|
2565
3339
|
return !("confidence" in finding) && finding.resolution === "recheck" && ["deleted-reference", "dead-command", "new-module"].includes(finding.type) && ["missing-path", "missing-script", "unmentioned-dir"].includes(finding.evidence.kind);
|
|
2566
3340
|
}
|
|
@@ -2572,7 +3346,8 @@ async function docState(root) {
|
|
|
2572
3346
|
for (const doc of await discoverDocPaths(root)) {
|
|
2573
3347
|
try {
|
|
2574
3348
|
const content = await readBoundedFile(await storePath(root, doc), 10 * 1024 * 1024);
|
|
2575
|
-
if (content === null)
|
|
3349
|
+
if (content === null)
|
|
3350
|
+
throw new Error("Context file is not regular or exceeds 10 MiB: " + doc);
|
|
2576
3351
|
docs.push([doc, digest(content)]);
|
|
2577
3352
|
} catch (error) {
|
|
2578
3353
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -2589,7 +3364,9 @@ async function readStableAudit(root, checks, options) {
|
|
|
2589
3364
|
const before = await docState(root);
|
|
2590
3365
|
const report = await computeAudit(root, { ...options, checks });
|
|
2591
3366
|
if (head !== await getCurrentGitHash(root) || before !== await docState(root) || report && report.headHash !== head) {
|
|
2592
|
-
throw new Error(
|
|
3367
|
+
throw new Error(
|
|
3368
|
+
"HEAD or context files changed during the audit; retry against a stable checkout."
|
|
3369
|
+
);
|
|
2593
3370
|
}
|
|
2594
3371
|
return report;
|
|
2595
3372
|
}
|
|
@@ -2617,7 +3394,11 @@ async function withRepairSession(root, options, run, validate, cachedAudit) {
|
|
|
2617
3394
|
}
|
|
2618
3395
|
return computeAudit(root, { ...options, checks });
|
|
2619
3396
|
}),
|
|
2620
|
-
document: (file) => memo(
|
|
3397
|
+
document: (file) => memo(
|
|
3398
|
+
documents,
|
|
3399
|
+
file,
|
|
3400
|
+
async () => readBoundedFile(await storePath(root, file), 10 * 1024 * 1024)
|
|
3401
|
+
),
|
|
2621
3402
|
history: (head2) => memo(history, head2, () => getChangesWithStatus(root, head2)),
|
|
2622
3403
|
assessment: (dir, finding, head2) => memo(assessments, digest([dir, finding, head2]), () => assessAdvisory(dir, finding, head2))
|
|
2623
3404
|
};
|
|
@@ -2630,11 +3411,12 @@ async function withRepairSession(root, options, run, validate, cachedAudit) {
|
|
|
2630
3411
|
return result;
|
|
2631
3412
|
}
|
|
2632
3413
|
async function prepareOriginal(rootDir, checks, options, session) {
|
|
2633
|
-
const root = await
|
|
2634
|
-
const selected =
|
|
3414
|
+
const root = await fs11.realpath(rootDir);
|
|
3415
|
+
const selected = z10.array(checkSchema).nonempty().parse(checks);
|
|
2635
3416
|
const report = await (session ? session.audit(selected) : stableAudit(root, selected, options));
|
|
2636
3417
|
if (!report) throw new Error("No context files found to prepare a repair.");
|
|
2637
|
-
if (!report.gitAvailable)
|
|
3418
|
+
if (!report.gitAvailable)
|
|
3419
|
+
throw new Error("Readable Git history is required to prepare a repair.");
|
|
2638
3420
|
const storedReport = reportSchema.parse(report);
|
|
2639
3421
|
const payload = {
|
|
2640
3422
|
kind: "mason-audit-repair",
|
|
@@ -2647,13 +3429,15 @@ async function prepareOriginal(rootDir, checks, options, session) {
|
|
|
2647
3429
|
return { version: 1, action: "prepare", baselinePath, report };
|
|
2648
3430
|
}
|
|
2649
3431
|
async function loadRepairBaseline(rootDir, baselinePath) {
|
|
2650
|
-
const root = await
|
|
2651
|
-
const declaredRoot =
|
|
2652
|
-
const relative =
|
|
3432
|
+
const root = await fs11.realpath(rootDir);
|
|
3433
|
+
const declaredRoot = path18.resolve(rootDir);
|
|
3434
|
+
const relative = path18.isAbsolute(baselinePath) ? path18.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
|
|
2653
3435
|
const stored = baselineSchema.parse(await readStoreJson(root, relative));
|
|
2654
3436
|
const { digest: savedDigest, ...payload } = stored;
|
|
2655
|
-
if (digest(payload) !== savedDigest)
|
|
2656
|
-
|
|
3437
|
+
if (digest(payload) !== savedDigest)
|
|
3438
|
+
throw new Error("Repair baseline was modified; use the original baseline.");
|
|
3439
|
+
if (stored.report.root !== root)
|
|
3440
|
+
throw new Error("Repair baseline belongs to a different repository.");
|
|
2657
3441
|
const original = stored.report;
|
|
2658
3442
|
return { root, relative, original };
|
|
2659
3443
|
}
|
|
@@ -2671,14 +3455,19 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2671
3455
|
if (!current) diagnostics.push("No context files remain available to audit.");
|
|
2672
3456
|
else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
|
|
2673
3457
|
for (const doc of original.docs) {
|
|
2674
|
-
if (!allFindings(original).some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path))
|
|
3458
|
+
if (!allFindings(original).some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path))
|
|
3459
|
+
continue;
|
|
2675
3460
|
const content = await (session ? session.document(doc.path) : readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024));
|
|
2676
3461
|
if (content === null || !content.trim()) {
|
|
2677
|
-
diagnostics.push(
|
|
3462
|
+
diagnostics.push(
|
|
3463
|
+
"Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair."
|
|
3464
|
+
);
|
|
2678
3465
|
}
|
|
2679
3466
|
}
|
|
2680
3467
|
if (await (session ? session.history(original.headHash) : getChangesWithStatus(root, original.headHash)) === null) {
|
|
2681
|
-
diagnostics.push(
|
|
3468
|
+
diagnostics.push(
|
|
3469
|
+
"The original audit commit is unavailable; repair history cannot be verified."
|
|
3470
|
+
);
|
|
2682
3471
|
}
|
|
2683
3472
|
} catch (error) {
|
|
2684
3473
|
diagnostics.push(error instanceof Error ? error.message : String(error));
|
|
@@ -2686,63 +3475,116 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2686
3475
|
const currentById = new Map((current ? allFindings(current) : []).map((f) => [findingId(f), f]));
|
|
2687
3476
|
const originalFindings = allFindings(original);
|
|
2688
3477
|
const originalIds = new Set(originalFindings.map(findingId));
|
|
2689
|
-
const missingDocs = original.docs.filter(
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
3478
|
+
const missingDocs = original.docs.filter(
|
|
3479
|
+
(doc) => !current?.docs.some((d) => d.path === doc.path)
|
|
3480
|
+
);
|
|
3481
|
+
for (const doc of missingDocs)
|
|
3482
|
+
diagnostics.push(
|
|
3483
|
+
"Original context file " + doc.path + " is unavailable; removing it does not verify a repair."
|
|
3484
|
+
);
|
|
3485
|
+
const findings = await Promise.all(
|
|
3486
|
+
originalFindings.map(async (finding) => {
|
|
3487
|
+
const id = findingId(finding);
|
|
3488
|
+
const now = currentById.get(id);
|
|
3489
|
+
const base = { id, original: finding, ...now ? { current: now } : {} };
|
|
3490
|
+
if (diagnostics.length || !current) {
|
|
3491
|
+
return {
|
|
3492
|
+
...base,
|
|
3493
|
+
status: "unverified",
|
|
3494
|
+
reason: "The original audit scope could not be verified. See diagnostics."
|
|
3495
|
+
};
|
|
3496
|
+
}
|
|
3497
|
+
if (now && ("confidence" in finding || "confidence" in now)) {
|
|
3498
|
+
return {
|
|
3499
|
+
...base,
|
|
3500
|
+
status: "unresolved",
|
|
3501
|
+
reason: "The original check still reports this claim."
|
|
3502
|
+
};
|
|
3503
|
+
}
|
|
3504
|
+
if (current.excludedChecks?.some(
|
|
3505
|
+
(excluded) => excluded.check === finding.type && excluded.doc === finding.anchor.doc
|
|
3506
|
+
)) {
|
|
3507
|
+
return {
|
|
3508
|
+
...base,
|
|
3509
|
+
status: "resolved",
|
|
3510
|
+
reason: "Excluded by project audit policy; this is not a correctness assessment. Original evidence is retained."
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3513
|
+
const skipped = current.skippedChecks.filter(
|
|
3514
|
+
(s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc)
|
|
3515
|
+
);
|
|
3516
|
+
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
3517
|
+
return {
|
|
3518
|
+
...base,
|
|
3519
|
+
status: "unverified",
|
|
3520
|
+
reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run."
|
|
3521
|
+
};
|
|
3522
|
+
}
|
|
3523
|
+
const review = await (session?.assessment ?? assessAdvisory)(
|
|
3524
|
+
root,
|
|
3525
|
+
now ?? finding,
|
|
3526
|
+
original.headHash
|
|
3527
|
+
);
|
|
3528
|
+
if (review) {
|
|
3529
|
+
if (review.status === "unverified")
|
|
3530
|
+
return { ...base, review, status: "unverified", reason: review.reason };
|
|
3531
|
+
if (review.status === "current")
|
|
3532
|
+
return { ...base, review, status: "resolved", reason: review.reason };
|
|
3533
|
+
return { ...base, review, status: "review-required", reason: review.reason };
|
|
3534
|
+
}
|
|
3535
|
+
if (!("confidence" in finding) && finding.evidence.kind === "doc-behind-manifests" && !finding.evidence.matches?.length && (!now || !("confidence" in now) && now.resolution === "informational")) {
|
|
3536
|
+
return {
|
|
3537
|
+
...base,
|
|
3538
|
+
status: "resolved",
|
|
3539
|
+
reason: "Manifest recency alone establishes no affected passage. Retained as information; no assessment is required."
|
|
3540
|
+
};
|
|
3541
|
+
}
|
|
3542
|
+
if (!("confidence" in finding) && (!recheckable(finding) || now)) {
|
|
3543
|
+
return {
|
|
3544
|
+
...base,
|
|
3545
|
+
status: "review-required",
|
|
3546
|
+
reason: (now ? "The current check still reports this advisory. " : finding.type === "deps-changed" ? "The current check no longer reports this condition; the historical advisory remains awaiting assessment. " : "The current check no longer reports the original finding; retained evidence still requires assessment. ") + "An audit cannot establish that this advisory was reviewed. Retain its original evidence and report a separate assessment; editing or committing the doc is not approval."
|
|
3547
|
+
};
|
|
3548
|
+
}
|
|
2718
3549
|
return {
|
|
2719
3550
|
...base,
|
|
2720
|
-
status: "
|
|
2721
|
-
reason:
|
|
3551
|
+
status: "resolved",
|
|
3552
|
+
reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness."
|
|
2722
3553
|
};
|
|
2723
|
-
}
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
3554
|
+
})
|
|
3555
|
+
);
|
|
3556
|
+
if (!session)
|
|
3557
|
+
try {
|
|
3558
|
+
if (reviewDigest !== digest(await advisoryReviewInventory(root)) || current?.headHash !== await getCurrentGitHash(root)) {
|
|
3559
|
+
diagnostics.push(
|
|
3560
|
+
"HEAD or advisory review records changed during verification; repeat against stable evidence."
|
|
3561
|
+
);
|
|
3562
|
+
for (const finding of findings) {
|
|
3563
|
+
finding.status = "unverified";
|
|
3564
|
+
finding.reason = diagnostics.at(-1);
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
} catch (error) {
|
|
3568
|
+
diagnostics.push(String(error));
|
|
2729
3569
|
for (const finding of findings) {
|
|
2730
3570
|
finding.status = "unverified";
|
|
2731
|
-
finding.reason =
|
|
3571
|
+
finding.reason = "Advisory review evidence became unavailable during verification.";
|
|
2732
3572
|
}
|
|
2733
3573
|
}
|
|
2734
|
-
} catch (error) {
|
|
2735
|
-
diagnostics.push(String(error));
|
|
2736
|
-
for (const finding of findings) {
|
|
2737
|
-
finding.status = "unverified";
|
|
2738
|
-
finding.reason = "Advisory review evidence became unavailable during verification.";
|
|
2739
|
-
}
|
|
2740
|
-
}
|
|
2741
3574
|
const newFindings = [...currentById].filter(([id]) => !originalIds.has(id)).map(([, f]) => f);
|
|
2742
|
-
const reviewedIds = new Set(
|
|
2743
|
-
|
|
3575
|
+
const reviewedIds = new Set(
|
|
3576
|
+
current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id)
|
|
3577
|
+
);
|
|
3578
|
+
const counts = {
|
|
3579
|
+
resolved: 0,
|
|
3580
|
+
unresolved: 0,
|
|
3581
|
+
"review-required": 0,
|
|
3582
|
+
unverified: 0
|
|
3583
|
+
};
|
|
2744
3584
|
for (const f of findings) counts[f.status]++;
|
|
2745
|
-
const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some(
|
|
3585
|
+
const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some(
|
|
3586
|
+
(f) => !("confidence" in f) && f.resolution !== "informational" && !reviewedIds.has(findingId(f))
|
|
3587
|
+
);
|
|
2746
3588
|
const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
|
|
2747
3589
|
return {
|
|
2748
3590
|
version: 1,
|
|
@@ -2765,10 +3607,14 @@ function repairExitCode(report) {
|
|
|
2765
3607
|
function formatRepairSummary(report) {
|
|
2766
3608
|
return [
|
|
2767
3609
|
"Repair verification: " + report.status + ". Baseline: " + report.baselinePath,
|
|
2768
|
-
...report.findings.map(
|
|
3610
|
+
...report.findings.map(
|
|
3611
|
+
(f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + (f.current?.message ?? "Historical evidence at " + report.baselineHead.slice(0, 7) + ": " + f.original.message) + "\n " + f.reason + "\n findingId: " + f.id
|
|
3612
|
+
),
|
|
2769
3613
|
...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
|
|
2770
3614
|
...report.diagnostics.map((d) => " [unverified] " + d),
|
|
2771
|
-
...(report.currentAudit?.skippedChecks ?? []).map(
|
|
3615
|
+
...(report.currentAudit?.skippedChecks ?? []).map(
|
|
3616
|
+
(s) => " [skipped] " + s.check + ": " + s.reason
|
|
3617
|
+
),
|
|
2772
3618
|
report.scope
|
|
2773
3619
|
].join("\n");
|
|
2774
3620
|
}
|
|
@@ -2788,109 +3634,149 @@ var init_repair = __esm({
|
|
|
2788
3634
|
init_findings();
|
|
2789
3635
|
init_advisory_review();
|
|
2790
3636
|
init_findings();
|
|
2791
|
-
count2 =
|
|
2792
|
-
reportSchema =
|
|
2793
|
-
version:
|
|
2794
|
-
root:
|
|
2795
|
-
gitAvailable:
|
|
3637
|
+
count2 = z10.number().int().nonnegative();
|
|
3638
|
+
reportSchema = z10.object({
|
|
3639
|
+
version: z10.literal(1),
|
|
3640
|
+
root: z10.string(),
|
|
3641
|
+
gitAvailable: z10.literal(true),
|
|
2796
3642
|
headHash: commitSchema.shape.hash,
|
|
2797
|
-
checksRun:
|
|
2798
|
-
docs:
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
3643
|
+
checksRun: z10.array(checkSchema).nonempty(),
|
|
3644
|
+
docs: z10.array(
|
|
3645
|
+
z10.object({
|
|
3646
|
+
path: z10.string().refine((value) => normalizeRepoPath(value) === value),
|
|
3647
|
+
scope: z10.string().optional(),
|
|
3648
|
+
kind: z10.enum(["instructions", "readme", "guide"]).optional(),
|
|
3649
|
+
lastCommit: commitSchema.nullable(),
|
|
3650
|
+
dirty: z10.boolean(),
|
|
3651
|
+
lineCount: count2
|
|
3652
|
+
})
|
|
3653
|
+
).nonempty(),
|
|
3654
|
+
decisionsChecked: z10.boolean(),
|
|
3655
|
+
clean: z10.boolean(),
|
|
3656
|
+
excludedChecks: z10.array(z10.object({ check: checkSchema, doc: z10.string() })).optional(),
|
|
3657
|
+
issues: z10.array(issueSchema),
|
|
3658
|
+
advisories: z10.array(advisorySchema),
|
|
3659
|
+
suppressedAdvisories: z10.array(advisorySchema).optional(),
|
|
3660
|
+
skippedChecks: z10.array(
|
|
3661
|
+
z10.object({ check: z10.string(), reason: z10.string(), doc: z10.string().optional() })
|
|
3662
|
+
)
|
|
2813
3663
|
});
|
|
2814
|
-
baselineSchema =
|
|
2815
|
-
kind:
|
|
2816
|
-
version:
|
|
2817
|
-
createdAt:
|
|
3664
|
+
baselineSchema = z10.object({
|
|
3665
|
+
kind: z10.literal("mason-audit-repair"),
|
|
3666
|
+
version: z10.literal(1),
|
|
3667
|
+
createdAt: z10.string().datetime(),
|
|
2818
3668
|
report: reportSchema,
|
|
2819
|
-
digest:
|
|
3669
|
+
digest: z10.string().regex(/^[a-f0-9]{64}$/)
|
|
2820
3670
|
});
|
|
2821
|
-
repairFindingSchema =
|
|
2822
|
-
id:
|
|
2823
|
-
original:
|
|
2824
|
-
status:
|
|
2825
|
-
reason:
|
|
2826
|
-
current:
|
|
3671
|
+
repairFindingSchema = z10.object({
|
|
3672
|
+
id: z10.string(),
|
|
3673
|
+
original: z10.union([issueSchema, advisorySchema]),
|
|
3674
|
+
status: z10.enum(["resolved", "unresolved", "review-required", "unverified"]),
|
|
3675
|
+
reason: z10.string(),
|
|
3676
|
+
current: z10.union([issueSchema, advisorySchema]).optional(),
|
|
2827
3677
|
review: reviewSummarySchema.optional()
|
|
2828
3678
|
});
|
|
2829
3679
|
}
|
|
2830
3680
|
});
|
|
2831
3681
|
|
|
2832
3682
|
// src/audit/advisory-review.ts
|
|
2833
|
-
import
|
|
2834
|
-
import { z as
|
|
3683
|
+
import fs12 from "fs/promises";
|
|
3684
|
+
import { z as z11 } from "zod";
|
|
2835
3685
|
async function readRecord(root, id) {
|
|
2836
3686
|
const raw = await readStoreJson(root, recordPath(id));
|
|
2837
3687
|
if (raw === null) return null;
|
|
2838
3688
|
const record = recordSchema.parse(raw);
|
|
2839
3689
|
const { digest: stored, ...payload } = record;
|
|
2840
|
-
if (record.id !== id || digest(payload) !== stored || record.events.some(
|
|
3690
|
+
if (record.id !== id || digest(payload) !== stored || record.events.some(
|
|
3691
|
+
(event) => findingId(event.finding) !== id || (event.outcome === "dismissed" ? !event.reasonCode || event.finding.type !== "deps-changed" : !event.reviewer || !!event.reasonCode) || event.evidence.fingerprint !== evidenceFingerprint(event.evidence, event.reasonCode)
|
|
3692
|
+
)) {
|
|
2841
3693
|
throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
|
|
2842
3694
|
}
|
|
2843
3695
|
return record;
|
|
2844
3696
|
}
|
|
2845
3697
|
function evidenceFingerprint(evidence, reason) {
|
|
2846
|
-
return digest([
|
|
3698
|
+
return digest([
|
|
3699
|
+
evidence.scope,
|
|
3700
|
+
evidence.tree,
|
|
3701
|
+
...reason === "no-dependency-claims" ? [] : [evidence.lastScopeCommit],
|
|
3702
|
+
evidence.paths
|
|
3703
|
+
]);
|
|
2847
3704
|
}
|
|
2848
3705
|
function scopes(finding, reason) {
|
|
2849
3706
|
const files = [finding.anchor.doc], e = finding.evidence;
|
|
2850
|
-
if (e.kind === "decision-anchor")
|
|
2851
|
-
|
|
2852
|
-
if (e.kind === "doc-behind-manifests")
|
|
2853
|
-
|
|
2854
|
-
|
|
3707
|
+
if (e.kind === "decision-anchor")
|
|
3708
|
+
throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
|
|
3709
|
+
if (reason === "no-dependency-claims" && e.kind === "doc-behind-manifests")
|
|
3710
|
+
return [`:(literal)${finding.anchor.doc}`];
|
|
3711
|
+
if (e.kind === "doc-behind-manifests")
|
|
3712
|
+
return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
|
|
3713
|
+
if (e.kind === "missing-path")
|
|
3714
|
+
files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
|
|
3715
|
+
else if (e.kind === "missing-script")
|
|
3716
|
+
files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
|
|
2855
3717
|
else if (e.kind === "unmentioned-dir") files.push(e.dir, ...e.checkedDocs);
|
|
2856
3718
|
else throw new Error("This advisory has no supported review scope.");
|
|
2857
3719
|
return [...new Set(files)].map((file) => {
|
|
2858
|
-
if (normalizeRepoPath(file) !== file)
|
|
3720
|
+
if (normalizeRepoPath(file) !== file)
|
|
3721
|
+
throw new Error("Advisory contains an invalid scoped path: " + file);
|
|
2859
3722
|
return `:(literal)${file}`;
|
|
2860
3723
|
}).sort();
|
|
2861
3724
|
}
|
|
2862
3725
|
async function reviewEvidence(root, finding, reason) {
|
|
2863
|
-
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc)
|
|
3726
|
+
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc)
|
|
3727
|
+
throw new Error("Invalid advisory document path.");
|
|
2864
3728
|
const scope = scopes(finding, reason);
|
|
2865
3729
|
const head = (await git2(root, "rev-parse", "HEAD")).trim();
|
|
2866
3730
|
const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
|
|
2867
3731
|
if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
|
|
2868
3732
|
const [tree, status, lastScopeCommit, document] = await Promise.all([
|
|
2869
|
-
tracked.length ? git2(
|
|
3733
|
+
tracked.length ? git2(
|
|
3734
|
+
root,
|
|
3735
|
+
"ls-tree",
|
|
3736
|
+
"-r",
|
|
3737
|
+
"-z",
|
|
3738
|
+
"HEAD",
|
|
3739
|
+
"--",
|
|
3740
|
+
...tracked.map((file) => `:(literal)${file}`)
|
|
3741
|
+
) : "",
|
|
2870
3742
|
git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...scope),
|
|
2871
3743
|
git2(root, "log", "-1", "--format=%H", "HEAD", "--", ...scope).then((value) => value.trim()),
|
|
2872
3744
|
readAuditInput(root, finding.anchor.doc)
|
|
2873
3745
|
]);
|
|
2874
|
-
if (!document?.trim())
|
|
2875
|
-
|
|
3746
|
+
if (!document?.trim())
|
|
3747
|
+
throw new Error("Original advisory document is missing, empty or unreadable.");
|
|
3748
|
+
if (tree.length > 2 * 1024 * 1024 || tree.split("\0").length > 2e4)
|
|
3749
|
+
throw new Error(
|
|
3750
|
+
"Advisory scope is too large for a complete review; narrow the documentation scope."
|
|
3751
|
+
);
|
|
2876
3752
|
for (const entry of tree.split("\0").filter(Boolean)) {
|
|
2877
3753
|
const tab = entry.indexOf(" "), file = entry.slice(tab + 1);
|
|
2878
|
-
if (entry.startsWith("120000 ") || entry.startsWith("160000 "))
|
|
3754
|
+
if (entry.startsWith("120000 ") || entry.startsWith("160000 "))
|
|
3755
|
+
throw new Error("Symlink or submodule in advisory review scope: " + file);
|
|
2879
3756
|
await storePath(root, file);
|
|
2880
3757
|
}
|
|
2881
3758
|
const paths = finding.evidence.kind === "missing-path" ? await Promise.all(
|
|
2882
|
-
(finding.evidence.scope?.candidates ?? [
|
|
3759
|
+
(finding.evidence.scope?.candidates ?? [
|
|
3760
|
+
finding.evidence.resolvedPath ?? finding.evidence.claimed
|
|
3761
|
+
]).map(async (file) => ({ path: file, exists: await pathExists(root, file) }))
|
|
2883
3762
|
) : [];
|
|
2884
3763
|
let dirty = status.length > 0;
|
|
2885
3764
|
if (finding.evidence.kind === "missing-script") {
|
|
2886
|
-
for (const file of [
|
|
3765
|
+
for (const file of [
|
|
3766
|
+
...finding.evidence.manifestsChecked,
|
|
3767
|
+
...finding.evidence.scope?.candidates ?? []
|
|
3768
|
+
]) {
|
|
2887
3769
|
if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
|
|
2888
3770
|
}
|
|
2889
3771
|
}
|
|
2890
|
-
const fingerprint = evidenceFingerprint(
|
|
2891
|
-
|
|
3772
|
+
const fingerprint = evidenceFingerprint(
|
|
3773
|
+
{ head, scope, tree, lastScopeCommit, paths, fingerprint: "" },
|
|
3774
|
+
reason
|
|
3775
|
+
);
|
|
3776
|
+
if ((await git2(root, "rev-parse", "HEAD")).trim() !== head)
|
|
3777
|
+
throw new Error("HEAD changed while collecting review evidence.");
|
|
2892
3778
|
return {
|
|
2893
|
-
...
|
|
3779
|
+
...evidenceSchema3.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
|
|
2894
3780
|
dirty,
|
|
2895
3781
|
documentDigest: digest(document)
|
|
2896
3782
|
};
|
|
@@ -2900,12 +3786,42 @@ async function decisionAssessment(root, finding, baselineHead) {
|
|
|
2900
3786
|
if (evidence.kind !== "decision-anchor") return null;
|
|
2901
3787
|
const id = findingId(finding);
|
|
2902
3788
|
const store = await loadDecisionStore(root);
|
|
2903
|
-
if (store.diagnostics.length)
|
|
3789
|
+
if (store.diagnostics.length)
|
|
3790
|
+
throw new Error(
|
|
3791
|
+
"Decision records have unavailable or malformed evidence; review cannot be confirmed."
|
|
3792
|
+
);
|
|
2904
3793
|
const record = store.records.find((record2) => record2.id === evidence.decisionId);
|
|
2905
3794
|
if (!record || record.version !== 2) return null;
|
|
2906
|
-
const
|
|
3795
|
+
const inspection = latestDecisionInspection(
|
|
3796
|
+
record,
|
|
3797
|
+
evidence.provenance?.approval === "accepted" ? "operative" : "all"
|
|
3798
|
+
);
|
|
3799
|
+
if (inspection && record.status === "active") {
|
|
3800
|
+
const capture = await captureAnchors(root, inspection.capture.anchors);
|
|
3801
|
+
let ancestor = true;
|
|
3802
|
+
try {
|
|
3803
|
+
await git2(root, "merge-base", "--is-ancestor", baselineHead, inspection.headHash);
|
|
3804
|
+
} catch {
|
|
3805
|
+
ancestor = false;
|
|
3806
|
+
}
|
|
3807
|
+
if (ancestor)
|
|
3808
|
+
return {
|
|
3809
|
+
id,
|
|
3810
|
+
status: !capture.complete || !inspection.capture.complete ? "unverified" : changedCaptureFiles(inspection.capture, capture).length ? "reopened" : "current",
|
|
3811
|
+
outcome: "no_contradiction_found",
|
|
3812
|
+
reviewer: inspection.inspector,
|
|
3813
|
+
note: inspection.note,
|
|
3814
|
+
reviewedHead: inspection.headHash,
|
|
3815
|
+
recordPath: `.mason/decisions/${record.id}.json`,
|
|
3816
|
+
reason: "A separate source inspection reports no contradiction; this does not accept or reaffirm the decision. Relevant anchor changes reopen the advisory."
|
|
3817
|
+
};
|
|
3818
|
+
}
|
|
3819
|
+
const events = record.history.filter(
|
|
3820
|
+
(event2) => ["accepted", "reaffirmed", "retired"].includes(event2.kind)
|
|
3821
|
+
);
|
|
2907
3822
|
const event = events.at(-1);
|
|
2908
|
-
if (!event?.evidence || !event.actor || !event.note || !event.evidence.historyAvailable || event.evidence.localChanges.length)
|
|
3823
|
+
if (!event?.evidence || !event.actor || !event.note || !event.evidence.historyAvailable || event.evidence.localChanges.length)
|
|
3824
|
+
return null;
|
|
2909
3825
|
try {
|
|
2910
3826
|
await git2(root, "merge-base", "--is-ancestor", baselineHead, event.evidence.headHash);
|
|
2911
3827
|
} catch {
|
|
@@ -2930,7 +3846,8 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
2930
3846
|
if ("confidence" in finding) return null;
|
|
2931
3847
|
const id = findingId(finding);
|
|
2932
3848
|
try {
|
|
2933
|
-
if (finding.evidence.kind === "decision-anchor")
|
|
3849
|
+
if (finding.evidence.kind === "decision-anchor")
|
|
3850
|
+
return await decisionAssessment(root, finding, baselineHead);
|
|
2934
3851
|
const record = await readRecord(root, id);
|
|
2935
3852
|
if (!record) return null;
|
|
2936
3853
|
const event = record.events.at(-1);
|
|
@@ -2944,37 +3861,63 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
2944
3861
|
outcome: event.outcome,
|
|
2945
3862
|
reviewedHead: event.evidence.head
|
|
2946
3863
|
};
|
|
2947
|
-
if (current.dirty || current.fingerprint !== event.evidence.fingerprint || digest(scopes(finding, event.reasonCode)) !== digest(event.evidence.scope))
|
|
2948
|
-
|
|
3864
|
+
if (current.dirty || current.fingerprint !== event.evidence.fingerprint || digest(scopes(finding, event.reasonCode)) !== digest(event.evidence.scope))
|
|
3865
|
+
return {
|
|
3866
|
+
...base,
|
|
3867
|
+
status: "reopened",
|
|
3868
|
+
reason: "Relevant document or repository evidence changed after the assessment; prepare a new review."
|
|
3869
|
+
};
|
|
3870
|
+
return {
|
|
3871
|
+
...base,
|
|
3872
|
+
status: event.outcome === "deferred" ? "deferred" : "current",
|
|
3873
|
+
reason: event.outcome === "deferred" ? "The review was deferred and remains outstanding." : "An explicit assessment covers the current committed scope. This does not approve an engineering decision or certify correctness."
|
|
3874
|
+
};
|
|
2949
3875
|
} catch (error) {
|
|
2950
|
-
return {
|
|
3876
|
+
return {
|
|
3877
|
+
id,
|
|
3878
|
+
status: "unverified",
|
|
3879
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
3880
|
+
};
|
|
2951
3881
|
}
|
|
2952
3882
|
}
|
|
2953
3883
|
async function advisoryReviewInventory(root) {
|
|
2954
3884
|
let names;
|
|
2955
3885
|
try {
|
|
2956
|
-
names = await
|
|
3886
|
+
names = await fs12.readdir(await storePath(root, ".mason/reviews/advisories"));
|
|
2957
3887
|
} catch (error) {
|
|
2958
3888
|
if (error.code === "ENOENT") return [];
|
|
2959
3889
|
throw error;
|
|
2960
3890
|
}
|
|
2961
3891
|
if (names.length > 5e3) throw new Error("Advisory review inventory exceeds 5,000 records.");
|
|
2962
|
-
return Promise.all(
|
|
3892
|
+
return Promise.all(
|
|
3893
|
+
names.filter((name) => name.endsWith(".json")).sort().map(async (name) => [name, await readAuditInput(root, ".mason/reviews/advisories/" + name)])
|
|
3894
|
+
);
|
|
2963
3895
|
}
|
|
2964
3896
|
async function reviewAdvisory(rootDir, input) {
|
|
2965
|
-
const root = await
|
|
3897
|
+
const root = await fs12.realpath(rootDir), request = advisoryReviewRequest.parse(input);
|
|
2966
3898
|
const { loadRepairBaseline: loadRepairBaseline2 } = await Promise.resolve().then(() => (init_repair(), repair_exports));
|
|
2967
3899
|
const baseline = await loadRepairBaseline2(root, request.baselinePath);
|
|
2968
|
-
const finding = [
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
3900
|
+
const finding = [
|
|
3901
|
+
...baseline.original.advisories,
|
|
3902
|
+
...baseline.original.suppressedAdvisories ?? []
|
|
3903
|
+
].find((f) => findingId(f) === request.findingId);
|
|
3904
|
+
if (!finding)
|
|
3905
|
+
throw new Error(
|
|
3906
|
+
"No advisory with this findingId in the original baseline. Provable issues must be repaired and rechecked."
|
|
3907
|
+
);
|
|
3908
|
+
if (finding.evidence.kind === "decision-anchor")
|
|
3909
|
+
return {
|
|
3910
|
+
status: "decision-review-required",
|
|
3911
|
+
decisionId: finding.evidence.decisionId,
|
|
3912
|
+
hint: "Use review_decision to prepare evidence and record a source inspection without human approval, or an authorized acceptance, reaffirmation or retirement, then verify this original repair baseline. An advisory assessment cannot approve a decision."
|
|
3913
|
+
};
|
|
2975
3914
|
if (request.action === "dismiss") {
|
|
2976
|
-
if (finding.type !== "deps-changed" || finding.evidence.kind !== "doc-behind-manifests")
|
|
2977
|
-
|
|
3915
|
+
if (finding.type !== "deps-changed" || finding.evidence.kind !== "doc-behind-manifests")
|
|
3916
|
+
throw new Error("One-step dismissal is only available for dependency advisories.");
|
|
3917
|
+
if (!request.reasonCode || !request.note)
|
|
3918
|
+
throw new Error(
|
|
3919
|
+
"Dismissal requires reasonCode and note; it does not require a reviewer or prepared token."
|
|
3920
|
+
);
|
|
2978
3921
|
} else if (request.reasonCode) throw new Error("reasonCode is only used with dismiss.");
|
|
2979
3922
|
const prepare = async () => {
|
|
2980
3923
|
const record = await readRecord(root, request.findingId);
|
|
@@ -2988,13 +3931,27 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
2988
3931
|
let diff = null, diffNote;
|
|
2989
3932
|
const from = finding.evidence.kind === "doc-behind-manifests" ? finding.evidence.docLastCommit.hash : baseline.original.headHash;
|
|
2990
3933
|
try {
|
|
2991
|
-
const full = await git2(
|
|
3934
|
+
const full = await git2(
|
|
3935
|
+
root,
|
|
3936
|
+
"diff",
|
|
3937
|
+
"--no-ext-diff",
|
|
3938
|
+
"--no-textconv",
|
|
3939
|
+
"--no-color",
|
|
3940
|
+
"--no-renames",
|
|
3941
|
+
"-U3",
|
|
3942
|
+
from,
|
|
3943
|
+
state.evidence.head,
|
|
3944
|
+
"--",
|
|
3945
|
+
...state.evidence.scope
|
|
3946
|
+
);
|
|
2992
3947
|
diff = full.slice(0, 16e3);
|
|
2993
|
-
if (full.length > diff.length)
|
|
3948
|
+
if (full.length > diff.length)
|
|
3949
|
+
diffNote = "Diff preview truncated; inspect the full scoped diff before reviewing.";
|
|
2994
3950
|
} catch {
|
|
2995
3951
|
diffNote = "Diff preview unavailable; inspect the original cited evidence directly before reviewing.";
|
|
2996
3952
|
}
|
|
2997
|
-
if ((await prepare()).token !== state.token)
|
|
3953
|
+
if ((await prepare()).token !== state.token)
|
|
3954
|
+
throw new Error("Review evidence changed during preparation; retry.");
|
|
2998
3955
|
return {
|
|
2999
3956
|
status: "prepared",
|
|
3000
3957
|
findingId: request.findingId,
|
|
@@ -3007,12 +3964,22 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3007
3964
|
hint: "Inspect the original finding, scoped document and relevant code changes. Record addressed, inapplicable or deferred only when the user or cited project review authorizes that assessment. Supply the actual reviewer, reason in note and reviewToken. Relevant edits must be committed first. Deferral remains outstanding. Review identities are recorded assertions, not authenticated approvals."
|
|
3008
3965
|
};
|
|
3009
3966
|
}
|
|
3010
|
-
if (request.action !== "dismiss" && (!request.reviewer || !request.note || !request.reviewToken))
|
|
3967
|
+
if (request.action !== "dismiss" && (!request.reviewer || !request.note || !request.reviewToken))
|
|
3968
|
+
throw new Error(
|
|
3969
|
+
"Prepare first; recording an assessment requires reviewToken, reviewer and note."
|
|
3970
|
+
);
|
|
3011
3971
|
return withLock(root, ".mason/reports/advisory-review-locks/" + request.findingId, async () => {
|
|
3012
3972
|
const state = await prepare();
|
|
3013
|
-
if (request.action !== "dismiss" && state.token !== request.reviewToken)
|
|
3014
|
-
|
|
3015
|
-
|
|
3973
|
+
if (request.action !== "dismiss" && state.token !== request.reviewToken)
|
|
3974
|
+
throw new Error(
|
|
3975
|
+
"Review conflict: evidence or review history changed since preparation. Prepare and inspect a new review."
|
|
3976
|
+
);
|
|
3977
|
+
if (state.evidence.dirty)
|
|
3978
|
+
throw new Error(
|
|
3979
|
+
"Commit relevant document and repository edits before recording the assessment. Unrelated local work may remain."
|
|
3980
|
+
);
|
|
3981
|
+
if ((state.record?.events.length ?? 0) >= 200)
|
|
3982
|
+
throw new Error("Review history limit reached; existing events were retained.");
|
|
3016
3983
|
const { dirty, documentDigest, ...evidence } = state.evidence;
|
|
3017
3984
|
const event = eventSchema2.parse({
|
|
3018
3985
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3023,9 +3990,17 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3023
3990
|
finding,
|
|
3024
3991
|
evidence
|
|
3025
3992
|
});
|
|
3026
|
-
const payload = {
|
|
3027
|
-
|
|
3028
|
-
|
|
3993
|
+
const payload = {
|
|
3994
|
+
version: 1,
|
|
3995
|
+
id: request.findingId,
|
|
3996
|
+
events: [...state.record?.events ?? [], event]
|
|
3997
|
+
};
|
|
3998
|
+
if ((await prepare()).token !== state.token)
|
|
3999
|
+
throw new Error("Review conflict: evidence changed while recording the assessment.");
|
|
4000
|
+
await writeStoreJson(root, recordPath(request.findingId), {
|
|
4001
|
+
...payload,
|
|
4002
|
+
digest: digest(payload)
|
|
4003
|
+
});
|
|
3029
4004
|
return {
|
|
3030
4005
|
status: "recorded",
|
|
3031
4006
|
recordPath: recordPath(request.findingId),
|
|
@@ -3034,7 +4009,7 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3034
4009
|
};
|
|
3035
4010
|
});
|
|
3036
4011
|
}
|
|
3037
|
-
var git2, hashSchema, commitSchema2,
|
|
4012
|
+
var git2, hashSchema, commitSchema2, evidenceSchema3, dismissalReasonSchema, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
3038
4013
|
var init_advisory_review = __esm({
|
|
3039
4014
|
"src/audit/advisory-review.ts"() {
|
|
3040
4015
|
"use strict";
|
|
@@ -3048,43 +4023,62 @@ var init_advisory_review = __esm({
|
|
|
3048
4023
|
init_paths();
|
|
3049
4024
|
init_store();
|
|
3050
4025
|
init_decisions();
|
|
4026
|
+
init_provenance();
|
|
4027
|
+
init_anchors();
|
|
3051
4028
|
init_drift2();
|
|
3052
4029
|
git2 = async (root, ...args) => (await (["log", "ls-tree", "ls-files", "merge-base"].includes(args[0]) ? inspectionGit : execGit)(args, { cwd: root, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 })).stdout;
|
|
3053
|
-
hashSchema =
|
|
3054
|
-
commitSchema2 =
|
|
3055
|
-
|
|
4030
|
+
hashSchema = z11.string().regex(/^[a-f0-9]{64}$/);
|
|
4031
|
+
commitSchema2 = z11.string().regex(/^[a-f0-9]{40,64}$/);
|
|
4032
|
+
evidenceSchema3 = z11.object({
|
|
3056
4033
|
head: commitSchema2,
|
|
3057
4034
|
fingerprint: hashSchema,
|
|
3058
|
-
scope:
|
|
3059
|
-
tree:
|
|
4035
|
+
scope: z11.array(z11.string()).min(1).max(1e3),
|
|
4036
|
+
tree: z11.string().max(2 * 1024 * 1024),
|
|
3060
4037
|
lastScopeCommit: commitSchema2,
|
|
3061
|
-
paths:
|
|
4038
|
+
paths: z11.array(z11.object({ path: z11.string(), exists: z11.boolean() })).max(1e3)
|
|
3062
4039
|
});
|
|
3063
|
-
dismissalReasonSchema =
|
|
3064
|
-
eventSchema2 =
|
|
3065
|
-
at:
|
|
3066
|
-
reviewer:
|
|
3067
|
-
note:
|
|
3068
|
-
outcome:
|
|
4040
|
+
dismissalReasonSchema = z11.enum(["no-dependency-claims", "unrelated-manifest-change"]);
|
|
4041
|
+
eventSchema2 = z11.object({
|
|
4042
|
+
at: z11.string().datetime(),
|
|
4043
|
+
reviewer: z11.string().trim().min(1).max(200).optional(),
|
|
4044
|
+
note: z11.string().trim().min(1).max(2500),
|
|
4045
|
+
outcome: z11.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
|
|
3069
4046
|
reasonCode: dismissalReasonSchema.optional(),
|
|
3070
|
-
finding: advisorySchema.refine(
|
|
3071
|
-
|
|
4047
|
+
finding: advisorySchema.refine(
|
|
4048
|
+
(finding) => finding.evidence.kind !== "decision-anchor",
|
|
4049
|
+
"Decision findings use decision reviews"
|
|
4050
|
+
),
|
|
4051
|
+
evidence: evidenceSchema3
|
|
4052
|
+
});
|
|
4053
|
+
recordSchema = z11.object({
|
|
4054
|
+
version: z11.literal(1),
|
|
4055
|
+
id: hashSchema,
|
|
4056
|
+
events: z11.array(eventSchema2).min(1).max(200),
|
|
4057
|
+
digest: hashSchema
|
|
3072
4058
|
});
|
|
3073
|
-
|
|
3074
|
-
reviewSummarySchema = z8.object({
|
|
4059
|
+
reviewSummarySchema = z11.object({
|
|
3075
4060
|
id: hashSchema,
|
|
3076
|
-
status:
|
|
3077
|
-
reason:
|
|
3078
|
-
recordPath:
|
|
3079
|
-
reviewer:
|
|
3080
|
-
note:
|
|
3081
|
-
outcome:
|
|
3082
|
-
|
|
4061
|
+
status: z11.enum(["current", "reopened", "deferred", "unverified"]),
|
|
4062
|
+
reason: z11.string(),
|
|
4063
|
+
recordPath: z11.string().optional(),
|
|
4064
|
+
reviewer: z11.string().optional(),
|
|
4065
|
+
note: z11.string().optional(),
|
|
4066
|
+
outcome: z11.enum([
|
|
4067
|
+
"addressed",
|
|
4068
|
+
"inapplicable",
|
|
4069
|
+
"deferred",
|
|
4070
|
+
"accepted",
|
|
4071
|
+
"reaffirmed",
|
|
4072
|
+
"retired",
|
|
4073
|
+
"dismissed",
|
|
4074
|
+
"no_contradiction_found"
|
|
4075
|
+
]).optional(),
|
|
4076
|
+
reviewedHead: z11.string().optional()
|
|
3083
4077
|
});
|
|
3084
|
-
advisoryReviewRequest =
|
|
3085
|
-
baselinePath:
|
|
4078
|
+
advisoryReviewRequest = z11.object({
|
|
4079
|
+
baselinePath: z11.string().min(1),
|
|
3086
4080
|
findingId: hashSchema,
|
|
3087
|
-
action:
|
|
4081
|
+
action: z11.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
|
|
3088
4082
|
reasonCode: dismissalReasonSchema.optional(),
|
|
3089
4083
|
reviewer: eventSchema2.shape.reviewer.optional(),
|
|
3090
4084
|
note: eventSchema2.shape.note.optional(),
|
|
@@ -3095,14 +4089,17 @@ var init_advisory_review = __esm({
|
|
|
3095
4089
|
});
|
|
3096
4090
|
|
|
3097
4091
|
// src/audit/audit.ts
|
|
3098
|
-
import
|
|
3099
|
-
import
|
|
4092
|
+
import fs13 from "fs/promises";
|
|
4093
|
+
import path19 from "path";
|
|
3100
4094
|
async function computeAudit(rootDir, options = {}) {
|
|
3101
4095
|
return profilePhase("audit.current", () => auditCurrent(rootDir, options));
|
|
3102
4096
|
}
|
|
3103
4097
|
async function auditCurrent(rootDir, options) {
|
|
3104
|
-
const resolvedRoot =
|
|
3105
|
-
const [docs, headHash] = await Promise.all([
|
|
4098
|
+
const resolvedRoot = path19.resolve(rootDir);
|
|
4099
|
+
const [docs, headHash] = await Promise.all([
|
|
4100
|
+
discoverDocs(resolvedRoot),
|
|
4101
|
+
getCurrentGitHash(resolvedRoot)
|
|
4102
|
+
]);
|
|
3106
4103
|
if (docs.length === 0) return null;
|
|
3107
4104
|
const report = {
|
|
3108
4105
|
version: 1,
|
|
@@ -3130,12 +4127,13 @@ async function auditCurrent(rootDir, options) {
|
|
|
3130
4127
|
const changesByCommit = /* @__PURE__ */ new Map();
|
|
3131
4128
|
for (const doc of docs) {
|
|
3132
4129
|
const hash = doc.lastCommit?.hash;
|
|
3133
|
-
if (hash && !changesByCommit.has(hash))
|
|
4130
|
+
if (hash && !changesByCommit.has(hash))
|
|
4131
|
+
changesByCommit.set(hash, await getChangesWithStatus(resolvedRoot, hash));
|
|
3134
4132
|
changesSinceDoc.set(doc.path, hash ? changesByCommit.get(hash) : null);
|
|
3135
4133
|
}
|
|
3136
4134
|
let decisionsPresent = false;
|
|
3137
4135
|
try {
|
|
3138
|
-
await
|
|
4136
|
+
await fs13.access(path19.join(resolvedRoot, ".mason", "decisions"));
|
|
3139
4137
|
decisionsPresent = true;
|
|
3140
4138
|
} catch {
|
|
3141
4139
|
}
|
|
@@ -3152,21 +4150,35 @@ async function auditCurrent(rootDir, options) {
|
|
|
3152
4150
|
for (const name of ALL_CHECKS) {
|
|
3153
4151
|
if (!selected.includes(name)) continue;
|
|
3154
4152
|
const exclusions = policy?.exclude?.[name] ?? [];
|
|
3155
|
-
const included = await includedCheckPaths(
|
|
4153
|
+
const included = await includedCheckPaths(
|
|
4154
|
+
resolvedRoot,
|
|
4155
|
+
docs.map((doc) => doc.path),
|
|
4156
|
+
exclusions
|
|
4157
|
+
);
|
|
3156
4158
|
const excluded = docs.filter((doc) => !included.has(doc.path)).map((doc) => ({ check: name, doc: doc.path }));
|
|
3157
4159
|
if (excluded.length) (report.excludedChecks ??= []).push(...excluded);
|
|
3158
4160
|
const scoped = { ...ctx, docs: docs.filter((doc) => included.has(doc.path)) };
|
|
3159
4161
|
const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, scoped) : CHECKS[name](scoped));
|
|
3160
4162
|
report.checksRun.push(name);
|
|
3161
4163
|
report.issues.push(...issues);
|
|
3162
|
-
const advisoryPaths = await includedCheckPaths(
|
|
4164
|
+
const advisoryPaths = await includedCheckPaths(
|
|
4165
|
+
resolvedRoot,
|
|
4166
|
+
[...new Set(advisories.map((f) => f.anchor.doc))],
|
|
4167
|
+
exclusions
|
|
4168
|
+
);
|
|
3163
4169
|
report.advisories.push(...advisories.filter((f) => advisoryPaths.has(f.anchor.doc)));
|
|
3164
|
-
for (const f of advisories)
|
|
4170
|
+
for (const f of advisories)
|
|
4171
|
+
if (!advisoryPaths.has(f.anchor.doc))
|
|
4172
|
+
(report.excludedChecks ??= []).push({ check: name, doc: f.anchor.doc });
|
|
3165
4173
|
report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
|
|
3166
4174
|
report.skippedChecks.push(...skipped);
|
|
3167
4175
|
}
|
|
3168
4176
|
report.clean = report.issues.length === 0;
|
|
3169
|
-
const reviews = (await Promise.all(
|
|
4177
|
+
const reviews = (await Promise.all(
|
|
4178
|
+
[...report.advisories, ...report.suppressedAdvisories].map(
|
|
4179
|
+
(finding) => assessAdvisory(resolvedRoot, finding, headHash)
|
|
4180
|
+
)
|
|
4181
|
+
)).filter((review) => review !== null);
|
|
3170
4182
|
if (reviews.length) report.advisoryReviews = reviews;
|
|
3171
4183
|
return report;
|
|
3172
4184
|
}
|
|
@@ -3190,11 +4202,11 @@ init_audit();
|
|
|
3190
4202
|
init_types();
|
|
3191
4203
|
init_repair();
|
|
3192
4204
|
init_findings();
|
|
3193
|
-
import
|
|
4205
|
+
import path21 from "path";
|
|
3194
4206
|
|
|
3195
4207
|
// src/audit/advisory-cli.ts
|
|
3196
4208
|
init_advisory_review();
|
|
3197
|
-
import
|
|
4209
|
+
import path20 from "path";
|
|
3198
4210
|
import { stripVTControlCharacters } from "util";
|
|
3199
4211
|
var USAGE = `Usage: mason audit review --baseline <path> --finding <id> [options]
|
|
3200
4212
|
|
|
@@ -3228,12 +4240,23 @@ async function runAdvisoryCli(args, io = { out: (text2) => console.log(text2), e
|
|
|
3228
4240
|
json = true;
|
|
3229
4241
|
continue;
|
|
3230
4242
|
}
|
|
3231
|
-
if (![
|
|
4243
|
+
if (![
|
|
4244
|
+
"--dir",
|
|
4245
|
+
"--baseline",
|
|
4246
|
+
"--finding",
|
|
4247
|
+
"--outcome",
|
|
4248
|
+
"--reviewer",
|
|
4249
|
+
"--note",
|
|
4250
|
+
"--token",
|
|
4251
|
+
"--reason"
|
|
4252
|
+
].includes(flag) || flags[flag])
|
|
4253
|
+
throw new Error("Unexpected or repeated argument: " + flag);
|
|
3232
4254
|
const value = args[++i];
|
|
3233
4255
|
if (!value || value.startsWith("--")) throw new Error(flag + " requires a value.");
|
|
3234
4256
|
flags[flag] = value;
|
|
3235
4257
|
}
|
|
3236
|
-
if (!flags["--outcome"] && ["--reviewer", "--note", "--token", "--reason"].some((key) => flags[key]))
|
|
4258
|
+
if (!flags["--outcome"] && ["--reviewer", "--note", "--token", "--reason"].some((key) => flags[key]))
|
|
4259
|
+
throw new Error("Use --outcome to record an assessment.");
|
|
3237
4260
|
const input = {
|
|
3238
4261
|
baselinePath: flags["--baseline"],
|
|
3239
4262
|
findingId: flags["--finding"],
|
|
@@ -3243,20 +4266,25 @@ async function runAdvisoryCli(args, io = { out: (text2) => console.log(text2), e
|
|
|
3243
4266
|
reviewToken: flags["--token"],
|
|
3244
4267
|
reasonCode: flags["--reason"]
|
|
3245
4268
|
};
|
|
3246
|
-
const result = await reviewAdvisory(
|
|
4269
|
+
const result = await reviewAdvisory(path20.resolve(flags["--dir"] ?? process.cwd()), input);
|
|
3247
4270
|
if (json) io.out(JSON.stringify(result, null, 2));
|
|
3248
4271
|
else {
|
|
3249
4272
|
const lines = [`Advisory review: ${result.status}.`];
|
|
3250
|
-
if ("finding" in result && result.finding && result.evidence)
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
4273
|
+
if ("finding" in result && result.finding && result.evidence)
|
|
4274
|
+
lines.push(
|
|
4275
|
+
`[${result.finding.type}] ${result.finding.anchor.doc}: ${result.finding.message}`,
|
|
4276
|
+
`Finding: ${result.findingId}`,
|
|
4277
|
+
`Prepared against commit ${result.evidence.head.slice(0, 12)}.`,
|
|
4278
|
+
result.evidence.dirty ? "Relevant files have uncommitted edits; commit them and prepare again before recording." : "Relevant files are committed.",
|
|
4279
|
+
`Review token: ${result.reviewToken}`,
|
|
4280
|
+
...result.diff ? ["Scoped changes:", result.diff] : [],
|
|
4281
|
+
...result.diffNote ? [result.diffNote] : []
|
|
4282
|
+
);
|
|
4283
|
+
if ("event" in result)
|
|
4284
|
+
lines.push(
|
|
4285
|
+
`${result.event.outcome} \u2014 ${result.event.reviewer}: ${result.event.note}`,
|
|
4286
|
+
`Record: ${result.recordPath}`
|
|
4287
|
+
);
|
|
3260
4288
|
lines.push(result.hint);
|
|
3261
4289
|
io.out(stripVTControlCharacters(lines.join("\n")));
|
|
3262
4290
|
}
|
|
@@ -3318,7 +4346,8 @@ function parseArgs(argv) {
|
|
|
3318
4346
|
parsed.prepareRepair = true;
|
|
3319
4347
|
} else if (arg === "--verify-repair") {
|
|
3320
4348
|
const value = argv[++i];
|
|
3321
|
-
if (!value || value.startsWith("--"))
|
|
4349
|
+
if (!value || value.startsWith("--"))
|
|
4350
|
+
throw new Error("--verify-repair requires a baseline path");
|
|
3322
4351
|
parsed.baseline = value;
|
|
3323
4352
|
} else if (arg === "--help" || arg === "-h") {
|
|
3324
4353
|
parsed.help = true;
|
|
@@ -3333,9 +4362,7 @@ function parseArgs(argv) {
|
|
|
3333
4362
|
if (!names.length) throw new Error("--checks requires at least one check");
|
|
3334
4363
|
for (const name of names) {
|
|
3335
4364
|
if (!ALL_CHECKS.includes(name)) {
|
|
3336
|
-
throw new Error(
|
|
3337
|
-
`Unknown check: ${name} (valid: ${ALL_CHECKS.join(", ")})`
|
|
3338
|
-
);
|
|
4365
|
+
throw new Error(`Unknown check: ${name} (valid: ${ALL_CHECKS.join(", ")})`);
|
|
3339
4366
|
}
|
|
3340
4367
|
}
|
|
3341
4368
|
parsed.checks = names;
|
|
@@ -3354,9 +4381,15 @@ function issueLine(issue) {
|
|
|
3354
4381
|
}
|
|
3355
4382
|
function formatAuditSummary(report) {
|
|
3356
4383
|
const lines = [];
|
|
3357
|
-
const reviewed = new Set(
|
|
3358
|
-
|
|
3359
|
-
|
|
4384
|
+
const reviewed = new Set(
|
|
4385
|
+
report.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id)
|
|
4386
|
+
);
|
|
4387
|
+
const open = report.advisories.filter(
|
|
4388
|
+
(finding) => finding.resolution !== "informational" && !reviewed.has(findingId(finding))
|
|
4389
|
+
);
|
|
4390
|
+
const suppressed = (report.suppressedAdvisories ?? []).filter(
|
|
4391
|
+
(finding) => finding.resolution !== "informational" && !reviewed.has(findingId(finding))
|
|
4392
|
+
);
|
|
3360
4393
|
const reviewCount = open.length + suppressed.length;
|
|
3361
4394
|
for (const doc of report.docs) {
|
|
3362
4395
|
const docIssues = report.issues.filter((i) => i.anchor.doc === doc.path);
|
|
@@ -3374,9 +4407,17 @@ function formatAuditSummary(report) {
|
|
|
3374
4407
|
for (const issue of report.issues) {
|
|
3375
4408
|
if (!docPaths.has(issue.anchor.doc)) lines.push(issueLine(issue));
|
|
3376
4409
|
}
|
|
3377
|
-
const information = [...report.advisories, ...report.suppressedAdvisories ?? []].filter(
|
|
3378
|
-
|
|
3379
|
-
|
|
4410
|
+
const information = [...report.advisories, ...report.suppressedAdvisories ?? []].filter(
|
|
4411
|
+
(f) => f.resolution === "informational"
|
|
4412
|
+
);
|
|
4413
|
+
if (information.length)
|
|
4414
|
+
lines.push(
|
|
4415
|
+
`${information.length} informational dependency notices; no review required (see --json).`
|
|
4416
|
+
);
|
|
4417
|
+
if (report.excludedChecks?.length)
|
|
4418
|
+
lines.push(
|
|
4419
|
+
`${report.excludedChecks.length} document/check pairs excluded by project policy (see --json).`
|
|
4420
|
+
);
|
|
3380
4421
|
if (open.length > 0) {
|
|
3381
4422
|
lines.push("Advisories (do not affect the exit code):");
|
|
3382
4423
|
for (const advisory of open) {
|
|
@@ -3388,10 +4429,16 @@ function formatAuditSummary(report) {
|
|
|
3388
4429
|
lines.push(` [skipped] ${skip.check}: ${skip.reason}`);
|
|
3389
4430
|
}
|
|
3390
4431
|
}
|
|
3391
|
-
if (reviewed.size)
|
|
3392
|
-
|
|
4432
|
+
if (reviewed.size)
|
|
4433
|
+
lines.push(
|
|
4434
|
+
`${reviewed.size} advisories have recorded assessments covering current evidence (see --json).`
|
|
4435
|
+
);
|
|
4436
|
+
for (const review of report.advisoryReviews ?? [])
|
|
4437
|
+
if (review.status !== "current") lines.push(` [${review.status}] ${review.reason}`);
|
|
3393
4438
|
for (const advisory of suppressed) {
|
|
3394
|
-
lines.push(
|
|
4439
|
+
lines.push(
|
|
4440
|
+
` [suppressed; unresolved] ${advisory.type} ${advisory.anchor.doc}: ${advisory.message}`
|
|
4441
|
+
);
|
|
3395
4442
|
}
|
|
3396
4443
|
lines.push(
|
|
3397
4444
|
report.clean ? reviewCount || report.skippedChecks.length ? `No audit issues detected (${report.docs.length} docs audited); ${reviewCount} advisories remain for review, ${report.skippedChecks.length} checks skipped.` : `Context files are clean (${report.docs.length} doc${report.docs.length === 1 ? "" : "s"} audited).` : `${report.issues.length} issue${report.issues.length === 1 ? "" : "s"} across ${report.docs.length} doc${report.docs.length === 1 ? "" : "s"}.`
|
|
@@ -3406,23 +4453,23 @@ function formatFixPrompt(report, baselinePath) {
|
|
|
3406
4453
|
);
|
|
3407
4454
|
lines.push("");
|
|
3408
4455
|
lines.push("RULES:");
|
|
3409
|
-
lines.push(baselinePath ? `- Preserve the original repair baseline: ${JSON.stringify(baselinePath)}. Do not replace it after editing.` : "- Before the first edit, call mason_repair with action: prepare, or run mason-audit --prepare-repair --json with the same --dir and --checks. Keep the returned baselinePath through verification.");
|
|
3410
4456
|
lines.push(
|
|
3411
|
-
`-
|
|
4457
|
+
baselinePath ? `- Preserve the original repair baseline: ${JSON.stringify(baselinePath)}. Do not replace it after editing.` : "- Before the first edit, run mason audit --prepare-repair --json with the same --dir and --checks. Keep the returned baselinePath through verification."
|
|
3412
4458
|
);
|
|
3413
4459
|
lines.push(
|
|
3414
|
-
|
|
4460
|
+
`- Edit ONLY these files: ${flaggedDocs.join(", ") || "none (advisory review only)"}. Bring the docs into agreement with verified source evidence; do not change source code or configs to silence findings.`
|
|
3415
4461
|
);
|
|
4462
|
+
lines.push("- Keep diffs minimal: change the smallest span that makes each claim true.");
|
|
3416
4463
|
lines.push(
|
|
3417
4464
|
"- Never invent content. Every replacement must be grounded in the evidence below or in files you read from this repository."
|
|
3418
4465
|
);
|
|
3419
|
-
lines.push("- Inspect likely findings before editing: heuristic evidence may describe an intentional omission or an example.");
|
|
3420
4466
|
lines.push(
|
|
3421
|
-
"-
|
|
4467
|
+
"- Inspect likely findings before editing: heuristic evidence may describe an intentional omission or an example."
|
|
3422
4468
|
);
|
|
3423
4469
|
lines.push(
|
|
3424
|
-
"-
|
|
4470
|
+
"- deleted-reference: if evidence shows renamedTo, update the path; otherwise remove the reference, or rephrase to past tense if the sentence is about history. Deleted paths inside directory trees: delete the tree line."
|
|
3425
4471
|
);
|
|
4472
|
+
lines.push("- stale-count: replace the number with the actual count from the evidence.");
|
|
3426
4473
|
lines.push(
|
|
3427
4474
|
"- dead-command: replace with the correct script from availableScripts if an obvious rename exists; otherwise remove the command mention."
|
|
3428
4475
|
);
|
|
@@ -3430,10 +4477,12 @@ function formatFixPrompt(report, baselinePath) {
|
|
|
3430
4477
|
"- new-module: review whether the directory needs documenting; an omission alone does not establish a defect. Preserve scope and describe only what you verified."
|
|
3431
4478
|
);
|
|
3432
4479
|
lines.push(
|
|
3433
|
-
"- Specific historical ADVISORIES require a separate authorized assessment using
|
|
4480
|
+
"- Specific historical ADVISORIES require a separate authorized assessment using mason audit review with --baseline and --finding from verification. Prepare and inspect evidence first; record addressed, inapplicable or deferred with the actual reviewer, note and reviewToken. Dependency notices marked resolution: informational require no review. Dependency advisories may be dismissed with --outcome dismiss, --reason (no-dependency-claims or unrelated-manifest-change), and --note, without a reviewer or token. Decision findings use review_decision. Candidates marked resolution: recheck can be rerun to see whether the condition remains; this does not establish semantic approval. Report what remains unknown."
|
|
3434
4481
|
);
|
|
3435
4482
|
lines.push("");
|
|
3436
|
-
lines.push(
|
|
4483
|
+
lines.push(
|
|
4484
|
+
"AUDIT REPORT (current context files and repository evidence, including local edits):"
|
|
4485
|
+
);
|
|
3437
4486
|
lines.push(
|
|
3438
4487
|
JSON.stringify(
|
|
3439
4488
|
{
|
|
@@ -3450,7 +4499,7 @@ function formatFixPrompt(report, baselinePath) {
|
|
|
3450
4499
|
);
|
|
3451
4500
|
lines.push("");
|
|
3452
4501
|
lines.push(
|
|
3453
|
-
"After edits,
|
|
4502
|
+
"After edits, run mason audit --verify-repair <baselinePath> --dir <project>. Repeat against the same baseline after any final documentation commit. Summarize resolved, unresolved, review-required, unverified, and new findings with their evidence. Do not report a suppressed or unavailable check as fixed. This audit covers the listed documents and supported claims; application behavior and code examples need their own validation."
|
|
3454
4503
|
);
|
|
3455
4504
|
return lines.join("\n");
|
|
3456
4505
|
}
|
|
@@ -3468,7 +4517,9 @@ async function runAuditCli(argv, io = {
|
|
|
3468
4517
|
throw new Error("--json and --fix-prompt are mutually exclusive");
|
|
3469
4518
|
}
|
|
3470
4519
|
if (args.baseline && (args.prepareRepair || args.checks || args.fixPrompt)) {
|
|
3471
|
-
throw new Error(
|
|
4520
|
+
throw new Error(
|
|
4521
|
+
"--verify-repair cannot be combined with --prepare-repair, --checks, or --fix-prompt; verification uses the original scope"
|
|
4522
|
+
);
|
|
3472
4523
|
}
|
|
3473
4524
|
} catch (error) {
|
|
3474
4525
|
io.err(error instanceof Error ? error.message : String(error));
|
|
@@ -3479,17 +4530,25 @@ async function runAuditCli(argv, io = {
|
|
|
3479
4530
|
io.out(USAGE2);
|
|
3480
4531
|
return 0;
|
|
3481
4532
|
}
|
|
3482
|
-
const rootDir =
|
|
4533
|
+
const rootDir = path21.resolve(args.dir);
|
|
3483
4534
|
if (args.baseline || args.prepareRepair) {
|
|
3484
4535
|
try {
|
|
3485
4536
|
if (args.baseline) {
|
|
3486
4537
|
const verification = await verifyRepair(rootDir, args.baseline);
|
|
3487
|
-
io.out(
|
|
4538
|
+
io.out(
|
|
4539
|
+
args.json ? JSON.stringify(verification, null, 2) : formatRepairSummary(verification)
|
|
4540
|
+
);
|
|
3488
4541
|
return repairExitCode(verification);
|
|
3489
4542
|
}
|
|
3490
4543
|
const prepared = await prepareRepair(rootDir, args.checks);
|
|
3491
|
-
io.out(
|
|
3492
|
-
|
|
4544
|
+
io.out(
|
|
4545
|
+
args.json ? JSON.stringify(
|
|
4546
|
+
{ ...prepared, workOrder: formatFixPrompt(prepared.report, prepared.baselinePath) },
|
|
4547
|
+
null,
|
|
4548
|
+
2
|
|
4549
|
+
) : args.fixPrompt ? formatFixPrompt(prepared.report, prepared.baselinePath) : `Repair baseline: ${prepared.baselinePath}
|
|
4550
|
+
${formatAuditSummary(prepared.report)}`
|
|
4551
|
+
);
|
|
3493
4552
|
return prepared.report.clean ? 0 : 1;
|
|
3494
4553
|
} catch (error) {
|
|
3495
4554
|
io.err(error instanceof Error ? error.message : String(error));
|
|
@@ -3498,9 +4557,7 @@ ${formatAuditSummary(prepared.report)}`);
|
|
|
3498
4557
|
}
|
|
3499
4558
|
const report = await computeAudit(rootDir, { checks: args.checks });
|
|
3500
4559
|
if (!report) {
|
|
3501
|
-
io.err(
|
|
3502
|
-
`No README.md or agent instruction files found in ${rootDir}.`
|
|
3503
|
-
);
|
|
4560
|
+
io.err(`No README.md or agent instruction files found in ${rootDir}.`);
|
|
3504
4561
|
return 2;
|
|
3505
4562
|
}
|
|
3506
4563
|
if (!report.gitAvailable) {
|