mason-context 0.20.2 → 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.
@@ -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(["rev-parse", "symbolic-ref", "for-each-ref", "ls-files", "check-ignore", "status", "log", "diff", "ls-tree", "show", "merge-base"]);
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)) return null;
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((file) => anchors.some((anchor) => anchorMatches(anchor, file)));
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(file, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
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)) throw new Error("Project configuration resolves outside the repository");
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) throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
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 (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected a configuration object");
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 (!Array.isArray(value[key]) || !value[key].every((s) => typeof s === "string")) {
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 (!audit || typeof audit !== "object" || Array.isArray(audit)) throw new Error("Configuration audit must be an object");
219
+ if (!isRecord(audit)) throw new Error("Configuration audit must be an object");
150
220
  const patterns = (raw2) => {
151
- if (!Array.isArray(raw2) || raw2.length > 100 || !raw2.every((s) => typeof s === "string" && normalizeRepoPath(s) === s && !s.startsWith("!") && !/[\x00-\x1f]/.test(s))) {
152
- throw new Error("Audit patterns must be at most 100 repository-relative globs without traversal or negation");
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 (!audit.exclude || typeof audit.exclude !== "object" || Array.isArray(audit.exclude)) throw new Error("audit.exclude must map check names to patterns");
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)) throw new Error("Unknown audit check: " + 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(`Cannot apply project file policy: ${error instanceof Error ? error.message : String(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 = ["ts", "tsx", "js", "jsx", "mts", "cts", "mjs", "cjs", "vue", "svelte", "kt", "kts", "java", "py", "go", "rs", "swift", "rb", "cs", "cpp", "c", "h", "hpp", "dart", "php"];
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(`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`, { cause: 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(options.cwd, "git:" + JSON.stringify([args, options]), () => execGit(args, options));
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) throw new Error("Audit include patterns expand to more than 1,000 globs");
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((file) => kind === "documents" ? custom.length > 0 || /^(readme|agents|claude)\.md$/i.test(path4.posix.basename(file)) : sourceSuffixes.has(file.slice(file.lastIndexOf("."))));
311
- if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
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 = [":(icase,glob)**/readme.md", ":(icase,glob)**/agents.md", ":(icase,glob)**/claude.md"];
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(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: 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(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
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(directory, opts, callback);
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 = [...matches.reduce((counts, item) => {
392
- const dir = item.split("/")[0];
393
- counts.set(dir, (counts.get(dir) ?? 0) + 1);
394
- return counts;
395
- }, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
396
- throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count3]) => `${dir} (${count3})`).join(", ")}. Narrow this check's inputs.`);
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(root, paths.slice(offset, offset + 256).map((file2) => fg4.escapePath(file2)), {
444
- ignore: exclude,
445
- followSymbolicLinks: false,
446
- label: "Audit check exclusions"
447
- })) included.add(file);
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 || typeof owner.host !== "string" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
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(() => true, () => false)) {
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) throw new Error(label + " is busy or its lock needs inspection: " + file + ". " + await lockDiagnostic(file));
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 = z.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
811
+ repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
594
812
  verificationFields = {
595
- refreshedHash: z.string().optional(),
596
- verifiedAt: z.string().optional(),
597
- verifiedHash: z.string().optional(),
598
- verificationToken: z.string().optional(),
599
- verificationFailed: z.boolean().optional(),
600
- verificationNote: z.string().optional()
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 = z.object({
603
- description: z.string(),
604
- files: z.array(repoPath),
605
- tests: z.array(repoPath).optional(),
606
- type: z.enum(["capability", "infrastructure"]).optional(),
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 = z.object({ description: z.string(), chain: z.array(repoPath), ...verificationFields }).passthrough();
610
- snapshotSchema = z.object({
611
- version: z.literal(2),
612
- createdAt: z.string(),
613
- updatedAt: z.string(),
614
- gitHash: z.string(),
615
- features: z.record(featureSchema),
616
- flows: z.record(flowSchema)
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/"))) continue;
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 [...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))].sort();
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("-")) return null;
863
+ if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-"))
864
+ return null;
643
865
  try {
644
- const { stdout } = await execGit(["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
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", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
654
- execGit(["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
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 { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
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("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
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(tail)) {
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(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
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 /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path9.posix.isAbsolute(unquoted) ? null : unquoted;
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+.-]*:|[\/#~])/i.test(target)) {
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) addCommand(claim);
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) addCommand(claim);
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
- ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...files.slice(offset, offset + 256).map((file) => `:(literal)${file}`)],
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(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) => fg5.escapePath(p)), {
1195
- ignore: excluded,
1196
- followSymbolicLinks: false,
1197
- label: "Documentation discovery"
1198
- }));
1199
- if (paths.length > 1e4) throw new Error("Documentation discovery exceeds 10,000 relevant files; narrow .mason/config.json ignore patterns.");
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((p) => p.toLowerCase() === file.toLowerCase());
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) if (/^(?:agents|claude|readme)\.md$/i.test(entry.name)) {
1218
- paths.push(path10.posix.join(dir, entry.name));
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(...await Promise.all(files.slice(offset, offset + 4).map(async (candidate) => {
1237
- const [content, lastCommit] = await Promise.all([
1238
- readAuditInput(resolvedRoot, candidate),
1239
- profilePhase("audit.document-history", () => lastCommitOf(resolvedRoot, candidate))
1240
- ]);
1241
- if (content === null) throw new Error("Document disappeared during discovery: " + candidate);
1242
- return {
1243
- path: candidate,
1244
- content,
1245
- scope: documentScope(candidate),
1246
- kind: /^readme\.md$/i.test(path10.posix.basename(candidate)) ? "readme" : /^(agents|claude)\.md$/i.test(path10.posix.basename(candidate)) ? "instructions" : "guide",
1247
- lineCount: content.split("\n").length,
1248
- lastCommit,
1249
- dirty: dirty.has(candidate),
1250
- claims: extractClaims(content)
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)) return null;
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((changes ?? []).filter((c) => c.status === "renamed" && c.previousPath).map((c) => [c.previousPath, c.path]));
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(Boolean)) continue;
1344
- const candidates = await Promise.all(scope.candidates.map(async (file) => ({
1345
- file,
1346
- tracked: await lastCommitOf(ctx.root, file),
1347
- parent: await pathExists(ctx.root, path12.posix.dirname(file))
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({ type: "deleted-reference", message, anchor, confidence: "certain", evidence });
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({ resolution: "recheck", type: "deleted-reference", 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."), anchor, evidence });
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((doc) => doc.content + "\n" + (path13.posix.dirname(doc.path) === "." ? "" : path13.posix.dirname(doc.path) + "/")).join("\n");
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(root, "modules:" + combinedDocs, () => collectModuleCandidates(root, combinedDocs));
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((file) => !file.split("/").some((part) => part.startsWith(".")));
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(parents.slice(offset, offset + 16).map(async (dir) => {
1438
- const entries = await fs8.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1439
- const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink() && sources.some((file) => file.startsWith(path13.posix.join(dir, entry.name) + "/"))) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path13.posix.join(dir, entry.name));
1440
- byParent.set(dir, dirs);
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(byParent.get(dir).filter((candidate) => !ignored.has(candidate) || sources.some((file) => file.startsWith(candidate + "/"))));
1447
- result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg6.escapePath(dir)}/*`, {
1448
- ignore,
1449
- onlyDirectories: true,
1450
- label: "Module directory discovery",
1451
- select: (file) => visible.has(file)
1452
- })).map((file) => path13.posix.basename(file)));
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(root, files.slice(offset, offset + 256).map((file) => fg6.escapePath(file)), {
1461
- ignore,
1462
- followSymbolicLinks: false,
1463
- label: `Source discovery in ${dir}`
1464
- })).length;
1465
- if (count3 > 1e5) throw new Error(`Source discovery in ${dir} exceeds 100,000 relevant files. Exclude generated source with .mason/config.json ignore patterns or narrow this audit's checks.`);
1754
+ 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
- globs = Array.isArray(pkg.workspaces) ? pkg.workspaces : Array.isArray(pkg.workspaces?.packages) ? pkg.workspaces.packages : [];
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 (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return null;
1993
+ if (!isRecord(pkg)) return null;
1691
1994
  if (pkg.scripts === void 0) return [];
1692
- return pkg.scripts && typeof pkg.scripts === "object" && !Array.isArray(pkg.scripts) && Object.values(pkg.scripts).every((value) => typeof value === "string") ? Object.keys(pkg.scripts) : null;
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) for (const claim of doc.claims.commands) {
1707
- const base = documentScope(doc.path);
1708
- const directory = scopedPath(base, claim.directory ?? ".");
1709
- const scope = {
1710
- basis: claim.directory === void 0 ? "document" : "explicit",
1711
- directory: directory ?? base,
1712
- candidates: []
1713
- };
1714
- const resolution = { doc: doc.path, claim, scope, manifest: null, scripts: [], elsewhere: [] };
1715
- resolutions.push(resolution);
1716
- if (claim.scopeUnknown || directory === null) {
1717
- resolution.problem = claim.scopeUnknown ?? "The command's directory leaves the repository.";
1718
- continue;
1719
- }
1720
- let cursor = directory;
1721
- while (true) {
1722
- const manifest = path15.posix.join(cursor, "package.json");
1723
- scope.candidates.push(manifest);
1724
- const raw = await read(manifest);
1725
- if (raw !== null) {
1726
- resolution.manifest = manifest;
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) resolution.problem = "Invalid package manifest: " + manifest;
1729
- else resolution.scripts = scripts;
1730
- break;
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) result.issues.push({ type: "dead-command", message, anchor, confidence: "certain", evidence });
1773
- else result.advisories.push({ resolution: "recheck", type: "dead-command", message: message + (elsewhere.length ? `; it exists in ${elsewhere.join(", ")}. Clarify the intended working directory.` : "; the intended working directory needs review."), anchor, evidence });
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: ["**/node_modules/**", "**/dist/**", "**/build/**", ".mason/reports/**", "package.json"],
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(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, (text2) => text2.replace(/[^\n]/g, " ")).replace(/<!--[\s\S]*?-->/g, (text2) => text2.replace(/[^\n]/g, " "));
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
- for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies", "engines"]) {
1807
- for (const [name, value] of Object.entries(json[section] ?? {})) {
1808
- if (typeof value === "string") result.set(name, [...result.has(name) ? [result.get(name)] : [], value].join("; "));
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
- for (const section of ["libraries", "plugins"]) for (const [alias, entry] of Object.entries(catalog[section] ?? {})) {
1814
- if (typeof entry === "string") {
1815
- const parts = entry.split(":");
1816
- if (parts.length >= 3) result.set(parts.slice(0, -1).join(":"), parts.at(-1));
1817
- } else if (entry && typeof entry === "object") {
1818
- const name = entry.module ?? (entry.group && entry.name ? `${entry.group}:${entry.name}` : entry.id);
1819
- const version = typeof entry.version === "string" ? entry.version : catalog.versions?.[entry.version?.ref];
1820
- if (typeof name === "string") result.set(name, typeof version === "string" ? version : "unspecified");
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)) result.set(match[1], match[2]);
1825
- for (const match of source.matchAll(/\bid\s*\(?\s*["']([\w.-]+)["']\s*\)?\s*version\s*["']([^"']+)["']/g)) result.set(match[1], match[2]);
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(["ls-tree", revision, "--", `:(literal)${file}`], { cwd: root, timeout: 1e4 });
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 ")) throw new Error("Unsupported manifest mode");
1843
- return (await inspectionGit(["show", `${revision}:${file}`], { cwd: root, maxBuffer: 1024 * 1024, timeout: 1e4 })).stdout;
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(`(^|[^a-zA-Z0-9_@./-])${escaped}(?![a-zA-Z0-9_/-]|\\.[a-zA-Z0-9_])`, "i");
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)) return null;
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(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/.*$/g, (token) => token.startsWith("//") ? "" : '""');
1887
- const assignment = line.match(/^(\s*)(versionName|versionCode)(\s*(?:=\s*|\s+))("[A-Za-z0-9._+-]+"|'[A-Za-z0-9._+-]+'|\d+)(\s*)$/);
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(assignment[1] + assignment[2] + assignment[3] + "<release-value>" + assignment[5]);
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))) return false;
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, ["diff", "--raw", "-z", "--no-renames", "--no-ext-diff", "--no-textconv", parents[1], commit.hash, "--", file]);
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(text2) || /\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(text2) || /\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(text2) || /\b(?:npm|pnpm|yarn|pip3?|cargo|gem|composer)\s+(?:install|add|require)\b/i.test(text2);
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(/[\\*?\[\]]/g, "\\$&");
1940
- return MANIFEST_PATHSPECS.map((spec) => spec.startsWith(":(glob)") ? ":(glob)" + escaped + "/" + spec.slice(":(glob)".length) : ":(literal)" + scope + "/" + spec);
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
- ["show", `${doc.lastCommit.hash}:${doc.path}`],
1960
- { cwd: ctx.root, maxBuffer: 10 * 1024 * 1024, timeout: 1e4 }
1961
- )).stdout;
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({ check: "deps-changed", doc: doc.path, reason: `${doc.path}: committed dependency content is unavailable` });
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) result.skipped.push({
2000
- check: "deps-changed",
2001
- doc: doc.path,
2002
- reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
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 z2 } from "zod";
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") return record;
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((e) => ["accepted", "reaffirmed"].includes(e.kind) && e.revision === record.revision) : void 0;
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 ? { reviewer: review.actor, at: review.at, note: review.note, gitHash: review.refreshedHash } : null
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) => z2.string().trim().min(1).max(max);
2116
- decisionSourceSchema = z2.object({
2117
- kind: z2.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
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 = z2.object({
2692
+ attributionSchema = z4.object({
2122
2693
  owner: text(200).nullable().optional(),
2123
- sources: z2.array(decisionSourceSchema).max(20).optional(),
2694
+ sources: z4.array(decisionSourceSchema).max(20).optional(),
2124
2695
  actor: text(200).optional()
2125
2696
  });
2126
- contentSchema = z2.object({
2127
- title: z2.string().min(1),
2128
- body: z2.string().min(1),
2129
- category: z2.enum(["decision", "gotcha", "deprecation", "convention"]),
2130
- files: z2.array(z2.string().refine((f) => normalizeRepoPath(f) !== null)),
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: z2.array(decisionSourceSchema).max(20)
2703
+ sources: z4.array(decisionSourceSchema).max(20)
2133
2704
  });
2134
- approvalSchema = z2.enum(["unreviewed", "proposed", "accepted"]);
2135
- statusSchema = z2.enum(["active", "superseded", "retired"]);
2136
- reviewEvidenceSchema = z2.object({
2137
- baseHash: z2.string(),
2138
- headHash: z2.string(),
2139
- historyAvailable: z2.boolean(),
2140
- changedFiles: z2.array(z2.string()),
2141
- localChanges: z2.array(z2.string())
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 = z2.object({
2144
- kind: z2.enum(["imported", "created", "revised", "accepted", "reaffirmed", "retired", "superseded"]),
2145
- at: z2.string().datetime(),
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: z2.number().int().positive(),
2727
+ revision: z4.number().int().positive(),
2149
2728
  content: contentSchema,
2150
2729
  approval: approvalSchema,
2151
2730
  status: statusSchema,
2152
- refreshedHash: z2.string(),
2153
- evidence: reviewEvidenceSchema.optional()
2731
+ refreshedHash: z4.string(),
2732
+ evidence: reviewEvidenceSchema.optional(),
2733
+ capture: anchorCaptureSchema.optional()
2154
2734
  });
2155
- legacySchema = z2.object({
2156
- version: z2.literal(1),
2157
- id: z2.string().regex(/^[a-zA-Z0-9_-]+$/),
2158
- title: z2.string().min(1),
2159
- body: z2.string().min(1),
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: z2.string(),
2163
- updatedAt: z2.string(),
2164
- refreshedHash: z2.string(),
2165
- status: z2.enum(["active", "superseded"]),
2166
- supersededBy: z2.string().optional()
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: z2.literal(2),
2749
+ version: z4.literal(2),
2170
2750
  status: statusSchema,
2171
2751
  approval: approvalSchema,
2172
- revision: z2.number().int().positive(),
2752
+ revision: z4.number().int().positive(),
2173
2753
  owner: text(200).optional(),
2174
- sources: z2.array(decisionSourceSchema).max(20),
2175
- history: z2.array(eventSchema).min(1)
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) invalid("History must begin with creation or legacy import at revision 1");
2183
- if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed")) invalid("Initial records cannot claim acceptance");
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)) invalid("Invalid revision sequence");
2188
- if (event.kind !== "revised" && !same(event.content, previous.content)) invalid("A review cannot silently revise decision content");
2189
- if (event.kind === "reaffirmed" && previous.approval !== "accepted") invalid("Only accepted decisions can be reaffirmed");
2190
- if (event.kind === "accepted" && previous.approval === "accepted") invalid("Use reaffirmation for an accepted decision");
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) invalid("Only a review can refresh the evidence baseline");
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")) invalid("Lifecycle disagrees with history");
2196
- if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence)) invalid("Reviews require a named reviewer, reason, and code evidence");
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) invalid("Accepted decisions require an owner and source");
2199
- if (!event.evidence || !/^[a-f0-9]{40,64}$/.test(event.evidence.headHash) || event.refreshedHash !== event.evidence.headHash || event.evidence.localChanges.length) invalid("Acceptance requires a committed evidence baseline");
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 (!previous || !same(previous.content, decisionContent(record)) || previous.approval !== record.approval || previous.status !== record.status || previous.revision !== record.revision || previous.refreshedHash !== record.refreshedHash) invalid("Decision does not match the final history event");
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 = z2.union([legacySchema, currentSchema]);
2813
+ decisionSchema = z4.union([legacySchema, currentSchema]);
2206
2814
  }
2207
2815
  });
2208
2816
 
2209
2817
  // src/decisions/decisions.ts
2210
- import fs9 from "fs/promises";
2818
+ import fs10 from "fs/promises";
2211
2819
  import path18 from "path";
2212
- import { createHash } from "crypto";
2213
- import { z as z3 } from "zod";
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
- const records = [];
2216
- const diagnostics = [];
2217
- let entries;
2218
- try {
2219
- entries = await fs9.readdir(await storePath(rootDir, ".mason/decisions"));
2220
- } catch (error) {
2221
- if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
2222
- return { records, diagnostics };
2223
- }
2224
- for (const entry of entries.sort()) {
2225
- if (!entry.endsWith(".json")) continue;
2226
- const relative = `.mason/decisions/${entry}`;
2227
- try {
2228
- const record = decisionSchema.parse(await readStoreJson(rootDir, relative));
2229
- if (entry !== `${record.id}.json`) throw new Error("Record id does not match its filename");
2230
- records.push(record);
2231
- } catch (error) {
2232
- diagnostics.push({ path: relative, message: error instanceof Error ? error.message : String(error) });
2233
- }
2234
- }
2235
- return { records, diagnostics };
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) => z3.string({
2250
- errorMap: (issue, ctx) => ({ message: issue.code === "too_big" ? `${field} has ${ctx.data.length} characters; maximum is ${max} (${ctx.data.length - max} over). ${hint}` : ctx.defaultError })
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("title", TITLE_MAX_CHARS, "Tighten it to a specific headline.");
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 = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
2267
- const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
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 inspect = async (record) => {
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 { freshness: touched === null || !workingTree.available ? "unknown" : hits.length || localHits.length ? "changed" : "current", changedFiles: hits };
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 state = await inspect(effective);
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) (report.pendingProposals ??= {})[record.id] = await inspect(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) result.skipped.push({ check: "decision-anchor-drift", reason: `${diagnostic.path}: ${diagnostic.message}` });
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.flatMap((record) => [
2318
- { record: effectiveDecision(record), changedFiles: drift.staleDecisions[record.id] ?? [], freshness: drift.freshness?.[record.id] ?? "unknown" },
2319
- { record, changedFiles: drift.pendingProposals?.[record.id]?.changedFiles ?? [], freshness: drift.pendingProposals?.[record.id]?.freshness ?? "unknown" }
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 anchor files that changed since its evidence baseline \u2013 needs human review`,
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 createHash2 } from "crypto";
2382
- import { z as z4 } from "zod";
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) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
2413
- checkSchema = z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
2414
- commitSchema = z4.object({ hash: z4.string().regex(/^[a-f0-9]{40,64}$/), date: z4.string(), subject: z4.string() });
2415
- anchorSchema = z4.object({ doc: z4.string(), line: z4.number().int().positive().nullable(), excerpt: z4.string().nullable() });
2416
- count = z4.number().int().nonnegative();
2417
- scopeSchema = z4.object({
2418
- basis: z4.enum(["repository", "document", "document-link", "explicit"]),
2419
- directory: z4.string(),
2420
- candidates: z4.array(z4.string())
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
- evidenceSchema = z4.discriminatedUnion("kind", [
2423
- z4.object({
2424
- kind: z4.literal("missing-path"),
2425
- claimed: z4.string(),
2426
- renamedTo: z4.string().nullable(),
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: z4.boolean(),
2429
- parentDirExists: z4.boolean(),
2430
- resolvedPath: z4.string().optional(),
3150
+ everTracked: z6.boolean(),
3151
+ parentDirExists: z6.boolean(),
3152
+ resolvedPath: z6.string().optional(),
2431
3153
  scope: scopeSchema.optional()
2432
3154
  }),
2433
- z4.object({
2434
- kind: z4.literal("unmentioned-dir"),
2435
- dir: z4.string(),
3155
+ z6.object({
3156
+ kind: z6.literal("unmentioned-dir"),
3157
+ dir: z6.string(),
2436
3158
  sourceFileCount: count,
2437
3159
  firstCommit: commitSchema.nullable(),
2438
- checkedDocs: z4.array(z4.string())
3160
+ checkedDocs: z6.array(z6.string())
2439
3161
  }),
2440
- z4.object({
2441
- kind: z4.literal("count-mismatch"),
3162
+ z6.object({
3163
+ kind: z6.literal("count-mismatch"),
2442
3164
  claimed: count,
2443
3165
  actual: count,
2444
- unit: z4.string(),
2445
- countedFrom: z4.string(),
2446
- members: z4.array(z4.string())
3166
+ unit: z6.string(),
3167
+ countedFrom: z6.string(),
3168
+ members: z6.array(z6.string())
2447
3169
  }),
2448
- z4.object({
2449
- kind: z4.literal("missing-script"),
2450
- scriptName: z4.string(),
2451
- invocation: z4.string(),
2452
- manifestsChecked: z4.array(z4.string()),
2453
- availableScripts: z4.array(z4.string()),
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
- z4.object({
2457
- kind: z4.literal("doc-behind-manifests"),
3178
+ z6.object({
3179
+ kind: z6.literal("doc-behind-manifests"),
2458
3180
  docLastCommit: commitSchema,
2459
- manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
3181
+ manifestCommits: z6.array(commitSchema.extend({ files: z6.array(z6.string()) })),
2460
3182
  totalCommits: count,
2461
- matches: z4.array(z4.object({ dependency: z4.string(), manifest: z4.string(), before: z4.string().nullable(), after: z4.string().nullable(), line: z4.number().int().positive(), excerpt: z4.string() })).optional(),
2462
- matchingIncomplete: z4.boolean().optional()
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
- z4.object({
2465
- kind: z4.literal("decision-anchor"),
2466
- decisionId: z4.string(),
2467
- title: z4.string(),
2468
- changedFiles: z4.array(z4.string()),
2469
- refreshedHash: z4.string(),
2470
- provenance: z4.object({}).passthrough().optional()
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 = z4.object({ message: z4.string(), anchor: anchorSchema, evidence: evidenceSchema });
3204
+ findingSchema = z6.object({
3205
+ message: z6.string(),
3206
+ anchor: anchorSchema,
3207
+ evidence: evidenceSchema
3208
+ });
2474
3209
  issueSchema = findingSchema.extend({
2475
- type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
2476
- confidence: z4.enum(["certain", "likely"])
3210
+ type: z6.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
3211
+ confidence: z6.enum(["certain", "likely"])
2477
3212
  });
2478
- advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.enum(["recheck", "informational"]).optional() });
2479
- checkResultSchema = z4.object({
2480
- issues: z4.array(issueSchema),
2481
- advisories: z4.array(advisorySchema),
2482
- suppressedAdvisories: z4.array(advisorySchema).optional(),
2483
- skipped: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
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 z5 } from "zod";
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 = z5.object({
2496
- level: z5.number(),
2497
- facts: z5.array(z5.string()),
2498
- magnitude: z5.number().optional()
3250
+ conditionSchema = z8.object({
3251
+ level: z8.number(),
3252
+ facts: z8.array(z8.string()),
3253
+ magnitude: z8.number().optional()
2499
3254
  });
2500
- notificationStateSchema = z5.object({
2501
- version: z5.literal(1),
2502
- baselineHead: z5.string(),
2503
- baseline: z5.record(conditionSchema),
2504
- delivered: z5.record(conditionSchema),
2505
- cleared: z5.array(z5.string())
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 z6 } from "zod";
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 = z6.enum(["claude", "codex"]);
2522
- stateSchema = z6.object({
2523
- version: z6.literal(1),
2524
- root: z6.string(),
2525
- gitDir: z6.string(),
2526
- branch: z6.string(),
2527
- baselines: z6.array(z6.object({ path: z6.string(), at: z6.string(), event: z6.string(), fingerprint: z6.string() })).max(128),
2528
- sessions: z6.record(z6.object({
2529
- host: hostSchema,
2530
- lastUsed: z6.string(),
2531
- notifiedDiagnostics: z6.array(z6.string()).default([]),
2532
- pending: z6.record(z6.string()),
2533
- coverageGaps: z6.array(z6.string()),
2534
- events: z6.record(z6.object({ at: z6.string(), count: z6.number().int().positive() }))
2535
- })),
2536
- updatedAt: z6.string(),
2537
- fingerprint: z6.string().nullable(),
2538
- latest: z6.string().nullable(),
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: z6.object({ id: z6.string(), fingerprint: z6.string(), completedAt: z6.number(), path: z6.string() }).optional()
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 fs10 from "fs/promises";
3321
+ import fs11 from "fs/promises";
2561
3322
  import path20 from "path";
2562
3323
  import { randomUUID as randomUUID2 } from "crypto";
2563
- import { z as z7 } from "zod";
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) throw new Error("Context file is not regular or exceeds 10 MiB: " + doc);
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("HEAD or context files changed during the audit; retry against a stable checkout.");
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(documents, file, async () => readBoundedFile(await storePath(root, file), 10 * 1024 * 1024)),
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 fs10.realpath(rootDir);
2634
- const selected = z7.array(checkSchema).nonempty().parse(checks);
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) throw new Error("Readable Git history is required to prepare a repair.");
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 fs10.realpath(rootDir);
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) throw new Error("Repair baseline was modified; use the original baseline.");
2656
- if (stored.report.root !== root) throw new Error("Repair baseline belongs to a different repository.");
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)) continue;
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("Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair.");
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("The original audit commit is unavailable; repair history cannot be verified.");
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((doc) => !current?.docs.some((d) => d.path === doc.path));
2690
- for (const doc of missingDocs) diagnostics.push("Original context file " + doc.path + " is unavailable; removing it does not verify a repair.");
2691
- const findings = await Promise.all(originalFindings.map(async (finding) => {
2692
- const id = findingId(finding);
2693
- const now = currentById.get(id);
2694
- const base = { id, original: finding, ...now ? { current: now } : {} };
2695
- if (diagnostics.length || !current) {
2696
- return { ...base, status: "unverified", reason: "The original audit scope could not be verified. See diagnostics." };
2697
- }
2698
- if (now && ("confidence" in finding || "confidence" in now)) {
2699
- return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
2700
- }
2701
- if (current.excludedChecks?.some((excluded) => excluded.check === finding.type && excluded.doc === finding.anchor.doc)) {
2702
- return { ...base, status: "resolved", reason: "Excluded by project audit policy; this is not a correctness assessment. Original evidence is retained." };
2703
- }
2704
- const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
2705
- if (!current.checksRun?.includes(finding.type) || skipped.length) {
2706
- return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
2707
- }
2708
- const review = await (session?.assessment ?? assessAdvisory)(root, now ?? finding, original.headHash);
2709
- if (review) {
2710
- if (review.status === "unverified") return { ...base, review, status: "unverified", reason: review.reason };
2711
- if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
2712
- return { ...base, review, status: "review-required", reason: review.reason };
2713
- }
2714
- if (!("confidence" in finding) && finding.evidence.kind === "doc-behind-manifests" && !finding.evidence.matches?.length && (!now || !("confidence" in now) && now.resolution === "informational")) {
2715
- return { ...base, status: "resolved", reason: "Manifest recency alone establishes no affected passage. Retained as information; no assessment is required." };
2716
- }
2717
- if (!("confidence" in finding) && (!recheckable(finding) || now)) {
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: "review-required",
2721
- 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."
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
- return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
2725
- }));
2726
- if (!session) try {
2727
- if (reviewDigest !== digest(await advisoryReviewInventory(root)) || current?.headHash !== await getCurrentGitHash(root)) {
2728
- diagnostics.push("HEAD or advisory review records changed during verification; repeat against stable evidence.");
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 = diagnostics.at(-1);
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(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
2743
- const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
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((f) => !("confidence" in f) && f.resolution !== "informational" && !reviewedIds.has(findingId(f)));
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((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),
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((s) => " [skipped] " + s.check + ": " + s.reason),
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 = z7.number().int().nonnegative();
2792
- reportSchema = z7.object({
2793
- version: z7.literal(1),
2794
- root: z7.string(),
2795
- gitAvailable: z7.literal(true),
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: z7.array(checkSchema).nonempty(),
2798
- docs: z7.array(z7.object({
2799
- path: z7.string().refine((value) => normalizeRepoPath(value) === value),
2800
- scope: z7.string().optional(),
2801
- kind: z7.enum(["instructions", "readme", "guide"]).optional(),
2802
- lastCommit: commitSchema.nullable(),
2803
- dirty: z7.boolean(),
2804
- lineCount: count2
2805
- })).nonempty(),
2806
- decisionsChecked: z7.boolean(),
2807
- clean: z7.boolean(),
2808
- excludedChecks: z7.array(z7.object({ check: checkSchema, doc: z7.string() })).optional(),
2809
- issues: z7.array(issueSchema),
2810
- advisories: z7.array(advisorySchema),
2811
- suppressedAdvisories: z7.array(advisorySchema).optional(),
2812
- skippedChecks: z7.array(z7.object({ check: z7.string(), reason: z7.string(), doc: z7.string().optional() }))
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 = z7.object({
2815
- kind: z7.literal("mason-audit-repair"),
2816
- version: z7.literal(1),
2817
- createdAt: z7.string().datetime(),
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: z7.string().regex(/^[a-f0-9]{64}$/)
3656
+ digest: z10.string().regex(/^[a-f0-9]{64}$/)
2820
3657
  });
2821
- repairFindingSchema = z7.object({
2822
- id: z7.string(),
2823
- original: z7.union([issueSchema, advisorySchema]),
2824
- status: z7.enum(["resolved", "unresolved", "review-required", "unverified"]),
2825
- reason: z7.string(),
2826
- current: z7.union([issueSchema, advisorySchema]).optional(),
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 fs11 from "fs/promises";
2834
- import { z as z8 } from "zod";
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((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))) {
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([evidence.scope, evidence.tree, ...reason === "no-dependency-claims" ? [] : [evidence.lastScopeCommit], evidence.paths]);
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") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
2851
- if (reason === "no-dependency-claims" && e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`];
2852
- if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
2853
- if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
2854
- else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
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) throw new Error("Advisory contains an invalid scoped path: " + 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) throw new Error("Invalid advisory document path.");
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(root, "ls-tree", "-r", "-z", "HEAD", "--", ...tracked.map((file) => `:(literal)${file}`)) : "",
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()) throw new Error("Original advisory document is missing, empty or unreadable.");
2875
- if (tree.length > 2 * 1024 * 1024 || tree.split("\0").length > 2e4) throw new Error("Advisory scope is too large for a complete review; narrow the documentation scope.");
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 ")) throw new Error("Symlink or submodule in advisory review scope: " + file);
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 ?? [finding.evidence.resolvedPath ?? finding.evidence.claimed]).map(async (file) => ({ path: file, exists: await pathExists(root, file) }))
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 [...finding.evidence.manifestsChecked, ...finding.evidence.scope?.candidates ?? []]) {
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({ head, scope, tree, lastScopeCommit, paths, fingerprint: "" }, reason);
2891
- if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
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
- ...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
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) throw new Error("Decision records have unavailable or malformed evidence; review cannot be confirmed.");
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 events = record.history.filter((event2) => ["accepted", "reaffirmed", "retired"].includes(event2.kind));
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) return null;
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") return await decisionAssessment(root, finding, baselineHead);
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)) return { ...base, status: "reopened", reason: "Relevant document or repository evidence changed after the assessment; prepare a new review." };
2948
- return { ...base, status: event.outcome === "deferred" ? "deferred" : "current", 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." };
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 { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
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 fs11.readdir(await storePath(root, ".mason/reviews/advisories"));
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(names.filter((name) => name.endsWith(".json")).sort().map(async (name) => [name, await readAuditInput(root, ".mason/reviews/advisories/" + name)]));
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 fs11.realpath(rootDir), request = advisoryReviewRequest.parse(input);
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 = [...baseline.original.advisories, ...baseline.original.suppressedAdvisories ?? []].find((f) => findingId(f) === request.findingId);
2969
- if (!finding) throw new Error("No advisory with this findingId in the original baseline. Provable issues must be repaired and rechecked.");
2970
- if (finding.evidence.kind === "decision-anchor") return {
2971
- status: "decision-review-required",
2972
- decisionId: finding.evidence.decisionId,
2973
- hint: "Use review_decision to prepare evidence and record authorized acceptance, reaffirmation or retirement, then verify this original repair baseline. An advisory assessment cannot approve a decision."
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") throw new Error("One-step dismissal is only available for dependency advisories.");
2977
- if (!request.reasonCode || !request.note) throw new Error("Dismissal requires reasonCode and note; it does not require a reviewer or prepared token.");
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(root, "diff", "--no-ext-diff", "--no-textconv", "--no-color", "--no-renames", "-U3", from, state.evidence.head, "--", ...state.evidence.scope);
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) diffNote = "Diff preview truncated; inspect the full scoped diff before reviewing.";
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) throw new Error("Review evidence changed during preparation; retry.");
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)) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
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) throw new Error("Review conflict: evidence or review history changed since preparation. Prepare and inspect a new review.");
3014
- if (state.evidence.dirty) throw new Error("Commit relevant document and repository edits before recording the assessment. Unrelated local work may remain.");
3015
- if ((state.record?.events.length ?? 0) >= 200) throw new Error("Review history limit reached; existing events were retained.");
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 = { version: 1, id: request.findingId, events: [...state.record?.events ?? [], event] };
3027
- if ((await prepare()).token !== state.token) throw new Error("Review conflict: evidence changed while recording the assessment.");
3028
- await writeStoreJson(root, recordPath(request.findingId), { ...payload, digest: digest(payload) });
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, evidenceSchema2, dismissalReasonSchema, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
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 = z8.string().regex(/^[a-f0-9]{64}$/);
3054
- commitSchema2 = z8.string().regex(/^[a-f0-9]{40,64}$/);
3055
- evidenceSchema2 = z8.object({
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: z8.array(z8.string()).min(1).max(1e3),
3059
- tree: z8.string().max(2 * 1024 * 1024),
4019
+ scope: z11.array(z11.string()).min(1).max(1e3),
4020
+ tree: z11.string().max(2 * 1024 * 1024),
3060
4021
  lastScopeCommit: commitSchema2,
3061
- paths: z8.array(z8.object({ path: z8.string(), exists: z8.boolean() })).max(1e3)
4022
+ paths: z11.array(z11.object({ path: z11.string(), exists: z11.boolean() })).max(1e3)
3062
4023
  });
3063
- dismissalReasonSchema = z8.enum(["no-dependency-claims", "unrelated-manifest-change"]);
3064
- eventSchema2 = z8.object({
3065
- at: z8.string().datetime(),
3066
- reviewer: z8.string().trim().min(1).max(200).optional(),
3067
- note: z8.string().trim().min(1).max(2500),
3068
- outcome: z8.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
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((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
3071
- evidence: evidenceSchema2
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
- recordSchema = z8.object({ version: z8.literal(1), id: hashSchema, events: z8.array(eventSchema2).min(1).max(200), digest: hashSchema });
3074
- reviewSummarySchema = z8.object({
4043
+ reviewSummarySchema = z11.object({
3075
4044
  id: hashSchema,
3076
- status: z8.enum(["current", "reopened", "deferred", "unverified"]),
3077
- reason: z8.string(),
3078
- recordPath: z8.string().optional(),
3079
- reviewer: z8.string().optional(),
3080
- note: z8.string().optional(),
3081
- outcome: z8.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
3082
- reviewedHead: z8.string().optional()
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 = z8.object({
3085
- baselinePath: z8.string().min(1),
4062
+ advisoryReviewRequest = z11.object({
4063
+ baselinePath: z11.string().min(1),
3086
4064
  findingId: hashSchema,
3087
- action: z8.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
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 fs12 from "fs/promises";
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([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
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)) changesByCommit.set(hash, await getChangesWithStatus(resolvedRoot, 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 fs12.access(path21.join(resolvedRoot, ".mason", "decisions"));
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(resolvedRoot, docs.map((doc) => doc.path), exclusions);
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(resolvedRoot, [...new Set(advisories.map((f) => f.anchor.doc))], exclusions);
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) if (!advisoryPaths.has(f.anchor.doc)) (report.excludedChecks ??= []).push({ check: name, doc: f.anchor.doc });
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([...report.advisories, ...report.suppressedAdvisories].map((finding) => assessAdvisory(resolvedRoot, finding, headHash)))).filter((review) => review !== null);
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 (!["--dir", "--baseline", "--finding", "--outcome", "--reviewer", "--note", "--token", "--reason"].includes(flag) || flags[flag]) throw new Error("Unexpected or repeated argument: " + flag);
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])) throw new Error("Use --outcome to record an assessment.");
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) lines.push(
3251
- `[${result.finding.type}] ${result.finding.anchor.doc}: ${result.finding.message}`,
3252
- `Finding: ${result.findingId}`,
3253
- `Prepared against commit ${result.evidence.head.slice(0, 12)}.`,
3254
- result.evidence.dirty ? "Relevant files have uncommitted edits; commit them and prepare again before recording." : "Relevant files are committed.",
3255
- `Review token: ${result.reviewToken}`,
3256
- ...result.diff ? ["Scoped changes:", result.diff] : [],
3257
- ...result.diffNote ? [result.diffNote] : []
3258
- );
3259
- if ("event" in result) lines.push(`${result.event.outcome} \u2014 ${result.event.reviewer}: ${result.event.note}`, `Record: ${result.recordPath}`);
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("--")) throw new Error("--verify-repair requires a baseline path");
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(report.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
3358
- const open = report.advisories.filter((finding) => finding.resolution !== "informational" && !reviewed.has(findingId(finding)));
3359
- const suppressed = (report.suppressedAdvisories ?? []).filter((finding) => finding.resolution !== "informational" && !reviewed.has(findingId(finding)));
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((f) => f.resolution === "informational");
3378
- if (information.length) lines.push(`${information.length} informational dependency notices; no review required (see --json).`);
3379
- if (report.excludedChecks?.length) lines.push(`${report.excludedChecks.length} document/check pairs excluded by project policy (see --json).`);
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) lines.push(`${reviewed.size} advisories have recorded assessments covering current evidence (see --json).`);
3392
- for (const review of report.advisoryReviews ?? []) if (review.status !== "current") lines.push(` [${review.status}] ${review.reason}`);
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(` [suppressed; unresolved] ${advisory.type} ${advisory.anchor.doc}: ${advisory.message}`);
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
- `- 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.`
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
- "- Keep diffs minimal: change the smallest span that makes each claim true."
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
- "- 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."
4451
+ "- Inspect likely findings before editing: heuristic evidence may describe an intentional omission or an example."
3422
4452
  );
3423
4453
  lines.push(
3424
- "- stale-count: replace the number with the actual count from the evidence."
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("AUDIT REPORT (current context files and repository evidence, including local edits):");
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("--verify-repair cannot be combined with --prepare-repair, --checks, or --fix-prompt; verification uses the original scope");
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(args.json ? JSON.stringify(verification, null, 2) : formatRepairSummary(verification));
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(args.json ? JSON.stringify({ ...prepared, workOrder: formatFixPrompt(prepared.report, prepared.baselinePath) }, null, 2) : args.fixPrompt ? formatFixPrompt(prepared.report, prepared.baselinePath) : `Repair baseline: ${prepared.baselinePath}
3492
- ${formatAuditSummary(prepared.report)}`);
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) {