mason-context 0.19.0 → 0.19.1

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.
@@ -85,6 +85,26 @@ var init_paths = __esm({
85
85
  }
86
86
  });
87
87
 
88
+ // src/audit/types.ts
89
+ var types_exports = {};
90
+ __export(types_exports, {
91
+ ALL_CHECKS: () => ALL_CHECKS
92
+ });
93
+ var ALL_CHECKS;
94
+ var init_types = __esm({
95
+ "src/audit/types.ts"() {
96
+ "use strict";
97
+ ALL_CHECKS = [
98
+ "deleted-reference",
99
+ "new-module",
100
+ "stale-count",
101
+ "dead-command",
102
+ "deps-changed",
103
+ "decision-anchor-drift"
104
+ ];
105
+ }
106
+ });
107
+
88
108
  // src/utils/files.ts
89
109
  import fs from "fs/promises";
90
110
  import { constants } from "fs";
@@ -124,6 +144,27 @@ async function loadProjectConfig(root) {
124
144
  }
125
145
  config[key] = value[key];
126
146
  }
147
+ if (value.audit !== void 0) {
148
+ const audit = value.audit;
149
+ if (!audit || typeof audit !== "object" || Array.isArray(audit)) throw new Error("Configuration audit must be an object");
150
+ 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");
153
+ }
154
+ return raw2;
155
+ };
156
+ config.audit = {};
157
+ if (audit.include !== void 0) config.audit.include = patterns(audit.include);
158
+ 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");
160
+ const { ALL_CHECKS: ALL_CHECKS2 } = await Promise.resolve().then(() => (init_types(), types_exports));
161
+ config.audit.exclude = {};
162
+ for (const [check, globs] of Object.entries(audit.exclude)) {
163
+ if (!ALL_CHECKS2.includes(check)) throw new Error("Unknown audit check: " + check);
164
+ config.audit.exclude[check] = patterns(globs);
165
+ }
166
+ }
167
+ }
127
168
  return config;
128
169
  } catch (error) {
129
170
  if (error.code === "ENOENT") return {};
@@ -235,13 +276,192 @@ var init_storage = __esm({
235
276
  }
236
277
  });
237
278
 
238
- // src/utils/store-lock.ts
279
+ // src/audit/inspection.ts
280
+ import fg2 from "fast-glob";
281
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
282
+ import path4 from "path";
283
+ function inspectionRead(root, key, read) {
284
+ const inspection = inspections.getStore();
285
+ if (!inspection || inspection.root !== path4.resolve(root)) return read();
286
+ if (!inspection.reads.has(key)) {
287
+ const pending = read();
288
+ inspection.reads.set(key, pending);
289
+ void pending.catch(() => {
290
+ if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
291
+ });
292
+ }
293
+ return inspection.reads.get(key);
294
+ }
295
+ function inspectionGit(args, options) {
296
+ return inspectionRead(options.cwd, "git:" + JSON.stringify([args, options]), () => execGit(args, options));
297
+ }
298
+ async function auditGitPaths(root, kind) {
299
+ const custom = (await loadProjectConfig(root)).audit?.include ?? [];
300
+ const shared = inspections.getStore()?.root === path4.resolve(root) && !custom.length;
301
+ 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");
303
+ const documentSpecs = [...docSpecs, ...expanded.map((pattern) => `:(glob)${pattern}`)];
304
+ const specs = shared ? [...documentSpecs, ...sourceSpecs] : kind === "documents" ? documentSpecs : sourceSpecs;
305
+ const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
306
+ const { stdout } = await inspectionGit(
307
+ ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
308
+ { cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
309
+ );
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.");
312
+ return paths.sort();
313
+ }
314
+ var inspections, sourceSpecs, docSpecs, sourceSuffixes;
315
+ var init_inspection = __esm({
316
+ "src/audit/inspection.ts"() {
317
+ "use strict";
318
+ init_git_read();
319
+ init_files();
320
+ inspections = new AsyncLocalStorage2();
321
+ sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
322
+ docSpecs = [":(icase,glob)**/readme.md", ":(icase,glob)**/agents.md", ":(icase,glob)**/claude.md"];
323
+ sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
324
+ }
325
+ });
326
+
327
+ // src/audit/inputs.ts
239
328
  import fs3 from "fs/promises";
329
+ import * as nativeFs from "fs";
330
+ import path5 from "path";
331
+ import fg3 from "fast-glob";
332
+ async function auditInputPath(root, file) {
333
+ if (file === ".") return fs3.realpath(root);
334
+ try {
335
+ return await storePath(root, file);
336
+ } catch (error) {
337
+ 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 });
339
+ }
340
+ throw error;
341
+ }
342
+ }
343
+ async function readAuditInput(root, file) {
344
+ try {
345
+ const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
346
+ if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
347
+ return value;
348
+ } catch (error) {
349
+ if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
350
+ throw error;
351
+ }
352
+ }
353
+ async function auditGlob(root, patterns, options) {
354
+ if (Array.isArray(patterns) && !patterns.length) return [];
355
+ const directories = /* @__PURE__ */ new Set();
356
+ const readdir2 = ((directory, opts, callback) => {
357
+ const relative = path5.relative(root, directory).split(path5.sep).join("/") || ".";
358
+ directories.add(relative);
359
+ 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.`));
361
+ return;
362
+ }
363
+ auditInputPath(root, relative).then(() => {
364
+ nativeFs.readdir(directory, opts, callback);
365
+ }, callback);
366
+ });
367
+ const lstat2 = ((file, callback) => {
368
+ const relative = path5.relative(root, file).split(path5.sep).join("/") || ".";
369
+ auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
370
+ });
371
+ const stream = fg3.stream(patterns, {
372
+ cwd: root,
373
+ ignore: options.ignore,
374
+ onlyFiles: false,
375
+ onlyDirectories: options.onlyDirectories,
376
+ objectMode: true,
377
+ followSymbolicLinks: options.followSymbolicLinks ?? true,
378
+ fs: { readdir: readdir2, lstat: lstat2 },
379
+ concurrency: 16
380
+ });
381
+ const matches = [];
382
+ try {
383
+ for await (const value of stream) {
384
+ const entry = value;
385
+ const file = entry.path;
386
+ if (options.select && !options.select(file)) continue;
387
+ await auditInputPath(root, file);
388
+ if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
389
+ matches.push(file);
390
+ 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.`);
397
+ }
398
+ }
399
+ } finally {
400
+ stream.destroy();
401
+ }
402
+ return matches.sort();
403
+ }
404
+ async function gitSourcePaths(root) {
405
+ return auditGitPaths(root, "sources");
406
+ }
407
+ async function gitIgnoredPaths(root, files) {
408
+ if (!files.length) return /* @__PURE__ */ new Set();
409
+ const ignored = /* @__PURE__ */ new Set();
410
+ for (let offset = 0; offset < files.length; offset += 1e3) {
411
+ try {
412
+ const { stdout } = await execGit(["check-ignore", "-z", "--stdin"], {
413
+ cwd: root,
414
+ maxBuffer: 4 * 1024 * 1024,
415
+ timeout: 1e4,
416
+ input: files.slice(offset, offset + 1e3).join("\0") + "\0"
417
+ });
418
+ for (const file of stdout.split("\0").filter(Boolean)) ignored.add(file);
419
+ } catch (error) {
420
+ if (error.code !== 1) throw error;
421
+ }
422
+ }
423
+ return ignored;
424
+ }
425
+ var MAX_INPUTS;
426
+ var init_inputs = __esm({
427
+ "src/audit/inputs.ts"() {
428
+ "use strict";
429
+ init_files();
430
+ init_storage();
431
+ init_git_read();
432
+ init_inspection();
433
+ MAX_INPUTS = 1e5;
434
+ }
435
+ });
436
+
437
+ // src/audit/policy.ts
438
+ import fg4 from "fast-glob";
439
+ async function includedCheckPaths(root, paths, exclude) {
440
+ if (!exclude.length) return new Set(paths);
441
+ const included = /* @__PURE__ */ new Set();
442
+ 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);
448
+ }
449
+ return included;
450
+ }
451
+ var init_policy = __esm({
452
+ "src/audit/policy.ts"() {
453
+ "use strict";
454
+ init_inputs();
455
+ }
456
+ });
457
+
458
+ // src/utils/store-lock.ts
459
+ import fs4 from "fs/promises";
240
460
  import os from "os";
241
461
  async function lockDiagnostic(file) {
242
462
  let reason;
243
463
  try {
244
- const owner = JSON.parse(await fs3.readFile(file, "utf8"));
464
+ const owner = JSON.parse(await fs4.readFile(file, "utf8"));
245
465
  if (!owner || typeof owner.host !== "string" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
246
466
  reason = "Lock owner data is incomplete or malformed.";
247
467
  } else if (owner.host !== os.hostname()) {
@@ -257,7 +477,7 @@ async function lockDiagnostic(file) {
257
477
  } catch (error) {
258
478
  reason = error instanceof SyntaxError ? "Lock owner data is incomplete or malformed." : "Lock owner data could not be read; the lock may have changed.";
259
479
  }
260
- if (await fs3.lstat(file + ".reclaim").then(() => true, () => false)) {
480
+ if (await fs4.lstat(file + ".reclaim").then(() => true, () => false)) {
261
481
  reason += " A recovery guard also needs inspection: " + file + ".reclaim.";
262
482
  }
263
483
  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.";
@@ -268,21 +488,21 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
268
488
  let handle;
269
489
  while (!handle) {
270
490
  try {
271
- handle = await fs3.open(file, "wx", 384);
491
+ handle = await fs4.open(file, "wx", 384);
272
492
  try {
273
493
  await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
274
494
  } catch (error) {
275
495
  await handle.close().catch(() => {
276
496
  });
277
497
  handle = void 0;
278
- await fs3.rm(file, { force: true }).catch(() => {
498
+ await fs4.rm(file, { force: true }).catch(() => {
279
499
  });
280
500
  throw error;
281
501
  }
282
502
  } catch (error) {
283
503
  if (error.code !== "EEXIST") throw error;
284
504
  try {
285
- const owner = JSON.parse(await fs3.readFile(file, "utf8"));
505
+ const owner = JSON.parse(await fs4.readFile(file, "utf8"));
286
506
  if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
287
507
  try {
288
508
  process.kill(owner.pid, 0);
@@ -291,13 +511,13 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
291
511
  const reclaim = file + ".reclaim";
292
512
  let guard;
293
513
  try {
294
- guard = await fs3.open(reclaim, "wx", 384);
295
- const current = JSON.parse(await fs3.readFile(file, "utf8"));
296
- if (current.pid === owner.pid && current.host === owner.host) await fs3.unlink(file);
514
+ guard = await fs4.open(reclaim, "wx", 384);
515
+ const current = JSON.parse(await fs4.readFile(file, "utf8"));
516
+ if (current.pid === owner.pid && current.host === owner.host) await fs4.unlink(file);
297
517
  } finally {
298
518
  if (guard) {
299
519
  await guard.close();
300
- await fs3.rm(reclaim, { force: true });
520
+ await fs4.rm(reclaim, { force: true });
301
521
  }
302
522
  }
303
523
  }
@@ -313,7 +533,7 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
313
533
  return await run();
314
534
  } finally {
315
535
  await handle.close();
316
- await fs3.unlink(file);
536
+ await fs4.unlink(file);
317
537
  }
318
538
  }
319
539
  var init_store_lock = __esm({
@@ -332,7 +552,7 @@ var init_lock = __esm({
332
552
  });
333
553
 
334
554
  // src/test-map.ts
335
- import path4 from "path";
555
+ import path6 from "path";
336
556
  var init_test_map = __esm({
337
557
  "src/test-map.ts"() {
338
558
  "use strict";
@@ -341,7 +561,7 @@ var init_test_map = __esm({
341
561
  });
342
562
 
343
563
  // src/snapshot/snapshot.ts
344
- import path5 from "path";
564
+ import path7 from "path";
345
565
  import { z } from "zod";
346
566
  async function getCurrentGitHash(rootDir) {
347
567
  try {
@@ -393,8 +613,8 @@ var init_snapshot = __esm({
393
613
  });
394
614
 
395
615
  // src/drift/drift.ts
396
- import fs4 from "fs/promises";
397
- import path6 from "path";
616
+ import fs5 from "fs/promises";
617
+ import path8 from "path";
398
618
  function parseChanges(output) {
399
619
  const fields = output.split("\0");
400
620
  const changes = [];
@@ -443,7 +663,7 @@ var init_drift = __esm({
443
663
  });
444
664
 
445
665
  // src/audit/commands.ts
446
- import path7 from "path";
666
+ import path9 from "path";
447
667
  function splitCommands(line) {
448
668
  const parts = [];
449
669
  let quote = "", start = 0;
@@ -479,7 +699,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
479
699
  const cd = text2.match(/^cd\s+(.+)$/);
480
700
  if (cd) {
481
701
  const directory = literal(cd[1]);
482
- cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path7.posix.join(cwd ?? ".", directory);
702
+ cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path9.posix.join(cwd ?? ".", directory);
483
703
  continue;
484
704
  }
485
705
  for (const match of text2.matchAll(COMMAND)) {
@@ -490,7 +710,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
490
710
  if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
491
711
  if (selector) {
492
712
  const selected = literal(selector[1]);
493
- directory = selected === null || cwd === null ? null : path7.posix.join(cwd ?? ".", selected);
713
+ directory = selected === null || cwd === null ? null : path9.posix.join(cwd ?? ".", selected);
494
714
  } else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
495
715
  const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
496
716
  if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
@@ -520,7 +740,7 @@ var init_commands = __esm({
520
740
  COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
521
741
  literal = (value) => {
522
742
  const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
523
- return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path7.posix.isAbsolute(unquoted) ? null : unquoted;
743
+ return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path9.posix.isAbsolute(unquoted) ? null : unquoted;
524
744
  };
525
745
  }
526
746
  });
@@ -825,49 +1045,6 @@ var init_claims = __esm({
825
1045
  }
826
1046
  });
827
1047
 
828
- // src/audit/inspection.ts
829
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
830
- import path8 from "path";
831
- function inspectionRead(root, key, read) {
832
- const inspection = inspections.getStore();
833
- if (!inspection || inspection.root !== path8.resolve(root)) return read();
834
- if (!inspection.reads.has(key)) {
835
- const pending = read();
836
- inspection.reads.set(key, pending);
837
- void pending.catch(() => {
838
- if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
839
- });
840
- }
841
- return inspection.reads.get(key);
842
- }
843
- function inspectionGit(args, options) {
844
- return inspectionRead(options.cwd, "git:" + JSON.stringify([args, options]), () => execGit(args, options));
845
- }
846
- async function auditGitPaths(root, kind) {
847
- const shared = inspections.getStore()?.root === path8.resolve(root);
848
- const specs = shared ? [...docSpecs, ...sourceSpecs] : kind === "documents" ? docSpecs : sourceSpecs;
849
- const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
850
- const { stdout } = await inspectionGit(
851
- ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
852
- { cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
853
- );
854
- const paths = [...new Set(stdout.split("\0").filter(Boolean))].filter((file) => kind === "documents" ? /^(readme|agents|claude)\.md$/i.test(path8.posix.basename(file)) : sourceSuffixes.has(file.slice(file.lastIndexOf("."))));
855
- if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
856
- return paths.sort();
857
- }
858
- var inspections, sourceSpecs, docSpecs, sourceSuffixes;
859
- var init_inspection = __esm({
860
- "src/audit/inspection.ts"() {
861
- "use strict";
862
- init_git_read();
863
- init_files();
864
- inspections = new AsyncLocalStorage2();
865
- sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
866
- docSpecs = [":(icase,glob)**/readme.md", ":(icase,glob)**/agents.md", ":(icase,glob)**/claude.md"];
867
- sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
868
- }
869
- });
870
-
871
1048
  // src/audit/git.ts
872
1049
  function parseCommitLine(line) {
873
1050
  const parts = line.split(" ");
@@ -953,120 +1130,10 @@ var init_git = __esm({
953
1130
  }
954
1131
  });
955
1132
 
956
- // src/audit/inputs.ts
957
- import fs5 from "fs/promises";
958
- import * as nativeFs from "fs";
959
- import path9 from "path";
960
- import fg2 from "fast-glob";
961
- async function auditInputPath(root, file) {
962
- if (file === ".") return fs5.realpath(root);
963
- try {
964
- return await storePath(root, file);
965
- } catch (error) {
966
- if (error instanceof Error && error.message.startsWith("Symlink")) {
967
- throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
968
- }
969
- throw error;
970
- }
971
- }
972
- async function readAuditInput(root, file) {
973
- try {
974
- const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
975
- if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
976
- return value;
977
- } catch (error) {
978
- if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
979
- throw error;
980
- }
981
- }
982
- async function auditGlob(root, patterns, options) {
983
- if (Array.isArray(patterns) && !patterns.length) return [];
984
- const directories = /* @__PURE__ */ new Set();
985
- const readdir2 = ((directory, opts, callback) => {
986
- const relative = path9.relative(root, directory).split(path9.sep).join("/") || ".";
987
- directories.add(relative);
988
- if (directories.size > MAX_INPUTS) {
989
- callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
990
- return;
991
- }
992
- auditInputPath(root, relative).then(() => {
993
- nativeFs.readdir(directory, opts, callback);
994
- }, callback);
995
- });
996
- const lstat2 = ((file, callback) => {
997
- const relative = path9.relative(root, file).split(path9.sep).join("/") || ".";
998
- auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
999
- });
1000
- const stream = fg2.stream(patterns, {
1001
- cwd: root,
1002
- ignore: options.ignore,
1003
- onlyFiles: false,
1004
- onlyDirectories: options.onlyDirectories,
1005
- objectMode: true,
1006
- followSymbolicLinks: options.followSymbolicLinks ?? true,
1007
- fs: { readdir: readdir2, lstat: lstat2 },
1008
- concurrency: 16
1009
- });
1010
- const matches = [];
1011
- try {
1012
- for await (const value of stream) {
1013
- const entry = value;
1014
- const file = entry.path;
1015
- if (options.select && !options.select(file)) continue;
1016
- await auditInputPath(root, file);
1017
- if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
1018
- matches.push(file);
1019
- if (matches.length > MAX_INPUTS) {
1020
- const largest = [...matches.reduce((counts, item) => {
1021
- const dir = item.split("/")[0];
1022
- counts.set(dir, (counts.get(dir) ?? 0) + 1);
1023
- return counts;
1024
- }, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
1025
- 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.`);
1026
- }
1027
- }
1028
- } finally {
1029
- stream.destroy();
1030
- }
1031
- return matches.sort();
1032
- }
1033
- async function gitSourcePaths(root) {
1034
- return auditGitPaths(root, "sources");
1035
- }
1036
- async function gitIgnoredPaths(root, files) {
1037
- if (!files.length) return /* @__PURE__ */ new Set();
1038
- const ignored = /* @__PURE__ */ new Set();
1039
- for (let offset = 0; offset < files.length; offset += 1e3) {
1040
- try {
1041
- const { stdout } = await execGit(["check-ignore", "-z", "--stdin"], {
1042
- cwd: root,
1043
- maxBuffer: 4 * 1024 * 1024,
1044
- timeout: 1e4,
1045
- input: files.slice(offset, offset + 1e3).join("\0") + "\0"
1046
- });
1047
- for (const file of stdout.split("\0").filter(Boolean)) ignored.add(file);
1048
- } catch (error) {
1049
- if (error.code !== 1) throw error;
1050
- }
1051
- }
1052
- return ignored;
1053
- }
1054
- var MAX_INPUTS;
1055
- var init_inputs = __esm({
1056
- "src/audit/inputs.ts"() {
1057
- "use strict";
1058
- init_files();
1059
- init_storage();
1060
- init_git_read();
1061
- init_inspection();
1062
- MAX_INPUTS = 1e5;
1063
- }
1064
- });
1065
-
1066
1133
  // src/audit/docs.ts
1067
1134
  import fs6 from "fs/promises";
1068
1135
  import path10 from "path";
1069
- import fg3 from "fast-glob";
1136
+ import fg5 from "fast-glob";
1070
1137
  async function dirtyDocs(resolvedRoot, files) {
1071
1138
  if (!files.length) return /* @__PURE__ */ new Set();
1072
1139
  try {
@@ -1118,7 +1185,7 @@ async function discoverDocPaths(root) {
1118
1185
  const paths = [];
1119
1186
  const unique = [...new Set(candidates)].sort();
1120
1187
  for (let offset = 0; offset < unique.length; offset += 256) {
1121
- paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) => fg3.escapePath(p)), {
1188
+ paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) => fg5.escapePath(p)), {
1122
1189
  ignore: excluded,
1123
1190
  followSymbolicLinks: false,
1124
1191
  label: "Documentation discovery"
@@ -1148,6 +1215,7 @@ async function localDocPaths(root) {
1148
1215
  return paths;
1149
1216
  }
1150
1217
  function documentScope(file) {
1218
+ if (!/^(readme|agents|claude)\.md$/i.test(path10.posix.basename(file))) return ".";
1151
1219
  const parent = path10.posix.dirname(file);
1152
1220
  return path10.posix.basename(parent).toLowerCase() === ".claude" ? path10.posix.dirname(parent) : parent;
1153
1221
  }
@@ -1169,7 +1237,7 @@ async function readDocs(resolvedRoot) {
1169
1237
  path: candidate,
1170
1238
  content,
1171
1239
  scope: documentScope(candidate),
1172
- kind: /^readme\.md$/i.test(path10.posix.basename(candidate)) ? "readme" : "instructions",
1240
+ kind: /^readme\.md$/i.test(path10.posix.basename(candidate)) ? "readme" : /^(agents|claude)\.md$/i.test(path10.posix.basename(candidate)) ? "instructions" : "guide",
1173
1241
  lineCount: content.split("\n").length,
1174
1242
  lastCommit,
1175
1243
  dirty: dirty.has(candidate),
@@ -1198,22 +1266,6 @@ var init_docs = __esm({
1198
1266
  }
1199
1267
  });
1200
1268
 
1201
- // src/audit/types.ts
1202
- var ALL_CHECKS;
1203
- var init_types = __esm({
1204
- "src/audit/types.ts"() {
1205
- "use strict";
1206
- ALL_CHECKS = [
1207
- "deleted-reference",
1208
- "new-module",
1209
- "stale-count",
1210
- "dead-command",
1211
- "deps-changed",
1212
- "decision-anchor-drift"
1213
- ];
1214
- }
1215
- });
1216
-
1217
1269
  // src/audit/scope.ts
1218
1270
  import fs7 from "fs/promises";
1219
1271
  import path11 from "path";
@@ -1324,7 +1376,7 @@ var init_deleted_reference = __esm({
1324
1376
  });
1325
1377
 
1326
1378
  // src/audit/checks/new-module.ts
1327
- import fg4 from "fast-glob";
1379
+ import fg6 from "fast-glob";
1328
1380
  import fs8 from "fs/promises";
1329
1381
  import path13 from "path";
1330
1382
  function escapeRegExp(text2) {
@@ -1386,7 +1438,7 @@ async function collectModuleCandidates(root, combinedDocs) {
1386
1438
  const result = /* @__PURE__ */ new Map();
1387
1439
  for (const dir of parents) {
1388
1440
  const visible = new Set(byParent.get(dir).filter((candidate) => !ignored.has(candidate) || sources.some((file) => file.startsWith(candidate + "/"))));
1389
- result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg4.escapePath(dir)}/*`, {
1441
+ result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg6.escapePath(dir)}/*`, {
1390
1442
  ignore,
1391
1443
  onlyDirectories: true,
1392
1444
  label: "Module directory discovery",
@@ -1399,7 +1451,7 @@ async function collectModuleCandidates(root, combinedDocs) {
1399
1451
  const files = sources.filter((file) => file.startsWith(dir + "/"));
1400
1452
  let count3 = 0;
1401
1453
  for (let offset = 0; offset < files.length; offset += 256) {
1402
- count3 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg4.escapePath(file)), {
1454
+ count3 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg6.escapePath(file)), {
1403
1455
  ignore,
1404
1456
  followSymbolicLinks: false,
1405
1457
  label: `Source discovery in ${dir}`
@@ -1734,6 +1786,87 @@ var init_dead_command = __esm({
1734
1786
  }
1735
1787
  });
1736
1788
 
1789
+ // src/audit/dependency-claims.ts
1790
+ import path16 from "path";
1791
+ import { parse as parseToml } from "smol-toml";
1792
+ function dependencyText(content) {
1793
+ return content.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, (text2) => text2.replace(/[^\n]/g, " ")).replace(/<!--[\s\S]*?-->/g, (text2) => text2.replace(/[^\n]/g, " "));
1794
+ }
1795
+ function declarations(file, source) {
1796
+ const result = /* @__PURE__ */ new Map();
1797
+ if (!source) return result;
1798
+ if (path16.posix.basename(file) === "package.json") {
1799
+ const json = JSON.parse(source);
1800
+ for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies", "engines"]) {
1801
+ for (const [name, value] of Object.entries(json[section] ?? {})) {
1802
+ if (typeof value === "string") result.set(name, [...result.has(name) ? [result.get(name)] : [], value].join("; "));
1803
+ }
1804
+ }
1805
+ } else if (file.endsWith("libs.versions.toml")) {
1806
+ const catalog = parseToml(source);
1807
+ for (const section of ["libraries", "plugins"]) for (const [alias, entry] of Object.entries(catalog[section] ?? {})) {
1808
+ if (typeof entry === "string") {
1809
+ const parts = entry.split(":");
1810
+ if (parts.length >= 3) result.set(parts.slice(0, -1).join(":"), parts.at(-1));
1811
+ } else if (entry && typeof entry === "object") {
1812
+ const name = entry.module ?? (entry.group && entry.name ? `${entry.group}:${entry.name}` : entry.id);
1813
+ const version = typeof entry.version === "string" ? entry.version : catalog.versions?.[entry.version?.ref];
1814
+ if (typeof name === "string") result.set(name, typeof version === "string" ? version : "unspecified");
1815
+ }
1816
+ }
1817
+ } else if (/build\.gradle(?:\.kts)?$/.test(file)) {
1818
+ for (const match of source.matchAll(/["']([\w.-]+:[\w.-]+):([^"'\s]+)["']/g)) result.set(match[1], match[2]);
1819
+ for (const match of source.matchAll(/\bid\s*\(?\s*["']([\w.-]+)["']\s*\)?\s*version\s*["']([^"']+)["']/g)) result.set(match[1], match[2]);
1820
+ }
1821
+ return result;
1822
+ }
1823
+ async function matchDependencyChanges(root, from, head, files, content) {
1824
+ const matches = [];
1825
+ const lines = dependencyText(content).split("\n");
1826
+ let incomplete = files.length > 64;
1827
+ for (const file of files.slice(0, 64)) {
1828
+ if (!/(?:^|\/)(?:package\.json|libs\.versions\.toml|build\.gradle(?:\.kts)?)$/.test(file)) {
1829
+ incomplete = true;
1830
+ continue;
1831
+ }
1832
+ try {
1833
+ const read = async (revision) => {
1834
+ const { stdout: entry } = await inspectionGit(["ls-tree", revision, "--", `:(literal)${file}`], { cwd: root, timeout: 1e4 });
1835
+ if (!entry) return "";
1836
+ if (!entry.startsWith("100644 ") && !entry.startsWith("100755 ")) throw new Error("Unsupported manifest mode");
1837
+ return (await inspectionGit(["show", `${revision}:${file}`], { cwd: root, maxBuffer: 1024 * 1024, timeout: 1e4 })).stdout;
1838
+ };
1839
+ const [before, after] = await Promise.all([read(from), read(head)]);
1840
+ const old = declarations(file, before), next = declarations(file, after);
1841
+ for (const dependency of /* @__PURE__ */ new Set([...old.keys(), ...next.keys()])) {
1842
+ if (old.get(dependency) === next.get(dependency)) continue;
1843
+ const escaped = dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1844
+ const mention = new RegExp(`(^|[^a-zA-Z0-9_@./-])${escaped}(?![a-zA-Z0-9_/-]|\\.[a-zA-Z0-9_])`, "i");
1845
+ const line = lines.findIndex((text2) => mention.test(text2));
1846
+ if (line < 0) continue;
1847
+ matches.push({
1848
+ dependency,
1849
+ manifest: file,
1850
+ before: old.get(dependency) ?? null,
1851
+ after: next.get(dependency) ?? null,
1852
+ line: line + 1,
1853
+ excerpt: lines[line].trim().slice(0, 500)
1854
+ });
1855
+ if (matches.length >= 100) return { matches, incomplete: true };
1856
+ }
1857
+ } catch {
1858
+ incomplete = true;
1859
+ }
1860
+ }
1861
+ return { matches, incomplete };
1862
+ }
1863
+ var init_dependency_claims = __esm({
1864
+ "src/audit/dependency-claims.ts"() {
1865
+ "use strict";
1866
+ init_inspection();
1867
+ }
1868
+ });
1869
+
1737
1870
  // src/audit/release-metadata.ts
1738
1871
  async function git(root, args) {
1739
1872
  return (await execGit(args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
@@ -1813,13 +1946,6 @@ async function checkDepsChanged(ctx) {
1813
1946
  });
1814
1947
  continue;
1815
1948
  }
1816
- if (doc.dirty) {
1817
- result.skipped.push({
1818
- check: "deps-changed",
1819
- doc: doc.path,
1820
- reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
1821
- });
1822
- }
1823
1949
  let content = doc.content;
1824
1950
  if (doc.dirty) {
1825
1951
  try {
@@ -1832,7 +1958,6 @@ async function checkDepsChanged(ctx) {
1832
1958
  continue;
1833
1959
  }
1834
1960
  }
1835
- if (!hasDependencyContent(content)) continue;
1836
1961
  const range = await commitsTouchingSince(
1837
1962
  ctx.root,
1838
1963
  doc.lastCommit.hash,
@@ -1856,16 +1981,32 @@ async function checkDepsChanged(ctx) {
1856
1981
  range.commits = relevant;
1857
1982
  range.total = relevant.length;
1858
1983
  if (range.total === 0) continue;
1859
- const latest = range.commits[0];
1984
+ const relevance = await matchDependencyChanges(
1985
+ ctx.root,
1986
+ doc.lastCommit.hash,
1987
+ ctx.headHash,
1988
+ [...new Set(range.commits.flatMap((commit) => commit.files))],
1989
+ content
1990
+ );
1991
+ if (!relevance.matches.length && !hasDependencyContent(content)) continue;
1992
+ const match = relevance.matches[0];
1993
+ if (doc.dirty && match) result.skipped.push({
1994
+ check: "deps-changed",
1995
+ doc: doc.path,
1996
+ reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
1997
+ });
1860
1998
  (doc.dirty ? result.suppressedAdvisories : result.advisories).push({
1861
1999
  type: "deps-changed",
1862
- message: `dependency manifests touched by ${range.total} commit${range.total === 1 ? "" : "s"} since ${doc.path} was last committed (latest: ${latest.hash.slice(0, 7)} "${latest.subject}")`,
1863
- anchor: { doc: doc.path, line: null, excerpt: null },
2000
+ ...match ? {} : { resolution: "informational" },
2001
+ message: match ? `${match.dependency} changed from ${match.before ?? "absent"} to ${match.after ?? "removed"} in ${match.manifest}; review ${doc.path}:${match.line}: ${match.excerpt}${relevance.matches.length > 1 ? ` (${relevance.matches.length - 1} more matches in evidence)` : ""}` : `Dependency manifests changed after the document baseline ${doc.lastCommit.hash.slice(0, 7)} (${doc.lastCommit.date.slice(0, 10)}); no specific affected passage was established. Informational only.${relevance.incomplete ? " Some manifest declarations could not be matched." : ""}`,
2002
+ anchor: { doc: doc.path, line: match?.line ?? null, excerpt: match?.excerpt ?? null },
1864
2003
  evidence: {
1865
2004
  kind: "doc-behind-manifests",
1866
2005
  docLastCommit: doc.lastCommit,
1867
2006
  manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
1868
- totalCommits: range.total
2007
+ totalCommits: range.total,
2008
+ matches: relevance.matches,
2009
+ matchingIncomplete: relevance.incomplete
1869
2010
  }
1870
2011
  });
1871
2012
  }
@@ -1875,6 +2016,7 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
1875
2016
  var init_deps_changed = __esm({
1876
2017
  "src/audit/checks/deps-changed.ts"() {
1877
2018
  "use strict";
2019
+ init_dependency_claims();
1878
2020
  init_docs();
1879
2021
  init_release_metadata();
1880
2022
  init_git();
@@ -1899,7 +2041,7 @@ var init_deps_changed = __esm({
1899
2041
  });
1900
2042
 
1901
2043
  // src/context/lexical.ts
1902
- import path16 from "path";
2044
+ import path17 from "path";
1903
2045
  var init_lexical = __esm({
1904
2046
  "src/context/lexical.ts"() {
1905
2047
  "use strict";
@@ -2060,7 +2202,7 @@ var init_provenance = __esm({
2060
2202
 
2061
2203
  // src/decisions/decisions.ts
2062
2204
  import fs9 from "fs/promises";
2063
- import path17 from "path";
2205
+ import path18 from "path";
2064
2206
  import { createHash } from "crypto";
2065
2207
  import { z as z3 } from "zod";
2066
2208
  async function loadDecisionStore(rootDir) {
@@ -2111,9 +2253,9 @@ var init_decisions = __esm({
2111
2253
  });
2112
2254
 
2113
2255
  // src/decisions/drift.ts
2114
- import path18 from "path";
2256
+ import path19 from "path";
2115
2257
  async function computeDecisionDrift(rootDir, decisions) {
2116
- const resolvedRoot = path18.resolve(rootDir);
2258
+ const resolvedRoot = path19.resolve(rootDir);
2117
2259
  const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
2118
2260
  const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
2119
2261
  const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
@@ -2309,7 +2451,9 @@ var init_findings = __esm({
2309
2451
  kind: z4.literal("doc-behind-manifests"),
2310
2452
  docLastCommit: commitSchema,
2311
2453
  manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
2312
- totalCommits: count
2454
+ totalCommits: count,
2455
+ 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(),
2456
+ matchingIncomplete: z4.boolean().optional()
2313
2457
  }),
2314
2458
  z4.object({
2315
2459
  kind: z4.literal("decision-anchor"),
@@ -2325,7 +2469,7 @@ var init_findings = __esm({
2325
2469
  type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
2326
2470
  confidence: z4.enum(["certain", "likely"])
2327
2471
  });
2328
- advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.literal("recheck").optional() });
2472
+ advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.enum(["recheck", "informational"]).optional() });
2329
2473
  checkResultSchema = z4.object({
2330
2474
  issues: z4.array(issueSchema),
2331
2475
  advisories: z4.array(advisorySchema),
@@ -2387,7 +2531,7 @@ __export(repair_exports, {
2387
2531
  withRepairSession: () => withRepairSession
2388
2532
  });
2389
2533
  import fs10 from "fs/promises";
2390
- import path19 from "path";
2534
+ import path20 from "path";
2391
2535
  import { randomUUID as randomUUID2 } from "crypto";
2392
2536
  import { z as z6 } from "zod";
2393
2537
  function recheckable(finding) {
@@ -2408,7 +2552,7 @@ async function docState(root) {
2408
2552
  docs.push([doc, null]);
2409
2553
  }
2410
2554
  }
2411
- return digest(docs);
2555
+ return digest([docs, (await loadProjectConfig(root)).audit]);
2412
2556
  }
2413
2557
  async function stableAudit(root, checks, options = {}) {
2414
2558
  return profilePhase("repair.audit", () => readStableAudit(root, checks, options));
@@ -2477,8 +2621,8 @@ async function prepareOriginal(rootDir, checks, options, session) {
2477
2621
  }
2478
2622
  async function loadRepairBaseline(rootDir, baselinePath) {
2479
2623
  const root = await fs10.realpath(rootDir);
2480
- const declaredRoot = path19.resolve(rootDir);
2481
- const relative = path19.isAbsolute(baselinePath) ? path19.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
2624
+ const declaredRoot = path20.resolve(rootDir);
2625
+ const relative = path20.isAbsolute(baselinePath) ? path20.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
2482
2626
  const stored = baselineSchema.parse(await readStoreJson(root, relative));
2483
2627
  const { digest: savedDigest, ...payload } = stored;
2484
2628
  if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
@@ -2527,6 +2671,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
2527
2671
  if (now && ("confidence" in finding || "confidence" in now)) {
2528
2672
  return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
2529
2673
  }
2674
+ if (current.excludedChecks?.some((excluded) => excluded.check === finding.type && excluded.doc === finding.anchor.doc)) {
2675
+ return { ...base, status: "resolved", reason: "Excluded by project audit policy; this is not a correctness assessment. Original evidence is retained." };
2676
+ }
2530
2677
  const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
2531
2678
  if (!current.checksRun?.includes(finding.type) || skipped.length) {
2532
2679
  return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
@@ -2537,6 +2684,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
2537
2684
  if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
2538
2685
  return { ...base, review, status: "review-required", reason: review.reason };
2539
2686
  }
2687
+ if (!("confidence" in finding) && finding.evidence.kind === "doc-behind-manifests" && !finding.evidence.matches?.length && (!now || !("confidence" in now) && now.resolution === "informational")) {
2688
+ return { ...base, status: "resolved", reason: "Manifest recency alone establishes no affected passage. Retained as information; no assessment is required." };
2689
+ }
2540
2690
  if (!("confidence" in finding) && (!recheckable(finding) || now)) {
2541
2691
  return {
2542
2692
  ...base,
@@ -2565,7 +2715,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
2565
2715
  const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
2566
2716
  const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
2567
2717
  for (const f of findings) counts[f.status]++;
2568
- const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f) && !reviewedIds.has(findingId(f)));
2718
+ 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)));
2569
2719
  const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
2570
2720
  return {
2571
2721
  version: 1,
@@ -2588,7 +2738,7 @@ function repairExitCode(report) {
2588
2738
  function formatRepairSummary(report) {
2589
2739
  return [
2590
2740
  "Repair verification: " + report.status + ". Baseline: " + report.baselinePath,
2591
- ...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + f.original.message + "\n " + f.reason + "\n findingId: " + f.id),
2741
+ ...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),
2592
2742
  ...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
2593
2743
  ...report.diagnostics.map((d) => " [unverified] " + d),
2594
2744
  ...(report.currentAudit?.skippedChecks ?? []).map((s) => " [skipped] " + s.check + ": " + s.reason),
@@ -2621,13 +2771,14 @@ var init_repair = __esm({
2621
2771
  docs: z6.array(z6.object({
2622
2772
  path: z6.string().refine((value) => normalizeRepoPath(value) === value),
2623
2773
  scope: z6.string().optional(),
2624
- kind: z6.enum(["instructions", "readme"]).optional(),
2774
+ kind: z6.enum(["instructions", "readme", "guide"]).optional(),
2625
2775
  lastCommit: commitSchema.nullable(),
2626
2776
  dirty: z6.boolean(),
2627
2777
  lineCount: count2
2628
2778
  })).nonempty(),
2629
2779
  decisionsChecked: z6.boolean(),
2630
2780
  clean: z6.boolean(),
2781
+ excludedChecks: z6.array(z6.object({ check: checkSchema, doc: z6.string() })).optional(),
2631
2782
  issues: z6.array(issueSchema),
2632
2783
  advisories: z6.array(advisorySchema),
2633
2784
  suppressedAdvisories: z6.array(advisorySchema).optional(),
@@ -2659,14 +2810,18 @@ async function readRecord(root, id) {
2659
2810
  if (raw === null) return null;
2660
2811
  const record = recordSchema.parse(raw);
2661
2812
  const { digest: stored, ...payload } = record;
2662
- if (record.id !== id || digest(payload) !== stored || record.events.some((event) => findingId(event.finding) !== id || event.evidence.fingerprint !== digest([event.evidence.scope, event.evidence.tree, event.evidence.lastScopeCommit, event.evidence.paths]))) {
2813
+ 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))) {
2663
2814
  throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
2664
2815
  }
2665
2816
  return record;
2666
2817
  }
2667
- function scopes(finding) {
2818
+ function evidenceFingerprint(evidence, reason) {
2819
+ return digest([evidence.scope, evidence.tree, ...reason === "no-dependency-claims" ? [] : [evidence.lastScopeCommit], evidence.paths]);
2820
+ }
2821
+ function scopes(finding, reason) {
2668
2822
  const files = [finding.anchor.doc], e = finding.evidence;
2669
2823
  if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
2824
+ if (reason === "no-dependency-claims" && e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`];
2670
2825
  if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
2671
2826
  if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
2672
2827
  else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
@@ -2677,9 +2832,9 @@ function scopes(finding) {
2677
2832
  return `:(literal)${file}`;
2678
2833
  }).sort();
2679
2834
  }
2680
- async function reviewEvidence(root, finding) {
2835
+ async function reviewEvidence(root, finding, reason) {
2681
2836
  if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
2682
- const scope = scopes(finding);
2837
+ const scope = scopes(finding, reason);
2683
2838
  const head = (await git2(root, "rev-parse", "HEAD")).trim();
2684
2839
  const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
2685
2840
  if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
@@ -2705,7 +2860,7 @@ async function reviewEvidence(root, finding) {
2705
2860
  if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
2706
2861
  }
2707
2862
  }
2708
- const fingerprint = digest([scope, tree, lastScopeCommit, paths]);
2863
+ const fingerprint = evidenceFingerprint({ head, scope, tree, lastScopeCommit, paths, fingerprint: "" }, reason);
2709
2864
  if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
2710
2865
  return {
2711
2866
  ...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
@@ -2752,7 +2907,7 @@ async function assessAdvisory(root, finding, baselineHead) {
2752
2907
  const record = await readRecord(root, id);
2753
2908
  if (!record) return null;
2754
2909
  const event = record.events.at(-1);
2755
- const current = await reviewEvidence(root, event.finding);
2910
+ const current = await reviewEvidence(root, event.finding, event.reasonCode);
2756
2911
  await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
2757
2912
  const base = {
2758
2913
  id,
@@ -2762,7 +2917,7 @@ async function assessAdvisory(root, finding, baselineHead) {
2762
2917
  outcome: event.outcome,
2763
2918
  reviewedHead: event.evidence.head
2764
2919
  };
2765
- if (current.dirty || current.fingerprint !== event.evidence.fingerprint || digest(scopes(finding)) !== digest(event.evidence.scope)) return { ...base, status: "reopened", reason: "Relevant document or repository evidence changed after the assessment; prepare a new review." };
2920
+ 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." };
2766
2921
  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." };
2767
2922
  } catch (error) {
2768
2923
  return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
@@ -2790,9 +2945,13 @@ async function reviewAdvisory(rootDir, input) {
2790
2945
  decisionId: finding.evidence.decisionId,
2791
2946
  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."
2792
2947
  };
2948
+ if (request.action === "dismiss") {
2949
+ if (finding.type !== "deps-changed" || finding.evidence.kind !== "doc-behind-manifests") throw new Error("One-step dismissal is only available for dependency advisories.");
2950
+ if (!request.reasonCode || !request.note) throw new Error("Dismissal requires reasonCode and note; it does not require a reviewer or prepared token.");
2951
+ } else if (request.reasonCode) throw new Error("reasonCode is only used with dismiss.");
2793
2952
  const prepare = async () => {
2794
2953
  const record = await readRecord(root, request.findingId);
2795
- const evidence = await reviewEvidence(root, finding);
2954
+ const evidence = await reviewEvidence(root, finding, request.reasonCode);
2796
2955
  await git2(root, "merge-base", "--is-ancestor", baseline.original.headHash, evidence.head);
2797
2956
  const token = digest({ baseline: baseline.original, finding, evidence, record });
2798
2957
  return { record, evidence, token };
@@ -2821,10 +2980,10 @@ async function reviewAdvisory(rootDir, input) {
2821
2980
  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."
2822
2981
  };
2823
2982
  }
2824
- if (!request.reviewer || !request.note || !request.reviewToken) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
2983
+ if (request.action !== "dismiss" && (!request.reviewer || !request.note || !request.reviewToken)) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
2825
2984
  return withLock(root, ".mason/reports/advisory-review-locks/" + request.findingId, async () => {
2826
2985
  const state = await prepare();
2827
- if (state.token !== request.reviewToken) throw new Error("Review conflict: evidence or review history changed since preparation. Prepare and inspect a new review.");
2986
+ 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.");
2828
2987
  if (state.evidence.dirty) throw new Error("Commit relevant document and repository edits before recording the assessment. Unrelated local work may remain.");
2829
2988
  if ((state.record?.events.length ?? 0) >= 200) throw new Error("Review history limit reached; existing events were retained.");
2830
2989
  const { dirty, documentDigest, ...evidence } = state.evidence;
@@ -2832,7 +2991,8 @@ async function reviewAdvisory(rootDir, input) {
2832
2991
  at: (/* @__PURE__ */ new Date()).toISOString(),
2833
2992
  reviewer: request.reviewer,
2834
2993
  note: request.note,
2835
- outcome: request.action,
2994
+ outcome: request.action === "dismiss" ? "dismissed" : request.action,
2995
+ reasonCode: request.reasonCode,
2836
2996
  finding,
2837
2997
  evidence
2838
2998
  });
@@ -2843,11 +3003,11 @@ async function reviewAdvisory(rootDir, input) {
2843
3003
  status: "recorded",
2844
3004
  recordPath: recordPath(request.findingId),
2845
3005
  event,
2846
- hint: "Review and commit this assessment record through the normal project workflow, then verify the original repair baseline. Relevant changes reopen the assessment; deferral remains outstanding."
3006
+ hint: request.action === "dismiss" ? "Dismissal saved and effective locally. Commit the record to share it. No-dependency-claims dismissals reopen on document content changes; unrelated-manifest-change dismissals reopen on scoped document or manifest changes. This does not approve a decision." : "Review and commit this assessment record through the normal project workflow, then verify the original repair baseline. Relevant changes reopen the assessment; deferral remains outstanding."
2847
3007
  };
2848
3008
  });
2849
3009
  }
2850
- var git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
3010
+ var git2, hashSchema, commitSchema2, evidenceSchema2, dismissalReasonSchema, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
2851
3011
  var init_advisory_review = __esm({
2852
3012
  "src/audit/advisory-review.ts"() {
2853
3013
  "use strict";
@@ -2873,11 +3033,13 @@ var init_advisory_review = __esm({
2873
3033
  lastScopeCommit: commitSchema2,
2874
3034
  paths: z7.array(z7.object({ path: z7.string(), exists: z7.boolean() })).max(1e3)
2875
3035
  });
3036
+ dismissalReasonSchema = z7.enum(["no-dependency-claims", "unrelated-manifest-change"]);
2876
3037
  eventSchema2 = z7.object({
2877
3038
  at: z7.string().datetime(),
2878
- reviewer: z7.string().trim().min(1).max(200),
3039
+ reviewer: z7.string().trim().min(1).max(200).optional(),
2879
3040
  note: z7.string().trim().min(1).max(2500),
2880
- outcome: z7.enum(["addressed", "inapplicable", "deferred"]),
3041
+ outcome: z7.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
3042
+ reasonCode: dismissalReasonSchema.optional(),
2881
3043
  finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
2882
3044
  evidence: evidenceSchema2
2883
3045
  });
@@ -2889,13 +3051,14 @@ var init_advisory_review = __esm({
2889
3051
  recordPath: z7.string().optional(),
2890
3052
  reviewer: z7.string().optional(),
2891
3053
  note: z7.string().optional(),
2892
- outcome: z7.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
3054
+ outcome: z7.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
2893
3055
  reviewedHead: z7.string().optional()
2894
3056
  });
2895
3057
  advisoryReviewRequest = z7.object({
2896
3058
  baselinePath: z7.string().min(1),
2897
3059
  findingId: hashSchema,
2898
- action: z7.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
3060
+ action: z7.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
3061
+ reasonCode: dismissalReasonSchema.optional(),
2899
3062
  reviewer: eventSchema2.shape.reviewer.optional(),
2900
3063
  note: eventSchema2.shape.note.optional(),
2901
3064
  reviewToken: hashSchema.optional()
@@ -2906,12 +3069,12 @@ var init_advisory_review = __esm({
2906
3069
 
2907
3070
  // src/audit/audit.ts
2908
3071
  import fs12 from "fs/promises";
2909
- import path20 from "path";
3072
+ import path21 from "path";
2910
3073
  async function computeAudit(rootDir, options = {}) {
2911
3074
  return profilePhase("audit.current", () => auditCurrent(rootDir, options));
2912
3075
  }
2913
3076
  async function auditCurrent(rootDir, options) {
2914
- const resolvedRoot = path20.resolve(rootDir);
3077
+ const resolvedRoot = path21.resolve(rootDir);
2915
3078
  const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
2916
3079
  if (docs.length === 0) return null;
2917
3080
  const report = {
@@ -2945,7 +3108,7 @@ async function auditCurrent(rootDir, options) {
2945
3108
  }
2946
3109
  let decisionsPresent = false;
2947
3110
  try {
2948
- await fs12.access(path20.join(resolvedRoot, ".mason", "decisions"));
3111
+ await fs12.access(path21.join(resolvedRoot, ".mason", "decisions"));
2949
3112
  decisionsPresent = true;
2950
3113
  } catch {
2951
3114
  }
@@ -2957,13 +3120,21 @@ async function auditCurrent(rootDir, options) {
2957
3120
  changesSinceDoc,
2958
3121
  decisionsPresent
2959
3122
  };
3123
+ const policy = (await loadProjectConfig(resolvedRoot)).audit;
2960
3124
  const selected = options.checks ?? ALL_CHECKS;
2961
3125
  for (const name of ALL_CHECKS) {
2962
3126
  if (!selected.includes(name)) continue;
2963
- const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, ctx) : CHECKS[name](ctx));
3127
+ const exclusions = policy?.exclude?.[name] ?? [];
3128
+ const included = await includedCheckPaths(resolvedRoot, docs.map((doc) => doc.path), exclusions);
3129
+ const excluded = docs.filter((doc) => !included.has(doc.path)).map((doc) => ({ check: name, doc: doc.path }));
3130
+ if (excluded.length) (report.excludedChecks ??= []).push(...excluded);
3131
+ const scoped = { ...ctx, docs: docs.filter((doc) => included.has(doc.path)) };
3132
+ const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, scoped) : CHECKS[name](scoped));
2964
3133
  report.checksRun.push(name);
2965
3134
  report.issues.push(...issues);
2966
- report.advisories.push(...advisories);
3135
+ const advisoryPaths = await includedCheckPaths(resolvedRoot, [...new Set(advisories.map((f) => f.anchor.doc))], exclusions);
3136
+ report.advisories.push(...advisories.filter((f) => advisoryPaths.has(f.anchor.doc)));
3137
+ for (const f of advisories) if (!advisoryPaths.has(f.anchor.doc)) (report.excludedChecks ??= []).push({ check: name, doc: f.anchor.doc });
2967
3138
  report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
2968
3139
  report.skippedChecks.push(...skipped);
2969
3140
  }
@@ -2975,6 +3146,8 @@ async function auditCurrent(rootDir, options) {
2975
3146
  var init_audit = __esm({
2976
3147
  "src/audit/audit.ts"() {
2977
3148
  "use strict";
3149
+ init_files();
3150
+ init_policy();
2978
3151
  init_profile();
2979
3152
  init_drift();
2980
3153
  init_snapshot();
@@ -2990,11 +3163,11 @@ init_audit();
2990
3163
  init_types();
2991
3164
  init_repair();
2992
3165
  init_findings();
2993
- import path22 from "path";
3166
+ import path23 from "path";
2994
3167
 
2995
3168
  // src/audit/advisory-cli.ts
2996
3169
  init_advisory_review();
2997
- import path21 from "path";
3170
+ import path22 from "path";
2998
3171
  import { stripVTControlCharacters } from "util";
2999
3172
  var USAGE = `Usage: mason audit review --baseline <path> --finding <id> [options]
3000
3173
 
@@ -3002,13 +3175,15 @@ Prepare an advisory assessment against retained evidence (read-only by default).
3002
3175
  --dir <path> Repository root (default: current directory)
3003
3176
  --baseline <path> Original repair baseline
3004
3177
  --finding <id> findingId from repair verification or automation
3005
- --outcome <value> Record addressed, inapplicable, or deferred
3178
+ --outcome <value> Record addressed, inapplicable, deferred, or dismiss
3179
+ --reason <code> Dismiss: no-dependency-claims or unrelated-manifest-change
3006
3180
  --reviewer <name> Actual authorized reviewer
3007
3181
  --note <reason> Assessment reason
3008
3182
  --token <token> reviewToken returned by preparation
3009
3183
  --json Full evidence and review history
3010
3184
 
3011
- Recording requires prior preparation and committed relevant edits. Review records
3185
+ Assessments require preparation and committed relevant edits. Dismiss requires only
3186
+ --reason and --note, with a committed document; no reviewer or token is needed. Review records
3012
3187
  belong in Git under .mason/reviews/advisories/. Deferral remains outstanding.
3013
3188
  Decision findings use review_decision; this command never approves decisions.
3014
3189
  Exit 0: prepared/recorded; 2: unavailable, conflict, or decision review required.`;
@@ -3026,21 +3201,22 @@ async function runAdvisoryCli(args, io = { out: (text2) => console.log(text2), e
3026
3201
  json = true;
3027
3202
  continue;
3028
3203
  }
3029
- if (!["--dir", "--baseline", "--finding", "--outcome", "--reviewer", "--note", "--token"].includes(flag) || flags[flag]) throw new Error("Unexpected or repeated argument: " + flag);
3204
+ if (!["--dir", "--baseline", "--finding", "--outcome", "--reviewer", "--note", "--token", "--reason"].includes(flag) || flags[flag]) throw new Error("Unexpected or repeated argument: " + flag);
3030
3205
  const value = args[++i];
3031
3206
  if (!value || value.startsWith("--")) throw new Error(flag + " requires a value.");
3032
3207
  flags[flag] = value;
3033
3208
  }
3034
- if (!flags["--outcome"] && ["--reviewer", "--note", "--token"].some((key) => flags[key])) throw new Error("Use --outcome to record an assessment.");
3209
+ if (!flags["--outcome"] && ["--reviewer", "--note", "--token", "--reason"].some((key) => flags[key])) throw new Error("Use --outcome to record an assessment.");
3035
3210
  const input = {
3036
3211
  baselinePath: flags["--baseline"],
3037
3212
  findingId: flags["--finding"],
3038
3213
  action: flags["--outcome"] ?? "prepare",
3039
3214
  reviewer: flags["--reviewer"],
3040
3215
  note: flags["--note"],
3041
- reviewToken: flags["--token"]
3216
+ reviewToken: flags["--token"],
3217
+ reasonCode: flags["--reason"]
3042
3218
  };
3043
- const result = await reviewAdvisory(path21.resolve(flags["--dir"] ?? process.cwd()), input);
3219
+ const result = await reviewAdvisory(path22.resolve(flags["--dir"] ?? process.cwd()), input);
3044
3220
  if (json) io.out(JSON.stringify(result, null, 2));
3045
3221
  else {
3046
3222
  const lines = [`Advisory review: ${result.status}.`];
@@ -3152,8 +3328,8 @@ function issueLine(issue) {
3152
3328
  function formatAuditSummary(report) {
3153
3329
  const lines = [];
3154
3330
  const reviewed = new Set(report.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
3155
- const open = report.advisories.filter((finding) => !reviewed.has(findingId(finding)));
3156
- const suppressed = (report.suppressedAdvisories ?? []).filter((finding) => !reviewed.has(findingId(finding)));
3331
+ const open = report.advisories.filter((finding) => finding.resolution !== "informational" && !reviewed.has(findingId(finding)));
3332
+ const suppressed = (report.suppressedAdvisories ?? []).filter((finding) => finding.resolution !== "informational" && !reviewed.has(findingId(finding)));
3157
3333
  const reviewCount = open.length + suppressed.length;
3158
3334
  for (const doc of report.docs) {
3159
3335
  const docIssues = report.issues.filter((i) => i.anchor.doc === doc.path);
@@ -3171,6 +3347,9 @@ function formatAuditSummary(report) {
3171
3347
  for (const issue of report.issues) {
3172
3348
  if (!docPaths.has(issue.anchor.doc)) lines.push(issueLine(issue));
3173
3349
  }
3350
+ const information = [...report.advisories, ...report.suppressedAdvisories ?? []].filter((f) => f.resolution === "informational");
3351
+ if (information.length) lines.push(`${information.length} informational dependency notices; no review required (see --json).`);
3352
+ if (report.excludedChecks?.length) lines.push(`${report.excludedChecks.length} document/check pairs excluded by project policy (see --json).`);
3174
3353
  if (open.length > 0) {
3175
3354
  lines.push("Advisories (do not affect the exit code):");
3176
3355
  for (const advisory of open) {
@@ -3224,7 +3403,7 @@ function formatFixPrompt(report, baselinePath) {
3224
3403
  "- new-module: review whether the directory needs documenting; an omission alone does not establish a defect. Preserve scope and describe only what you verified."
3225
3404
  );
3226
3405
  lines.push(
3227
- "- 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. 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."
3406
+ "- 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."
3228
3407
  );
3229
3408
  lines.push("");
3230
3409
  lines.push("AUDIT REPORT (current context files and repository evidence, including local edits):");
@@ -3273,7 +3452,7 @@ async function runAuditCli(argv, io = {
3273
3452
  io.out(USAGE2);
3274
3453
  return 0;
3275
3454
  }
3276
- const rootDir = path22.resolve(args.dir);
3455
+ const rootDir = path23.resolve(args.dir);
3277
3456
  if (args.baseline || args.prepareRepair) {
3278
3457
  try {
3279
3458
  if (args.baseline) {