mason-context 0.18.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.
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ if (!import.meta.url.endsWith("/mason-runtime.js")) await import("./mason-runtime.js").then(m => m.registerRuntime());
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
5
  var __esm = (fn, res) => function __init() {
@@ -84,6 +85,26 @@ var init_paths = __esm({
84
85
  }
85
86
  });
86
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
+
87
108
  // src/utils/files.ts
88
109
  import fs from "fs/promises";
89
110
  import { constants } from "fs";
@@ -123,6 +144,27 @@ async function loadProjectConfig(root) {
123
144
  }
124
145
  config[key] = value[key];
125
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
+ }
126
168
  return config;
127
169
  } catch (error) {
128
170
  if (error.code === "ENOENT") return {};
@@ -234,13 +276,192 @@ var init_storage = __esm({
234
276
  }
235
277
  });
236
278
 
237
- // 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
238
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";
239
460
  import os from "os";
240
461
  async function lockDiagnostic(file) {
241
462
  let reason;
242
463
  try {
243
- const owner = JSON.parse(await fs3.readFile(file, "utf8"));
464
+ const owner = JSON.parse(await fs4.readFile(file, "utf8"));
244
465
  if (!owner || typeof owner.host !== "string" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
245
466
  reason = "Lock owner data is incomplete or malformed.";
246
467
  } else if (owner.host !== os.hostname()) {
@@ -256,7 +477,7 @@ async function lockDiagnostic(file) {
256
477
  } catch (error) {
257
478
  reason = error instanceof SyntaxError ? "Lock owner data is incomplete or malformed." : "Lock owner data could not be read; the lock may have changed.";
258
479
  }
259
- if (await fs3.lstat(file + ".reclaim").then(() => true, () => false)) {
480
+ if (await fs4.lstat(file + ".reclaim").then(() => true, () => false)) {
260
481
  reason += " A recovery guard also needs inspection: " + file + ".reclaim.";
261
482
  }
262
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.";
@@ -267,21 +488,21 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
267
488
  let handle;
268
489
  while (!handle) {
269
490
  try {
270
- handle = await fs3.open(file, "wx", 384);
491
+ handle = await fs4.open(file, "wx", 384);
271
492
  try {
272
493
  await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
273
494
  } catch (error) {
274
495
  await handle.close().catch(() => {
275
496
  });
276
497
  handle = void 0;
277
- await fs3.rm(file, { force: true }).catch(() => {
498
+ await fs4.rm(file, { force: true }).catch(() => {
278
499
  });
279
500
  throw error;
280
501
  }
281
502
  } catch (error) {
282
503
  if (error.code !== "EEXIST") throw error;
283
504
  try {
284
- const owner = JSON.parse(await fs3.readFile(file, "utf8"));
505
+ const owner = JSON.parse(await fs4.readFile(file, "utf8"));
285
506
  if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
286
507
  try {
287
508
  process.kill(owner.pid, 0);
@@ -290,13 +511,13 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
290
511
  const reclaim = file + ".reclaim";
291
512
  let guard;
292
513
  try {
293
- guard = await fs3.open(reclaim, "wx", 384);
294
- const current = JSON.parse(await fs3.readFile(file, "utf8"));
295
- 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);
296
517
  } finally {
297
518
  if (guard) {
298
519
  await guard.close();
299
- await fs3.rm(reclaim, { force: true });
520
+ await fs4.rm(reclaim, { force: true });
300
521
  }
301
522
  }
302
523
  }
@@ -312,7 +533,7 @@ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason
312
533
  return await run();
313
534
  } finally {
314
535
  await handle.close();
315
- await fs3.unlink(file);
536
+ await fs4.unlink(file);
316
537
  }
317
538
  }
318
539
  var init_store_lock = __esm({
@@ -331,7 +552,7 @@ var init_lock = __esm({
331
552
  });
332
553
 
333
554
  // src/test-map.ts
334
- import path4 from "path";
555
+ import path6 from "path";
335
556
  var init_test_map = __esm({
336
557
  "src/test-map.ts"() {
337
558
  "use strict";
@@ -340,7 +561,7 @@ var init_test_map = __esm({
340
561
  });
341
562
 
342
563
  // src/snapshot/snapshot.ts
343
- import path5 from "path";
564
+ import path7 from "path";
344
565
  import { z } from "zod";
345
566
  async function getCurrentGitHash(rootDir) {
346
567
  try {
@@ -392,8 +613,8 @@ var init_snapshot = __esm({
392
613
  });
393
614
 
394
615
  // src/drift/drift.ts
395
- import fs4 from "fs/promises";
396
- import path6 from "path";
616
+ import fs5 from "fs/promises";
617
+ import path8 from "path";
397
618
  function parseChanges(output) {
398
619
  const fields = output.split("\0");
399
620
  const changes = [];
@@ -442,7 +663,7 @@ var init_drift = __esm({
442
663
  });
443
664
 
444
665
  // src/audit/commands.ts
445
- import path7 from "path";
666
+ import path9 from "path";
446
667
  function splitCommands(line) {
447
668
  const parts = [];
448
669
  let quote = "", start = 0;
@@ -478,7 +699,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
478
699
  const cd = text2.match(/^cd\s+(.+)$/);
479
700
  if (cd) {
480
701
  const directory = literal(cd[1]);
481
- 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);
482
703
  continue;
483
704
  }
484
705
  for (const match of text2.matchAll(COMMAND)) {
@@ -489,7 +710,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
489
710
  if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
490
711
  if (selector) {
491
712
  const selected = literal(selector[1]);
492
- directory = selected === null || cwd === null ? null : path7.posix.join(cwd ?? ".", selected);
713
+ directory = selected === null || cwd === null ? null : path9.posix.join(cwd ?? ".", selected);
493
714
  } else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
494
715
  const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
495
716
  if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
@@ -519,7 +740,7 @@ var init_commands = __esm({
519
740
  COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
520
741
  literal = (value) => {
521
742
  const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
522
- return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path7.posix.isAbsolute(unquoted) ? null : unquoted;
743
+ return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path9.posix.isAbsolute(unquoted) ? null : unquoted;
523
744
  };
524
745
  }
525
746
  });
@@ -824,49 +1045,6 @@ var init_claims = __esm({
824
1045
  }
825
1046
  });
826
1047
 
827
- // src/audit/inspection.ts
828
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
829
- import path8 from "path";
830
- function inspectionRead(root, key, read) {
831
- const inspection = inspections.getStore();
832
- if (!inspection || inspection.root !== path8.resolve(root)) return read();
833
- if (!inspection.reads.has(key)) {
834
- const pending = read();
835
- inspection.reads.set(key, pending);
836
- void pending.catch(() => {
837
- if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
838
- });
839
- }
840
- return inspection.reads.get(key);
841
- }
842
- function inspectionGit(args, options) {
843
- return inspectionRead(options.cwd, "git:" + JSON.stringify([args, options]), () => execGit(args, options));
844
- }
845
- async function auditGitPaths(root, kind) {
846
- const shared = inspections.getStore()?.root === path8.resolve(root);
847
- const specs = shared ? [...docSpecs, ...sourceSpecs] : kind === "documents" ? docSpecs : sourceSpecs;
848
- const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
849
- const { stdout } = await inspectionGit(
850
- ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
851
- { cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
852
- );
853
- 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("."))));
854
- if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
855
- return paths.sort();
856
- }
857
- var inspections, sourceSpecs, docSpecs, sourceSuffixes;
858
- var init_inspection = __esm({
859
- "src/audit/inspection.ts"() {
860
- "use strict";
861
- init_git_read();
862
- init_files();
863
- inspections = new AsyncLocalStorage2();
864
- sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
865
- docSpecs = [":(icase,glob)**/readme.md", ":(icase,glob)**/agents.md", ":(icase,glob)**/claude.md"];
866
- sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
867
- }
868
- });
869
-
870
1048
  // src/audit/git.ts
871
1049
  function parseCommitLine(line) {
872
1050
  const parts = line.split(" ");
@@ -952,120 +1130,10 @@ var init_git = __esm({
952
1130
  }
953
1131
  });
954
1132
 
955
- // src/audit/inputs.ts
956
- import fs5 from "fs/promises";
957
- import * as nativeFs from "fs";
958
- import path9 from "path";
959
- import fg2 from "fast-glob";
960
- async function auditInputPath(root, file) {
961
- if (file === ".") return fs5.realpath(root);
962
- try {
963
- return await storePath(root, file);
964
- } catch (error) {
965
- if (error instanceof Error && error.message.startsWith("Symlink")) {
966
- throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
967
- }
968
- throw error;
969
- }
970
- }
971
- async function readAuditInput(root, file) {
972
- try {
973
- const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
974
- if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
975
- return value;
976
- } catch (error) {
977
- if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
978
- throw error;
979
- }
980
- }
981
- async function auditGlob(root, patterns, options) {
982
- if (Array.isArray(patterns) && !patterns.length) return [];
983
- const directories = /* @__PURE__ */ new Set();
984
- const readdir2 = ((directory, opts, callback) => {
985
- const relative = path9.relative(root, directory).split(path9.sep).join("/") || ".";
986
- directories.add(relative);
987
- if (directories.size > MAX_INPUTS) {
988
- callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
989
- return;
990
- }
991
- auditInputPath(root, relative).then(() => {
992
- nativeFs.readdir(directory, opts, callback);
993
- }, callback);
994
- });
995
- const lstat2 = ((file, callback) => {
996
- const relative = path9.relative(root, file).split(path9.sep).join("/") || ".";
997
- auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
998
- });
999
- const stream = fg2.stream(patterns, {
1000
- cwd: root,
1001
- ignore: options.ignore,
1002
- onlyFiles: false,
1003
- onlyDirectories: options.onlyDirectories,
1004
- objectMode: true,
1005
- followSymbolicLinks: options.followSymbolicLinks ?? true,
1006
- fs: { readdir: readdir2, lstat: lstat2 },
1007
- concurrency: 16
1008
- });
1009
- const matches = [];
1010
- try {
1011
- for await (const value of stream) {
1012
- const entry = value;
1013
- const file = entry.path;
1014
- if (options.select && !options.select(file)) continue;
1015
- await auditInputPath(root, file);
1016
- if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
1017
- matches.push(file);
1018
- if (matches.length > MAX_INPUTS) {
1019
- const largest = [...matches.reduce((counts, item) => {
1020
- const dir = item.split("/")[0];
1021
- counts.set(dir, (counts.get(dir) ?? 0) + 1);
1022
- return counts;
1023
- }, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
1024
- 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.`);
1025
- }
1026
- }
1027
- } finally {
1028
- stream.destroy();
1029
- }
1030
- return matches.sort();
1031
- }
1032
- async function gitSourcePaths(root) {
1033
- return auditGitPaths(root, "sources");
1034
- }
1035
- async function gitIgnoredPaths(root, files) {
1036
- if (!files.length) return /* @__PURE__ */ new Set();
1037
- const ignored = /* @__PURE__ */ new Set();
1038
- for (let offset = 0; offset < files.length; offset += 1e3) {
1039
- try {
1040
- const { stdout } = await execGit(["check-ignore", "-z", "--stdin"], {
1041
- cwd: root,
1042
- maxBuffer: 4 * 1024 * 1024,
1043
- timeout: 1e4,
1044
- input: files.slice(offset, offset + 1e3).join("\0") + "\0"
1045
- });
1046
- for (const file of stdout.split("\0").filter(Boolean)) ignored.add(file);
1047
- } catch (error) {
1048
- if (error.code !== 1) throw error;
1049
- }
1050
- }
1051
- return ignored;
1052
- }
1053
- var MAX_INPUTS;
1054
- var init_inputs = __esm({
1055
- "src/audit/inputs.ts"() {
1056
- "use strict";
1057
- init_files();
1058
- init_storage();
1059
- init_git_read();
1060
- init_inspection();
1061
- MAX_INPUTS = 1e5;
1062
- }
1063
- });
1064
-
1065
1133
  // src/audit/docs.ts
1066
1134
  import fs6 from "fs/promises";
1067
1135
  import path10 from "path";
1068
- import fg3 from "fast-glob";
1136
+ import fg5 from "fast-glob";
1069
1137
  async function dirtyDocs(resolvedRoot, files) {
1070
1138
  if (!files.length) return /* @__PURE__ */ new Set();
1071
1139
  try {
@@ -1117,7 +1185,7 @@ async function discoverDocPaths(root) {
1117
1185
  const paths = [];
1118
1186
  const unique = [...new Set(candidates)].sort();
1119
1187
  for (let offset = 0; offset < unique.length; offset += 256) {
1120
- 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)), {
1121
1189
  ignore: excluded,
1122
1190
  followSymbolicLinks: false,
1123
1191
  label: "Documentation discovery"
@@ -1147,6 +1215,7 @@ async function localDocPaths(root) {
1147
1215
  return paths;
1148
1216
  }
1149
1217
  function documentScope(file) {
1218
+ if (!/^(readme|agents|claude)\.md$/i.test(path10.posix.basename(file))) return ".";
1150
1219
  const parent = path10.posix.dirname(file);
1151
1220
  return path10.posix.basename(parent).toLowerCase() === ".claude" ? path10.posix.dirname(parent) : parent;
1152
1221
  }
@@ -1168,7 +1237,7 @@ async function readDocs(resolvedRoot) {
1168
1237
  path: candidate,
1169
1238
  content,
1170
1239
  scope: documentScope(candidate),
1171
- 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",
1172
1241
  lineCount: content.split("\n").length,
1173
1242
  lastCommit,
1174
1243
  dirty: dirty.has(candidate),
@@ -1197,22 +1266,6 @@ var init_docs = __esm({
1197
1266
  }
1198
1267
  });
1199
1268
 
1200
- // src/audit/types.ts
1201
- var ALL_CHECKS;
1202
- var init_types = __esm({
1203
- "src/audit/types.ts"() {
1204
- "use strict";
1205
- ALL_CHECKS = [
1206
- "deleted-reference",
1207
- "new-module",
1208
- "stale-count",
1209
- "dead-command",
1210
- "deps-changed",
1211
- "decision-anchor-drift"
1212
- ];
1213
- }
1214
- });
1215
-
1216
1269
  // src/audit/scope.ts
1217
1270
  import fs7 from "fs/promises";
1218
1271
  import path11 from "path";
@@ -1323,7 +1376,7 @@ var init_deleted_reference = __esm({
1323
1376
  });
1324
1377
 
1325
1378
  // src/audit/checks/new-module.ts
1326
- import fg4 from "fast-glob";
1379
+ import fg6 from "fast-glob";
1327
1380
  import fs8 from "fs/promises";
1328
1381
  import path13 from "path";
1329
1382
  function escapeRegExp(text2) {
@@ -1385,7 +1438,7 @@ async function collectModuleCandidates(root, combinedDocs) {
1385
1438
  const result = /* @__PURE__ */ new Map();
1386
1439
  for (const dir of parents) {
1387
1440
  const visible = new Set(byParent.get(dir).filter((candidate) => !ignored.has(candidate) || sources.some((file) => file.startsWith(candidate + "/"))));
1388
- result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg4.escapePath(dir)}/*`, {
1441
+ result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg6.escapePath(dir)}/*`, {
1389
1442
  ignore,
1390
1443
  onlyDirectories: true,
1391
1444
  label: "Module directory discovery",
@@ -1398,7 +1451,7 @@ async function collectModuleCandidates(root, combinedDocs) {
1398
1451
  const files = sources.filter((file) => file.startsWith(dir + "/"));
1399
1452
  let count3 = 0;
1400
1453
  for (let offset = 0; offset < files.length; offset += 256) {
1401
- 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)), {
1402
1455
  ignore,
1403
1456
  followSymbolicLinks: false,
1404
1457
  label: `Source discovery in ${dir}`
@@ -1733,6 +1786,87 @@ var init_dead_command = __esm({
1733
1786
  }
1734
1787
  });
1735
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
+
1736
1870
  // src/audit/release-metadata.ts
1737
1871
  async function git(root, args) {
1738
1872
  return (await execGit(args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
@@ -1789,6 +1923,10 @@ var init_release_metadata = __esm({
1789
1923
  });
1790
1924
 
1791
1925
  // src/audit/checks/deps-changed.ts
1926
+ function hasDependencyContent(content) {
1927
+ const text2 = content.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, "").replace(/<!--[\s\S]*?-->/g, "");
1928
+ 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);
1929
+ }
1792
1930
  function manifestPathspecs(doc) {
1793
1931
  const scope = documentScope(doc);
1794
1932
  if (scope === ".") return MANIFEST_PATHSPECS;
@@ -1808,12 +1946,17 @@ async function checkDepsChanged(ctx) {
1808
1946
  });
1809
1947
  continue;
1810
1948
  }
1949
+ let content = doc.content;
1811
1950
  if (doc.dirty) {
1812
- result.skipped.push({
1813
- check: "deps-changed",
1814
- doc: doc.path,
1815
- reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
1816
- });
1951
+ try {
1952
+ content = (await inspectionGit(
1953
+ ["show", `${doc.lastCommit.hash}:${doc.path}`],
1954
+ { cwd: ctx.root, maxBuffer: 10 * 1024 * 1024, timeout: 1e4 }
1955
+ )).stdout;
1956
+ } catch {
1957
+ result.skipped.push({ check: "deps-changed", doc: doc.path, reason: `${doc.path}: committed dependency content is unavailable` });
1958
+ continue;
1959
+ }
1817
1960
  }
1818
1961
  const range = await commitsTouchingSince(
1819
1962
  ctx.root,
@@ -1838,16 +1981,32 @@ async function checkDepsChanged(ctx) {
1838
1981
  range.commits = relevant;
1839
1982
  range.total = relevant.length;
1840
1983
  if (range.total === 0) continue;
1841
- 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
+ });
1842
1998
  (doc.dirty ? result.suppressedAdvisories : result.advisories).push({
1843
1999
  type: "deps-changed",
1844
- 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}")`,
1845
- 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 },
1846
2003
  evidence: {
1847
2004
  kind: "doc-behind-manifests",
1848
2005
  docLastCommit: doc.lastCommit,
1849
2006
  manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
1850
- totalCommits: range.total
2007
+ totalCommits: range.total,
2008
+ matches: relevance.matches,
2009
+ matchingIncomplete: relevance.incomplete
1851
2010
  }
1852
2011
  });
1853
2012
  }
@@ -1857,9 +2016,11 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
1857
2016
  var init_deps_changed = __esm({
1858
2017
  "src/audit/checks/deps-changed.ts"() {
1859
2018
  "use strict";
2019
+ init_dependency_claims();
1860
2020
  init_docs();
1861
2021
  init_release_metadata();
1862
2022
  init_git();
2023
+ init_inspection();
1863
2024
  init_checks();
1864
2025
  MANIFEST_COMMITS_CAP = 10;
1865
2026
  MANIFEST_PATHSPECS = [
@@ -1880,7 +2041,7 @@ var init_deps_changed = __esm({
1880
2041
  });
1881
2042
 
1882
2043
  // src/context/lexical.ts
1883
- import path16 from "path";
2044
+ import path17 from "path";
1884
2045
  var init_lexical = __esm({
1885
2046
  "src/context/lexical.ts"() {
1886
2047
  "use strict";
@@ -2041,7 +2202,7 @@ var init_provenance = __esm({
2041
2202
 
2042
2203
  // src/decisions/decisions.ts
2043
2204
  import fs9 from "fs/promises";
2044
- import path17 from "path";
2205
+ import path18 from "path";
2045
2206
  import { createHash } from "crypto";
2046
2207
  import { z as z3 } from "zod";
2047
2208
  async function loadDecisionStore(rootDir) {
@@ -2092,9 +2253,9 @@ var init_decisions = __esm({
2092
2253
  });
2093
2254
 
2094
2255
  // src/decisions/drift.ts
2095
- import path18 from "path";
2256
+ import path19 from "path";
2096
2257
  async function computeDecisionDrift(rootDir, decisions) {
2097
- const resolvedRoot = path18.resolve(rootDir);
2258
+ const resolvedRoot = path19.resolve(rootDir);
2098
2259
  const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
2099
2260
  const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
2100
2261
  const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
@@ -2290,7 +2451,9 @@ var init_findings = __esm({
2290
2451
  kind: z4.literal("doc-behind-manifests"),
2291
2452
  docLastCommit: commitSchema,
2292
2453
  manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
2293
- 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()
2294
2457
  }),
2295
2458
  z4.object({
2296
2459
  kind: z4.literal("decision-anchor"),
@@ -2306,7 +2469,7 @@ var init_findings = __esm({
2306
2469
  type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
2307
2470
  confidence: z4.enum(["certain", "likely"])
2308
2471
  });
2309
- advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.literal("recheck").optional() });
2472
+ advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.enum(["recheck", "informational"]).optional() });
2310
2473
  checkResultSchema = z4.object({
2311
2474
  issues: z4.array(issueSchema),
2312
2475
  advisories: z4.array(advisorySchema),
@@ -2368,7 +2531,7 @@ __export(repair_exports, {
2368
2531
  withRepairSession: () => withRepairSession
2369
2532
  });
2370
2533
  import fs10 from "fs/promises";
2371
- import path19 from "path";
2534
+ import path20 from "path";
2372
2535
  import { randomUUID as randomUUID2 } from "crypto";
2373
2536
  import { z as z6 } from "zod";
2374
2537
  function recheckable(finding) {
@@ -2389,7 +2552,7 @@ async function docState(root) {
2389
2552
  docs.push([doc, null]);
2390
2553
  }
2391
2554
  }
2392
- return digest(docs);
2555
+ return digest([docs, (await loadProjectConfig(root)).audit]);
2393
2556
  }
2394
2557
  async function stableAudit(root, checks, options = {}) {
2395
2558
  return profilePhase("repair.audit", () => readStableAudit(root, checks, options));
@@ -2458,8 +2621,8 @@ async function prepareOriginal(rootDir, checks, options, session) {
2458
2621
  }
2459
2622
  async function loadRepairBaseline(rootDir, baselinePath) {
2460
2623
  const root = await fs10.realpath(rootDir);
2461
- const declaredRoot = path19.resolve(rootDir);
2462
- 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;
2463
2626
  const stored = baselineSchema.parse(await readStoreJson(root, relative));
2464
2627
  const { digest: savedDigest, ...payload } = stored;
2465
2628
  if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
@@ -2508,6 +2671,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
2508
2671
  if (now && ("confidence" in finding || "confidence" in now)) {
2509
2672
  return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
2510
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
+ }
2511
2677
  const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
2512
2678
  if (!current.checksRun?.includes(finding.type) || skipped.length) {
2513
2679
  return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
@@ -2518,11 +2684,14 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
2518
2684
  if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
2519
2685
  return { ...base, review, status: "review-required", reason: review.reason };
2520
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
+ }
2521
2690
  if (!("confidence" in finding) && (!recheckable(finding) || now)) {
2522
2691
  return {
2523
2692
  ...base,
2524
2693
  status: "review-required",
2525
- reason: "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."
2694
+ 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."
2526
2695
  };
2527
2696
  }
2528
2697
  return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
@@ -2546,7 +2715,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
2546
2715
  const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
2547
2716
  const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
2548
2717
  for (const f of findings) counts[f.status]++;
2549
- 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)));
2550
2719
  const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
2551
2720
  return {
2552
2721
  version: 1,
@@ -2569,7 +2738,7 @@ function repairExitCode(report) {
2569
2738
  function formatRepairSummary(report) {
2570
2739
  return [
2571
2740
  "Repair verification: " + report.status + ". Baseline: " + report.baselinePath,
2572
- ...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),
2573
2742
  ...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
2574
2743
  ...report.diagnostics.map((d) => " [unverified] " + d),
2575
2744
  ...(report.currentAudit?.skippedChecks ?? []).map((s) => " [skipped] " + s.check + ": " + s.reason),
@@ -2602,13 +2771,14 @@ var init_repair = __esm({
2602
2771
  docs: z6.array(z6.object({
2603
2772
  path: z6.string().refine((value) => normalizeRepoPath(value) === value),
2604
2773
  scope: z6.string().optional(),
2605
- kind: z6.enum(["instructions", "readme"]).optional(),
2774
+ kind: z6.enum(["instructions", "readme", "guide"]).optional(),
2606
2775
  lastCommit: commitSchema.nullable(),
2607
2776
  dirty: z6.boolean(),
2608
2777
  lineCount: count2
2609
2778
  })).nonempty(),
2610
2779
  decisionsChecked: z6.boolean(),
2611
2780
  clean: z6.boolean(),
2781
+ excludedChecks: z6.array(z6.object({ check: checkSchema, doc: z6.string() })).optional(),
2612
2782
  issues: z6.array(issueSchema),
2613
2783
  advisories: z6.array(advisorySchema),
2614
2784
  suppressedAdvisories: z6.array(advisorySchema).optional(),
@@ -2640,14 +2810,18 @@ async function readRecord(root, id) {
2640
2810
  if (raw === null) return null;
2641
2811
  const record = recordSchema.parse(raw);
2642
2812
  const { digest: stored, ...payload } = record;
2643
- 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))) {
2644
2814
  throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
2645
2815
  }
2646
2816
  return record;
2647
2817
  }
2648
- 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) {
2649
2822
  const files = [finding.anchor.doc], e = finding.evidence;
2650
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}`];
2651
2825
  if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
2652
2826
  if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
2653
2827
  else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
@@ -2658,9 +2832,9 @@ function scopes(finding) {
2658
2832
  return `:(literal)${file}`;
2659
2833
  }).sort();
2660
2834
  }
2661
- async function reviewEvidence(root, finding) {
2835
+ async function reviewEvidence(root, finding, reason) {
2662
2836
  if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
2663
- const scope = scopes(finding);
2837
+ const scope = scopes(finding, reason);
2664
2838
  const head = (await git2(root, "rev-parse", "HEAD")).trim();
2665
2839
  const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
2666
2840
  if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
@@ -2686,7 +2860,7 @@ async function reviewEvidence(root, finding) {
2686
2860
  if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
2687
2861
  }
2688
2862
  }
2689
- const fingerprint = digest([scope, tree, lastScopeCommit, paths]);
2863
+ const fingerprint = evidenceFingerprint({ head, scope, tree, lastScopeCommit, paths, fingerprint: "" }, reason);
2690
2864
  if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
2691
2865
  return {
2692
2866
  ...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
@@ -2733,7 +2907,7 @@ async function assessAdvisory(root, finding, baselineHead) {
2733
2907
  const record = await readRecord(root, id);
2734
2908
  if (!record) return null;
2735
2909
  const event = record.events.at(-1);
2736
- const current = await reviewEvidence(root, event.finding);
2910
+ const current = await reviewEvidence(root, event.finding, event.reasonCode);
2737
2911
  await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
2738
2912
  const base = {
2739
2913
  id,
@@ -2743,7 +2917,7 @@ async function assessAdvisory(root, finding, baselineHead) {
2743
2917
  outcome: event.outcome,
2744
2918
  reviewedHead: event.evidence.head
2745
2919
  };
2746
- 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." };
2747
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." };
2748
2922
  } catch (error) {
2749
2923
  return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
@@ -2771,9 +2945,13 @@ async function reviewAdvisory(rootDir, input) {
2771
2945
  decisionId: finding.evidence.decisionId,
2772
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."
2773
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.");
2774
2952
  const prepare = async () => {
2775
2953
  const record = await readRecord(root, request.findingId);
2776
- const evidence = await reviewEvidence(root, finding);
2954
+ const evidence = await reviewEvidence(root, finding, request.reasonCode);
2777
2955
  await git2(root, "merge-base", "--is-ancestor", baseline.original.headHash, evidence.head);
2778
2956
  const token = digest({ baseline: baseline.original, finding, evidence, record });
2779
2957
  return { record, evidence, token };
@@ -2802,10 +2980,10 @@ async function reviewAdvisory(rootDir, input) {
2802
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."
2803
2981
  };
2804
2982
  }
2805
- 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.");
2806
2984
  return withLock(root, ".mason/reports/advisory-review-locks/" + request.findingId, async () => {
2807
2985
  const state = await prepare();
2808
- 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.");
2809
2987
  if (state.evidence.dirty) throw new Error("Commit relevant document and repository edits before recording the assessment. Unrelated local work may remain.");
2810
2988
  if ((state.record?.events.length ?? 0) >= 200) throw new Error("Review history limit reached; existing events were retained.");
2811
2989
  const { dirty, documentDigest, ...evidence } = state.evidence;
@@ -2813,7 +2991,8 @@ async function reviewAdvisory(rootDir, input) {
2813
2991
  at: (/* @__PURE__ */ new Date()).toISOString(),
2814
2992
  reviewer: request.reviewer,
2815
2993
  note: request.note,
2816
- outcome: request.action,
2994
+ outcome: request.action === "dismiss" ? "dismissed" : request.action,
2995
+ reasonCode: request.reasonCode,
2817
2996
  finding,
2818
2997
  evidence
2819
2998
  });
@@ -2824,11 +3003,11 @@ async function reviewAdvisory(rootDir, input) {
2824
3003
  status: "recorded",
2825
3004
  recordPath: recordPath(request.findingId),
2826
3005
  event,
2827
- 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."
2828
3007
  };
2829
3008
  });
2830
3009
  }
2831
- var git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
3010
+ var git2, hashSchema, commitSchema2, evidenceSchema2, dismissalReasonSchema, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
2832
3011
  var init_advisory_review = __esm({
2833
3012
  "src/audit/advisory-review.ts"() {
2834
3013
  "use strict";
@@ -2854,11 +3033,13 @@ var init_advisory_review = __esm({
2854
3033
  lastScopeCommit: commitSchema2,
2855
3034
  paths: z7.array(z7.object({ path: z7.string(), exists: z7.boolean() })).max(1e3)
2856
3035
  });
3036
+ dismissalReasonSchema = z7.enum(["no-dependency-claims", "unrelated-manifest-change"]);
2857
3037
  eventSchema2 = z7.object({
2858
3038
  at: z7.string().datetime(),
2859
- reviewer: z7.string().trim().min(1).max(200),
3039
+ reviewer: z7.string().trim().min(1).max(200).optional(),
2860
3040
  note: z7.string().trim().min(1).max(2500),
2861
- outcome: z7.enum(["addressed", "inapplicable", "deferred"]),
3041
+ outcome: z7.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
3042
+ reasonCode: dismissalReasonSchema.optional(),
2862
3043
  finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
2863
3044
  evidence: evidenceSchema2
2864
3045
  });
@@ -2870,13 +3051,14 @@ var init_advisory_review = __esm({
2870
3051
  recordPath: z7.string().optional(),
2871
3052
  reviewer: z7.string().optional(),
2872
3053
  note: z7.string().optional(),
2873
- outcome: z7.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
3054
+ outcome: z7.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
2874
3055
  reviewedHead: z7.string().optional()
2875
3056
  });
2876
3057
  advisoryReviewRequest = z7.object({
2877
3058
  baselinePath: z7.string().min(1),
2878
3059
  findingId: hashSchema,
2879
- action: z7.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
3060
+ action: z7.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
3061
+ reasonCode: dismissalReasonSchema.optional(),
2880
3062
  reviewer: eventSchema2.shape.reviewer.optional(),
2881
3063
  note: eventSchema2.shape.note.optional(),
2882
3064
  reviewToken: hashSchema.optional()
@@ -2887,12 +3069,12 @@ var init_advisory_review = __esm({
2887
3069
 
2888
3070
  // src/audit/audit.ts
2889
3071
  import fs12 from "fs/promises";
2890
- import path20 from "path";
3072
+ import path21 from "path";
2891
3073
  async function computeAudit(rootDir, options = {}) {
2892
3074
  return profilePhase("audit.current", () => auditCurrent(rootDir, options));
2893
3075
  }
2894
3076
  async function auditCurrent(rootDir, options) {
2895
- const resolvedRoot = path20.resolve(rootDir);
3077
+ const resolvedRoot = path21.resolve(rootDir);
2896
3078
  const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
2897
3079
  if (docs.length === 0) return null;
2898
3080
  const report = {
@@ -2926,7 +3108,7 @@ async function auditCurrent(rootDir, options) {
2926
3108
  }
2927
3109
  let decisionsPresent = false;
2928
3110
  try {
2929
- await fs12.access(path20.join(resolvedRoot, ".mason", "decisions"));
3111
+ await fs12.access(path21.join(resolvedRoot, ".mason", "decisions"));
2930
3112
  decisionsPresent = true;
2931
3113
  } catch {
2932
3114
  }
@@ -2938,13 +3120,21 @@ async function auditCurrent(rootDir, options) {
2938
3120
  changesSinceDoc,
2939
3121
  decisionsPresent
2940
3122
  };
3123
+ const policy = (await loadProjectConfig(resolvedRoot)).audit;
2941
3124
  const selected = options.checks ?? ALL_CHECKS;
2942
3125
  for (const name of ALL_CHECKS) {
2943
3126
  if (!selected.includes(name)) continue;
2944
- 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));
2945
3133
  report.checksRun.push(name);
2946
3134
  report.issues.push(...issues);
2947
- 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 });
2948
3138
  report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
2949
3139
  report.skippedChecks.push(...skipped);
2950
3140
  }
@@ -2956,6 +3146,8 @@ async function auditCurrent(rootDir, options) {
2956
3146
  var init_audit = __esm({
2957
3147
  "src/audit/audit.ts"() {
2958
3148
  "use strict";
3149
+ init_files();
3150
+ init_policy();
2959
3151
  init_profile();
2960
3152
  init_drift();
2961
3153
  init_snapshot();
@@ -2971,11 +3163,11 @@ init_audit();
2971
3163
  init_types();
2972
3164
  init_repair();
2973
3165
  init_findings();
2974
- import path22 from "path";
3166
+ import path23 from "path";
2975
3167
 
2976
3168
  // src/audit/advisory-cli.ts
2977
3169
  init_advisory_review();
2978
- import path21 from "path";
3170
+ import path22 from "path";
2979
3171
  import { stripVTControlCharacters } from "util";
2980
3172
  var USAGE = `Usage: mason audit review --baseline <path> --finding <id> [options]
2981
3173
 
@@ -2983,13 +3175,15 @@ Prepare an advisory assessment against retained evidence (read-only by default).
2983
3175
  --dir <path> Repository root (default: current directory)
2984
3176
  --baseline <path> Original repair baseline
2985
3177
  --finding <id> findingId from repair verification or automation
2986
- --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
2987
3180
  --reviewer <name> Actual authorized reviewer
2988
3181
  --note <reason> Assessment reason
2989
3182
  --token <token> reviewToken returned by preparation
2990
3183
  --json Full evidence and review history
2991
3184
 
2992
- 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
2993
3187
  belong in Git under .mason/reviews/advisories/. Deferral remains outstanding.
2994
3188
  Decision findings use review_decision; this command never approves decisions.
2995
3189
  Exit 0: prepared/recorded; 2: unavailable, conflict, or decision review required.`;
@@ -3007,21 +3201,22 @@ async function runAdvisoryCli(args, io = { out: (text2) => console.log(text2), e
3007
3201
  json = true;
3008
3202
  continue;
3009
3203
  }
3010
- 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);
3011
3205
  const value = args[++i];
3012
3206
  if (!value || value.startsWith("--")) throw new Error(flag + " requires a value.");
3013
3207
  flags[flag] = value;
3014
3208
  }
3015
- 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.");
3016
3210
  const input = {
3017
3211
  baselinePath: flags["--baseline"],
3018
3212
  findingId: flags["--finding"],
3019
3213
  action: flags["--outcome"] ?? "prepare",
3020
3214
  reviewer: flags["--reviewer"],
3021
3215
  note: flags["--note"],
3022
- reviewToken: flags["--token"]
3216
+ reviewToken: flags["--token"],
3217
+ reasonCode: flags["--reason"]
3023
3218
  };
3024
- const result = await reviewAdvisory(path21.resolve(flags["--dir"] ?? process.cwd()), input);
3219
+ const result = await reviewAdvisory(path22.resolve(flags["--dir"] ?? process.cwd()), input);
3025
3220
  if (json) io.out(JSON.stringify(result, null, 2));
3026
3221
  else {
3027
3222
  const lines = [`Advisory review: ${result.status}.`];
@@ -3133,8 +3328,8 @@ function issueLine(issue) {
3133
3328
  function formatAuditSummary(report) {
3134
3329
  const lines = [];
3135
3330
  const reviewed = new Set(report.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
3136
- const open = report.advisories.filter((finding) => !reviewed.has(findingId(finding)));
3137
- 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)));
3138
3333
  const reviewCount = open.length + suppressed.length;
3139
3334
  for (const doc of report.docs) {
3140
3335
  const docIssues = report.issues.filter((i) => i.anchor.doc === doc.path);
@@ -3152,6 +3347,9 @@ function formatAuditSummary(report) {
3152
3347
  for (const issue of report.issues) {
3153
3348
  if (!docPaths.has(issue.anchor.doc)) lines.push(issueLine(issue));
3154
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).`);
3155
3353
  if (open.length > 0) {
3156
3354
  lines.push("Advisories (do not affect the exit code):");
3157
3355
  for (const advisory of open) {
@@ -3205,7 +3403,7 @@ function formatFixPrompt(report, baselinePath) {
3205
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."
3206
3404
  );
3207
3405
  lines.push(
3208
- "- 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."
3209
3407
  );
3210
3408
  lines.push("");
3211
3409
  lines.push("AUDIT REPORT (current context files and repository evidence, including local edits):");
@@ -3254,7 +3452,7 @@ async function runAuditCli(argv, io = {
3254
3452
  io.out(USAGE2);
3255
3453
  return 0;
3256
3454
  }
3257
- const rootDir = path22.resolve(args.dir);
3455
+ const rootDir = path23.resolve(args.dir);
3258
3456
  if (args.baseline || args.prepareRepair) {
3259
3457
  try {
3260
3458
  if (args.baseline) {