mason-context 0.20.2 → 0.22.0

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