mason-context 0.20.1 → 0.21.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 +12 -1
- package/README.md +14 -0
- package/dist/mason-audit.js +1690 -649
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-drift.js +630 -175
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +828 -194
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +6420 -3828
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +990 -312
- package/dist/mason-review.js.map +1 -1
- package/dist/mason-runtime.js +296 -148
- package/dist/mason-runtime.js.map +1 -1
- package/dist/mason.js +4526 -2210
- package/dist/mason.js.map +1 -1
- package/package.json +11 -1
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");
|
|
172
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
|
+
}
|
|
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
|
|
|
@@ -568,7 +786,7 @@ var init_test_map = __esm({
|
|
|
568
786
|
|
|
569
787
|
// src/snapshot/snapshot.ts
|
|
570
788
|
import path7 from "path";
|
|
571
|
-
import { z } from "zod";
|
|
789
|
+
import { z as z2 } from "zod";
|
|
572
790
|
async function getCurrentGitHash(rootDir) {
|
|
573
791
|
try {
|
|
574
792
|
const { stdout } = await execGit(["rev-parse", "HEAD"], {
|
|
@@ -590,30 +808,30 @@ var init_snapshot = __esm({
|
|
|
590
808
|
init_lock();
|
|
591
809
|
init_paths();
|
|
592
810
|
init_test_map();
|
|
593
|
-
repoPath =
|
|
811
|
+
repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
594
812
|
verificationFields = {
|
|
595
|
-
refreshedHash:
|
|
596
|
-
verifiedAt:
|
|
597
|
-
verifiedHash:
|
|
598
|
-
verificationToken:
|
|
599
|
-
verificationFailed:
|
|
600
|
-
verificationNote:
|
|
813
|
+
refreshedHash: z2.string().optional(),
|
|
814
|
+
verifiedAt: z2.string().optional(),
|
|
815
|
+
verifiedHash: z2.string().optional(),
|
|
816
|
+
verificationToken: z2.string().optional(),
|
|
817
|
+
verificationFailed: z2.boolean().optional(),
|
|
818
|
+
verificationNote: z2.string().optional()
|
|
601
819
|
};
|
|
602
|
-
featureSchema =
|
|
603
|
-
description:
|
|
604
|
-
files:
|
|
605
|
-
tests:
|
|
606
|
-
type:
|
|
820
|
+
featureSchema = z2.object({
|
|
821
|
+
description: z2.string(),
|
|
822
|
+
files: z2.array(repoPath),
|
|
823
|
+
tests: z2.array(repoPath).optional(),
|
|
824
|
+
type: z2.enum(["capability", "infrastructure"]).optional(),
|
|
607
825
|
...verificationFields
|
|
608
826
|
}).passthrough();
|
|
609
|
-
flowSchema =
|
|
610
|
-
snapshotSchema =
|
|
611
|
-
version:
|
|
612
|
-
createdAt:
|
|
613
|
-
updatedAt:
|
|
614
|
-
gitHash:
|
|
615
|
-
features:
|
|
616
|
-
flows:
|
|
827
|
+
flowSchema = z2.object({ description: z2.string(), chain: z2.array(repoPath), ...verificationFields }).passthrough();
|
|
828
|
+
snapshotSchema = z2.object({
|
|
829
|
+
version: z2.literal(2),
|
|
830
|
+
createdAt: z2.string(),
|
|
831
|
+
updatedAt: z2.string(),
|
|
832
|
+
gitHash: z2.string(),
|
|
833
|
+
features: z2.record(featureSchema),
|
|
834
|
+
flows: z2.record(flowSchema)
|
|
617
835
|
}).passthrough();
|
|
618
836
|
}
|
|
619
837
|
});
|
|
@@ -630,18 +848,25 @@ function parseChanges(output) {
|
|
|
630
848
|
if (!first) break;
|
|
631
849
|
const second = /^[RC]/.test(code) ? fields[i++] : void 0;
|
|
632
850
|
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/")))
|
|
851
|
+
if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/")))
|
|
852
|
+
continue;
|
|
634
853
|
changes.push(change);
|
|
635
854
|
}
|
|
636
855
|
return changes;
|
|
637
856
|
}
|
|
638
857
|
function touchedPaths(changes) {
|
|
639
|
-
return [
|
|
858
|
+
return [
|
|
859
|
+
...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))
|
|
860
|
+
].sort();
|
|
640
861
|
}
|
|
641
862
|
async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
|
|
642
|
-
if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-"))
|
|
863
|
+
if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-"))
|
|
864
|
+
return null;
|
|
643
865
|
try {
|
|
644
|
-
const { stdout } = await execGit(
|
|
866
|
+
const { stdout } = await execGit(
|
|
867
|
+
["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"],
|
|
868
|
+
{ cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
|
|
869
|
+
);
|
|
645
870
|
return parseChanges(stdout);
|
|
646
871
|
} catch {
|
|
647
872
|
return null;
|
|
@@ -650,11 +875,23 @@ async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
|
|
|
650
875
|
async function getWorkingTree(resolvedRoot) {
|
|
651
876
|
try {
|
|
652
877
|
const [diff, untracked] = await Promise.all([
|
|
653
|
-
execGit(["diff", "--name-status", "-z", "-M", "HEAD", "--"], {
|
|
654
|
-
|
|
878
|
+
execGit(["diff", "--name-status", "-z", "-M", "HEAD", "--"], {
|
|
879
|
+
cwd: resolvedRoot,
|
|
880
|
+
maxBuffer: 10 * 1024 * 1024
|
|
881
|
+
}),
|
|
882
|
+
execGit(["ls-files", "-z", "--others", "--exclude-standard"], {
|
|
883
|
+
cwd: resolvedRoot,
|
|
884
|
+
maxBuffer: 10 * 1024 * 1024
|
|
885
|
+
})
|
|
655
886
|
]);
|
|
656
887
|
const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
|
|
657
|
-
return {
|
|
888
|
+
return {
|
|
889
|
+
available: true,
|
|
890
|
+
changedFiles: [
|
|
891
|
+
.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])
|
|
892
|
+
].sort(),
|
|
893
|
+
untrackedFiles
|
|
894
|
+
};
|
|
658
895
|
} catch {
|
|
659
896
|
return { available: false, changedFiles: [], untrackedFiles: [] };
|
|
660
897
|
}
|
|
@@ -713,13 +950,16 @@ function commandClaims(line, lineNo, cwd, shell = false) {
|
|
|
713
950
|
const selector = flags.match(/^(?:--prefix|--dir|-C)(?:\s+|=)(.+)$/);
|
|
714
951
|
let directory = cwd;
|
|
715
952
|
let scopeUnknown;
|
|
716
|
-
if (shell && match.index !== 0 || match[3].startsWith("-"))
|
|
953
|
+
if (shell && match.index !== 0 || match[3].startsWith("-"))
|
|
954
|
+
scopeUnknown = "Unsupported shell wrapper or options before the script.";
|
|
717
955
|
if (selector) {
|
|
718
956
|
const selected = literal(selector[1]);
|
|
719
957
|
directory = selected === null || cwd === null ? null : path9.posix.join(cwd ?? ".", selected);
|
|
720
958
|
} else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
|
|
721
959
|
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(
|
|
960
|
+
if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(
|
|
961
|
+
tail
|
|
962
|
+
)) {
|
|
723
963
|
scopeUnknown = "Package selection or conditional options after the script need review.";
|
|
724
964
|
}
|
|
725
965
|
if (/[|()]/.test(text2) || /\b(?:pushd|popd|if|for|while)\b/.test(text2) || directory === null) {
|
|
@@ -743,10 +983,13 @@ var init_commands = __esm({
|
|
|
743
983
|
"src/audit/commands.ts"() {
|
|
744
984
|
"use strict";
|
|
745
985
|
TOKEN = `(?:"[^"\\n]*"|'[^'\\n]*'|[^\\s\`;&|]+)`;
|
|
746
|
-
COMMAND = new RegExp(
|
|
986
|
+
COMMAND = new RegExp(
|
|
987
|
+
`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`,
|
|
988
|
+
"g"
|
|
989
|
+
);
|
|
747
990
|
literal = (value) => {
|
|
748
991
|
const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
|
|
749
|
-
return /[
|
|
992
|
+
return /[$`*?[\]{}~\\\x00-\x1f]/.test(unquoted) || path9.posix.isAbsolute(unquoted) ? null : unquoted;
|
|
750
993
|
};
|
|
751
994
|
}
|
|
752
995
|
});
|
|
@@ -806,11 +1049,7 @@ function extractTreeClaims(blockLines, blockStartLine) {
|
|
|
806
1049
|
}
|
|
807
1050
|
const isDir = name.endsWith("/");
|
|
808
1051
|
const cleanName = name.replace(/\/+$/, "");
|
|
809
|
-
const segments = [
|
|
810
|
-
...rootPrefix ? [rootPrefix] : [],
|
|
811
|
-
...stack.map((s) => s.name),
|
|
812
|
-
cleanName
|
|
813
|
-
];
|
|
1052
|
+
const segments = [...rootPrefix ? [rootPrefix] : [], ...stack.map((s) => s.name), cleanName];
|
|
814
1053
|
claims.push({
|
|
815
1054
|
path: segments.join("/"),
|
|
816
1055
|
line: blockStartLine + i,
|
|
@@ -907,7 +1146,7 @@ function extractClaims(content) {
|
|
|
907
1146
|
let line = token.map[0] + 1;
|
|
908
1147
|
for (const child of token.children ?? []) {
|
|
909
1148
|
const target = child.type === "link_open" ? child.attrGet("href") : child.type === "image" ? child.attrGet("src") : null;
|
|
910
|
-
if (target && !/^(?:[a-z][a-z0-9+.-]*:|[
|
|
1149
|
+
if (target && !/^(?:[a-z][a-z0-9+.-]*:|[/#~])/i.test(target)) {
|
|
911
1150
|
let pathname = null;
|
|
912
1151
|
try {
|
|
913
1152
|
pathname = decodeURIComponent(target.split(/[?#]/)[0]);
|
|
@@ -993,9 +1232,11 @@ function extractClaims(content) {
|
|
|
993
1232
|
});
|
|
994
1233
|
}
|
|
995
1234
|
for (const match of line.matchAll(/`([^`]+)`/g)) {
|
|
996
|
-
for (const claim of commandClaims(match[1], lineNo, void 0, true).claims)
|
|
1235
|
+
for (const claim of commandClaims(match[1], lineNo, void 0, true).claims)
|
|
1236
|
+
addCommand(claim);
|
|
997
1237
|
}
|
|
998
|
-
for (const claim of commandClaims(line.replace(/`[^`]+`/g, ""), lineNo).claims)
|
|
1238
|
+
for (const claim of commandClaims(line.replace(/`[^`]+`/g, ""), lineNo).claims)
|
|
1239
|
+
addCommand(claim);
|
|
999
1240
|
}
|
|
1000
1241
|
if (inFence) processBlock();
|
|
1001
1242
|
return {
|
|
@@ -1072,14 +1313,7 @@ async function lastCommitOf(resolvedRoot, relPath) {
|
|
|
1072
1313
|
async function deletingCommitOf(resolvedRoot, relPath) {
|
|
1073
1314
|
try {
|
|
1074
1315
|
const { stdout } = await inspectionGit(
|
|
1075
|
-
[
|
|
1076
|
-
"log",
|
|
1077
|
-
"-1",
|
|
1078
|
-
"--diff-filter=D",
|
|
1079
|
-
`--format=${COMMIT_FORMAT}`,
|
|
1080
|
-
"--",
|
|
1081
|
-
`:(literal)${relPath}`
|
|
1082
|
-
],
|
|
1316
|
+
["log", "-1", "--diff-filter=D", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
|
|
1083
1317
|
{ cwd: resolvedRoot }
|
|
1084
1318
|
);
|
|
1085
1319
|
const line = stdout.trim().split("\n")[0];
|
|
@@ -1146,7 +1380,14 @@ async function dirtyDocs(resolvedRoot, files) {
|
|
|
1146
1380
|
const dirty = /* @__PURE__ */ new Set();
|
|
1147
1381
|
for (let offset = 0; offset < files.length; offset += 256) {
|
|
1148
1382
|
const { stdout } = await execGit(
|
|
1149
|
-
[
|
|
1383
|
+
[
|
|
1384
|
+
"status",
|
|
1385
|
+
"--porcelain=v1",
|
|
1386
|
+
"-z",
|
|
1387
|
+
"--untracked-files=all",
|
|
1388
|
+
"--",
|
|
1389
|
+
...files.slice(offset, offset + 256).map((file) => `:(literal)${file}`)
|
|
1390
|
+
],
|
|
1150
1391
|
{ cwd: resolvedRoot, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 }
|
|
1151
1392
|
);
|
|
1152
1393
|
const entries = stdout.split("\0");
|
|
@@ -1163,7 +1404,7 @@ async function dirtyDocs(resolvedRoot, files) {
|
|
|
1163
1404
|
}
|
|
1164
1405
|
}
|
|
1165
1406
|
async function discoverDocPaths(root) {
|
|
1166
|
-
let candidates
|
|
1407
|
+
let candidates;
|
|
1167
1408
|
try {
|
|
1168
1409
|
candidates = await auditGitPaths(root, "documents");
|
|
1169
1410
|
} catch (error) {
|
|
@@ -1191,15 +1432,26 @@ async function discoverDocPaths(root) {
|
|
|
1191
1432
|
const paths = [];
|
|
1192
1433
|
const unique = [...new Set(candidates)].sort();
|
|
1193
1434
|
for (let offset = 0; offset < unique.length; offset += 256) {
|
|
1194
|
-
paths.push(
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1435
|
+
paths.push(
|
|
1436
|
+
...await auditGlob(
|
|
1437
|
+
root,
|
|
1438
|
+
unique.slice(offset, offset + 256).map((p) => fg5.escapePath(p)),
|
|
1439
|
+
{
|
|
1440
|
+
ignore: excluded,
|
|
1441
|
+
followSymbolicLinks: false,
|
|
1442
|
+
label: "Documentation discovery"
|
|
1443
|
+
}
|
|
1444
|
+
)
|
|
1445
|
+
);
|
|
1446
|
+
if (paths.length > 1e4)
|
|
1447
|
+
throw new Error(
|
|
1448
|
+
"Documentation discovery exceeds 10,000 relevant files; narrow .mason/config.json ignore patterns."
|
|
1449
|
+
);
|
|
1200
1450
|
}
|
|
1201
1451
|
const priority = (file) => {
|
|
1202
|
-
const index = DOC_CANDIDATES.findIndex(
|
|
1452
|
+
const index = DOC_CANDIDATES.findIndex(
|
|
1453
|
+
(p) => p.toLowerCase() === file.toLowerCase()
|
|
1454
|
+
);
|
|
1203
1455
|
return index < 0 ? DOC_CANDIDATES.length : index;
|
|
1204
1456
|
};
|
|
1205
1457
|
return paths.sort((a, b) => priority(a) - priority(b) || a.localeCompare(b));
|
|
@@ -1214,9 +1466,10 @@ async function localDocPaths(root) {
|
|
|
1214
1466
|
if (error.code === "ENOENT") continue;
|
|
1215
1467
|
throw error;
|
|
1216
1468
|
}
|
|
1217
|
-
for (const entry of entries)
|
|
1218
|
-
|
|
1219
|
-
|
|
1469
|
+
for (const entry of entries)
|
|
1470
|
+
if (/^(?:agents|claude|readme)\.md$/i.test(entry.name)) {
|
|
1471
|
+
paths.push(path10.posix.join(dir, entry.name));
|
|
1472
|
+
}
|
|
1220
1473
|
}
|
|
1221
1474
|
return paths;
|
|
1222
1475
|
}
|
|
@@ -1233,23 +1486,28 @@ async function readDocs(resolvedRoot) {
|
|
|
1233
1486
|
const files = await discoverDocPaths(resolvedRoot);
|
|
1234
1487
|
const dirty = await dirtyDocs(resolvedRoot, files);
|
|
1235
1488
|
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
|
-
|
|
1489
|
+
docs.push(
|
|
1490
|
+
...await Promise.all(
|
|
1491
|
+
files.slice(offset, offset + 4).map(async (candidate) => {
|
|
1492
|
+
const [content, lastCommit] = await Promise.all([
|
|
1493
|
+
readAuditInput(resolvedRoot, candidate),
|
|
1494
|
+
profilePhase("audit.document-history", () => lastCommitOf(resolvedRoot, candidate))
|
|
1495
|
+
]);
|
|
1496
|
+
if (content === null)
|
|
1497
|
+
throw new Error("Document disappeared during discovery: " + candidate);
|
|
1498
|
+
return {
|
|
1499
|
+
path: candidate,
|
|
1500
|
+
content,
|
|
1501
|
+
scope: documentScope(candidate),
|
|
1502
|
+
kind: /^readme\.md$/i.test(path10.posix.basename(candidate)) ? "readme" : /^(agents|claude)\.md$/i.test(path10.posix.basename(candidate)) ? "instructions" : "guide",
|
|
1503
|
+
lineCount: content.split("\n").length,
|
|
1504
|
+
lastCommit,
|
|
1505
|
+
dirty: dirty.has(candidate),
|
|
1506
|
+
claims: extractClaims(content)
|
|
1507
|
+
};
|
|
1508
|
+
})
|
|
1509
|
+
)
|
|
1510
|
+
);
|
|
1253
1511
|
}
|
|
1254
1512
|
return docs;
|
|
1255
1513
|
}
|
|
@@ -1264,11 +1522,7 @@ var init_docs = __esm({
|
|
|
1264
1522
|
init_inputs();
|
|
1265
1523
|
init_files();
|
|
1266
1524
|
init_inspection();
|
|
1267
|
-
DOC_CANDIDATES = [
|
|
1268
|
-
"AGENTS.md",
|
|
1269
|
-
"CLAUDE.md",
|
|
1270
|
-
".claude/CLAUDE.md"
|
|
1271
|
-
];
|
|
1525
|
+
DOC_CANDIDATES = ["AGENTS.md", "CLAUDE.md", ".claude/CLAUDE.md"];
|
|
1272
1526
|
}
|
|
1273
1527
|
});
|
|
1274
1528
|
|
|
@@ -1276,7 +1530,8 @@ var init_docs = __esm({
|
|
|
1276
1530
|
import fs7 from "fs/promises";
|
|
1277
1531
|
import path11 from "path";
|
|
1278
1532
|
function scopedPath(directory, value) {
|
|
1279
|
-
if (path11.posix.isAbsolute(value) || value.includes("\\") || /[\x00-\x1f]/.test(value))
|
|
1533
|
+
if (path11.posix.isAbsolute(value) || value.includes("\\") || /[\x00-\x1f]/.test(value))
|
|
1534
|
+
return null;
|
|
1280
1535
|
const resolved = path11.posix.normalize(path11.posix.join(directory, value));
|
|
1281
1536
|
return resolved === ".." || resolved.startsWith("../") ? null : resolved;
|
|
1282
1537
|
}
|
|
@@ -1320,7 +1575,9 @@ async function checkDeletedReferences(ctx) {
|
|
|
1320
1575
|
const result = emptyResult();
|
|
1321
1576
|
for (const doc of ctx.docs) {
|
|
1322
1577
|
const changes = ctx.changesSinceDoc.get(doc.path);
|
|
1323
|
-
const renames = new Map(
|
|
1578
|
+
const renames = new Map(
|
|
1579
|
+
(changes ?? []).filter((c) => c.status === "renamed" && c.previousPath).map((c) => [c.previousPath, c.path])
|
|
1580
|
+
);
|
|
1324
1581
|
for (const claim of doc.claims.paths) {
|
|
1325
1582
|
const scope = pathClaimScope(doc.path, claim);
|
|
1326
1583
|
if (!scope) {
|
|
@@ -1340,12 +1597,17 @@ async function checkDeletedReferences(ctx) {
|
|
|
1340
1597
|
});
|
|
1341
1598
|
continue;
|
|
1342
1599
|
}
|
|
1343
|
-
if ((await Promise.all(scope.candidates.map((file) => pathExists(ctx.root, file)))).some(
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1600
|
+
if ((await Promise.all(scope.candidates.map((file) => pathExists(ctx.root, file)))).some(
|
|
1601
|
+
Boolean
|
|
1602
|
+
))
|
|
1603
|
+
continue;
|
|
1604
|
+
const candidates = await Promise.all(
|
|
1605
|
+
scope.candidates.map(async (file) => ({
|
|
1606
|
+
file,
|
|
1607
|
+
tracked: await lastCommitOf(ctx.root, file),
|
|
1608
|
+
parent: await pathExists(ctx.root, path12.posix.dirname(file))
|
|
1609
|
+
}))
|
|
1610
|
+
);
|
|
1349
1611
|
const candidate = candidates.find((c) => c.tracked || renames.has(c.file)) ?? candidates.find((c) => c.parent);
|
|
1350
1612
|
if (!candidate && claim.relativeTo !== "document") continue;
|
|
1351
1613
|
const resolvedPath = candidate?.file ?? scope.candidates[0];
|
|
@@ -1364,9 +1626,21 @@ async function checkDeletedReferences(ctx) {
|
|
|
1364
1626
|
const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
|
|
1365
1627
|
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
1628
|
if (scope.basis !== "document" && (renamedTo || candidate?.tracked)) {
|
|
1367
|
-
result.issues.push({
|
|
1629
|
+
result.issues.push({
|
|
1630
|
+
type: "deleted-reference",
|
|
1631
|
+
message,
|
|
1632
|
+
anchor,
|
|
1633
|
+
confidence: "certain",
|
|
1634
|
+
evidence
|
|
1635
|
+
});
|
|
1368
1636
|
} else {
|
|
1369
|
-
result.advisories.push({
|
|
1637
|
+
result.advisories.push({
|
|
1638
|
+
resolution: "recheck",
|
|
1639
|
+
type: "deleted-reference",
|
|
1640
|
+
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."),
|
|
1641
|
+
anchor,
|
|
1642
|
+
evidence
|
|
1643
|
+
});
|
|
1370
1644
|
}
|
|
1371
1645
|
}
|
|
1372
1646
|
}
|
|
@@ -1389,10 +1663,7 @@ function escapeRegExp(text2) {
|
|
|
1389
1663
|
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1390
1664
|
}
|
|
1391
1665
|
function isMentioned(combinedDocs, name) {
|
|
1392
|
-
const re = new RegExp(
|
|
1393
|
-
`(^|[^A-Za-z0-9_-])${escapeRegExp(name)}(/|[^A-Za-z0-9_-]|$)`,
|
|
1394
|
-
"im"
|
|
1395
|
-
);
|
|
1666
|
+
const re = new RegExp(`(^|[^A-Za-z0-9_-])${escapeRegExp(name)}(/|[^A-Za-z0-9_-]|$)`, "im");
|
|
1396
1667
|
return re.test(combinedDocs);
|
|
1397
1668
|
}
|
|
1398
1669
|
async function checkNewModules(ctx) {
|
|
@@ -1422,34 +1693,57 @@ async function checkNewModules(ctx) {
|
|
|
1422
1693
|
return result;
|
|
1423
1694
|
}
|
|
1424
1695
|
function moduleDocumentation(docs) {
|
|
1425
|
-
return docs.map(
|
|
1696
|
+
return docs.map(
|
|
1697
|
+
(doc) => doc.content + "\n" + (path13.posix.dirname(doc.path) === "." ? "" : path13.posix.dirname(doc.path) + "/")
|
|
1698
|
+
).join("\n");
|
|
1426
1699
|
}
|
|
1427
1700
|
function moduleCandidates(root, combinedDocs) {
|
|
1428
|
-
return inspectionRead(
|
|
1701
|
+
return inspectionRead(
|
|
1702
|
+
root,
|
|
1703
|
+
"modules:" + combinedDocs,
|
|
1704
|
+
() => collectModuleCandidates(root, combinedDocs)
|
|
1705
|
+
);
|
|
1429
1706
|
}
|
|
1430
1707
|
async function collectModuleCandidates(root, combinedDocs) {
|
|
1431
1708
|
const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
|
|
1432
1709
|
const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
|
|
1433
|
-
const sources = sourcePaths.filter(
|
|
1710
|
+
const sources = sourcePaths.filter(
|
|
1711
|
+
(file) => !file.split("/").some((part) => part.startsWith("."))
|
|
1712
|
+
);
|
|
1434
1713
|
const listSubdirs = async (parents) => {
|
|
1435
1714
|
const byParent = /* @__PURE__ */ new Map();
|
|
1436
1715
|
for (let offset = 0; offset < parents.length; offset += 16) {
|
|
1437
|
-
await Promise.all(
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1716
|
+
await Promise.all(
|
|
1717
|
+
parents.slice(offset, offset + 16).map(async (dir) => {
|
|
1718
|
+
const entries = await fs8.readdir(await auditInputPath(root, dir), {
|
|
1719
|
+
withFileTypes: true
|
|
1720
|
+
});
|
|
1721
|
+
const dirs = entries.filter(
|
|
1722
|
+
(entry) => (entry.isDirectory() || entry.isSymbolicLink() && sources.some(
|
|
1723
|
+
(file) => file.startsWith(path13.posix.join(dir, entry.name) + "/")
|
|
1724
|
+
)) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)
|
|
1725
|
+
).map((entry) => path13.posix.join(dir, entry.name));
|
|
1726
|
+
byParent.set(dir, dirs);
|
|
1727
|
+
})
|
|
1728
|
+
);
|
|
1442
1729
|
}
|
|
1443
1730
|
const ignored = await gitIgnoredPaths(root, [...byParent.values()].flat());
|
|
1444
1731
|
const result = /* @__PURE__ */ new Map();
|
|
1445
1732
|
for (const dir of parents) {
|
|
1446
|
-
const visible = new Set(
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1733
|
+
const visible = new Set(
|
|
1734
|
+
byParent.get(dir).filter(
|
|
1735
|
+
(candidate) => !ignored.has(candidate) || sources.some((file) => file.startsWith(candidate + "/"))
|
|
1736
|
+
)
|
|
1737
|
+
);
|
|
1738
|
+
result.set(
|
|
1739
|
+
dir,
|
|
1740
|
+
(await auditGlob(root, dir === "." ? "*" : `${fg6.escapePath(dir)}/*`, {
|
|
1741
|
+
ignore,
|
|
1742
|
+
onlyDirectories: true,
|
|
1743
|
+
label: "Module directory discovery",
|
|
1744
|
+
select: (file) => visible.has(file)
|
|
1745
|
+
})).map((file) => path13.posix.basename(file))
|
|
1746
|
+
);
|
|
1453
1747
|
}
|
|
1454
1748
|
return result;
|
|
1455
1749
|
};
|
|
@@ -1457,12 +1751,19 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1457
1751
|
const files = sources.filter((file) => file.startsWith(dir + "/"));
|
|
1458
1752
|
let count3 = 0;
|
|
1459
1753
|
for (let offset = 0; offset < files.length; offset += 256) {
|
|
1460
|
-
count3 += (await auditGlob(
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1754
|
+
count3 += (await auditGlob(
|
|
1755
|
+
root,
|
|
1756
|
+
files.slice(offset, offset + 256).map((file) => fg6.escapePath(file)),
|
|
1757
|
+
{
|
|
1758
|
+
ignore,
|
|
1759
|
+
followSymbolicLinks: false,
|
|
1760
|
+
label: `Source discovery in ${dir}`
|
|
1761
|
+
}
|
|
1762
|
+
)).length;
|
|
1763
|
+
if (count3 > 1e5)
|
|
1764
|
+
throw new Error(
|
|
1765
|
+
`Source discovery in ${dir} exceeds 100,000 relevant files. Exclude generated source with .mason/config.json ignore patterns or narrow this audit's checks.`
|
|
1766
|
+
);
|
|
1466
1767
|
}
|
|
1467
1768
|
return count3;
|
|
1468
1769
|
};
|
|
@@ -1545,7 +1846,10 @@ async function countNpmWorkspaces(root) {
|
|
|
1545
1846
|
let globs = [];
|
|
1546
1847
|
try {
|
|
1547
1848
|
const pkg = JSON.parse(pkgRaw);
|
|
1548
|
-
|
|
1849
|
+
if (!isRecord(pkg)) return null;
|
|
1850
|
+
const workspaces = isRecord(pkg.workspaces) ? pkg.workspaces.packages : pkg.workspaces;
|
|
1851
|
+
if (workspaces !== void 0 && !isStringArray(workspaces)) return null;
|
|
1852
|
+
globs = workspaces ?? [];
|
|
1549
1853
|
} catch {
|
|
1550
1854
|
}
|
|
1551
1855
|
if (globs.length > 0) {
|
|
@@ -1599,9 +1903,7 @@ async function countCargoCrates(root) {
|
|
|
1599
1903
|
if (content === null) return null;
|
|
1600
1904
|
const membersBlock = content.match(/members\s*=\s*\[([\s\S]*?)\]/);
|
|
1601
1905
|
if (!membersBlock) return null;
|
|
1602
|
-
const entries = [...membersBlock[1].matchAll(/["']([^"']+)["']/g)].map(
|
|
1603
|
-
(m) => m[1]
|
|
1604
|
-
);
|
|
1906
|
+
const entries = [...membersBlock[1].matchAll(/["']([^"']+)["']/g)].map((m) => m[1]);
|
|
1605
1907
|
if (entries.length === 0) return null;
|
|
1606
1908
|
const members = /* @__PURE__ */ new Set();
|
|
1607
1909
|
for (const entry of entries) {
|
|
@@ -1675,6 +1977,7 @@ var MEMBERS_CAP;
|
|
|
1675
1977
|
var init_stale_count = __esm({
|
|
1676
1978
|
"src/audit/checks/stale-count.ts"() {
|
|
1677
1979
|
"use strict";
|
|
1980
|
+
init_validation();
|
|
1678
1981
|
init_docs();
|
|
1679
1982
|
init_inputs();
|
|
1680
1983
|
init_checks();
|
|
@@ -1687,9 +1990,9 @@ import path15 from "path";
|
|
|
1687
1990
|
function scriptsOf(raw) {
|
|
1688
1991
|
try {
|
|
1689
1992
|
const pkg = JSON.parse(raw);
|
|
1690
|
-
if (!
|
|
1993
|
+
if (!isRecord(pkg)) return null;
|
|
1691
1994
|
if (pkg.scripts === void 0) return [];
|
|
1692
|
-
return
|
|
1995
|
+
return isRecord(pkg.scripts) && Object.values(pkg.scripts).every((value) => typeof value === "string") ? Object.keys(pkg.scripts) : null;
|
|
1693
1996
|
} catch {
|
|
1694
1997
|
return null;
|
|
1695
1998
|
}
|
|
@@ -1703,50 +2006,60 @@ async function commandInputs(root, docs) {
|
|
|
1703
2006
|
await read("package.json");
|
|
1704
2007
|
let inventory;
|
|
1705
2008
|
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
|
-
|
|
2009
|
+
for (const doc of docs)
|
|
2010
|
+
for (const claim of doc.claims.commands) {
|
|
2011
|
+
const base = documentScope(doc.path);
|
|
2012
|
+
const directory = scopedPath(base, claim.directory ?? ".");
|
|
2013
|
+
const scope = {
|
|
2014
|
+
basis: claim.directory === void 0 ? "document" : "explicit",
|
|
2015
|
+
directory: directory ?? base,
|
|
2016
|
+
candidates: []
|
|
2017
|
+
};
|
|
2018
|
+
const resolution = {
|
|
2019
|
+
doc: doc.path,
|
|
2020
|
+
claim,
|
|
2021
|
+
scope,
|
|
2022
|
+
manifest: null,
|
|
2023
|
+
scripts: [],
|
|
2024
|
+
elsewhere: []
|
|
2025
|
+
};
|
|
2026
|
+
resolutions.push(resolution);
|
|
2027
|
+
if (claim.scopeUnknown || directory === null) {
|
|
2028
|
+
resolution.problem = claim.scopeUnknown ?? "The command's directory leaves the repository.";
|
|
2029
|
+
continue;
|
|
2030
|
+
}
|
|
2031
|
+
let cursor = directory;
|
|
2032
|
+
while (true) {
|
|
2033
|
+
const manifest = path15.posix.join(cursor, "package.json");
|
|
2034
|
+
scope.candidates.push(manifest);
|
|
2035
|
+
const raw = await read(manifest);
|
|
2036
|
+
if (raw !== null) {
|
|
2037
|
+
resolution.manifest = manifest;
|
|
2038
|
+
const scripts = scriptsOf(raw);
|
|
2039
|
+
if (scripts === null) resolution.problem = "Invalid package manifest: " + manifest;
|
|
2040
|
+
else resolution.scripts = scripts;
|
|
2041
|
+
break;
|
|
2042
|
+
}
|
|
2043
|
+
if (claim.directory !== void 0 || cursor === ".") break;
|
|
2044
|
+
cursor = path15.posix.dirname(cursor);
|
|
2045
|
+
}
|
|
2046
|
+
if (!resolution.manifest && !resolution.problem) {
|
|
2047
|
+
resolution.problem = `No package.json for the command's scope (${directory}).`;
|
|
2048
|
+
}
|
|
2049
|
+
if (resolution.problem || resolution.scripts.includes(claim.scriptName) || claim.directory !== void 0)
|
|
2050
|
+
continue;
|
|
2051
|
+
inventory ??= await commandManifests(root);
|
|
2052
|
+
for (const manifest of [.../* @__PURE__ */ new Set(["package.json", ...inventory])].sort()) {
|
|
2053
|
+
if (scope.candidates.includes(manifest)) continue;
|
|
2054
|
+
scope.candidates.push(manifest);
|
|
2055
|
+
const raw = await read(manifest);
|
|
2056
|
+
if (raw === null) continue;
|
|
1727
2057
|
const scripts = scriptsOf(raw);
|
|
1728
|
-
if (scripts === null)
|
|
1729
|
-
|
|
1730
|
-
|
|
2058
|
+
if (scripts === null)
|
|
2059
|
+
resolution.problem = "Cannot establish script availability: invalid manifest " + manifest;
|
|
2060
|
+
else if (scripts.includes(claim.scriptName)) resolution.elsewhere.push(manifest);
|
|
1731
2061
|
}
|
|
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
2062
|
}
|
|
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
|
-
}
|
|
1749
|
-
}
|
|
1750
2063
|
return { manifests: [...contents].sort(([a], [b]) => a.localeCompare(b)), resolutions };
|
|
1751
2064
|
}
|
|
1752
2065
|
async function checkDeadCommands(ctx) {
|
|
@@ -1769,14 +2082,34 @@ async function checkDeadCommands(ctx) {
|
|
|
1769
2082
|
const anchor = { doc, line: claim.line, excerpt: claim.excerpt };
|
|
1770
2083
|
const message = `\`${claim.invocation}\` names script "${claim.scriptName}", which is absent from ${manifest}`;
|
|
1771
2084
|
const certain = documentScope(doc) === "." && (scope.basis === "explicit" || !elsewhere.length);
|
|
1772
|
-
if (certain)
|
|
1773
|
-
|
|
2085
|
+
if (certain)
|
|
2086
|
+
result.issues.push({
|
|
2087
|
+
type: "dead-command",
|
|
2088
|
+
message,
|
|
2089
|
+
anchor,
|
|
2090
|
+
confidence: "certain",
|
|
2091
|
+
evidence
|
|
2092
|
+
});
|
|
2093
|
+
else
|
|
2094
|
+
result.advisories.push({
|
|
2095
|
+
resolution: "recheck",
|
|
2096
|
+
type: "dead-command",
|
|
2097
|
+
message: message + (elsewhere.length ? `; it exists in ${elsewhere.join(", ")}. Clarify the intended working directory.` : "; the intended working directory needs review."),
|
|
2098
|
+
anchor,
|
|
2099
|
+
evidence
|
|
2100
|
+
});
|
|
1774
2101
|
}
|
|
1775
2102
|
return result;
|
|
1776
2103
|
}
|
|
1777
2104
|
function commandManifests(root) {
|
|
1778
2105
|
return auditGlob(root, "**/package.json", {
|
|
1779
|
-
ignore: [
|
|
2106
|
+
ignore: [
|
|
2107
|
+
"**/node_modules/**",
|
|
2108
|
+
"**/dist/**",
|
|
2109
|
+
"**/build/**",
|
|
2110
|
+
".mason/reports/**",
|
|
2111
|
+
"package.json"
|
|
2112
|
+
],
|
|
1780
2113
|
label: "Command manifest discovery"
|
|
1781
2114
|
});
|
|
1782
2115
|
}
|
|
@@ -1784,6 +2117,7 @@ var AVAILABLE_SCRIPTS_CAP;
|
|
|
1784
2117
|
var init_dead_command = __esm({
|
|
1785
2118
|
"src/audit/checks/dead-command.ts"() {
|
|
1786
2119
|
"use strict";
|
|
2120
|
+
init_validation();
|
|
1787
2121
|
init_inputs();
|
|
1788
2122
|
init_docs();
|
|
1789
2123
|
init_scope();
|
|
@@ -1796,33 +2130,60 @@ var init_dead_command = __esm({
|
|
|
1796
2130
|
import path16 from "path";
|
|
1797
2131
|
import { parse as parseToml } from "smol-toml";
|
|
1798
2132
|
function dependencyText(content) {
|
|
1799
|
-
return content.replace(
|
|
2133
|
+
return content.replace(
|
|
2134
|
+
/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g,
|
|
2135
|
+
(text2) => text2.replace(/[^\n]/g, " ")
|
|
2136
|
+
).replace(/<!--[\s\S]*?-->/g, (text2) => text2.replace(/[^\n]/g, " "));
|
|
1800
2137
|
}
|
|
1801
2138
|
function declarations(file, source) {
|
|
1802
2139
|
const result = /* @__PURE__ */ new Map();
|
|
1803
2140
|
if (!source) return result;
|
|
1804
2141
|
if (path16.posix.basename(file) === "package.json") {
|
|
1805
2142
|
const json = JSON.parse(source);
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
2143
|
+
if (!isRecord(json)) throw new Error("Invalid package manifest");
|
|
2144
|
+
for (const section of [
|
|
2145
|
+
"dependencies",
|
|
2146
|
+
"devDependencies",
|
|
2147
|
+
"peerDependencies",
|
|
2148
|
+
"optionalDependencies",
|
|
2149
|
+
"engines"
|
|
2150
|
+
]) {
|
|
2151
|
+
const entries = json[section];
|
|
2152
|
+
if (entries === void 0) continue;
|
|
2153
|
+
if (!isRecord(entries) || !Object.values(entries).every((value) => typeof value === "string"))
|
|
2154
|
+
throw new Error("Invalid dependency declarations: " + section);
|
|
2155
|
+
for (const [name, value] of Object.entries(entries)) {
|
|
2156
|
+
if (typeof value === "string")
|
|
2157
|
+
result.set(name, [...result.has(name) ? [result.get(name)] : [], value].join("; "));
|
|
1809
2158
|
}
|
|
1810
2159
|
}
|
|
1811
2160
|
} else if (file.endsWith("libs.versions.toml")) {
|
|
1812
2161
|
const catalog = parseToml(source);
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
2162
|
+
const versions = catalog.versions;
|
|
2163
|
+
for (const section of ["libraries", "plugins"]) {
|
|
2164
|
+
const entries = catalog[section];
|
|
2165
|
+
if (entries === void 0) continue;
|
|
2166
|
+
if (!isRecord(entries)) throw new Error("Invalid version catalog: " + section);
|
|
2167
|
+
for (const entry of Object.values(entries)) {
|
|
2168
|
+
if (typeof entry === "string") {
|
|
2169
|
+
const parts = entry.split(":");
|
|
2170
|
+
if (parts.length >= 3) result.set(parts.slice(0, -1).join(":"), parts.at(-1));
|
|
2171
|
+
} else if (isRecord(entry)) {
|
|
2172
|
+
const name = entry.module ?? (typeof entry.group === "string" && typeof entry.name === "string" ? `${entry.group}:${entry.name}` : entry.id);
|
|
2173
|
+
const version = typeof entry.version === "string" ? entry.version : isRecord(entry.version) && typeof entry.version.ref === "string" && isRecord(versions) ? versions[entry.version.ref] : void 0;
|
|
2174
|
+
if (typeof name === "string")
|
|
2175
|
+
result.set(name, typeof version === "string" ? version : "unspecified");
|
|
2176
|
+
else throw new Error("Invalid version catalog entry");
|
|
2177
|
+
} else throw new Error("Invalid version catalog entry");
|
|
1821
2178
|
}
|
|
1822
2179
|
}
|
|
1823
2180
|
} else if (/build\.gradle(?:\.kts)?$/.test(file)) {
|
|
1824
|
-
for (const match of source.matchAll(/["']([\w.-]+:[\w.-]+):([^"'\s]+)["']/g))
|
|
1825
|
-
|
|
2181
|
+
for (const match of source.matchAll(/["']([\w.-]+:[\w.-]+):([^"'\s]+)["']/g))
|
|
2182
|
+
result.set(match[1], match[2]);
|
|
2183
|
+
for (const match of source.matchAll(
|
|
2184
|
+
/\bid\s*\(?\s*["']([\w.-]+)["']\s*\)?\s*version\s*["']([^"']+)["']/g
|
|
2185
|
+
))
|
|
2186
|
+
result.set(match[1], match[2]);
|
|
1826
2187
|
}
|
|
1827
2188
|
return result;
|
|
1828
2189
|
}
|
|
@@ -1837,17 +2198,28 @@ async function matchDependencyChanges(root, from, head, files, content) {
|
|
|
1837
2198
|
}
|
|
1838
2199
|
try {
|
|
1839
2200
|
const read = async (revision) => {
|
|
1840
|
-
const { stdout: entry } = await inspectionGit(
|
|
2201
|
+
const { stdout: entry } = await inspectionGit(
|
|
2202
|
+
["ls-tree", revision, "--", `:(literal)${file}`],
|
|
2203
|
+
{ cwd: root, timeout: 1e4 }
|
|
2204
|
+
);
|
|
1841
2205
|
if (!entry) return "";
|
|
1842
|
-
if (!entry.startsWith("100644 ") && !entry.startsWith("100755 "))
|
|
1843
|
-
|
|
2206
|
+
if (!entry.startsWith("100644 ") && !entry.startsWith("100755 "))
|
|
2207
|
+
throw new Error("Unsupported manifest mode");
|
|
2208
|
+
return (await inspectionGit(["show", `${revision}:${file}`], {
|
|
2209
|
+
cwd: root,
|
|
2210
|
+
maxBuffer: 1024 * 1024,
|
|
2211
|
+
timeout: 1e4
|
|
2212
|
+
})).stdout;
|
|
1844
2213
|
};
|
|
1845
2214
|
const [before, after] = await Promise.all([read(from), read(head)]);
|
|
1846
2215
|
const old = declarations(file, before), next = declarations(file, after);
|
|
1847
2216
|
for (const dependency of /* @__PURE__ */ new Set([...old.keys(), ...next.keys()])) {
|
|
1848
2217
|
if (old.get(dependency) === next.get(dependency)) continue;
|
|
1849
2218
|
const escaped = dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1850
|
-
const mention = new RegExp(
|
|
2219
|
+
const mention = new RegExp(
|
|
2220
|
+
`(^|[^a-zA-Z0-9_@./-])${escaped}(?![a-zA-Z0-9_/-]|\\.[a-zA-Z0-9_])`,
|
|
2221
|
+
"i"
|
|
2222
|
+
);
|
|
1851
2223
|
const line = lines.findIndex((text2) => mention.test(text2));
|
|
1852
2224
|
if (line < 0) continue;
|
|
1853
2225
|
matches.push({
|
|
@@ -1869,6 +2241,7 @@ async function matchDependencyChanges(root, from, head, files, content) {
|
|
|
1869
2241
|
var init_dependency_claims = __esm({
|
|
1870
2242
|
"src/audit/dependency-claims.ts"() {
|
|
1871
2243
|
"use strict";
|
|
2244
|
+
init_validation();
|
|
1872
2245
|
init_inspection();
|
|
1873
2246
|
}
|
|
1874
2247
|
});
|
|
@@ -1878,15 +2251,23 @@ async function git(root, args) {
|
|
|
1878
2251
|
return (await execGit(args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
|
|
1879
2252
|
}
|
|
1880
2253
|
function withoutAndroidReleaseValues(text2) {
|
|
1881
|
-
if (/\/\*|"""|'''/.test(text2) || !/id\s*\(?\s*["']com\.android\.(application|library)["']/.test(text2))
|
|
2254
|
+
if (/\/\*|"""|'''/.test(text2) || !/id\s*\(?\s*["']com\.android\.(application|library)["']/.test(text2))
|
|
2255
|
+
return null;
|
|
1882
2256
|
const scopes2 = [];
|
|
1883
2257
|
const normalized = [];
|
|
1884
2258
|
let assignments = 0;
|
|
1885
2259
|
for (const line of text2.split("\n")) {
|
|
1886
|
-
const code = line.replace(
|
|
1887
|
-
|
|
2260
|
+
const code = line.replace(
|
|
2261
|
+
/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/.*$/g,
|
|
2262
|
+
(token) => token.startsWith("//") ? "" : '""'
|
|
2263
|
+
);
|
|
2264
|
+
const assignment = line.match(
|
|
2265
|
+
/^(\s*)(versionName|versionCode)(\s*(?:=\s*|\s+))("[A-Za-z0-9._+-]+"|'[A-Za-z0-9._+-]+'|\d+)(\s*)$/
|
|
2266
|
+
);
|
|
1888
2267
|
if (assignment && scopes2.join("/") === "android/defaultConfig" && (assignment[2] === "versionCode" ? /^\d+$/.test(assignment[4]) : /^["']/.test(assignment[4]))) {
|
|
1889
|
-
normalized.push(
|
|
2268
|
+
normalized.push(
|
|
2269
|
+
assignment[1] + assignment[2] + assignment[3] + "<release-value>" + assignment[5]
|
|
2270
|
+
);
|
|
1890
2271
|
assignments++;
|
|
1891
2272
|
} else {
|
|
1892
2273
|
if (/\bversion(?:Name|Code)\b/.test(line)) return null;
|
|
@@ -1902,12 +2283,24 @@ function withoutAndroidReleaseValues(text2) {
|
|
|
1902
2283
|
return assignments && !scopes2.length ? normalized.join("\n") : null;
|
|
1903
2284
|
}
|
|
1904
2285
|
async function releaseMetadataOnly(root, commit) {
|
|
1905
|
-
if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file)))
|
|
2286
|
+
if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file)))
|
|
2287
|
+
return false;
|
|
1906
2288
|
try {
|
|
1907
2289
|
const parents = (await git(root, ["rev-list", "--parents", "-n", "1", commit.hash])).trim().split(/\s+/);
|
|
1908
2290
|
if (parents.length !== 2) return false;
|
|
1909
2291
|
for (const file of commit.files) {
|
|
1910
|
-
const raw = await git(root, [
|
|
2292
|
+
const raw = await git(root, [
|
|
2293
|
+
"diff",
|
|
2294
|
+
"--raw",
|
|
2295
|
+
"-z",
|
|
2296
|
+
"--no-renames",
|
|
2297
|
+
"--no-ext-diff",
|
|
2298
|
+
"--no-textconv",
|
|
2299
|
+
parents[1],
|
|
2300
|
+
commit.hash,
|
|
2301
|
+
"--",
|
|
2302
|
+
file
|
|
2303
|
+
]);
|
|
1911
2304
|
if (!/^:(100644|100755) \1 [a-f0-9]+ [a-f0-9]+ M\0/.test(raw)) return false;
|
|
1912
2305
|
const [before, after] = await Promise.all([
|
|
1913
2306
|
git(root, ["show", parents[1] + ":" + file]),
|
|
@@ -1931,13 +2324,21 @@ var init_release_metadata = __esm({
|
|
|
1931
2324
|
// src/audit/checks/deps-changed.ts
|
|
1932
2325
|
function hasDependencyContent(content) {
|
|
1933
2326
|
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(
|
|
2327
|
+
return /\b(?:dependenc(?:y|ies)|librar(?:y|ies)|frameworks?|tech(?:nology)? stack|prerequisites|requirements)\b/i.test(
|
|
2328
|
+
text2
|
|
2329
|
+
) || /\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(
|
|
2330
|
+
text2
|
|
2331
|
+
) || /\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(
|
|
2332
|
+
text2
|
|
2333
|
+
) || /\b(?:npm|pnpm|yarn|pip3?|cargo|gem|composer)\s+(?:install|add|require)\b/i.test(text2);
|
|
1935
2334
|
}
|
|
1936
2335
|
function manifestPathspecs(doc) {
|
|
1937
2336
|
const scope = documentScope(doc);
|
|
1938
2337
|
if (scope === ".") return MANIFEST_PATHSPECS;
|
|
1939
|
-
const escaped = scope.replace(/[
|
|
1940
|
-
return MANIFEST_PATHSPECS.map(
|
|
2338
|
+
const escaped = scope.replace(/[\\*?[\]]/g, "\\$&");
|
|
2339
|
+
return MANIFEST_PATHSPECS.map(
|
|
2340
|
+
(spec) => spec.startsWith(":(glob)") ? ":(glob)" + escaped + "/" + spec.slice(":(glob)".length) : ":(literal)" + scope + "/" + spec
|
|
2341
|
+
);
|
|
1941
2342
|
}
|
|
1942
2343
|
async function checkDepsChanged(ctx) {
|
|
1943
2344
|
const result = emptyResult();
|
|
@@ -1955,12 +2356,17 @@ async function checkDepsChanged(ctx) {
|
|
|
1955
2356
|
let content = doc.content;
|
|
1956
2357
|
if (doc.dirty) {
|
|
1957
2358
|
try {
|
|
1958
|
-
content = (await inspectionGit(
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
2359
|
+
content = (await inspectionGit(["show", `${doc.lastCommit.hash}:${doc.path}`], {
|
|
2360
|
+
cwd: ctx.root,
|
|
2361
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
2362
|
+
timeout: 1e4
|
|
2363
|
+
})).stdout;
|
|
1962
2364
|
} catch {
|
|
1963
|
-
result.skipped.push({
|
|
2365
|
+
result.skipped.push({
|
|
2366
|
+
check: "deps-changed",
|
|
2367
|
+
doc: doc.path,
|
|
2368
|
+
reason: `${doc.path}: committed dependency content is unavailable`
|
|
2369
|
+
});
|
|
1964
2370
|
continue;
|
|
1965
2371
|
}
|
|
1966
2372
|
}
|
|
@@ -1996,11 +2402,12 @@ async function checkDepsChanged(ctx) {
|
|
|
1996
2402
|
);
|
|
1997
2403
|
if (!relevance.matches.length && !hasDependencyContent(content)) continue;
|
|
1998
2404
|
const match = relevance.matches[0];
|
|
1999
|
-
if (doc.dirty && match)
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2405
|
+
if (doc.dirty && match)
|
|
2406
|
+
result.skipped.push({
|
|
2407
|
+
check: "deps-changed",
|
|
2408
|
+
doc: doc.path,
|
|
2409
|
+
reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
|
|
2410
|
+
});
|
|
2004
2411
|
(doc.dirty ? result.suppressedAdvisories : result.advisories).push({
|
|
2005
2412
|
type: "deps-changed",
|
|
2006
2413
|
...match ? {} : { resolution: "informational" },
|
|
@@ -2046,6 +2453,143 @@ var init_deps_changed = __esm({
|
|
|
2046
2453
|
}
|
|
2047
2454
|
});
|
|
2048
2455
|
|
|
2456
|
+
// src/decisions/anchors.ts
|
|
2457
|
+
import { createHash } from "crypto";
|
|
2458
|
+
import fs9 from "fs/promises";
|
|
2459
|
+
import { constants as constants2 } from "fs";
|
|
2460
|
+
import { z as z3 } from "zod";
|
|
2461
|
+
import fg7 from "fast-glob";
|
|
2462
|
+
async function captureAnchorScopes(root, scopes2) {
|
|
2463
|
+
const union = [...new Set(scopes2.flat())].sort();
|
|
2464
|
+
let context;
|
|
2465
|
+
const prepareContext = async () => {
|
|
2466
|
+
const access = await inspectionRead(
|
|
2467
|
+
root,
|
|
2468
|
+
"decision-anchor-access",
|
|
2469
|
+
() => createFileAccess(root)
|
|
2470
|
+
);
|
|
2471
|
+
const inventory = access.inventory();
|
|
2472
|
+
if (!inventory) throw new Error("Git anchor inventory unavailable");
|
|
2473
|
+
const ignored = await inspectionGit(
|
|
2474
|
+
[
|
|
2475
|
+
"ls-files",
|
|
2476
|
+
"-z",
|
|
2477
|
+
"--others",
|
|
2478
|
+
"--ignored",
|
|
2479
|
+
"--exclude-standard",
|
|
2480
|
+
"--",
|
|
2481
|
+
...union.map((anchor) => `:(literal)${anchor}`)
|
|
2482
|
+
],
|
|
2483
|
+
{ cwd: root, maxBuffer: 8 * 1024 * 1024 }
|
|
2484
|
+
);
|
|
2485
|
+
const files = [
|
|
2486
|
+
.../* @__PURE__ */ new Set([...inventory, ...ignored.stdout.split("\0").filter(Boolean)])
|
|
2487
|
+
].sort();
|
|
2488
|
+
const patterns = await Promise.all(
|
|
2489
|
+
union.map(async (anchor) => {
|
|
2490
|
+
const literal2 = fg7.escapePath(anchor);
|
|
2491
|
+
try {
|
|
2492
|
+
return (await fs9.lstat(await storePath(root, anchor))).isDirectory() ? `${literal2}/**/*` : literal2;
|
|
2493
|
+
} catch {
|
|
2494
|
+
return literal2;
|
|
2495
|
+
}
|
|
2496
|
+
})
|
|
2497
|
+
);
|
|
2498
|
+
const eligible = new Set(await access.list(patterns, { dot: true }));
|
|
2499
|
+
const indexed = indexMatchingPaths(scopes2, files);
|
|
2500
|
+
const filesByScope = new Map(
|
|
2501
|
+
scopes2.map((scope, index) => [JSON.stringify([...new Set(scope)].sort()), indexed[index]])
|
|
2502
|
+
);
|
|
2503
|
+
return { filesByScope, eligible };
|
|
2504
|
+
};
|
|
2505
|
+
return Promise.all(
|
|
2506
|
+
scopes2.map((scope) => {
|
|
2507
|
+
const anchors = [...new Set(scope)].sort();
|
|
2508
|
+
return inspectionRead(
|
|
2509
|
+
root,
|
|
2510
|
+
"decision-anchor-capture:" + JSON.stringify(anchors),
|
|
2511
|
+
async () => {
|
|
2512
|
+
const capture = { anchors, files: {}, complete: true };
|
|
2513
|
+
if (!anchors.length) return capture;
|
|
2514
|
+
try {
|
|
2515
|
+
const { eligible, filesByScope } = await (context ??= prepareContext());
|
|
2516
|
+
const files = filesByScope.get(JSON.stringify(anchors));
|
|
2517
|
+
if (files.length > 2e3) return { ...capture, complete: false };
|
|
2518
|
+
for (const file of files) {
|
|
2519
|
+
const evidence = await inspectionRead(
|
|
2520
|
+
root,
|
|
2521
|
+
"decision-anchor-file:" + file,
|
|
2522
|
+
async () => {
|
|
2523
|
+
try {
|
|
2524
|
+
const absolute = await storePath(root, file);
|
|
2525
|
+
await fs9.lstat(absolute);
|
|
2526
|
+
if (!eligible.has(file)) return { complete: false };
|
|
2527
|
+
const handle = await fs9.open(
|
|
2528
|
+
absolute,
|
|
2529
|
+
constants2.O_RDONLY | constants2.O_NONBLOCK | constants2.O_NOFOLLOW
|
|
2530
|
+
);
|
|
2531
|
+
try {
|
|
2532
|
+
const stat = await handle.stat();
|
|
2533
|
+
if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES)
|
|
2534
|
+
return { complete: false };
|
|
2535
|
+
const bytes = Buffer.alloc(Math.min(stat.size + 1, MAX_SOURCE_BYTES + 1));
|
|
2536
|
+
let size = 0;
|
|
2537
|
+
while (size < bytes.length) {
|
|
2538
|
+
const read = await handle.read(bytes, size, bytes.length - size, null);
|
|
2539
|
+
if (!read.bytesRead) break;
|
|
2540
|
+
size += read.bytesRead;
|
|
2541
|
+
}
|
|
2542
|
+
if (size === bytes.length) return { complete: false };
|
|
2543
|
+
return {
|
|
2544
|
+
complete: true,
|
|
2545
|
+
hash: createHash("sha256").update(bytes.subarray(0, size)).digest("hex")
|
|
2546
|
+
};
|
|
2547
|
+
} finally {
|
|
2548
|
+
await handle.close();
|
|
2549
|
+
}
|
|
2550
|
+
} catch (error) {
|
|
2551
|
+
return { complete: error.code === "ENOENT" };
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
);
|
|
2555
|
+
if (!evidence.complete) capture.complete = false;
|
|
2556
|
+
if (evidence.hash) capture.files[file] = evidence.hash;
|
|
2557
|
+
}
|
|
2558
|
+
} catch {
|
|
2559
|
+
capture.complete = false;
|
|
2560
|
+
}
|
|
2561
|
+
return capture;
|
|
2562
|
+
}
|
|
2563
|
+
);
|
|
2564
|
+
})
|
|
2565
|
+
);
|
|
2566
|
+
}
|
|
2567
|
+
async function captureAnchors(root, anchors) {
|
|
2568
|
+
return (await captureAnchorScopes(root, [anchors]))[0];
|
|
2569
|
+
}
|
|
2570
|
+
function changedCaptureFiles(before, after) {
|
|
2571
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(before.files), ...Object.keys(after.files)])].filter((file) => before.files[file] !== after.files[file]).sort();
|
|
2572
|
+
}
|
|
2573
|
+
var repoPath2, anchorCaptureSchema;
|
|
2574
|
+
var init_anchors = __esm({
|
|
2575
|
+
"src/decisions/anchors.ts"() {
|
|
2576
|
+
"use strict";
|
|
2577
|
+
init_inspection();
|
|
2578
|
+
init_files();
|
|
2579
|
+
init_paths();
|
|
2580
|
+
init_storage();
|
|
2581
|
+
repoPath2 = z3.string().refine((p) => normalizeRepoPath(p) === p);
|
|
2582
|
+
anchorCaptureSchema = z3.object({
|
|
2583
|
+
anchors: z3.array(repoPath2),
|
|
2584
|
+
files: z3.record(repoPath2, z3.string().regex(/^[a-f0-9]{64}$/)),
|
|
2585
|
+
complete: z3.boolean()
|
|
2586
|
+
}).refine(
|
|
2587
|
+
(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,
|
|
2588
|
+
"Capture paths must belong to its normalized, sorted anchor scope"
|
|
2589
|
+
);
|
|
2590
|
+
}
|
|
2591
|
+
});
|
|
2592
|
+
|
|
2049
2593
|
// src/context/lexical.ts
|
|
2050
2594
|
import path17 from "path";
|
|
2051
2595
|
var init_lexical = __esm({
|
|
@@ -2062,7 +2606,8 @@ var init_trust = __esm({
|
|
|
2062
2606
|
});
|
|
2063
2607
|
|
|
2064
2608
|
// src/decisions/provenance.ts
|
|
2065
|
-
import { z as
|
|
2609
|
+
import { z as z4 } from "zod";
|
|
2610
|
+
import { createHash as createHash2 } from "crypto";
|
|
2066
2611
|
function decisionContent(record) {
|
|
2067
2612
|
return {
|
|
2068
2613
|
title: record.title,
|
|
@@ -2077,7 +2622,8 @@ function decisionApproval(record) {
|
|
|
2077
2622
|
return record.version === 1 ? "unreviewed" : record.approval;
|
|
2078
2623
|
}
|
|
2079
2624
|
function effectiveDecision(record) {
|
|
2080
|
-
if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed")
|
|
2625
|
+
if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed")
|
|
2626
|
+
return record;
|
|
2081
2627
|
let index = record.history.length - 1;
|
|
2082
2628
|
while (index >= 0 && !["accepted", "reaffirmed"].includes(record.history[index].kind)) index--;
|
|
2083
2629
|
if (index < 0) return record;
|
|
@@ -2089,13 +2635,16 @@ function effectiveDecision(record) {
|
|
|
2089
2635
|
approval: "accepted",
|
|
2090
2636
|
revision: event.revision,
|
|
2091
2637
|
refreshedHash: event.refreshedHash,
|
|
2638
|
+
capture: event.capture,
|
|
2092
2639
|
updatedAt: event.at,
|
|
2093
2640
|
history: record.history.slice(0, index + 1)
|
|
2094
2641
|
};
|
|
2095
2642
|
}
|
|
2096
2643
|
function decisionProvenance(record, freshness = "unknown") {
|
|
2097
2644
|
const approval = decisionApproval(record);
|
|
2098
|
-
const review = record.version === 2 ? [...record.history].reverse().find(
|
|
2645
|
+
const review = record.version === 2 ? [...record.history].reverse().find(
|
|
2646
|
+
(e) => ["accepted", "reaffirmed"].includes(e.kind) && e.revision === record.revision
|
|
2647
|
+
) : void 0;
|
|
2099
2648
|
return {
|
|
2100
2649
|
approval,
|
|
2101
2650
|
revision: record.version === 2 ? record.revision : 0,
|
|
@@ -2103,141 +2652,236 @@ function decisionProvenance(record, freshness = "unknown") {
|
|
|
2103
2652
|
sources: record.version === 2 ? record.sources : [],
|
|
2104
2653
|
guidance: record.status !== "active" ? "historical" : approval === "accepted" ? "constraint" : approval === "proposed" ? "proposal" : "unreviewed",
|
|
2105
2654
|
reviewRequired: record.status === "active" && (approval !== "accepted" || freshness !== "current"),
|
|
2106
|
-
lastReview: review ? {
|
|
2655
|
+
lastReview: review ? {
|
|
2656
|
+
reviewer: review.actor,
|
|
2657
|
+
at: review.at,
|
|
2658
|
+
note: review.note,
|
|
2659
|
+
gitHash: review.refreshedHash
|
|
2660
|
+
} : null
|
|
2107
2661
|
};
|
|
2108
2662
|
}
|
|
2663
|
+
function decisionInspectionDigest(record) {
|
|
2664
|
+
const effective = effectiveDecision(record);
|
|
2665
|
+
return createHash2("sha256").update(
|
|
2666
|
+
JSON.stringify([
|
|
2667
|
+
record.status,
|
|
2668
|
+
decisionApproval(record),
|
|
2669
|
+
record.version === 2 ? record.revision : 0,
|
|
2670
|
+
decisionContent(record),
|
|
2671
|
+
decisionContent(effective),
|
|
2672
|
+
effective.refreshedHash
|
|
2673
|
+
])
|
|
2674
|
+
).digest("hex");
|
|
2675
|
+
}
|
|
2676
|
+
function latestDecisionInspection(record) {
|
|
2677
|
+
return record.version === 2 ? record.inspections?.filter((i) => i.decisionDigest === decisionInspectionDigest(record)).at(-1) : void 0;
|
|
2678
|
+
}
|
|
2109
2679
|
var text, decisionSourceSchema, attributionSchema, contentSchema, approvalSchema, statusSchema, reviewEvidenceSchema, eventSchema, legacySchema, currentSchema, decisionSchema;
|
|
2110
2680
|
var init_provenance = __esm({
|
|
2111
2681
|
"src/decisions/provenance.ts"() {
|
|
2112
2682
|
"use strict";
|
|
2683
|
+
init_anchors();
|
|
2113
2684
|
init_paths();
|
|
2114
2685
|
init_trust();
|
|
2115
|
-
text = (max) =>
|
|
2116
|
-
decisionSourceSchema =
|
|
2117
|
-
kind:
|
|
2686
|
+
text = (max) => z4.string().trim().min(1).max(max);
|
|
2687
|
+
decisionSourceSchema = z4.object({
|
|
2688
|
+
kind: z4.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
|
|
2118
2689
|
reference: text(1e3),
|
|
2119
2690
|
note: text(500).optional()
|
|
2120
2691
|
}).strict();
|
|
2121
|
-
attributionSchema =
|
|
2692
|
+
attributionSchema = z4.object({
|
|
2122
2693
|
owner: text(200).nullable().optional(),
|
|
2123
|
-
sources:
|
|
2694
|
+
sources: z4.array(decisionSourceSchema).max(20).optional(),
|
|
2124
2695
|
actor: text(200).optional()
|
|
2125
2696
|
});
|
|
2126
|
-
contentSchema =
|
|
2127
|
-
title:
|
|
2128
|
-
body:
|
|
2129
|
-
category:
|
|
2130
|
-
files:
|
|
2697
|
+
contentSchema = z4.object({
|
|
2698
|
+
title: z4.string().min(1),
|
|
2699
|
+
body: z4.string().min(1),
|
|
2700
|
+
category: z4.enum(["decision", "gotcha", "deprecation", "convention"]),
|
|
2701
|
+
files: z4.array(z4.string().refine((f) => normalizeRepoPath(f) !== null)),
|
|
2131
2702
|
owner: text(200).optional(),
|
|
2132
|
-
sources:
|
|
2703
|
+
sources: z4.array(decisionSourceSchema).max(20)
|
|
2133
2704
|
});
|
|
2134
|
-
approvalSchema =
|
|
2135
|
-
statusSchema =
|
|
2136
|
-
reviewEvidenceSchema =
|
|
2137
|
-
baseHash:
|
|
2138
|
-
headHash:
|
|
2139
|
-
historyAvailable:
|
|
2140
|
-
changedFiles:
|
|
2141
|
-
localChanges:
|
|
2705
|
+
approvalSchema = z4.enum(["unreviewed", "proposed", "accepted"]);
|
|
2706
|
+
statusSchema = z4.enum(["active", "superseded", "retired"]);
|
|
2707
|
+
reviewEvidenceSchema = z4.object({
|
|
2708
|
+
baseHash: z4.string(),
|
|
2709
|
+
headHash: z4.string(),
|
|
2710
|
+
historyAvailable: z4.boolean(),
|
|
2711
|
+
changedFiles: z4.array(z4.string()),
|
|
2712
|
+
localChanges: z4.array(z4.string())
|
|
2142
2713
|
});
|
|
2143
|
-
eventSchema =
|
|
2144
|
-
kind:
|
|
2145
|
-
|
|
2714
|
+
eventSchema = z4.object({
|
|
2715
|
+
kind: z4.enum([
|
|
2716
|
+
"imported",
|
|
2717
|
+
"created",
|
|
2718
|
+
"revised",
|
|
2719
|
+
"accepted",
|
|
2720
|
+
"reaffirmed",
|
|
2721
|
+
"retired",
|
|
2722
|
+
"superseded"
|
|
2723
|
+
]),
|
|
2724
|
+
at: z4.string().datetime(),
|
|
2146
2725
|
actor: text(200).optional(),
|
|
2147
2726
|
note: text(1500).optional(),
|
|
2148
|
-
revision:
|
|
2727
|
+
revision: z4.number().int().positive(),
|
|
2149
2728
|
content: contentSchema,
|
|
2150
2729
|
approval: approvalSchema,
|
|
2151
2730
|
status: statusSchema,
|
|
2152
|
-
refreshedHash:
|
|
2153
|
-
evidence: reviewEvidenceSchema.optional()
|
|
2731
|
+
refreshedHash: z4.string(),
|
|
2732
|
+
evidence: reviewEvidenceSchema.optional(),
|
|
2733
|
+
capture: anchorCaptureSchema.optional()
|
|
2154
2734
|
});
|
|
2155
|
-
legacySchema =
|
|
2156
|
-
version:
|
|
2157
|
-
id:
|
|
2158
|
-
title:
|
|
2159
|
-
body:
|
|
2735
|
+
legacySchema = z4.object({
|
|
2736
|
+
version: z4.literal(1),
|
|
2737
|
+
id: z4.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
2738
|
+
title: z4.string().min(1),
|
|
2739
|
+
body: z4.string().min(1),
|
|
2160
2740
|
category: contentSchema.shape.category,
|
|
2161
2741
|
files: contentSchema.shape.files,
|
|
2162
|
-
createdAt:
|
|
2163
|
-
updatedAt:
|
|
2164
|
-
refreshedHash:
|
|
2165
|
-
status:
|
|
2166
|
-
supersededBy:
|
|
2742
|
+
createdAt: z4.string(),
|
|
2743
|
+
updatedAt: z4.string(),
|
|
2744
|
+
refreshedHash: z4.string(),
|
|
2745
|
+
status: z4.enum(["active", "superseded"]),
|
|
2746
|
+
supersededBy: z4.string().optional()
|
|
2167
2747
|
}).passthrough();
|
|
2168
2748
|
currentSchema = legacySchema.extend({
|
|
2169
|
-
version:
|
|
2749
|
+
version: z4.literal(2),
|
|
2170
2750
|
status: statusSchema,
|
|
2171
2751
|
approval: approvalSchema,
|
|
2172
|
-
revision:
|
|
2752
|
+
revision: z4.number().int().positive(),
|
|
2173
2753
|
owner: text(200).optional(),
|
|
2174
|
-
sources:
|
|
2175
|
-
history:
|
|
2754
|
+
sources: z4.array(decisionSourceSchema).max(20),
|
|
2755
|
+
history: z4.array(eventSchema).min(1),
|
|
2756
|
+
capture: anchorCaptureSchema.optional(),
|
|
2757
|
+
inspections: z4.array(
|
|
2758
|
+
z4.object({
|
|
2759
|
+
at: z4.string().datetime(),
|
|
2760
|
+
inspector: text(200),
|
|
2761
|
+
note: text(1500),
|
|
2762
|
+
outcome: z4.literal("no_contradiction_found"),
|
|
2763
|
+
decisionDigest: z4.string().regex(/^[a-f0-9]{64}$/),
|
|
2764
|
+
headHash: z4.string(),
|
|
2765
|
+
capture: anchorCaptureSchema
|
|
2766
|
+
})
|
|
2767
|
+
).max(200).optional()
|
|
2176
2768
|
}).superRefine((record, ctx) => {
|
|
2177
2769
|
const invalid = (message) => ctx.addIssue({ code: "custom", message });
|
|
2178
2770
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
2179
2771
|
let previous;
|
|
2180
2772
|
for (const event of record.history) {
|
|
2181
2773
|
if (!previous) {
|
|
2182
|
-
if (!["created", "imported"].includes(event.kind) || event.revision !== 1)
|
|
2183
|
-
|
|
2774
|
+
if (!["created", "imported"].includes(event.kind) || event.revision !== 1)
|
|
2775
|
+
invalid("History must begin with creation or legacy import at revision 1");
|
|
2776
|
+
if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed"))
|
|
2777
|
+
invalid("Initial records cannot claim acceptance");
|
|
2184
2778
|
} else {
|
|
2185
2779
|
if (["created", "imported"].includes(event.kind)) invalid("History cannot restart");
|
|
2186
2780
|
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
|
-
|
|
2781
|
+
if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0))
|
|
2782
|
+
invalid("Invalid revision sequence");
|
|
2783
|
+
if (event.kind !== "revised" && !same(event.content, previous.content))
|
|
2784
|
+
invalid("A review cannot silently revise decision content");
|
|
2785
|
+
if (event.kind === "reaffirmed" && previous.approval !== "accepted")
|
|
2786
|
+
invalid("Only accepted decisions can be reaffirmed");
|
|
2787
|
+
if (event.kind === "accepted" && previous.approval === "accepted")
|
|
2788
|
+
invalid("Use reaffirmation for an accepted decision");
|
|
2191
2789
|
const approval = event.kind === "revised" ? "proposed" : ["accepted", "reaffirmed"].includes(event.kind) ? "accepted" : previous.approval;
|
|
2192
2790
|
if (event.approval !== approval) invalid("Approval disagrees with review history");
|
|
2193
|
-
if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash)
|
|
2791
|
+
if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash)
|
|
2792
|
+
invalid("Only a review can refresh the evidence baseline");
|
|
2194
2793
|
}
|
|
2195
|
-
if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active"))
|
|
2196
|
-
|
|
2794
|
+
if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active"))
|
|
2795
|
+
invalid("Lifecycle disagrees with history");
|
|
2796
|
+
if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence))
|
|
2797
|
+
invalid("Reviews require a named reviewer, reason, and code evidence");
|
|
2197
2798
|
if (["accepted", "reaffirmed"].includes(event.kind)) {
|
|
2198
|
-
if (!event.content.owner || !event.content.sources.length)
|
|
2199
|
-
|
|
2799
|
+
if (!event.content.owner || !event.content.sources.length)
|
|
2800
|
+
invalid("Accepted decisions require an owner and source");
|
|
2801
|
+
if (!event.evidence || !/^[a-f0-9]{40,64}$/.test(event.evidence.headHash) || event.refreshedHash !== event.evidence.headHash || event.evidence.localChanges.length)
|
|
2802
|
+
invalid("Acceptance requires a committed evidence baseline");
|
|
2200
2803
|
}
|
|
2804
|
+
if (event.capture && !same([...event.content.files].sort(), event.capture.anchors))
|
|
2805
|
+
invalid("Capture scope disagrees with decision anchors");
|
|
2201
2806
|
previous = event;
|
|
2202
2807
|
}
|
|
2203
|
-
if (
|
|
2808
|
+
if (previous && !same(previous.capture, record.capture))
|
|
2809
|
+
invalid("Capture does not match the final history event");
|
|
2810
|
+
if (!previous || !same(previous.content, decisionContent(record)) || previous.approval !== record.approval || previous.status !== record.status || previous.revision !== record.revision || previous.refreshedHash !== record.refreshedHash)
|
|
2811
|
+
invalid("Decision does not match the final history event");
|
|
2204
2812
|
});
|
|
2205
|
-
decisionSchema =
|
|
2813
|
+
decisionSchema = z4.union([legacySchema, currentSchema]);
|
|
2206
2814
|
}
|
|
2207
2815
|
});
|
|
2208
2816
|
|
|
2209
2817
|
// src/decisions/decisions.ts
|
|
2210
|
-
import
|
|
2818
|
+
import fs10 from "fs/promises";
|
|
2211
2819
|
import path18 from "path";
|
|
2212
|
-
import { createHash } from "crypto";
|
|
2213
|
-
import { z as
|
|
2820
|
+
import { createHash as createHash3 } from "crypto";
|
|
2821
|
+
import { z as z5 } from "zod";
|
|
2822
|
+
async function readDecisionInputs(rootDir) {
|
|
2823
|
+
return structuredClone(
|
|
2824
|
+
await inspectionRead(rootDir, "decision-store:inputs", async () => {
|
|
2825
|
+
const inputs = [];
|
|
2826
|
+
const diagnostics = [];
|
|
2827
|
+
let entries;
|
|
2828
|
+
try {
|
|
2829
|
+
entries = await fs10.readdir(await storePath(rootDir, ".mason/decisions"));
|
|
2830
|
+
} catch (error) {
|
|
2831
|
+
if (error.code !== "ENOENT")
|
|
2832
|
+
diagnostics.push({ path: ".mason/decisions", message: String(error) });
|
|
2833
|
+
return { inputs, diagnostics };
|
|
2834
|
+
}
|
|
2835
|
+
for (const entry of entries.sort()) {
|
|
2836
|
+
if (!entry.endsWith(".json")) continue;
|
|
2837
|
+
const relative = `.mason/decisions/${entry}`;
|
|
2838
|
+
try {
|
|
2839
|
+
const raw = await readBoundedFile(await storePath(rootDir, relative), 10 * 1024 * 1024);
|
|
2840
|
+
if (raw === null) throw new Error("file is not regular or exceeds 10 MiB");
|
|
2841
|
+
inputs.push([relative, raw]);
|
|
2842
|
+
} catch (error) {
|
|
2843
|
+
if (error.code === "ENOENT") inputs.push([relative, null]);
|
|
2844
|
+
else
|
|
2845
|
+
diagnostics.push({
|
|
2846
|
+
path: relative,
|
|
2847
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2848
|
+
});
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
return { inputs, diagnostics };
|
|
2852
|
+
})
|
|
2853
|
+
);
|
|
2854
|
+
}
|
|
2214
2855
|
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
|
-
|
|
2856
|
+
return structuredClone(
|
|
2857
|
+
await inspectionRead(rootDir, "decision-store:parsed", async () => {
|
|
2858
|
+
const raw = await readDecisionInputs(rootDir);
|
|
2859
|
+
const records = [];
|
|
2860
|
+
const diagnostics = [...raw.diagnostics];
|
|
2861
|
+
for (const [relative, content] of raw.inputs) {
|
|
2862
|
+
try {
|
|
2863
|
+
const record = decisionSchema.parse(content === null ? null : JSON.parse(content));
|
|
2864
|
+
if (relative !== `.mason/decisions/${record.id}.json`)
|
|
2865
|
+
throw new Error("Record id does not match its filename");
|
|
2866
|
+
records.push(record);
|
|
2867
|
+
} catch (error) {
|
|
2868
|
+
diagnostics.push({
|
|
2869
|
+
path: relative,
|
|
2870
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
return { records, diagnostics };
|
|
2875
|
+
})
|
|
2876
|
+
);
|
|
2236
2877
|
}
|
|
2237
2878
|
var TITLE_MAX_CHARS, BODY_RECOMMENDED_CHARS, BODY_MAX_CHARS, boundedText, decisionTitleSchema, decisionBodySchema;
|
|
2238
2879
|
var init_decisions = __esm({
|
|
2239
2880
|
"src/decisions/decisions.ts"() {
|
|
2240
2881
|
"use strict";
|
|
2882
|
+
init_inspection();
|
|
2883
|
+
init_files();
|
|
2884
|
+
init_anchors();
|
|
2241
2885
|
init_storage();
|
|
2242
2886
|
init_paths();
|
|
2243
2887
|
init_snapshot();
|
|
@@ -2246,10 +2890,16 @@ var init_decisions = __esm({
|
|
|
2246
2890
|
TITLE_MAX_CHARS = 80;
|
|
2247
2891
|
BODY_RECOMMENDED_CHARS = 1500;
|
|
2248
2892
|
BODY_MAX_CHARS = 2500;
|
|
2249
|
-
boundedText = (field, max, hint) =>
|
|
2250
|
-
errorMap: (issue, ctx) => ({
|
|
2893
|
+
boundedText = (field, max, hint) => z5.string({
|
|
2894
|
+
errorMap: (issue, ctx) => ({
|
|
2895
|
+
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
|
|
2896
|
+
})
|
|
2251
2897
|
}).trim().min(1, `${field} must be non-empty`).max(max);
|
|
2252
|
-
decisionTitleSchema = boundedText(
|
|
2898
|
+
decisionTitleSchema = boundedText(
|
|
2899
|
+
"title",
|
|
2900
|
+
TITLE_MAX_CHARS,
|
|
2901
|
+
"Tighten it to a specific headline."
|
|
2902
|
+
);
|
|
2253
2903
|
decisionBodySchema = boundedText(
|
|
2254
2904
|
"body",
|
|
2255
2905
|
BODY_MAX_CHARS,
|
|
@@ -2263,11 +2913,45 @@ import path19 from "path";
|
|
|
2263
2913
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
2264
2914
|
const resolvedRoot = path19.resolve(rootDir);
|
|
2265
2915
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
2266
|
-
const report = {
|
|
2267
|
-
|
|
2916
|
+
const report = {
|
|
2917
|
+
historyAvailable: true,
|
|
2918
|
+
totalDecisions: store.records.length,
|
|
2919
|
+
staleDecisions: {},
|
|
2920
|
+
freshness: {},
|
|
2921
|
+
diagnostics: store.diagnostics
|
|
2922
|
+
};
|
|
2923
|
+
const [head, workingTree] = await Promise.all([
|
|
2924
|
+
getCurrentGitHash(resolvedRoot),
|
|
2925
|
+
getWorkingTree(resolvedRoot)
|
|
2926
|
+
]);
|
|
2268
2927
|
const changesByHash = /* @__PURE__ */ new Map();
|
|
2269
|
-
const
|
|
2928
|
+
const baselines = store.records.filter((record) => record.status === "active").flatMap((record) => {
|
|
2929
|
+
const inspection = latestDecisionInspection(record);
|
|
2930
|
+
if (inspection) return [inspection.capture];
|
|
2931
|
+
return [.../* @__PURE__ */ new Set([effectiveDecision(record), record])].flatMap(
|
|
2932
|
+
(candidate) => candidate.version === 2 && candidate.capture ? [candidate.capture] : []
|
|
2933
|
+
);
|
|
2934
|
+
});
|
|
2935
|
+
const captures = await captureAnchorScopes(
|
|
2936
|
+
resolvedRoot,
|
|
2937
|
+
baselines.map((baseline) => baseline.anchors)
|
|
2938
|
+
);
|
|
2939
|
+
const currentCaptures = new Map(
|
|
2940
|
+
baselines.map((baseline, index) => [JSON.stringify(baseline.anchors), captures[index]])
|
|
2941
|
+
);
|
|
2942
|
+
const inspect = async (record, inspectionCapture) => {
|
|
2270
2943
|
if (record.files.length === 0) return { freshness: "unknown", changedFiles: [] };
|
|
2944
|
+
const baseline = inspectionCapture ?? (record.version === 2 ? record.capture : void 0);
|
|
2945
|
+
if (baseline) {
|
|
2946
|
+
const current = currentCaptures.get(JSON.stringify(baseline.anchors));
|
|
2947
|
+
const hits2 = changedCaptureFiles(baseline, current).filter(
|
|
2948
|
+
(file) => matchingPaths(record.files, [file]).length
|
|
2949
|
+
);
|
|
2950
|
+
return {
|
|
2951
|
+
freshness: !baseline.complete || !current.complete ? "unknown" : hits2.length ? "changed" : "current",
|
|
2952
|
+
changedFiles: hits2
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2271
2955
|
let touched = changesByHash.get(record.refreshedHash);
|
|
2272
2956
|
if (touched === void 0) {
|
|
2273
2957
|
const changes = record.refreshedHash === head && head !== "unknown" ? [] : await getChangesWithStatus(resolvedRoot, record.refreshedHash);
|
|
@@ -2277,21 +2961,27 @@ async function computeDecisionDrift(rootDir, decisions) {
|
|
|
2277
2961
|
if (touched === null) report.historyAvailable = false;
|
|
2278
2962
|
const hits = touched ? matchingPaths(record.files, touched) : [];
|
|
2279
2963
|
const localHits = matchingPaths(record.files, workingTree.changedFiles);
|
|
2280
|
-
return {
|
|
2964
|
+
return {
|
|
2965
|
+
freshness: touched === null || !workingTree.available ? "unknown" : hits.length || localHits.length ? "changed" : "current",
|
|
2966
|
+
changedFiles: [.../* @__PURE__ */ new Set([...hits, ...localHits])].sort()
|
|
2967
|
+
};
|
|
2281
2968
|
};
|
|
2282
2969
|
for (const record of store.records) {
|
|
2283
2970
|
if (record.status !== "active") continue;
|
|
2284
2971
|
const effective = effectiveDecision(record);
|
|
2285
|
-
const
|
|
2972
|
+
const inspection = latestDecisionInspection(record);
|
|
2973
|
+
const state = await inspect(effective, inspection?.capture);
|
|
2286
2974
|
report.freshness[record.id] = state.freshness;
|
|
2287
2975
|
if (state.changedFiles.length) report.staleDecisions[record.id] = state.changedFiles;
|
|
2288
|
-
if (effective !== record)
|
|
2976
|
+
if (effective !== record)
|
|
2977
|
+
(report.pendingProposals ??= {})[record.id] = await inspect(record, inspection?.capture);
|
|
2289
2978
|
}
|
|
2290
2979
|
return report;
|
|
2291
2980
|
}
|
|
2292
2981
|
var init_drift2 = __esm({
|
|
2293
2982
|
"src/decisions/drift.ts"() {
|
|
2294
2983
|
"use strict";
|
|
2984
|
+
init_anchors();
|
|
2295
2985
|
init_drift();
|
|
2296
2986
|
init_paths();
|
|
2297
2987
|
init_snapshot();
|
|
@@ -2306,7 +2996,11 @@ async function checkDecisionAnchors(ctx) {
|
|
|
2306
2996
|
if (!ctx.decisionsPresent) return result;
|
|
2307
2997
|
const store = await loadDecisionStore(ctx.root);
|
|
2308
2998
|
const records = store.records;
|
|
2309
|
-
for (const diagnostic of store.diagnostics)
|
|
2999
|
+
for (const diagnostic of store.diagnostics)
|
|
3000
|
+
result.skipped.push({
|
|
3001
|
+
check: "decision-anchor-drift",
|
|
3002
|
+
reason: `${diagnostic.path}: ${diagnostic.message}`
|
|
3003
|
+
});
|
|
2310
3004
|
const drift = await computeDecisionDrift(ctx.root, records);
|
|
2311
3005
|
if (!drift.historyAvailable) {
|
|
2312
3006
|
result.skipped.push({
|
|
@@ -2314,17 +3008,30 @@ async function checkDecisionAnchors(ctx) {
|
|
|
2314
3008
|
reason: "some decision base commits are unreachable (shallow clone?)"
|
|
2315
3009
|
});
|
|
2316
3010
|
}
|
|
2317
|
-
const changed = records.
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
3011
|
+
const changed = records.filter((record) => record.status === "active").flatMap(
|
|
3012
|
+
(record) => [
|
|
3013
|
+
{
|
|
3014
|
+
record: effectiveDecision(record),
|
|
3015
|
+
changedFiles: drift.staleDecisions[record.id] ?? [],
|
|
3016
|
+
freshness: drift.freshness?.[record.id] ?? "unknown"
|
|
3017
|
+
},
|
|
3018
|
+
...drift.pendingProposals?.[record.id] ? [{ record, ...drift.pendingProposals[record.id] }] : []
|
|
3019
|
+
]
|
|
3020
|
+
);
|
|
2321
3021
|
for (const { record, changedFiles, freshness } of changed) {
|
|
3022
|
+
if (freshness === "unknown" && record.files.length) {
|
|
3023
|
+
result.skipped.push({
|
|
3024
|
+
check: "decision-anchor-drift",
|
|
3025
|
+
reason: `Anchor evidence for decision ${record.id} is incomplete or unreadable`
|
|
3026
|
+
});
|
|
3027
|
+
continue;
|
|
3028
|
+
}
|
|
2322
3029
|
if (!changedFiles.length) continue;
|
|
2323
3030
|
const id = record.id;
|
|
2324
3031
|
const provenance = decisionProvenance(record, freshness);
|
|
2325
3032
|
result.advisories.push({
|
|
2326
3033
|
type: "decision-anchor-drift",
|
|
2327
|
-
message: `decision "${record.title}" (${provenance.approval}) has
|
|
3034
|
+
message: `decision "${record.title}" (${provenance.approval}) has anchors changed since capture or review; consistency is unchecked`,
|
|
2328
3035
|
anchor: {
|
|
2329
3036
|
doc: `.mason/decisions/${id}.json`,
|
|
2330
3037
|
line: null,
|
|
@@ -2378,8 +3085,8 @@ var init_checks = __esm({
|
|
|
2378
3085
|
});
|
|
2379
3086
|
|
|
2380
3087
|
// src/audit/findings.ts
|
|
2381
|
-
import { createHash as
|
|
2382
|
-
import { z as
|
|
3088
|
+
import { createHash as createHash4 } from "crypto";
|
|
3089
|
+
import { z as z6 } from "zod";
|
|
2383
3090
|
function findingId(finding) {
|
|
2384
3091
|
const e = finding.evidence;
|
|
2385
3092
|
let key;
|
|
@@ -2409,106 +3116,154 @@ var digest, checkSchema, commitSchema, anchorSchema, count, scopeSchema, evidenc
|
|
|
2409
3116
|
var init_findings = __esm({
|
|
2410
3117
|
"src/audit/findings.ts"() {
|
|
2411
3118
|
"use strict";
|
|
2412
|
-
digest = (value) =>
|
|
2413
|
-
checkSchema =
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
3119
|
+
digest = (value) => createHash4("sha256").update(JSON.stringify(value)).digest("hex");
|
|
3120
|
+
checkSchema = z6.enum([
|
|
3121
|
+
"deleted-reference",
|
|
3122
|
+
"new-module",
|
|
3123
|
+
"stale-count",
|
|
3124
|
+
"dead-command",
|
|
3125
|
+
"deps-changed",
|
|
3126
|
+
"decision-anchor-drift"
|
|
3127
|
+
]);
|
|
3128
|
+
commitSchema = z6.object({
|
|
3129
|
+
hash: z6.string().regex(/^[a-f0-9]{40,64}$/),
|
|
3130
|
+
date: z6.string(),
|
|
3131
|
+
subject: z6.string()
|
|
2421
3132
|
});
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
3133
|
+
anchorSchema = z6.object({
|
|
3134
|
+
doc: z6.string(),
|
|
3135
|
+
line: z6.number().int().positive().nullable(),
|
|
3136
|
+
excerpt: z6.string().nullable()
|
|
3137
|
+
});
|
|
3138
|
+
count = z6.number().int().nonnegative();
|
|
3139
|
+
scopeSchema = z6.object({
|
|
3140
|
+
basis: z6.enum(["repository", "document", "document-link", "explicit"]),
|
|
3141
|
+
directory: z6.string(),
|
|
3142
|
+
candidates: z6.array(z6.string())
|
|
3143
|
+
});
|
|
3144
|
+
evidenceSchema = z6.discriminatedUnion("kind", [
|
|
3145
|
+
z6.object({
|
|
3146
|
+
kind: z6.literal("missing-path"),
|
|
3147
|
+
claimed: z6.string(),
|
|
3148
|
+
renamedTo: z6.string().nullable(),
|
|
2427
3149
|
deletedInCommit: commitSchema.nullable(),
|
|
2428
|
-
everTracked:
|
|
2429
|
-
parentDirExists:
|
|
2430
|
-
resolvedPath:
|
|
3150
|
+
everTracked: z6.boolean(),
|
|
3151
|
+
parentDirExists: z6.boolean(),
|
|
3152
|
+
resolvedPath: z6.string().optional(),
|
|
2431
3153
|
scope: scopeSchema.optional()
|
|
2432
3154
|
}),
|
|
2433
|
-
|
|
2434
|
-
kind:
|
|
2435
|
-
dir:
|
|
3155
|
+
z6.object({
|
|
3156
|
+
kind: z6.literal("unmentioned-dir"),
|
|
3157
|
+
dir: z6.string(),
|
|
2436
3158
|
sourceFileCount: count,
|
|
2437
3159
|
firstCommit: commitSchema.nullable(),
|
|
2438
|
-
checkedDocs:
|
|
3160
|
+
checkedDocs: z6.array(z6.string())
|
|
2439
3161
|
}),
|
|
2440
|
-
|
|
2441
|
-
kind:
|
|
3162
|
+
z6.object({
|
|
3163
|
+
kind: z6.literal("count-mismatch"),
|
|
2442
3164
|
claimed: count,
|
|
2443
3165
|
actual: count,
|
|
2444
|
-
unit:
|
|
2445
|
-
countedFrom:
|
|
2446
|
-
members:
|
|
3166
|
+
unit: z6.string(),
|
|
3167
|
+
countedFrom: z6.string(),
|
|
3168
|
+
members: z6.array(z6.string())
|
|
2447
3169
|
}),
|
|
2448
|
-
|
|
2449
|
-
kind:
|
|
2450
|
-
scriptName:
|
|
2451
|
-
invocation:
|
|
2452
|
-
manifestsChecked:
|
|
2453
|
-
availableScripts:
|
|
3170
|
+
z6.object({
|
|
3171
|
+
kind: z6.literal("missing-script"),
|
|
3172
|
+
scriptName: z6.string(),
|
|
3173
|
+
invocation: z6.string(),
|
|
3174
|
+
manifestsChecked: z6.array(z6.string()),
|
|
3175
|
+
availableScripts: z6.array(z6.string()),
|
|
2454
3176
|
scope: scopeSchema.optional()
|
|
2455
3177
|
}),
|
|
2456
|
-
|
|
2457
|
-
kind:
|
|
3178
|
+
z6.object({
|
|
3179
|
+
kind: z6.literal("doc-behind-manifests"),
|
|
2458
3180
|
docLastCommit: commitSchema,
|
|
2459
|
-
manifestCommits:
|
|
3181
|
+
manifestCommits: z6.array(commitSchema.extend({ files: z6.array(z6.string()) })),
|
|
2460
3182
|
totalCommits: count,
|
|
2461
|
-
matches:
|
|
2462
|
-
|
|
3183
|
+
matches: z6.array(
|
|
3184
|
+
z6.object({
|
|
3185
|
+
dependency: z6.string(),
|
|
3186
|
+
manifest: z6.string(),
|
|
3187
|
+
before: z6.string().nullable(),
|
|
3188
|
+
after: z6.string().nullable(),
|
|
3189
|
+
line: z6.number().int().positive(),
|
|
3190
|
+
excerpt: z6.string()
|
|
3191
|
+
})
|
|
3192
|
+
).optional(),
|
|
3193
|
+
matchingIncomplete: z6.boolean().optional()
|
|
2463
3194
|
}),
|
|
2464
|
-
|
|
2465
|
-
kind:
|
|
2466
|
-
decisionId:
|
|
2467
|
-
title:
|
|
2468
|
-
changedFiles:
|
|
2469
|
-
refreshedHash:
|
|
2470
|
-
provenance:
|
|
3195
|
+
z6.object({
|
|
3196
|
+
kind: z6.literal("decision-anchor"),
|
|
3197
|
+
decisionId: z6.string(),
|
|
3198
|
+
title: z6.string(),
|
|
3199
|
+
changedFiles: z6.array(z6.string()),
|
|
3200
|
+
refreshedHash: z6.string(),
|
|
3201
|
+
provenance: z6.object({}).passthrough().optional()
|
|
2471
3202
|
})
|
|
2472
3203
|
]);
|
|
2473
|
-
findingSchema =
|
|
3204
|
+
findingSchema = z6.object({
|
|
3205
|
+
message: z6.string(),
|
|
3206
|
+
anchor: anchorSchema,
|
|
3207
|
+
evidence: evidenceSchema
|
|
3208
|
+
});
|
|
2474
3209
|
issueSchema = findingSchema.extend({
|
|
2475
|
-
type:
|
|
2476
|
-
confidence:
|
|
3210
|
+
type: z6.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
3211
|
+
confidence: z6.enum(["certain", "likely"])
|
|
2477
3212
|
});
|
|
2478
|
-
advisorySchema = findingSchema.extend({
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
3213
|
+
advisorySchema = findingSchema.extend({
|
|
3214
|
+
type: checkSchema,
|
|
3215
|
+
resolution: z6.enum(["recheck", "informational"]).optional()
|
|
3216
|
+
});
|
|
3217
|
+
checkResultSchema = z6.object({
|
|
3218
|
+
issues: z6.array(issueSchema),
|
|
3219
|
+
advisories: z6.array(advisorySchema),
|
|
3220
|
+
suppressedAdvisories: z6.array(advisorySchema).optional(),
|
|
3221
|
+
skipped: z6.array(z6.object({ check: z6.string(), reason: z6.string(), doc: z6.string().optional() }))
|
|
3222
|
+
});
|
|
3223
|
+
}
|
|
3224
|
+
});
|
|
3225
|
+
|
|
3226
|
+
// src/automation/stop-review.ts
|
|
3227
|
+
import { z as z7 } from "zod";
|
|
3228
|
+
var evidenceSchema2, stopReviewSchema;
|
|
3229
|
+
var init_stop_review = __esm({
|
|
3230
|
+
"src/automation/stop-review.ts"() {
|
|
3231
|
+
"use strict";
|
|
3232
|
+
init_anchors();
|
|
3233
|
+
init_findings();
|
|
3234
|
+
evidenceSchema2 = z7.record(z7.object({ digest: z7.string(), capture: anchorCaptureSchema }));
|
|
3235
|
+
stopReviewSchema = z7.object({
|
|
3236
|
+
pending: z7.record(z7.object({ head: z7.string(), evidence: evidenceSchema2 })),
|
|
3237
|
+
edited: z7.record(z7.record(z7.string().nullable())),
|
|
3238
|
+
delivered: z7.record(z7.object({ digest: z7.string(), files: z7.record(z7.string().nullable()) }))
|
|
2484
3239
|
});
|
|
2485
3240
|
}
|
|
2486
3241
|
});
|
|
2487
3242
|
|
|
2488
3243
|
// src/automation/notifications.ts
|
|
2489
|
-
import { z as
|
|
3244
|
+
import { z as z8 } from "zod";
|
|
2490
3245
|
var conditionSchema, notificationStateSchema;
|
|
2491
3246
|
var init_notifications = __esm({
|
|
2492
3247
|
"src/automation/notifications.ts"() {
|
|
2493
3248
|
"use strict";
|
|
2494
3249
|
init_findings();
|
|
2495
|
-
conditionSchema =
|
|
2496
|
-
level:
|
|
2497
|
-
facts:
|
|
2498
|
-
magnitude:
|
|
3250
|
+
conditionSchema = z8.object({
|
|
3251
|
+
level: z8.number(),
|
|
3252
|
+
facts: z8.array(z8.string()),
|
|
3253
|
+
magnitude: z8.number().optional()
|
|
2499
3254
|
});
|
|
2500
|
-
notificationStateSchema =
|
|
2501
|
-
version:
|
|
2502
|
-
baselineHead:
|
|
2503
|
-
baseline:
|
|
2504
|
-
delivered:
|
|
2505
|
-
cleared:
|
|
3255
|
+
notificationStateSchema = z8.object({
|
|
3256
|
+
version: z8.literal(1),
|
|
3257
|
+
baselineHead: z8.string(),
|
|
3258
|
+
baseline: z8.record(conditionSchema),
|
|
3259
|
+
delivered: z8.record(conditionSchema),
|
|
3260
|
+
cleared: z8.array(z8.string())
|
|
2506
3261
|
});
|
|
2507
3262
|
}
|
|
2508
3263
|
});
|
|
2509
3264
|
|
|
2510
3265
|
// src/automation/store.ts
|
|
2511
|
-
import { z as
|
|
3266
|
+
import { z as z9 } from "zod";
|
|
2512
3267
|
function withLock(root, directory, run, waitMs = 5e3) {
|
|
2513
3268
|
return withStoreLock(root, directory, run, waitMs, "Automation");
|
|
2514
3269
|
}
|
|
@@ -2517,28 +3272,34 @@ var init_store = __esm({
|
|
|
2517
3272
|
"src/automation/store.ts"() {
|
|
2518
3273
|
"use strict";
|
|
2519
3274
|
init_store_lock();
|
|
3275
|
+
init_stop_review();
|
|
2520
3276
|
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
|
-
|
|
3277
|
+
hostSchema = z9.enum(["claude", "codex"]);
|
|
3278
|
+
stateSchema = z9.object({
|
|
3279
|
+
version: z9.literal(1),
|
|
3280
|
+
root: z9.string(),
|
|
3281
|
+
gitDir: z9.string(),
|
|
3282
|
+
branch: z9.string(),
|
|
3283
|
+
baselines: z9.array(
|
|
3284
|
+
z9.object({ path: z9.string(), at: z9.string(), event: z9.string(), fingerprint: z9.string() })
|
|
3285
|
+
).max(128),
|
|
3286
|
+
sessions: z9.record(
|
|
3287
|
+
z9.object({
|
|
3288
|
+
host: hostSchema,
|
|
3289
|
+
lastUsed: z9.string(),
|
|
3290
|
+
notifiedDiagnostics: z9.array(z9.string()).default([]),
|
|
3291
|
+
pending: z9.record(z9.string()),
|
|
3292
|
+
stopReview: stopReviewSchema.optional(),
|
|
3293
|
+
coverageGaps: z9.array(z9.string()),
|
|
3294
|
+
events: z9.record(z9.object({ at: z9.string(), count: z9.number().int().positive() }))
|
|
3295
|
+
})
|
|
3296
|
+
),
|
|
3297
|
+
updatedAt: z9.string(),
|
|
3298
|
+
fingerprint: z9.string().nullable(),
|
|
3299
|
+
latest: z9.string().nullable(),
|
|
2539
3300
|
notifications: notificationStateSchema.optional(),
|
|
2540
3301
|
agentNotifications: notificationStateSchema.optional(),
|
|
2541
|
-
analysis:
|
|
3302
|
+
analysis: z9.object({ id: z9.string(), fingerprint: z9.string(), completedAt: z9.number(), path: z9.string() }).optional()
|
|
2542
3303
|
});
|
|
2543
3304
|
}
|
|
2544
3305
|
});
|
|
@@ -2557,10 +3318,10 @@ __export(repair_exports, {
|
|
|
2557
3318
|
verifyRepair: () => verifyRepair,
|
|
2558
3319
|
withRepairSession: () => withRepairSession
|
|
2559
3320
|
});
|
|
2560
|
-
import
|
|
3321
|
+
import fs11 from "fs/promises";
|
|
2561
3322
|
import path20 from "path";
|
|
2562
3323
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2563
|
-
import { z as
|
|
3324
|
+
import { z as z10 } from "zod";
|
|
2564
3325
|
function recheckable(finding) {
|
|
2565
3326
|
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
3327
|
}
|
|
@@ -2572,7 +3333,8 @@ async function docState(root) {
|
|
|
2572
3333
|
for (const doc of await discoverDocPaths(root)) {
|
|
2573
3334
|
try {
|
|
2574
3335
|
const content = await readBoundedFile(await storePath(root, doc), 10 * 1024 * 1024);
|
|
2575
|
-
if (content === null)
|
|
3336
|
+
if (content === null)
|
|
3337
|
+
throw new Error("Context file is not regular or exceeds 10 MiB: " + doc);
|
|
2576
3338
|
docs.push([doc, digest(content)]);
|
|
2577
3339
|
} catch (error) {
|
|
2578
3340
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -2589,7 +3351,9 @@ async function readStableAudit(root, checks, options) {
|
|
|
2589
3351
|
const before = await docState(root);
|
|
2590
3352
|
const report = await computeAudit(root, { ...options, checks });
|
|
2591
3353
|
if (head !== await getCurrentGitHash(root) || before !== await docState(root) || report && report.headHash !== head) {
|
|
2592
|
-
throw new Error(
|
|
3354
|
+
throw new Error(
|
|
3355
|
+
"HEAD or context files changed during the audit; retry against a stable checkout."
|
|
3356
|
+
);
|
|
2593
3357
|
}
|
|
2594
3358
|
return report;
|
|
2595
3359
|
}
|
|
@@ -2617,7 +3381,11 @@ async function withRepairSession(root, options, run, validate, cachedAudit) {
|
|
|
2617
3381
|
}
|
|
2618
3382
|
return computeAudit(root, { ...options, checks });
|
|
2619
3383
|
}),
|
|
2620
|
-
document: (file) => memo(
|
|
3384
|
+
document: (file) => memo(
|
|
3385
|
+
documents,
|
|
3386
|
+
file,
|
|
3387
|
+
async () => readBoundedFile(await storePath(root, file), 10 * 1024 * 1024)
|
|
3388
|
+
),
|
|
2621
3389
|
history: (head2) => memo(history, head2, () => getChangesWithStatus(root, head2)),
|
|
2622
3390
|
assessment: (dir, finding, head2) => memo(assessments, digest([dir, finding, head2]), () => assessAdvisory(dir, finding, head2))
|
|
2623
3391
|
};
|
|
@@ -2630,11 +3398,12 @@ async function withRepairSession(root, options, run, validate, cachedAudit) {
|
|
|
2630
3398
|
return result;
|
|
2631
3399
|
}
|
|
2632
3400
|
async function prepareOriginal(rootDir, checks, options, session) {
|
|
2633
|
-
const root = await
|
|
2634
|
-
const selected =
|
|
3401
|
+
const root = await fs11.realpath(rootDir);
|
|
3402
|
+
const selected = z10.array(checkSchema).nonempty().parse(checks);
|
|
2635
3403
|
const report = await (session ? session.audit(selected) : stableAudit(root, selected, options));
|
|
2636
3404
|
if (!report) throw new Error("No context files found to prepare a repair.");
|
|
2637
|
-
if (!report.gitAvailable)
|
|
3405
|
+
if (!report.gitAvailable)
|
|
3406
|
+
throw new Error("Readable Git history is required to prepare a repair.");
|
|
2638
3407
|
const storedReport = reportSchema.parse(report);
|
|
2639
3408
|
const payload = {
|
|
2640
3409
|
kind: "mason-audit-repair",
|
|
@@ -2647,13 +3416,15 @@ async function prepareOriginal(rootDir, checks, options, session) {
|
|
|
2647
3416
|
return { version: 1, action: "prepare", baselinePath, report };
|
|
2648
3417
|
}
|
|
2649
3418
|
async function loadRepairBaseline(rootDir, baselinePath) {
|
|
2650
|
-
const root = await
|
|
3419
|
+
const root = await fs11.realpath(rootDir);
|
|
2651
3420
|
const declaredRoot = path20.resolve(rootDir);
|
|
2652
3421
|
const relative = path20.isAbsolute(baselinePath) ? path20.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
|
|
2653
3422
|
const stored = baselineSchema.parse(await readStoreJson(root, relative));
|
|
2654
3423
|
const { digest: savedDigest, ...payload } = stored;
|
|
2655
|
-
if (digest(payload) !== savedDigest)
|
|
2656
|
-
|
|
3424
|
+
if (digest(payload) !== savedDigest)
|
|
3425
|
+
throw new Error("Repair baseline was modified; use the original baseline.");
|
|
3426
|
+
if (stored.report.root !== root)
|
|
3427
|
+
throw new Error("Repair baseline belongs to a different repository.");
|
|
2657
3428
|
const original = stored.report;
|
|
2658
3429
|
return { root, relative, original };
|
|
2659
3430
|
}
|
|
@@ -2671,14 +3442,19 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2671
3442
|
if (!current) diagnostics.push("No context files remain available to audit.");
|
|
2672
3443
|
else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
|
|
2673
3444
|
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))
|
|
3445
|
+
if (!allFindings(original).some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path))
|
|
3446
|
+
continue;
|
|
2675
3447
|
const content = await (session ? session.document(doc.path) : readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024));
|
|
2676
3448
|
if (content === null || !content.trim()) {
|
|
2677
|
-
diagnostics.push(
|
|
3449
|
+
diagnostics.push(
|
|
3450
|
+
"Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair."
|
|
3451
|
+
);
|
|
2678
3452
|
}
|
|
2679
3453
|
}
|
|
2680
3454
|
if (await (session ? session.history(original.headHash) : getChangesWithStatus(root, original.headHash)) === null) {
|
|
2681
|
-
diagnostics.push(
|
|
3455
|
+
diagnostics.push(
|
|
3456
|
+
"The original audit commit is unavailable; repair history cannot be verified."
|
|
3457
|
+
);
|
|
2682
3458
|
}
|
|
2683
3459
|
} catch (error) {
|
|
2684
3460
|
diagnostics.push(error instanceof Error ? error.message : String(error));
|
|
@@ -2686,63 +3462,116 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
|
2686
3462
|
const currentById = new Map((current ? allFindings(current) : []).map((f) => [findingId(f), f]));
|
|
2687
3463
|
const originalFindings = allFindings(original);
|
|
2688
3464
|
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
|
-
|
|
3465
|
+
const missingDocs = original.docs.filter(
|
|
3466
|
+
(doc) => !current?.docs.some((d) => d.path === doc.path)
|
|
3467
|
+
);
|
|
3468
|
+
for (const doc of missingDocs)
|
|
3469
|
+
diagnostics.push(
|
|
3470
|
+
"Original context file " + doc.path + " is unavailable; removing it does not verify a repair."
|
|
3471
|
+
);
|
|
3472
|
+
const findings = await Promise.all(
|
|
3473
|
+
originalFindings.map(async (finding) => {
|
|
3474
|
+
const id = findingId(finding);
|
|
3475
|
+
const now = currentById.get(id);
|
|
3476
|
+
const base = { id, original: finding, ...now ? { current: now } : {} };
|
|
3477
|
+
if (diagnostics.length || !current) {
|
|
3478
|
+
return {
|
|
3479
|
+
...base,
|
|
3480
|
+
status: "unverified",
|
|
3481
|
+
reason: "The original audit scope could not be verified. See diagnostics."
|
|
3482
|
+
};
|
|
3483
|
+
}
|
|
3484
|
+
if (now && ("confidence" in finding || "confidence" in now)) {
|
|
3485
|
+
return {
|
|
3486
|
+
...base,
|
|
3487
|
+
status: "unresolved",
|
|
3488
|
+
reason: "The original check still reports this claim."
|
|
3489
|
+
};
|
|
3490
|
+
}
|
|
3491
|
+
if (current.excludedChecks?.some(
|
|
3492
|
+
(excluded) => excluded.check === finding.type && excluded.doc === finding.anchor.doc
|
|
3493
|
+
)) {
|
|
3494
|
+
return {
|
|
3495
|
+
...base,
|
|
3496
|
+
status: "resolved",
|
|
3497
|
+
reason: "Excluded by project audit policy; this is not a correctness assessment. Original evidence is retained."
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
const skipped = current.skippedChecks.filter(
|
|
3501
|
+
(s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc)
|
|
3502
|
+
);
|
|
3503
|
+
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
3504
|
+
return {
|
|
3505
|
+
...base,
|
|
3506
|
+
status: "unverified",
|
|
3507
|
+
reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run."
|
|
3508
|
+
};
|
|
3509
|
+
}
|
|
3510
|
+
const review = await (session?.assessment ?? assessAdvisory)(
|
|
3511
|
+
root,
|
|
3512
|
+
now ?? finding,
|
|
3513
|
+
original.headHash
|
|
3514
|
+
);
|
|
3515
|
+
if (review) {
|
|
3516
|
+
if (review.status === "unverified")
|
|
3517
|
+
return { ...base, review, status: "unverified", reason: review.reason };
|
|
3518
|
+
if (review.status === "current")
|
|
3519
|
+
return { ...base, review, status: "resolved", reason: review.reason };
|
|
3520
|
+
return { ...base, review, status: "review-required", reason: review.reason };
|
|
3521
|
+
}
|
|
3522
|
+
if (!("confidence" in finding) && finding.evidence.kind === "doc-behind-manifests" && !finding.evidence.matches?.length && (!now || !("confidence" in now) && now.resolution === "informational")) {
|
|
3523
|
+
return {
|
|
3524
|
+
...base,
|
|
3525
|
+
status: "resolved",
|
|
3526
|
+
reason: "Manifest recency alone establishes no affected passage. Retained as information; no assessment is required."
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3529
|
+
if (!("confidence" in finding) && (!recheckable(finding) || now)) {
|
|
3530
|
+
return {
|
|
3531
|
+
...base,
|
|
3532
|
+
status: "review-required",
|
|
3533
|
+
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."
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
2718
3536
|
return {
|
|
2719
3537
|
...base,
|
|
2720
|
-
status: "
|
|
2721
|
-
reason:
|
|
3538
|
+
status: "resolved",
|
|
3539
|
+
reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness."
|
|
2722
3540
|
};
|
|
2723
|
-
}
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
3541
|
+
})
|
|
3542
|
+
);
|
|
3543
|
+
if (!session)
|
|
3544
|
+
try {
|
|
3545
|
+
if (reviewDigest !== digest(await advisoryReviewInventory(root)) || current?.headHash !== await getCurrentGitHash(root)) {
|
|
3546
|
+
diagnostics.push(
|
|
3547
|
+
"HEAD or advisory review records changed during verification; repeat against stable evidence."
|
|
3548
|
+
);
|
|
3549
|
+
for (const finding of findings) {
|
|
3550
|
+
finding.status = "unverified";
|
|
3551
|
+
finding.reason = diagnostics.at(-1);
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
} catch (error) {
|
|
3555
|
+
diagnostics.push(String(error));
|
|
2729
3556
|
for (const finding of findings) {
|
|
2730
3557
|
finding.status = "unverified";
|
|
2731
|
-
finding.reason =
|
|
3558
|
+
finding.reason = "Advisory review evidence became unavailable during verification.";
|
|
2732
3559
|
}
|
|
2733
3560
|
}
|
|
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
3561
|
const newFindings = [...currentById].filter(([id]) => !originalIds.has(id)).map(([, f]) => f);
|
|
2742
|
-
const reviewedIds = new Set(
|
|
2743
|
-
|
|
3562
|
+
const reviewedIds = new Set(
|
|
3563
|
+
current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id)
|
|
3564
|
+
);
|
|
3565
|
+
const counts = {
|
|
3566
|
+
resolved: 0,
|
|
3567
|
+
unresolved: 0,
|
|
3568
|
+
"review-required": 0,
|
|
3569
|
+
unverified: 0
|
|
3570
|
+
};
|
|
2744
3571
|
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(
|
|
3572
|
+
const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some(
|
|
3573
|
+
(f) => !("confidence" in f) && f.resolution !== "informational" && !reviewedIds.has(findingId(f))
|
|
3574
|
+
);
|
|
2746
3575
|
const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
|
|
2747
3576
|
return {
|
|
2748
3577
|
version: 1,
|
|
@@ -2765,10 +3594,14 @@ function repairExitCode(report) {
|
|
|
2765
3594
|
function formatRepairSummary(report) {
|
|
2766
3595
|
return [
|
|
2767
3596
|
"Repair verification: " + report.status + ". Baseline: " + report.baselinePath,
|
|
2768
|
-
...report.findings.map(
|
|
3597
|
+
...report.findings.map(
|
|
3598
|
+
(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
|
|
3599
|
+
),
|
|
2769
3600
|
...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
|
|
2770
3601
|
...report.diagnostics.map((d) => " [unverified] " + d),
|
|
2771
|
-
...(report.currentAudit?.skippedChecks ?? []).map(
|
|
3602
|
+
...(report.currentAudit?.skippedChecks ?? []).map(
|
|
3603
|
+
(s) => " [skipped] " + s.check + ": " + s.reason
|
|
3604
|
+
),
|
|
2772
3605
|
report.scope
|
|
2773
3606
|
].join("\n");
|
|
2774
3607
|
}
|
|
@@ -2788,109 +3621,149 @@ var init_repair = __esm({
|
|
|
2788
3621
|
init_findings();
|
|
2789
3622
|
init_advisory_review();
|
|
2790
3623
|
init_findings();
|
|
2791
|
-
count2 =
|
|
2792
|
-
reportSchema =
|
|
2793
|
-
version:
|
|
2794
|
-
root:
|
|
2795
|
-
gitAvailable:
|
|
3624
|
+
count2 = z10.number().int().nonnegative();
|
|
3625
|
+
reportSchema = z10.object({
|
|
3626
|
+
version: z10.literal(1),
|
|
3627
|
+
root: z10.string(),
|
|
3628
|
+
gitAvailable: z10.literal(true),
|
|
2796
3629
|
headHash: commitSchema.shape.hash,
|
|
2797
|
-
checksRun:
|
|
2798
|
-
docs:
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
3630
|
+
checksRun: z10.array(checkSchema).nonempty(),
|
|
3631
|
+
docs: z10.array(
|
|
3632
|
+
z10.object({
|
|
3633
|
+
path: z10.string().refine((value) => normalizeRepoPath(value) === value),
|
|
3634
|
+
scope: z10.string().optional(),
|
|
3635
|
+
kind: z10.enum(["instructions", "readme", "guide"]).optional(),
|
|
3636
|
+
lastCommit: commitSchema.nullable(),
|
|
3637
|
+
dirty: z10.boolean(),
|
|
3638
|
+
lineCount: count2
|
|
3639
|
+
})
|
|
3640
|
+
).nonempty(),
|
|
3641
|
+
decisionsChecked: z10.boolean(),
|
|
3642
|
+
clean: z10.boolean(),
|
|
3643
|
+
excludedChecks: z10.array(z10.object({ check: checkSchema, doc: z10.string() })).optional(),
|
|
3644
|
+
issues: z10.array(issueSchema),
|
|
3645
|
+
advisories: z10.array(advisorySchema),
|
|
3646
|
+
suppressedAdvisories: z10.array(advisorySchema).optional(),
|
|
3647
|
+
skippedChecks: z10.array(
|
|
3648
|
+
z10.object({ check: z10.string(), reason: z10.string(), doc: z10.string().optional() })
|
|
3649
|
+
)
|
|
2813
3650
|
});
|
|
2814
|
-
baselineSchema =
|
|
2815
|
-
kind:
|
|
2816
|
-
version:
|
|
2817
|
-
createdAt:
|
|
3651
|
+
baselineSchema = z10.object({
|
|
3652
|
+
kind: z10.literal("mason-audit-repair"),
|
|
3653
|
+
version: z10.literal(1),
|
|
3654
|
+
createdAt: z10.string().datetime(),
|
|
2818
3655
|
report: reportSchema,
|
|
2819
|
-
digest:
|
|
3656
|
+
digest: z10.string().regex(/^[a-f0-9]{64}$/)
|
|
2820
3657
|
});
|
|
2821
|
-
repairFindingSchema =
|
|
2822
|
-
id:
|
|
2823
|
-
original:
|
|
2824
|
-
status:
|
|
2825
|
-
reason:
|
|
2826
|
-
current:
|
|
3658
|
+
repairFindingSchema = z10.object({
|
|
3659
|
+
id: z10.string(),
|
|
3660
|
+
original: z10.union([issueSchema, advisorySchema]),
|
|
3661
|
+
status: z10.enum(["resolved", "unresolved", "review-required", "unverified"]),
|
|
3662
|
+
reason: z10.string(),
|
|
3663
|
+
current: z10.union([issueSchema, advisorySchema]).optional(),
|
|
2827
3664
|
review: reviewSummarySchema.optional()
|
|
2828
3665
|
});
|
|
2829
3666
|
}
|
|
2830
3667
|
});
|
|
2831
3668
|
|
|
2832
3669
|
// src/audit/advisory-review.ts
|
|
2833
|
-
import
|
|
2834
|
-
import { z as
|
|
3670
|
+
import fs12 from "fs/promises";
|
|
3671
|
+
import { z as z11 } from "zod";
|
|
2835
3672
|
async function readRecord(root, id) {
|
|
2836
3673
|
const raw = await readStoreJson(root, recordPath(id));
|
|
2837
3674
|
if (raw === null) return null;
|
|
2838
3675
|
const record = recordSchema.parse(raw);
|
|
2839
3676
|
const { digest: stored, ...payload } = record;
|
|
2840
|
-
if (record.id !== id || digest(payload) !== stored || record.events.some(
|
|
3677
|
+
if (record.id !== id || digest(payload) !== stored || record.events.some(
|
|
3678
|
+
(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)
|
|
3679
|
+
)) {
|
|
2841
3680
|
throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
|
|
2842
3681
|
}
|
|
2843
3682
|
return record;
|
|
2844
3683
|
}
|
|
2845
3684
|
function evidenceFingerprint(evidence, reason) {
|
|
2846
|
-
return digest([
|
|
3685
|
+
return digest([
|
|
3686
|
+
evidence.scope,
|
|
3687
|
+
evidence.tree,
|
|
3688
|
+
...reason === "no-dependency-claims" ? [] : [evidence.lastScopeCommit],
|
|
3689
|
+
evidence.paths
|
|
3690
|
+
]);
|
|
2847
3691
|
}
|
|
2848
3692
|
function scopes(finding, reason) {
|
|
2849
3693
|
const files = [finding.anchor.doc], e = finding.evidence;
|
|
2850
|
-
if (e.kind === "decision-anchor")
|
|
2851
|
-
|
|
2852
|
-
if (e.kind === "doc-behind-manifests")
|
|
2853
|
-
|
|
2854
|
-
|
|
3694
|
+
if (e.kind === "decision-anchor")
|
|
3695
|
+
throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
|
|
3696
|
+
if (reason === "no-dependency-claims" && e.kind === "doc-behind-manifests")
|
|
3697
|
+
return [`:(literal)${finding.anchor.doc}`];
|
|
3698
|
+
if (e.kind === "doc-behind-manifests")
|
|
3699
|
+
return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
|
|
3700
|
+
if (e.kind === "missing-path")
|
|
3701
|
+
files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
|
|
3702
|
+
else if (e.kind === "missing-script")
|
|
3703
|
+
files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
|
|
2855
3704
|
else if (e.kind === "unmentioned-dir") files.push(e.dir, ...e.checkedDocs);
|
|
2856
3705
|
else throw new Error("This advisory has no supported review scope.");
|
|
2857
3706
|
return [...new Set(files)].map((file) => {
|
|
2858
|
-
if (normalizeRepoPath(file) !== file)
|
|
3707
|
+
if (normalizeRepoPath(file) !== file)
|
|
3708
|
+
throw new Error("Advisory contains an invalid scoped path: " + file);
|
|
2859
3709
|
return `:(literal)${file}`;
|
|
2860
3710
|
}).sort();
|
|
2861
3711
|
}
|
|
2862
3712
|
async function reviewEvidence(root, finding, reason) {
|
|
2863
|
-
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc)
|
|
3713
|
+
if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc)
|
|
3714
|
+
throw new Error("Invalid advisory document path.");
|
|
2864
3715
|
const scope = scopes(finding, reason);
|
|
2865
3716
|
const head = (await git2(root, "rev-parse", "HEAD")).trim();
|
|
2866
3717
|
const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
|
|
2867
3718
|
if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
|
|
2868
3719
|
const [tree, status, lastScopeCommit, document] = await Promise.all([
|
|
2869
|
-
tracked.length ? git2(
|
|
3720
|
+
tracked.length ? git2(
|
|
3721
|
+
root,
|
|
3722
|
+
"ls-tree",
|
|
3723
|
+
"-r",
|
|
3724
|
+
"-z",
|
|
3725
|
+
"HEAD",
|
|
3726
|
+
"--",
|
|
3727
|
+
...tracked.map((file) => `:(literal)${file}`)
|
|
3728
|
+
) : "",
|
|
2870
3729
|
git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...scope),
|
|
2871
3730
|
git2(root, "log", "-1", "--format=%H", "HEAD", "--", ...scope).then((value) => value.trim()),
|
|
2872
3731
|
readAuditInput(root, finding.anchor.doc)
|
|
2873
3732
|
]);
|
|
2874
|
-
if (!document?.trim())
|
|
2875
|
-
|
|
3733
|
+
if (!document?.trim())
|
|
3734
|
+
throw new Error("Original advisory document is missing, empty or unreadable.");
|
|
3735
|
+
if (tree.length > 2 * 1024 * 1024 || tree.split("\0").length > 2e4)
|
|
3736
|
+
throw new Error(
|
|
3737
|
+
"Advisory scope is too large for a complete review; narrow the documentation scope."
|
|
3738
|
+
);
|
|
2876
3739
|
for (const entry of tree.split("\0").filter(Boolean)) {
|
|
2877
3740
|
const tab = entry.indexOf(" "), file = entry.slice(tab + 1);
|
|
2878
|
-
if (entry.startsWith("120000 ") || entry.startsWith("160000 "))
|
|
3741
|
+
if (entry.startsWith("120000 ") || entry.startsWith("160000 "))
|
|
3742
|
+
throw new Error("Symlink or submodule in advisory review scope: " + file);
|
|
2879
3743
|
await storePath(root, file);
|
|
2880
3744
|
}
|
|
2881
3745
|
const paths = finding.evidence.kind === "missing-path" ? await Promise.all(
|
|
2882
|
-
(finding.evidence.scope?.candidates ?? [
|
|
3746
|
+
(finding.evidence.scope?.candidates ?? [
|
|
3747
|
+
finding.evidence.resolvedPath ?? finding.evidence.claimed
|
|
3748
|
+
]).map(async (file) => ({ path: file, exists: await pathExists(root, file) }))
|
|
2883
3749
|
) : [];
|
|
2884
3750
|
let dirty = status.length > 0;
|
|
2885
3751
|
if (finding.evidence.kind === "missing-script") {
|
|
2886
|
-
for (const file of [
|
|
3752
|
+
for (const file of [
|
|
3753
|
+
...finding.evidence.manifestsChecked,
|
|
3754
|
+
...finding.evidence.scope?.candidates ?? []
|
|
3755
|
+
]) {
|
|
2887
3756
|
if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
|
|
2888
3757
|
}
|
|
2889
3758
|
}
|
|
2890
|
-
const fingerprint = evidenceFingerprint(
|
|
2891
|
-
|
|
3759
|
+
const fingerprint = evidenceFingerprint(
|
|
3760
|
+
{ head, scope, tree, lastScopeCommit, paths, fingerprint: "" },
|
|
3761
|
+
reason
|
|
3762
|
+
);
|
|
3763
|
+
if ((await git2(root, "rev-parse", "HEAD")).trim() !== head)
|
|
3764
|
+
throw new Error("HEAD changed while collecting review evidence.");
|
|
2892
3765
|
return {
|
|
2893
|
-
...
|
|
3766
|
+
...evidenceSchema3.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
|
|
2894
3767
|
dirty,
|
|
2895
3768
|
documentDigest: digest(document)
|
|
2896
3769
|
};
|
|
@@ -2900,12 +3773,39 @@ async function decisionAssessment(root, finding, baselineHead) {
|
|
|
2900
3773
|
if (evidence.kind !== "decision-anchor") return null;
|
|
2901
3774
|
const id = findingId(finding);
|
|
2902
3775
|
const store = await loadDecisionStore(root);
|
|
2903
|
-
if (store.diagnostics.length)
|
|
3776
|
+
if (store.diagnostics.length)
|
|
3777
|
+
throw new Error(
|
|
3778
|
+
"Decision records have unavailable or malformed evidence; review cannot be confirmed."
|
|
3779
|
+
);
|
|
2904
3780
|
const record = store.records.find((record2) => record2.id === evidence.decisionId);
|
|
2905
3781
|
if (!record || record.version !== 2) return null;
|
|
2906
|
-
const
|
|
3782
|
+
const inspection = latestDecisionInspection(record);
|
|
3783
|
+
if (inspection && record.status === "active") {
|
|
3784
|
+
const capture = await captureAnchors(root, inspection.capture.anchors);
|
|
3785
|
+
let ancestor = true;
|
|
3786
|
+
try {
|
|
3787
|
+
await git2(root, "merge-base", "--is-ancestor", baselineHead, inspection.headHash);
|
|
3788
|
+
} catch {
|
|
3789
|
+
ancestor = false;
|
|
3790
|
+
}
|
|
3791
|
+
if (ancestor)
|
|
3792
|
+
return {
|
|
3793
|
+
id,
|
|
3794
|
+
status: !capture.complete || !inspection.capture.complete ? "unverified" : changedCaptureFiles(inspection.capture, capture).length ? "reopened" : "current",
|
|
3795
|
+
outcome: "no_contradiction_found",
|
|
3796
|
+
reviewer: inspection.inspector,
|
|
3797
|
+
note: inspection.note,
|
|
3798
|
+
reviewedHead: inspection.headHash,
|
|
3799
|
+
recordPath: `.mason/decisions/${record.id}.json`,
|
|
3800
|
+
reason: "A separate source inspection reports no contradiction; this does not accept or reaffirm the decision. Relevant anchor changes reopen the advisory."
|
|
3801
|
+
};
|
|
3802
|
+
}
|
|
3803
|
+
const events = record.history.filter(
|
|
3804
|
+
(event2) => ["accepted", "reaffirmed", "retired"].includes(event2.kind)
|
|
3805
|
+
);
|
|
2907
3806
|
const event = events.at(-1);
|
|
2908
|
-
if (!event?.evidence || !event.actor || !event.note || !event.evidence.historyAvailable || event.evidence.localChanges.length)
|
|
3807
|
+
if (!event?.evidence || !event.actor || !event.note || !event.evidence.historyAvailable || event.evidence.localChanges.length)
|
|
3808
|
+
return null;
|
|
2909
3809
|
try {
|
|
2910
3810
|
await git2(root, "merge-base", "--is-ancestor", baselineHead, event.evidence.headHash);
|
|
2911
3811
|
} catch {
|
|
@@ -2930,7 +3830,8 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
2930
3830
|
if ("confidence" in finding) return null;
|
|
2931
3831
|
const id = findingId(finding);
|
|
2932
3832
|
try {
|
|
2933
|
-
if (finding.evidence.kind === "decision-anchor")
|
|
3833
|
+
if (finding.evidence.kind === "decision-anchor")
|
|
3834
|
+
return await decisionAssessment(root, finding, baselineHead);
|
|
2934
3835
|
const record = await readRecord(root, id);
|
|
2935
3836
|
if (!record) return null;
|
|
2936
3837
|
const event = record.events.at(-1);
|
|
@@ -2944,37 +3845,63 @@ async function assessAdvisory(root, finding, baselineHead) {
|
|
|
2944
3845
|
outcome: event.outcome,
|
|
2945
3846
|
reviewedHead: event.evidence.head
|
|
2946
3847
|
};
|
|
2947
|
-
if (current.dirty || current.fingerprint !== event.evidence.fingerprint || digest(scopes(finding, event.reasonCode)) !== digest(event.evidence.scope))
|
|
2948
|
-
|
|
3848
|
+
if (current.dirty || current.fingerprint !== event.evidence.fingerprint || digest(scopes(finding, event.reasonCode)) !== digest(event.evidence.scope))
|
|
3849
|
+
return {
|
|
3850
|
+
...base,
|
|
3851
|
+
status: "reopened",
|
|
3852
|
+
reason: "Relevant document or repository evidence changed after the assessment; prepare a new review."
|
|
3853
|
+
};
|
|
3854
|
+
return {
|
|
3855
|
+
...base,
|
|
3856
|
+
status: event.outcome === "deferred" ? "deferred" : "current",
|
|
3857
|
+
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."
|
|
3858
|
+
};
|
|
2949
3859
|
} catch (error) {
|
|
2950
|
-
return {
|
|
3860
|
+
return {
|
|
3861
|
+
id,
|
|
3862
|
+
status: "unverified",
|
|
3863
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
3864
|
+
};
|
|
2951
3865
|
}
|
|
2952
3866
|
}
|
|
2953
3867
|
async function advisoryReviewInventory(root) {
|
|
2954
3868
|
let names;
|
|
2955
3869
|
try {
|
|
2956
|
-
names = await
|
|
3870
|
+
names = await fs12.readdir(await storePath(root, ".mason/reviews/advisories"));
|
|
2957
3871
|
} catch (error) {
|
|
2958
3872
|
if (error.code === "ENOENT") return [];
|
|
2959
3873
|
throw error;
|
|
2960
3874
|
}
|
|
2961
3875
|
if (names.length > 5e3) throw new Error("Advisory review inventory exceeds 5,000 records.");
|
|
2962
|
-
return Promise.all(
|
|
3876
|
+
return Promise.all(
|
|
3877
|
+
names.filter((name) => name.endsWith(".json")).sort().map(async (name) => [name, await readAuditInput(root, ".mason/reviews/advisories/" + name)])
|
|
3878
|
+
);
|
|
2963
3879
|
}
|
|
2964
3880
|
async function reviewAdvisory(rootDir, input) {
|
|
2965
|
-
const root = await
|
|
3881
|
+
const root = await fs12.realpath(rootDir), request = advisoryReviewRequest.parse(input);
|
|
2966
3882
|
const { loadRepairBaseline: loadRepairBaseline2 } = await Promise.resolve().then(() => (init_repair(), repair_exports));
|
|
2967
3883
|
const baseline = await loadRepairBaseline2(root, request.baselinePath);
|
|
2968
|
-
const finding = [
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
3884
|
+
const finding = [
|
|
3885
|
+
...baseline.original.advisories,
|
|
3886
|
+
...baseline.original.suppressedAdvisories ?? []
|
|
3887
|
+
].find((f) => findingId(f) === request.findingId);
|
|
3888
|
+
if (!finding)
|
|
3889
|
+
throw new Error(
|
|
3890
|
+
"No advisory with this findingId in the original baseline. Provable issues must be repaired and rechecked."
|
|
3891
|
+
);
|
|
3892
|
+
if (finding.evidence.kind === "decision-anchor")
|
|
3893
|
+
return {
|
|
3894
|
+
status: "decision-review-required",
|
|
3895
|
+
decisionId: finding.evidence.decisionId,
|
|
3896
|
+
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."
|
|
3897
|
+
};
|
|
2975
3898
|
if (request.action === "dismiss") {
|
|
2976
|
-
if (finding.type !== "deps-changed" || finding.evidence.kind !== "doc-behind-manifests")
|
|
2977
|
-
|
|
3899
|
+
if (finding.type !== "deps-changed" || finding.evidence.kind !== "doc-behind-manifests")
|
|
3900
|
+
throw new Error("One-step dismissal is only available for dependency advisories.");
|
|
3901
|
+
if (!request.reasonCode || !request.note)
|
|
3902
|
+
throw new Error(
|
|
3903
|
+
"Dismissal requires reasonCode and note; it does not require a reviewer or prepared token."
|
|
3904
|
+
);
|
|
2978
3905
|
} else if (request.reasonCode) throw new Error("reasonCode is only used with dismiss.");
|
|
2979
3906
|
const prepare = async () => {
|
|
2980
3907
|
const record = await readRecord(root, request.findingId);
|
|
@@ -2988,13 +3915,27 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
2988
3915
|
let diff = null, diffNote;
|
|
2989
3916
|
const from = finding.evidence.kind === "doc-behind-manifests" ? finding.evidence.docLastCommit.hash : baseline.original.headHash;
|
|
2990
3917
|
try {
|
|
2991
|
-
const full = await git2(
|
|
3918
|
+
const full = await git2(
|
|
3919
|
+
root,
|
|
3920
|
+
"diff",
|
|
3921
|
+
"--no-ext-diff",
|
|
3922
|
+
"--no-textconv",
|
|
3923
|
+
"--no-color",
|
|
3924
|
+
"--no-renames",
|
|
3925
|
+
"-U3",
|
|
3926
|
+
from,
|
|
3927
|
+
state.evidence.head,
|
|
3928
|
+
"--",
|
|
3929
|
+
...state.evidence.scope
|
|
3930
|
+
);
|
|
2992
3931
|
diff = full.slice(0, 16e3);
|
|
2993
|
-
if (full.length > diff.length)
|
|
3932
|
+
if (full.length > diff.length)
|
|
3933
|
+
diffNote = "Diff preview truncated; inspect the full scoped diff before reviewing.";
|
|
2994
3934
|
} catch {
|
|
2995
3935
|
diffNote = "Diff preview unavailable; inspect the original cited evidence directly before reviewing.";
|
|
2996
3936
|
}
|
|
2997
|
-
if ((await prepare()).token !== state.token)
|
|
3937
|
+
if ((await prepare()).token !== state.token)
|
|
3938
|
+
throw new Error("Review evidence changed during preparation; retry.");
|
|
2998
3939
|
return {
|
|
2999
3940
|
status: "prepared",
|
|
3000
3941
|
findingId: request.findingId,
|
|
@@ -3007,12 +3948,22 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3007
3948
|
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
3949
|
};
|
|
3009
3950
|
}
|
|
3010
|
-
if (request.action !== "dismiss" && (!request.reviewer || !request.note || !request.reviewToken))
|
|
3951
|
+
if (request.action !== "dismiss" && (!request.reviewer || !request.note || !request.reviewToken))
|
|
3952
|
+
throw new Error(
|
|
3953
|
+
"Prepare first; recording an assessment requires reviewToken, reviewer and note."
|
|
3954
|
+
);
|
|
3011
3955
|
return withLock(root, ".mason/reports/advisory-review-locks/" + request.findingId, async () => {
|
|
3012
3956
|
const state = await prepare();
|
|
3013
|
-
if (request.action !== "dismiss" && state.token !== request.reviewToken)
|
|
3014
|
-
|
|
3015
|
-
|
|
3957
|
+
if (request.action !== "dismiss" && state.token !== request.reviewToken)
|
|
3958
|
+
throw new Error(
|
|
3959
|
+
"Review conflict: evidence or review history changed since preparation. Prepare and inspect a new review."
|
|
3960
|
+
);
|
|
3961
|
+
if (state.evidence.dirty)
|
|
3962
|
+
throw new Error(
|
|
3963
|
+
"Commit relevant document and repository edits before recording the assessment. Unrelated local work may remain."
|
|
3964
|
+
);
|
|
3965
|
+
if ((state.record?.events.length ?? 0) >= 200)
|
|
3966
|
+
throw new Error("Review history limit reached; existing events were retained.");
|
|
3016
3967
|
const { dirty, documentDigest, ...evidence } = state.evidence;
|
|
3017
3968
|
const event = eventSchema2.parse({
|
|
3018
3969
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3023,9 +3974,17 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3023
3974
|
finding,
|
|
3024
3975
|
evidence
|
|
3025
3976
|
});
|
|
3026
|
-
const payload = {
|
|
3027
|
-
|
|
3028
|
-
|
|
3977
|
+
const payload = {
|
|
3978
|
+
version: 1,
|
|
3979
|
+
id: request.findingId,
|
|
3980
|
+
events: [...state.record?.events ?? [], event]
|
|
3981
|
+
};
|
|
3982
|
+
if ((await prepare()).token !== state.token)
|
|
3983
|
+
throw new Error("Review conflict: evidence changed while recording the assessment.");
|
|
3984
|
+
await writeStoreJson(root, recordPath(request.findingId), {
|
|
3985
|
+
...payload,
|
|
3986
|
+
digest: digest(payload)
|
|
3987
|
+
});
|
|
3029
3988
|
return {
|
|
3030
3989
|
status: "recorded",
|
|
3031
3990
|
recordPath: recordPath(request.findingId),
|
|
@@ -3034,7 +3993,7 @@ async function reviewAdvisory(rootDir, input) {
|
|
|
3034
3993
|
};
|
|
3035
3994
|
});
|
|
3036
3995
|
}
|
|
3037
|
-
var git2, hashSchema, commitSchema2,
|
|
3996
|
+
var git2, hashSchema, commitSchema2, evidenceSchema3, dismissalReasonSchema, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
|
|
3038
3997
|
var init_advisory_review = __esm({
|
|
3039
3998
|
"src/audit/advisory-review.ts"() {
|
|
3040
3999
|
"use strict";
|
|
@@ -3048,43 +4007,62 @@ var init_advisory_review = __esm({
|
|
|
3048
4007
|
init_paths();
|
|
3049
4008
|
init_store();
|
|
3050
4009
|
init_decisions();
|
|
4010
|
+
init_provenance();
|
|
4011
|
+
init_anchors();
|
|
3051
4012
|
init_drift2();
|
|
3052
4013
|
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
|
-
|
|
4014
|
+
hashSchema = z11.string().regex(/^[a-f0-9]{64}$/);
|
|
4015
|
+
commitSchema2 = z11.string().regex(/^[a-f0-9]{40,64}$/);
|
|
4016
|
+
evidenceSchema3 = z11.object({
|
|
3056
4017
|
head: commitSchema2,
|
|
3057
4018
|
fingerprint: hashSchema,
|
|
3058
|
-
scope:
|
|
3059
|
-
tree:
|
|
4019
|
+
scope: z11.array(z11.string()).min(1).max(1e3),
|
|
4020
|
+
tree: z11.string().max(2 * 1024 * 1024),
|
|
3060
4021
|
lastScopeCommit: commitSchema2,
|
|
3061
|
-
paths:
|
|
4022
|
+
paths: z11.array(z11.object({ path: z11.string(), exists: z11.boolean() })).max(1e3)
|
|
3062
4023
|
});
|
|
3063
|
-
dismissalReasonSchema =
|
|
3064
|
-
eventSchema2 =
|
|
3065
|
-
at:
|
|
3066
|
-
reviewer:
|
|
3067
|
-
note:
|
|
3068
|
-
outcome:
|
|
4024
|
+
dismissalReasonSchema = z11.enum(["no-dependency-claims", "unrelated-manifest-change"]);
|
|
4025
|
+
eventSchema2 = z11.object({
|
|
4026
|
+
at: z11.string().datetime(),
|
|
4027
|
+
reviewer: z11.string().trim().min(1).max(200).optional(),
|
|
4028
|
+
note: z11.string().trim().min(1).max(2500),
|
|
4029
|
+
outcome: z11.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
|
|
3069
4030
|
reasonCode: dismissalReasonSchema.optional(),
|
|
3070
|
-
finding: advisorySchema.refine(
|
|
3071
|
-
|
|
4031
|
+
finding: advisorySchema.refine(
|
|
4032
|
+
(finding) => finding.evidence.kind !== "decision-anchor",
|
|
4033
|
+
"Decision findings use decision reviews"
|
|
4034
|
+
),
|
|
4035
|
+
evidence: evidenceSchema3
|
|
4036
|
+
});
|
|
4037
|
+
recordSchema = z11.object({
|
|
4038
|
+
version: z11.literal(1),
|
|
4039
|
+
id: hashSchema,
|
|
4040
|
+
events: z11.array(eventSchema2).min(1).max(200),
|
|
4041
|
+
digest: hashSchema
|
|
3072
4042
|
});
|
|
3073
|
-
|
|
3074
|
-
reviewSummarySchema = z8.object({
|
|
4043
|
+
reviewSummarySchema = z11.object({
|
|
3075
4044
|
id: hashSchema,
|
|
3076
|
-
status:
|
|
3077
|
-
reason:
|
|
3078
|
-
recordPath:
|
|
3079
|
-
reviewer:
|
|
3080
|
-
note:
|
|
3081
|
-
outcome:
|
|
3082
|
-
|
|
4045
|
+
status: z11.enum(["current", "reopened", "deferred", "unverified"]),
|
|
4046
|
+
reason: z11.string(),
|
|
4047
|
+
recordPath: z11.string().optional(),
|
|
4048
|
+
reviewer: z11.string().optional(),
|
|
4049
|
+
note: z11.string().optional(),
|
|
4050
|
+
outcome: z11.enum([
|
|
4051
|
+
"addressed",
|
|
4052
|
+
"inapplicable",
|
|
4053
|
+
"deferred",
|
|
4054
|
+
"accepted",
|
|
4055
|
+
"reaffirmed",
|
|
4056
|
+
"retired",
|
|
4057
|
+
"dismissed",
|
|
4058
|
+
"no_contradiction_found"
|
|
4059
|
+
]).optional(),
|
|
4060
|
+
reviewedHead: z11.string().optional()
|
|
3083
4061
|
});
|
|
3084
|
-
advisoryReviewRequest =
|
|
3085
|
-
baselinePath:
|
|
4062
|
+
advisoryReviewRequest = z11.object({
|
|
4063
|
+
baselinePath: z11.string().min(1),
|
|
3086
4064
|
findingId: hashSchema,
|
|
3087
|
-
action:
|
|
4065
|
+
action: z11.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
|
|
3088
4066
|
reasonCode: dismissalReasonSchema.optional(),
|
|
3089
4067
|
reviewer: eventSchema2.shape.reviewer.optional(),
|
|
3090
4068
|
note: eventSchema2.shape.note.optional(),
|
|
@@ -3095,14 +4073,17 @@ var init_advisory_review = __esm({
|
|
|
3095
4073
|
});
|
|
3096
4074
|
|
|
3097
4075
|
// src/audit/audit.ts
|
|
3098
|
-
import
|
|
4076
|
+
import fs13 from "fs/promises";
|
|
3099
4077
|
import path21 from "path";
|
|
3100
4078
|
async function computeAudit(rootDir, options = {}) {
|
|
3101
4079
|
return profilePhase("audit.current", () => auditCurrent(rootDir, options));
|
|
3102
4080
|
}
|
|
3103
4081
|
async function auditCurrent(rootDir, options) {
|
|
3104
4082
|
const resolvedRoot = path21.resolve(rootDir);
|
|
3105
|
-
const [docs, headHash] = await Promise.all([
|
|
4083
|
+
const [docs, headHash] = await Promise.all([
|
|
4084
|
+
discoverDocs(resolvedRoot),
|
|
4085
|
+
getCurrentGitHash(resolvedRoot)
|
|
4086
|
+
]);
|
|
3106
4087
|
if (docs.length === 0) return null;
|
|
3107
4088
|
const report = {
|
|
3108
4089
|
version: 1,
|
|
@@ -3130,12 +4111,13 @@ async function auditCurrent(rootDir, options) {
|
|
|
3130
4111
|
const changesByCommit = /* @__PURE__ */ new Map();
|
|
3131
4112
|
for (const doc of docs) {
|
|
3132
4113
|
const hash = doc.lastCommit?.hash;
|
|
3133
|
-
if (hash && !changesByCommit.has(hash))
|
|
4114
|
+
if (hash && !changesByCommit.has(hash))
|
|
4115
|
+
changesByCommit.set(hash, await getChangesWithStatus(resolvedRoot, hash));
|
|
3134
4116
|
changesSinceDoc.set(doc.path, hash ? changesByCommit.get(hash) : null);
|
|
3135
4117
|
}
|
|
3136
4118
|
let decisionsPresent = false;
|
|
3137
4119
|
try {
|
|
3138
|
-
await
|
|
4120
|
+
await fs13.access(path21.join(resolvedRoot, ".mason", "decisions"));
|
|
3139
4121
|
decisionsPresent = true;
|
|
3140
4122
|
} catch {
|
|
3141
4123
|
}
|
|
@@ -3152,21 +4134,35 @@ async function auditCurrent(rootDir, options) {
|
|
|
3152
4134
|
for (const name of ALL_CHECKS) {
|
|
3153
4135
|
if (!selected.includes(name)) continue;
|
|
3154
4136
|
const exclusions = policy?.exclude?.[name] ?? [];
|
|
3155
|
-
const included = await includedCheckPaths(
|
|
4137
|
+
const included = await includedCheckPaths(
|
|
4138
|
+
resolvedRoot,
|
|
4139
|
+
docs.map((doc) => doc.path),
|
|
4140
|
+
exclusions
|
|
4141
|
+
);
|
|
3156
4142
|
const excluded = docs.filter((doc) => !included.has(doc.path)).map((doc) => ({ check: name, doc: doc.path }));
|
|
3157
4143
|
if (excluded.length) (report.excludedChecks ??= []).push(...excluded);
|
|
3158
4144
|
const scoped = { ...ctx, docs: docs.filter((doc) => included.has(doc.path)) };
|
|
3159
4145
|
const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, scoped) : CHECKS[name](scoped));
|
|
3160
4146
|
report.checksRun.push(name);
|
|
3161
4147
|
report.issues.push(...issues);
|
|
3162
|
-
const advisoryPaths = await includedCheckPaths(
|
|
4148
|
+
const advisoryPaths = await includedCheckPaths(
|
|
4149
|
+
resolvedRoot,
|
|
4150
|
+
[...new Set(advisories.map((f) => f.anchor.doc))],
|
|
4151
|
+
exclusions
|
|
4152
|
+
);
|
|
3163
4153
|
report.advisories.push(...advisories.filter((f) => advisoryPaths.has(f.anchor.doc)));
|
|
3164
|
-
for (const f of advisories)
|
|
4154
|
+
for (const f of advisories)
|
|
4155
|
+
if (!advisoryPaths.has(f.anchor.doc))
|
|
4156
|
+
(report.excludedChecks ??= []).push({ check: name, doc: f.anchor.doc });
|
|
3165
4157
|
report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
|
|
3166
4158
|
report.skippedChecks.push(...skipped);
|
|
3167
4159
|
}
|
|
3168
4160
|
report.clean = report.issues.length === 0;
|
|
3169
|
-
const reviews = (await Promise.all(
|
|
4161
|
+
const reviews = (await Promise.all(
|
|
4162
|
+
[...report.advisories, ...report.suppressedAdvisories].map(
|
|
4163
|
+
(finding) => assessAdvisory(resolvedRoot, finding, headHash)
|
|
4164
|
+
)
|
|
4165
|
+
)).filter((review) => review !== null);
|
|
3170
4166
|
if (reviews.length) report.advisoryReviews = reviews;
|
|
3171
4167
|
return report;
|
|
3172
4168
|
}
|
|
@@ -3228,12 +4224,23 @@ async function runAdvisoryCli(args, io = { out: (text2) => console.log(text2), e
|
|
|
3228
4224
|
json = true;
|
|
3229
4225
|
continue;
|
|
3230
4226
|
}
|
|
3231
|
-
if (![
|
|
4227
|
+
if (![
|
|
4228
|
+
"--dir",
|
|
4229
|
+
"--baseline",
|
|
4230
|
+
"--finding",
|
|
4231
|
+
"--outcome",
|
|
4232
|
+
"--reviewer",
|
|
4233
|
+
"--note",
|
|
4234
|
+
"--token",
|
|
4235
|
+
"--reason"
|
|
4236
|
+
].includes(flag) || flags[flag])
|
|
4237
|
+
throw new Error("Unexpected or repeated argument: " + flag);
|
|
3232
4238
|
const value = args[++i];
|
|
3233
4239
|
if (!value || value.startsWith("--")) throw new Error(flag + " requires a value.");
|
|
3234
4240
|
flags[flag] = value;
|
|
3235
4241
|
}
|
|
3236
|
-
if (!flags["--outcome"] && ["--reviewer", "--note", "--token", "--reason"].some((key) => flags[key]))
|
|
4242
|
+
if (!flags["--outcome"] && ["--reviewer", "--note", "--token", "--reason"].some((key) => flags[key]))
|
|
4243
|
+
throw new Error("Use --outcome to record an assessment.");
|
|
3237
4244
|
const input = {
|
|
3238
4245
|
baselinePath: flags["--baseline"],
|
|
3239
4246
|
findingId: flags["--finding"],
|
|
@@ -3247,16 +4254,21 @@ async function runAdvisoryCli(args, io = { out: (text2) => console.log(text2), e
|
|
|
3247
4254
|
if (json) io.out(JSON.stringify(result, null, 2));
|
|
3248
4255
|
else {
|
|
3249
4256
|
const lines = [`Advisory review: ${result.status}.`];
|
|
3250
|
-
if ("finding" in result && result.finding && result.evidence)
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
4257
|
+
if ("finding" in result && result.finding && result.evidence)
|
|
4258
|
+
lines.push(
|
|
4259
|
+
`[${result.finding.type}] ${result.finding.anchor.doc}: ${result.finding.message}`,
|
|
4260
|
+
`Finding: ${result.findingId}`,
|
|
4261
|
+
`Prepared against commit ${result.evidence.head.slice(0, 12)}.`,
|
|
4262
|
+
result.evidence.dirty ? "Relevant files have uncommitted edits; commit them and prepare again before recording." : "Relevant files are committed.",
|
|
4263
|
+
`Review token: ${result.reviewToken}`,
|
|
4264
|
+
...result.diff ? ["Scoped changes:", result.diff] : [],
|
|
4265
|
+
...result.diffNote ? [result.diffNote] : []
|
|
4266
|
+
);
|
|
4267
|
+
if ("event" in result)
|
|
4268
|
+
lines.push(
|
|
4269
|
+
`${result.event.outcome} \u2014 ${result.event.reviewer}: ${result.event.note}`,
|
|
4270
|
+
`Record: ${result.recordPath}`
|
|
4271
|
+
);
|
|
3260
4272
|
lines.push(result.hint);
|
|
3261
4273
|
io.out(stripVTControlCharacters(lines.join("\n")));
|
|
3262
4274
|
}
|
|
@@ -3318,7 +4330,8 @@ function parseArgs(argv) {
|
|
|
3318
4330
|
parsed.prepareRepair = true;
|
|
3319
4331
|
} else if (arg === "--verify-repair") {
|
|
3320
4332
|
const value = argv[++i];
|
|
3321
|
-
if (!value || value.startsWith("--"))
|
|
4333
|
+
if (!value || value.startsWith("--"))
|
|
4334
|
+
throw new Error("--verify-repair requires a baseline path");
|
|
3322
4335
|
parsed.baseline = value;
|
|
3323
4336
|
} else if (arg === "--help" || arg === "-h") {
|
|
3324
4337
|
parsed.help = true;
|
|
@@ -3333,9 +4346,7 @@ function parseArgs(argv) {
|
|
|
3333
4346
|
if (!names.length) throw new Error("--checks requires at least one check");
|
|
3334
4347
|
for (const name of names) {
|
|
3335
4348
|
if (!ALL_CHECKS.includes(name)) {
|
|
3336
|
-
throw new Error(
|
|
3337
|
-
`Unknown check: ${name} (valid: ${ALL_CHECKS.join(", ")})`
|
|
3338
|
-
);
|
|
4349
|
+
throw new Error(`Unknown check: ${name} (valid: ${ALL_CHECKS.join(", ")})`);
|
|
3339
4350
|
}
|
|
3340
4351
|
}
|
|
3341
4352
|
parsed.checks = names;
|
|
@@ -3354,9 +4365,15 @@ function issueLine(issue) {
|
|
|
3354
4365
|
}
|
|
3355
4366
|
function formatAuditSummary(report) {
|
|
3356
4367
|
const lines = [];
|
|
3357
|
-
const reviewed = new Set(
|
|
3358
|
-
|
|
3359
|
-
|
|
4368
|
+
const reviewed = new Set(
|
|
4369
|
+
report.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id)
|
|
4370
|
+
);
|
|
4371
|
+
const open = report.advisories.filter(
|
|
4372
|
+
(finding) => finding.resolution !== "informational" && !reviewed.has(findingId(finding))
|
|
4373
|
+
);
|
|
4374
|
+
const suppressed = (report.suppressedAdvisories ?? []).filter(
|
|
4375
|
+
(finding) => finding.resolution !== "informational" && !reviewed.has(findingId(finding))
|
|
4376
|
+
);
|
|
3360
4377
|
const reviewCount = open.length + suppressed.length;
|
|
3361
4378
|
for (const doc of report.docs) {
|
|
3362
4379
|
const docIssues = report.issues.filter((i) => i.anchor.doc === doc.path);
|
|
@@ -3374,9 +4391,17 @@ function formatAuditSummary(report) {
|
|
|
3374
4391
|
for (const issue of report.issues) {
|
|
3375
4392
|
if (!docPaths.has(issue.anchor.doc)) lines.push(issueLine(issue));
|
|
3376
4393
|
}
|
|
3377
|
-
const information = [...report.advisories, ...report.suppressedAdvisories ?? []].filter(
|
|
3378
|
-
|
|
3379
|
-
|
|
4394
|
+
const information = [...report.advisories, ...report.suppressedAdvisories ?? []].filter(
|
|
4395
|
+
(f) => f.resolution === "informational"
|
|
4396
|
+
);
|
|
4397
|
+
if (information.length)
|
|
4398
|
+
lines.push(
|
|
4399
|
+
`${information.length} informational dependency notices; no review required (see --json).`
|
|
4400
|
+
);
|
|
4401
|
+
if (report.excludedChecks?.length)
|
|
4402
|
+
lines.push(
|
|
4403
|
+
`${report.excludedChecks.length} document/check pairs excluded by project policy (see --json).`
|
|
4404
|
+
);
|
|
3380
4405
|
if (open.length > 0) {
|
|
3381
4406
|
lines.push("Advisories (do not affect the exit code):");
|
|
3382
4407
|
for (const advisory of open) {
|
|
@@ -3388,10 +4413,16 @@ function formatAuditSummary(report) {
|
|
|
3388
4413
|
lines.push(` [skipped] ${skip.check}: ${skip.reason}`);
|
|
3389
4414
|
}
|
|
3390
4415
|
}
|
|
3391
|
-
if (reviewed.size)
|
|
3392
|
-
|
|
4416
|
+
if (reviewed.size)
|
|
4417
|
+
lines.push(
|
|
4418
|
+
`${reviewed.size} advisories have recorded assessments covering current evidence (see --json).`
|
|
4419
|
+
);
|
|
4420
|
+
for (const review of report.advisoryReviews ?? [])
|
|
4421
|
+
if (review.status !== "current") lines.push(` [${review.status}] ${review.reason}`);
|
|
3393
4422
|
for (const advisory of suppressed) {
|
|
3394
|
-
lines.push(
|
|
4423
|
+
lines.push(
|
|
4424
|
+
` [suppressed; unresolved] ${advisory.type} ${advisory.anchor.doc}: ${advisory.message}`
|
|
4425
|
+
);
|
|
3395
4426
|
}
|
|
3396
4427
|
lines.push(
|
|
3397
4428
|
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 +4437,23 @@ function formatFixPrompt(report, baselinePath) {
|
|
|
3406
4437
|
);
|
|
3407
4438
|
lines.push("");
|
|
3408
4439
|
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
4440
|
lines.push(
|
|
3411
|
-
`-
|
|
4441
|
+
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."
|
|
3412
4442
|
);
|
|
3413
4443
|
lines.push(
|
|
3414
|
-
|
|
4444
|
+
`- 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
4445
|
);
|
|
4446
|
+
lines.push("- Keep diffs minimal: change the smallest span that makes each claim true.");
|
|
3416
4447
|
lines.push(
|
|
3417
4448
|
"- Never invent content. Every replacement must be grounded in the evidence below or in files you read from this repository."
|
|
3418
4449
|
);
|
|
3419
|
-
lines.push("- Inspect likely findings before editing: heuristic evidence may describe an intentional omission or an example.");
|
|
3420
4450
|
lines.push(
|
|
3421
|
-
"-
|
|
4451
|
+
"- Inspect likely findings before editing: heuristic evidence may describe an intentional omission or an example."
|
|
3422
4452
|
);
|
|
3423
4453
|
lines.push(
|
|
3424
|
-
"-
|
|
4454
|
+
"- 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
4455
|
);
|
|
4456
|
+
lines.push("- stale-count: replace the number with the actual count from the evidence.");
|
|
3426
4457
|
lines.push(
|
|
3427
4458
|
"- dead-command: replace with the correct script from availableScripts if an obvious rename exists; otherwise remove the command mention."
|
|
3428
4459
|
);
|
|
@@ -3433,7 +4464,9 @@ function formatFixPrompt(report, baselinePath) {
|
|
|
3433
4464
|
"- Specific historical ADVISORIES require a separate authorized assessment using review_advisory with the baselinePath and findingId 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 action: dismiss, reasonCode (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
4465
|
);
|
|
3435
4466
|
lines.push("");
|
|
3436
|
-
lines.push(
|
|
4467
|
+
lines.push(
|
|
4468
|
+
"AUDIT REPORT (current context files and repository evidence, including local edits):"
|
|
4469
|
+
);
|
|
3437
4470
|
lines.push(
|
|
3438
4471
|
JSON.stringify(
|
|
3439
4472
|
{
|
|
@@ -3468,7 +4501,9 @@ async function runAuditCli(argv, io = {
|
|
|
3468
4501
|
throw new Error("--json and --fix-prompt are mutually exclusive");
|
|
3469
4502
|
}
|
|
3470
4503
|
if (args.baseline && (args.prepareRepair || args.checks || args.fixPrompt)) {
|
|
3471
|
-
throw new Error(
|
|
4504
|
+
throw new Error(
|
|
4505
|
+
"--verify-repair cannot be combined with --prepare-repair, --checks, or --fix-prompt; verification uses the original scope"
|
|
4506
|
+
);
|
|
3472
4507
|
}
|
|
3473
4508
|
} catch (error) {
|
|
3474
4509
|
io.err(error instanceof Error ? error.message : String(error));
|
|
@@ -3484,12 +4519,20 @@ async function runAuditCli(argv, io = {
|
|
|
3484
4519
|
try {
|
|
3485
4520
|
if (args.baseline) {
|
|
3486
4521
|
const verification = await verifyRepair(rootDir, args.baseline);
|
|
3487
|
-
io.out(
|
|
4522
|
+
io.out(
|
|
4523
|
+
args.json ? JSON.stringify(verification, null, 2) : formatRepairSummary(verification)
|
|
4524
|
+
);
|
|
3488
4525
|
return repairExitCode(verification);
|
|
3489
4526
|
}
|
|
3490
4527
|
const prepared = await prepareRepair(rootDir, args.checks);
|
|
3491
|
-
io.out(
|
|
3492
|
-
|
|
4528
|
+
io.out(
|
|
4529
|
+
args.json ? JSON.stringify(
|
|
4530
|
+
{ ...prepared, workOrder: formatFixPrompt(prepared.report, prepared.baselinePath) },
|
|
4531
|
+
null,
|
|
4532
|
+
2
|
|
4533
|
+
) : args.fixPrompt ? formatFixPrompt(prepared.report, prepared.baselinePath) : `Repair baseline: ${prepared.baselinePath}
|
|
4534
|
+
${formatAuditSummary(prepared.report)}`
|
|
4535
|
+
);
|
|
3493
4536
|
return prepared.report.clean ? 0 : 1;
|
|
3494
4537
|
} catch (error) {
|
|
3495
4538
|
io.err(error instanceof Error ? error.message : String(error));
|
|
@@ -3498,9 +4541,7 @@ ${formatAuditSummary(prepared.report)}`);
|
|
|
3498
4541
|
}
|
|
3499
4542
|
const report = await computeAudit(rootDir, { checks: args.checks });
|
|
3500
4543
|
if (!report) {
|
|
3501
|
-
io.err(
|
|
3502
|
-
`No README.md or agent instruction files found in ${rootDir}.`
|
|
3503
|
-
);
|
|
4544
|
+
io.err(`No README.md or agent instruction files found in ${rootDir}.`);
|
|
3504
4545
|
return 2;
|
|
3505
4546
|
}
|
|
3506
4547
|
if (!report.gitAvailable) {
|