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() {
@@ -95,6 +96,26 @@ var init_git_read = __esm({
95
96
  }
96
97
  });
97
98
 
99
+ // src/audit/types.ts
100
+ var types_exports = {};
101
+ __export(types_exports, {
102
+ ALL_CHECKS: () => ALL_CHECKS
103
+ });
104
+ var ALL_CHECKS;
105
+ var init_types = __esm({
106
+ "src/audit/types.ts"() {
107
+ "use strict";
108
+ ALL_CHECKS = [
109
+ "deleted-reference",
110
+ "new-module",
111
+ "stale-count",
112
+ "dead-command",
113
+ "deps-changed",
114
+ "decision-anchor-drift"
115
+ ];
116
+ }
117
+ });
118
+
98
119
  // src/utils/files.ts
99
120
  import fs from "fs/promises";
100
121
  import { constants } from "fs";
@@ -139,6 +160,27 @@ async function loadProjectConfig(root) {
139
160
  }
140
161
  config[key] = value[key];
141
162
  }
163
+ if (value.audit !== void 0) {
164
+ const audit = value.audit;
165
+ if (!audit || typeof audit !== "object" || Array.isArray(audit)) throw new Error("Configuration audit must be an object");
166
+ const patterns = (raw2) => {
167
+ if (!Array.isArray(raw2) || raw2.length > 100 || !raw2.every((s) => typeof s === "string" && normalizeRepoPath(s) === s && !s.startsWith("!") && !/[\x00-\x1f]/.test(s))) {
168
+ throw new Error("Audit patterns must be at most 100 repository-relative globs without traversal or negation");
169
+ }
170
+ return raw2;
171
+ };
172
+ config.audit = {};
173
+ if (audit.include !== void 0) config.audit.include = patterns(audit.include);
174
+ if (audit.exclude !== void 0) {
175
+ if (!audit.exclude || typeof audit.exclude !== "object" || Array.isArray(audit.exclude)) throw new Error("audit.exclude must map check names to patterns");
176
+ const { ALL_CHECKS: ALL_CHECKS2 } = await Promise.resolve().then(() => (init_types(), types_exports));
177
+ config.audit.exclude = {};
178
+ for (const [check, globs] of Object.entries(audit.exclude)) {
179
+ if (!ALL_CHECKS2.includes(check)) throw new Error("Unknown audit check: " + check);
180
+ config.audit.exclude[check] = patterns(globs);
181
+ }
182
+ }
183
+ }
142
184
  return config;
143
185
  } catch (error) {
144
186
  if (error.code === "ENOENT") return {};
@@ -438,8 +480,190 @@ var init_store = __esm({
438
480
  }
439
481
  });
440
482
 
441
- // src/audit/commands.ts
483
+ // src/audit/inspection.ts
484
+ import fg2 from "fast-glob";
485
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
442
486
  import path4 from "path";
487
+ function withRepositoryInspection(root, run) {
488
+ return inspections.run({ root: path4.resolve(root), reads: /* @__PURE__ */ new Map() }, run);
489
+ }
490
+ function inspectionRead(root, key, read) {
491
+ const inspection = inspections.getStore();
492
+ if (!inspection || inspection.root !== path4.resolve(root)) return read();
493
+ if (!inspection.reads.has(key)) {
494
+ const pending = read();
495
+ inspection.reads.set(key, pending);
496
+ void pending.catch(() => {
497
+ if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
498
+ });
499
+ }
500
+ return inspection.reads.get(key);
501
+ }
502
+ function inspectionGit(args, options) {
503
+ return inspectionRead(options.cwd, "git:" + JSON.stringify([args, options]), () => execGit(args, options));
504
+ }
505
+ async function auditGitPaths(root, kind) {
506
+ const custom = (await loadProjectConfig(root)).audit?.include ?? [];
507
+ const shared = inspections.getStore()?.root === path4.resolve(root) && !custom.length;
508
+ const expanded = fg2.generateTasks(custom).flatMap((task) => task.positive);
509
+ if (expanded.length > 1e3) throw new Error("Audit include patterns expand to more than 1,000 globs");
510
+ const documentSpecs = [...docSpecs, ...expanded.map((pattern) => `:(glob)${pattern}`)];
511
+ const specs = shared ? [...documentSpecs, ...sourceSpecs] : kind === "documents" ? documentSpecs : sourceSpecs;
512
+ const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
513
+ const { stdout } = await inspectionGit(
514
+ ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
515
+ { cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
516
+ );
517
+ 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("."))));
518
+ if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
519
+ return paths.sort();
520
+ }
521
+ var inspections, sourceSpecs, docSpecs, sourceSuffixes;
522
+ var init_inspection = __esm({
523
+ "src/audit/inspection.ts"() {
524
+ "use strict";
525
+ init_git_read();
526
+ init_files();
527
+ inspections = new AsyncLocalStorage2();
528
+ sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
529
+ docSpecs = [":(icase,glob)**/readme.md", ":(icase,glob)**/agents.md", ":(icase,glob)**/claude.md"];
530
+ sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
531
+ }
532
+ });
533
+
534
+ // src/audit/inputs.ts
535
+ import fs4 from "fs/promises";
536
+ import * as nativeFs from "fs";
537
+ import path5 from "path";
538
+ import fg3 from "fast-glob";
539
+ async function auditInputPath(root, file) {
540
+ if (file === ".") return fs4.realpath(root);
541
+ try {
542
+ return await storePath(root, file);
543
+ } catch (error) {
544
+ if (error instanceof Error && error.message.startsWith("Symlink")) {
545
+ throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
546
+ }
547
+ throw error;
548
+ }
549
+ }
550
+ async function readAuditInput(root, file) {
551
+ try {
552
+ const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
553
+ if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
554
+ return value;
555
+ } catch (error) {
556
+ if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
557
+ throw error;
558
+ }
559
+ }
560
+ async function auditGlob(root, patterns, options) {
561
+ if (Array.isArray(patterns) && !patterns.length) return [];
562
+ const directories = /* @__PURE__ */ new Set();
563
+ const readdir2 = ((directory, opts, callback) => {
564
+ const relative = path5.relative(root, directory).split(path5.sep).join("/") || ".";
565
+ directories.add(relative);
566
+ if (directories.size > MAX_INPUTS) {
567
+ callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
568
+ return;
569
+ }
570
+ auditInputPath(root, relative).then(() => {
571
+ nativeFs.readdir(directory, opts, callback);
572
+ }, callback);
573
+ });
574
+ const lstat2 = ((file, callback) => {
575
+ const relative = path5.relative(root, file).split(path5.sep).join("/") || ".";
576
+ auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
577
+ });
578
+ const stream = fg3.stream(patterns, {
579
+ cwd: root,
580
+ ignore: options.ignore,
581
+ onlyFiles: false,
582
+ onlyDirectories: options.onlyDirectories,
583
+ objectMode: true,
584
+ followSymbolicLinks: options.followSymbolicLinks ?? true,
585
+ fs: { readdir: readdir2, lstat: lstat2 },
586
+ concurrency: 16
587
+ });
588
+ const matches = [];
589
+ try {
590
+ for await (const value of stream) {
591
+ const entry = value;
592
+ const file = entry.path;
593
+ if (options.select && !options.select(file)) continue;
594
+ await auditInputPath(root, file);
595
+ if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
596
+ matches.push(file);
597
+ if (matches.length > MAX_INPUTS) {
598
+ const largest = [...matches.reduce((counts, item) => {
599
+ const dir = item.split("/")[0];
600
+ counts.set(dir, (counts.get(dir) ?? 0) + 1);
601
+ return counts;
602
+ }, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
603
+ throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count4]) => `${dir} (${count4})`).join(", ")}. Narrow this check's inputs.`);
604
+ }
605
+ }
606
+ } finally {
607
+ stream.destroy();
608
+ }
609
+ return matches.sort();
610
+ }
611
+ async function gitSourcePaths(root) {
612
+ return auditGitPaths(root, "sources");
613
+ }
614
+ async function gitIgnoredPaths(root, files) {
615
+ if (!files.length) return /* @__PURE__ */ new Set();
616
+ const ignored = /* @__PURE__ */ new Set();
617
+ for (let offset = 0; offset < files.length; offset += 1e3) {
618
+ try {
619
+ const { stdout } = await execGit(["check-ignore", "-z", "--stdin"], {
620
+ cwd: root,
621
+ maxBuffer: 4 * 1024 * 1024,
622
+ timeout: 1e4,
623
+ input: files.slice(offset, offset + 1e3).join("\0") + "\0"
624
+ });
625
+ for (const file of stdout.split("\0").filter(Boolean)) ignored.add(file);
626
+ } catch (error) {
627
+ if (error.code !== 1) throw error;
628
+ }
629
+ }
630
+ return ignored;
631
+ }
632
+ var MAX_INPUTS;
633
+ var init_inputs = __esm({
634
+ "src/audit/inputs.ts"() {
635
+ "use strict";
636
+ init_files();
637
+ init_storage();
638
+ init_git_read();
639
+ init_inspection();
640
+ MAX_INPUTS = 1e5;
641
+ }
642
+ });
643
+
644
+ // src/audit/policy.ts
645
+ import fg4 from "fast-glob";
646
+ async function includedCheckPaths(root, paths, exclude) {
647
+ if (!exclude.length) return new Set(paths);
648
+ const included = /* @__PURE__ */ new Set();
649
+ for (let offset = 0; offset < paths.length; offset += 256) {
650
+ for (const file of await auditGlob(root, paths.slice(offset, offset + 256).map((file2) => fg4.escapePath(file2)), {
651
+ ignore: exclude,
652
+ followSymbolicLinks: false,
653
+ label: "Audit check exclusions"
654
+ })) included.add(file);
655
+ }
656
+ return included;
657
+ }
658
+ var init_policy = __esm({
659
+ "src/audit/policy.ts"() {
660
+ "use strict";
661
+ init_inputs();
662
+ }
663
+ });
664
+
665
+ // src/audit/commands.ts
666
+ import path6 from "path";
443
667
  function splitCommands(line) {
444
668
  const parts = [];
445
669
  let quote = "", start = 0;
@@ -475,7 +699,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
475
699
  const cd = text2.match(/^cd\s+(.+)$/);
476
700
  if (cd) {
477
701
  const directory = literal(cd[1]);
478
- cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path4.posix.join(cwd ?? ".", directory);
702
+ cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path6.posix.join(cwd ?? ".", directory);
479
703
  continue;
480
704
  }
481
705
  for (const match of text2.matchAll(COMMAND)) {
@@ -486,7 +710,7 @@ function commandClaims(line, lineNo, cwd, shell = false) {
486
710
  if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
487
711
  if (selector) {
488
712
  const selected = literal(selector[1]);
489
- directory = selected === null || cwd === null ? null : path4.posix.join(cwd ?? ".", selected);
713
+ directory = selected === null || cwd === null ? null : path6.posix.join(cwd ?? ".", selected);
490
714
  } else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
491
715
  const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
492
716
  if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
@@ -516,7 +740,7 @@ var init_commands = __esm({
516
740
  COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
517
741
  literal = (value) => {
518
742
  const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
519
- return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path4.posix.isAbsolute(unquoted) ? null : unquoted;
743
+ return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path6.posix.isAbsolute(unquoted) ? null : unquoted;
520
744
  };
521
745
  }
522
746
  });
@@ -821,52 +1045,6 @@ var init_claims = __esm({
821
1045
  }
822
1046
  });
823
1047
 
824
- // src/audit/inspection.ts
825
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
826
- import path5 from "path";
827
- function withRepositoryInspection(root, run) {
828
- return inspections.run({ root: path5.resolve(root), reads: /* @__PURE__ */ new Map() }, run);
829
- }
830
- function inspectionRead(root, key, read) {
831
- const inspection = inspections.getStore();
832
- if (!inspection || inspection.root !== path5.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 === path5.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(path5.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 fs4 from "fs/promises";
957
- import * as nativeFs from "fs";
958
- import path6 from "path";
959
- import fg2 from "fast-glob";
960
- async function auditInputPath(root, file) {
961
- if (file === ".") return fs4.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 = path6.relative(root, directory).split(path6.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 = path6.relative(root, file).split(path6.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, count4]) => `${dir} (${count4})`).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 fs5 from "fs/promises";
1067
1135
  import path7 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(path7.posix.basename(file))) return ".";
1150
1219
  const parent = path7.posix.dirname(file);
1151
1220
  return path7.posix.basename(parent).toLowerCase() === ".claude" ? path7.posix.dirname(parent) : parent;
1152
1221
  }
@@ -1168,7 +1237,7 @@ async function readDocs(resolvedRoot) {
1168
1237
  path: candidate,
1169
1238
  content: content2,
1170
1239
  scope: documentScope(candidate),
1171
- kind: /^readme\.md$/i.test(path7.posix.basename(candidate)) ? "readme" : "instructions",
1240
+ kind: /^readme\.md$/i.test(path7.posix.basename(candidate)) ? "readme" : /^(agents|claude)\.md$/i.test(path7.posix.basename(candidate)) ? "instructions" : "guide",
1172
1241
  lineCount: content2.split("\n").length,
1173
1242
  lastCommit,
1174
1243
  dirty: dirty.has(candidate),
@@ -1437,22 +1506,6 @@ var init_drift = __esm({
1437
1506
  }
1438
1507
  });
1439
1508
 
1440
- // src/audit/types.ts
1441
- var ALL_CHECKS;
1442
- var init_types = __esm({
1443
- "src/audit/types.ts"() {
1444
- "use strict";
1445
- ALL_CHECKS = [
1446
- "deleted-reference",
1447
- "new-module",
1448
- "stale-count",
1449
- "dead-command",
1450
- "deps-changed",
1451
- "decision-anchor-drift"
1452
- ];
1453
- }
1454
- });
1455
-
1456
1509
  // src/audit/checks/deleted-reference.ts
1457
1510
  import path12 from "path";
1458
1511
  async function checkDeletedReferences(ctx) {
@@ -1521,7 +1574,7 @@ var init_deleted_reference = __esm({
1521
1574
  });
1522
1575
 
1523
1576
  // src/audit/checks/new-module.ts
1524
- import fg4 from "fast-glob";
1577
+ import fg6 from "fast-glob";
1525
1578
  import fs8 from "fs/promises";
1526
1579
  import path13 from "path";
1527
1580
  function escapeRegExp(text2) {
@@ -1583,7 +1636,7 @@ async function collectModuleCandidates(root, combinedDocs) {
1583
1636
  const result = /* @__PURE__ */ new Map();
1584
1637
  for (const dir of parents) {
1585
1638
  const visible = new Set(byParent.get(dir).filter((candidate) => !ignored.has(candidate) || sources.some((file) => file.startsWith(candidate + "/"))));
1586
- result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg4.escapePath(dir)}/*`, {
1639
+ result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg6.escapePath(dir)}/*`, {
1587
1640
  ignore,
1588
1641
  onlyDirectories: true,
1589
1642
  label: "Module directory discovery",
@@ -1596,7 +1649,7 @@ async function collectModuleCandidates(root, combinedDocs) {
1596
1649
  const files = sources.filter((file) => file.startsWith(dir + "/"));
1597
1650
  let count4 = 0;
1598
1651
  for (let offset = 0; offset < files.length; offset += 256) {
1599
- count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg4.escapePath(file)), {
1652
+ count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg6.escapePath(file)), {
1600
1653
  ignore,
1601
1654
  followSymbolicLinks: false,
1602
1655
  label: `Source discovery in ${dir}`
@@ -1931,6 +1984,87 @@ var init_dead_command = __esm({
1931
1984
  }
1932
1985
  });
1933
1986
 
1987
+ // src/audit/dependency-claims.ts
1988
+ import path16 from "path";
1989
+ import { parse as parseToml } from "smol-toml";
1990
+ function dependencyText(content2) {
1991
+ return content2.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, (text2) => text2.replace(/[^\n]/g, " ")).replace(/<!--[\s\S]*?-->/g, (text2) => text2.replace(/[^\n]/g, " "));
1992
+ }
1993
+ function declarations(file, source) {
1994
+ const result = /* @__PURE__ */ new Map();
1995
+ if (!source) return result;
1996
+ if (path16.posix.basename(file) === "package.json") {
1997
+ const json2 = JSON.parse(source);
1998
+ for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies", "engines"]) {
1999
+ for (const [name, value] of Object.entries(json2[section] ?? {})) {
2000
+ if (typeof value === "string") result.set(name, [...result.has(name) ? [result.get(name)] : [], value].join("; "));
2001
+ }
2002
+ }
2003
+ } else if (file.endsWith("libs.versions.toml")) {
2004
+ const catalog = parseToml(source);
2005
+ for (const section of ["libraries", "plugins"]) for (const [alias, entry] of Object.entries(catalog[section] ?? {})) {
2006
+ if (typeof entry === "string") {
2007
+ const parts = entry.split(":");
2008
+ if (parts.length >= 3) result.set(parts.slice(0, -1).join(":"), parts.at(-1));
2009
+ } else if (entry && typeof entry === "object") {
2010
+ const name = entry.module ?? (entry.group && entry.name ? `${entry.group}:${entry.name}` : entry.id);
2011
+ const version = typeof entry.version === "string" ? entry.version : catalog.versions?.[entry.version?.ref];
2012
+ if (typeof name === "string") result.set(name, typeof version === "string" ? version : "unspecified");
2013
+ }
2014
+ }
2015
+ } else if (/build\.gradle(?:\.kts)?$/.test(file)) {
2016
+ for (const match of source.matchAll(/["']([\w.-]+:[\w.-]+):([^"'\s]+)["']/g)) result.set(match[1], match[2]);
2017
+ for (const match of source.matchAll(/\bid\s*\(?\s*["']([\w.-]+)["']\s*\)?\s*version\s*["']([^"']+)["']/g)) result.set(match[1], match[2]);
2018
+ }
2019
+ return result;
2020
+ }
2021
+ async function matchDependencyChanges(root, from, head, files, content2) {
2022
+ const matches = [];
2023
+ const lines = dependencyText(content2).split("\n");
2024
+ let incomplete = files.length > 64;
2025
+ for (const file of files.slice(0, 64)) {
2026
+ if (!/(?:^|\/)(?:package\.json|libs\.versions\.toml|build\.gradle(?:\.kts)?)$/.test(file)) {
2027
+ incomplete = true;
2028
+ continue;
2029
+ }
2030
+ try {
2031
+ const read = async (revision) => {
2032
+ const { stdout: entry } = await inspectionGit(["ls-tree", revision, "--", `:(literal)${file}`], { cwd: root, timeout: 1e4 });
2033
+ if (!entry) return "";
2034
+ if (!entry.startsWith("100644 ") && !entry.startsWith("100755 ")) throw new Error("Unsupported manifest mode");
2035
+ return (await inspectionGit(["show", `${revision}:${file}`], { cwd: root, maxBuffer: 1024 * 1024, timeout: 1e4 })).stdout;
2036
+ };
2037
+ const [before, after] = await Promise.all([read(from), read(head)]);
2038
+ const old = declarations(file, before), next = declarations(file, after);
2039
+ for (const dependency of /* @__PURE__ */ new Set([...old.keys(), ...next.keys()])) {
2040
+ if (old.get(dependency) === next.get(dependency)) continue;
2041
+ const escaped = dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2042
+ const mention = new RegExp(`(^|[^a-zA-Z0-9_@./-])${escaped}(?![a-zA-Z0-9_/-]|\\.[a-zA-Z0-9_])`, "i");
2043
+ const line = lines.findIndex((text2) => mention.test(text2));
2044
+ if (line < 0) continue;
2045
+ matches.push({
2046
+ dependency,
2047
+ manifest: file,
2048
+ before: old.get(dependency) ?? null,
2049
+ after: next.get(dependency) ?? null,
2050
+ line: line + 1,
2051
+ excerpt: lines[line].trim().slice(0, 500)
2052
+ });
2053
+ if (matches.length >= 100) return { matches, incomplete: true };
2054
+ }
2055
+ } catch {
2056
+ incomplete = true;
2057
+ }
2058
+ }
2059
+ return { matches, incomplete };
2060
+ }
2061
+ var init_dependency_claims = __esm({
2062
+ "src/audit/dependency-claims.ts"() {
2063
+ "use strict";
2064
+ init_inspection();
2065
+ }
2066
+ });
2067
+
1934
2068
  // src/audit/release-metadata.ts
1935
2069
  async function git(root, args) {
1936
2070
  return (await execGit(args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
@@ -1987,6 +2121,10 @@ var init_release_metadata = __esm({
1987
2121
  });
1988
2122
 
1989
2123
  // src/audit/checks/deps-changed.ts
2124
+ function hasDependencyContent(content2) {
2125
+ const text2 = content2.replace(/<!-- mason:start -->[\s\S]*?<!-- mason:end -->/g, "").replace(/<!--[\s\S]*?-->/g, "");
2126
+ 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);
2127
+ }
1990
2128
  function manifestPathspecs(doc) {
1991
2129
  const scope = documentScope(doc);
1992
2130
  if (scope === ".") return MANIFEST_PATHSPECS;
@@ -2006,12 +2144,17 @@ async function checkDepsChanged(ctx) {
2006
2144
  });
2007
2145
  continue;
2008
2146
  }
2147
+ let content2 = doc.content;
2009
2148
  if (doc.dirty) {
2010
- result.skipped.push({
2011
- check: "deps-changed",
2012
- doc: doc.path,
2013
- reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
2014
- });
2149
+ try {
2150
+ content2 = (await inspectionGit(
2151
+ ["show", `${doc.lastCommit.hash}:${doc.path}`],
2152
+ { cwd: ctx.root, maxBuffer: 10 * 1024 * 1024, timeout: 1e4 }
2153
+ )).stdout;
2154
+ } catch {
2155
+ result.skipped.push({ check: "deps-changed", doc: doc.path, reason: `${doc.path}: committed dependency content is unavailable` });
2156
+ continue;
2157
+ }
2015
2158
  }
2016
2159
  const range = await commitsTouchingSince(
2017
2160
  ctx.root,
@@ -2036,16 +2179,32 @@ async function checkDepsChanged(ctx) {
2036
2179
  range.commits = relevant;
2037
2180
  range.total = relevant.length;
2038
2181
  if (range.total === 0) continue;
2039
- const latest = range.commits[0];
2182
+ const relevance = await matchDependencyChanges(
2183
+ ctx.root,
2184
+ doc.lastCommit.hash,
2185
+ ctx.headHash,
2186
+ [...new Set(range.commits.flatMap((commit) => commit.files))],
2187
+ content2
2188
+ );
2189
+ if (!relevance.matches.length && !hasDependencyContent(content2)) continue;
2190
+ const match = relevance.matches[0];
2191
+ if (doc.dirty && match) result.skipped.push({
2192
+ check: "deps-changed",
2193
+ doc: doc.path,
2194
+ reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
2195
+ });
2040
2196
  (doc.dirty ? result.suppressedAdvisories : result.advisories).push({
2041
2197
  type: "deps-changed",
2042
- 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}")`,
2043
- anchor: { doc: doc.path, line: null, excerpt: null },
2198
+ ...match ? {} : { resolution: "informational" },
2199
+ 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." : ""}`,
2200
+ anchor: { doc: doc.path, line: match?.line ?? null, excerpt: match?.excerpt ?? null },
2044
2201
  evidence: {
2045
2202
  kind: "doc-behind-manifests",
2046
2203
  docLastCommit: doc.lastCommit,
2047
2204
  manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
2048
- totalCommits: range.total
2205
+ totalCommits: range.total,
2206
+ matches: relevance.matches,
2207
+ matchingIncomplete: relevance.incomplete
2049
2208
  }
2050
2209
  });
2051
2210
  }
@@ -2055,9 +2214,11 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
2055
2214
  var init_deps_changed = __esm({
2056
2215
  "src/audit/checks/deps-changed.ts"() {
2057
2216
  "use strict";
2217
+ init_dependency_claims();
2058
2218
  init_docs();
2059
2219
  init_release_metadata();
2060
2220
  init_git();
2221
+ init_inspection();
2061
2222
  init_checks();
2062
2223
  MANIFEST_COMMITS_CAP = 10;
2063
2224
  MANIFEST_PATHSPECS = [
@@ -2078,7 +2239,7 @@ var init_deps_changed = __esm({
2078
2239
  });
2079
2240
 
2080
2241
  // src/context/lexical.ts
2081
- import path16 from "path";
2242
+ import path17 from "path";
2082
2243
  var init_lexical = __esm({
2083
2244
  "src/context/lexical.ts"() {
2084
2245
  "use strict";
@@ -2262,7 +2423,7 @@ var init_provenance = __esm({
2262
2423
 
2263
2424
  // src/decisions/decisions.ts
2264
2425
  import fs9 from "fs/promises";
2265
- import path17 from "path";
2426
+ import path18 from "path";
2266
2427
  import { createHash } from "crypto";
2267
2428
  import { z as z4 } from "zod";
2268
2429
  async function loadDecisionStore(rootDir) {
@@ -2313,9 +2474,9 @@ var init_decisions = __esm({
2313
2474
  });
2314
2475
 
2315
2476
  // src/decisions/drift.ts
2316
- import path18 from "path";
2477
+ import path19 from "path";
2317
2478
  async function computeDecisionDrift(rootDir, decisions) {
2318
- const resolvedRoot = path18.resolve(rootDir);
2479
+ const resolvedRoot = path19.resolve(rootDir);
2319
2480
  const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
2320
2481
  const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
2321
2482
  const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
@@ -2511,7 +2672,9 @@ var init_findings = __esm({
2511
2672
  kind: z5.literal("doc-behind-manifests"),
2512
2673
  docLastCommit: commitSchema,
2513
2674
  manifestCommits: z5.array(commitSchema.extend({ files: z5.array(z5.string()) })),
2514
- totalCommits: count
2675
+ totalCommits: count,
2676
+ matches: z5.array(z5.object({ dependency: z5.string(), manifest: z5.string(), before: z5.string().nullable(), after: z5.string().nullable(), line: z5.number().int().positive(), excerpt: z5.string() })).optional(),
2677
+ matchingIncomplete: z5.boolean().optional()
2515
2678
  }),
2516
2679
  z5.object({
2517
2680
  kind: z5.literal("decision-anchor"),
@@ -2527,7 +2690,7 @@ var init_findings = __esm({
2527
2690
  type: z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
2528
2691
  confidence: z5.enum(["certain", "likely"])
2529
2692
  });
2530
- advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z5.literal("recheck").optional() });
2693
+ advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z5.enum(["recheck", "informational"]).optional() });
2531
2694
  checkResultSchema = z5.object({
2532
2695
  issues: z5.array(issueSchema),
2533
2696
  advisories: z5.array(advisorySchema),
@@ -2545,14 +2708,18 @@ async function readRecord(root, id) {
2545
2708
  if (raw === null) return null;
2546
2709
  const record = recordSchema.parse(raw);
2547
2710
  const { digest: stored, ...payload } = record;
2548
- 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]))) {
2711
+ 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))) {
2549
2712
  throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
2550
2713
  }
2551
2714
  return record;
2552
2715
  }
2553
- function scopes(finding) {
2716
+ function evidenceFingerprint(evidence, reason2) {
2717
+ return digest([evidence.scope, evidence.tree, ...reason2 === "no-dependency-claims" ? [] : [evidence.lastScopeCommit], evidence.paths]);
2718
+ }
2719
+ function scopes(finding, reason2) {
2554
2720
  const files = [finding.anchor.doc], e = finding.evidence;
2555
2721
  if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
2722
+ if (reason2 === "no-dependency-claims" && e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`];
2556
2723
  if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
2557
2724
  if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
2558
2725
  else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
@@ -2563,9 +2730,9 @@ function scopes(finding) {
2563
2730
  return `:(literal)${file}`;
2564
2731
  }).sort();
2565
2732
  }
2566
- async function reviewEvidence(root, finding) {
2733
+ async function reviewEvidence(root, finding, reason2) {
2567
2734
  if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
2568
- const scope = scopes(finding);
2735
+ const scope = scopes(finding, reason2);
2569
2736
  const head = (await git2(root, "rev-parse", "HEAD")).trim();
2570
2737
  const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
2571
2738
  if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
@@ -2591,7 +2758,7 @@ async function reviewEvidence(root, finding) {
2591
2758
  if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
2592
2759
  }
2593
2760
  }
2594
- const fingerprint = digest([scope, tree, lastScopeCommit, paths]);
2761
+ const fingerprint = evidenceFingerprint({ head, scope, tree, lastScopeCommit, paths, fingerprint: "" }, reason2);
2595
2762
  if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
2596
2763
  return {
2597
2764
  ...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
@@ -2638,7 +2805,7 @@ async function assessAdvisory(root, finding, baselineHead) {
2638
2805
  const record = await readRecord(root, id);
2639
2806
  if (!record) return null;
2640
2807
  const event = record.events.at(-1);
2641
- const current = await reviewEvidence(root, event.finding);
2808
+ const current = await reviewEvidence(root, event.finding, event.reasonCode);
2642
2809
  await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
2643
2810
  const base = {
2644
2811
  id,
@@ -2648,7 +2815,7 @@ async function assessAdvisory(root, finding, baselineHead) {
2648
2815
  outcome: event.outcome,
2649
2816
  reviewedHead: event.evidence.head
2650
2817
  };
2651
- 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." };
2818
+ 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." };
2652
2819
  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." };
2653
2820
  } catch (error) {
2654
2821
  return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
@@ -2665,7 +2832,7 @@ async function advisoryReviewInventory(root) {
2665
2832
  if (names.length > 5e3) throw new Error("Advisory review inventory exceeds 5,000 records.");
2666
2833
  return Promise.all(names.filter((name) => name.endsWith(".json")).sort().map(async (name) => [name, await readAuditInput(root, ".mason/reviews/advisories/" + name)]));
2667
2834
  }
2668
- var git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
2835
+ var git2, hashSchema, commitSchema2, evidenceSchema2, dismissalReasonSchema, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
2669
2836
  var init_advisory_review = __esm({
2670
2837
  "src/audit/advisory-review.ts"() {
2671
2838
  "use strict";
@@ -2691,11 +2858,13 @@ var init_advisory_review = __esm({
2691
2858
  lastScopeCommit: commitSchema2,
2692
2859
  paths: z6.array(z6.object({ path: z6.string(), exists: z6.boolean() })).max(1e3)
2693
2860
  });
2861
+ dismissalReasonSchema = z6.enum(["no-dependency-claims", "unrelated-manifest-change"]);
2694
2862
  eventSchema2 = z6.object({
2695
2863
  at: z6.string().datetime(),
2696
- reviewer: z6.string().trim().min(1).max(200),
2864
+ reviewer: z6.string().trim().min(1).max(200).optional(),
2697
2865
  note: z6.string().trim().min(1).max(2500),
2698
- outcome: z6.enum(["addressed", "inapplicable", "deferred"]),
2866
+ outcome: z6.enum(["addressed", "inapplicable", "deferred", "dismissed"]),
2867
+ reasonCode: dismissalReasonSchema.optional(),
2699
2868
  finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
2700
2869
  evidence: evidenceSchema2
2701
2870
  });
@@ -2707,13 +2876,14 @@ var init_advisory_review = __esm({
2707
2876
  recordPath: z6.string().optional(),
2708
2877
  reviewer: z6.string().optional(),
2709
2878
  note: z6.string().optional(),
2710
- outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
2879
+ outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired", "dismissed"]).optional(),
2711
2880
  reviewedHead: z6.string().optional()
2712
2881
  });
2713
2882
  advisoryReviewRequest = z6.object({
2714
2883
  baselinePath: z6.string().min(1),
2715
2884
  findingId: hashSchema,
2716
- action: z6.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
2885
+ action: z6.enum(["prepare", "addressed", "inapplicable", "deferred", "dismiss"]).default("prepare"),
2886
+ reasonCode: dismissalReasonSchema.optional(),
2717
2887
  reviewer: eventSchema2.shape.reviewer.optional(),
2718
2888
  note: eventSchema2.shape.note.optional(),
2719
2889
  reviewToken: hashSchema.optional()
@@ -2724,12 +2894,12 @@ var init_advisory_review = __esm({
2724
2894
 
2725
2895
  // src/audit/audit.ts
2726
2896
  import fs11 from "fs/promises";
2727
- import path19 from "path";
2897
+ import path20 from "path";
2728
2898
  async function computeAudit(rootDir, options = {}) {
2729
2899
  return profilePhase("audit.current", () => auditCurrent(rootDir, options));
2730
2900
  }
2731
2901
  async function auditCurrent(rootDir, options) {
2732
- const resolvedRoot = path19.resolve(rootDir);
2902
+ const resolvedRoot = path20.resolve(rootDir);
2733
2903
  const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
2734
2904
  if (docs.length === 0) return null;
2735
2905
  const report = {
@@ -2763,7 +2933,7 @@ async function auditCurrent(rootDir, options) {
2763
2933
  }
2764
2934
  let decisionsPresent = false;
2765
2935
  try {
2766
- await fs11.access(path19.join(resolvedRoot, ".mason", "decisions"));
2936
+ await fs11.access(path20.join(resolvedRoot, ".mason", "decisions"));
2767
2937
  decisionsPresent = true;
2768
2938
  } catch {
2769
2939
  }
@@ -2775,13 +2945,21 @@ async function auditCurrent(rootDir, options) {
2775
2945
  changesSinceDoc,
2776
2946
  decisionsPresent
2777
2947
  };
2948
+ const policy = (await loadProjectConfig(resolvedRoot)).audit;
2778
2949
  const selected = options.checks ?? ALL_CHECKS;
2779
2950
  for (const name of ALL_CHECKS) {
2780
2951
  if (!selected.includes(name)) continue;
2781
- const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, ctx) : CHECKS[name](ctx));
2952
+ const exclusions = policy?.exclude?.[name] ?? [];
2953
+ const included = await includedCheckPaths(resolvedRoot, docs.map((doc) => doc.path), exclusions);
2954
+ const excluded = docs.filter((doc) => !included.has(doc.path)).map((doc) => ({ check: name, doc: doc.path }));
2955
+ if (excluded.length) (report.excludedChecks ??= []).push(...excluded);
2956
+ const scoped = { ...ctx, docs: docs.filter((doc) => included.has(doc.path)) };
2957
+ const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, scoped) : CHECKS[name](scoped));
2782
2958
  report.checksRun.push(name);
2783
2959
  report.issues.push(...issues);
2784
- report.advisories.push(...advisories);
2960
+ const advisoryPaths = await includedCheckPaths(resolvedRoot, [...new Set(advisories.map((f) => f.anchor.doc))], exclusions);
2961
+ report.advisories.push(...advisories.filter((f) => advisoryPaths.has(f.anchor.doc)));
2962
+ for (const f of advisories) if (!advisoryPaths.has(f.anchor.doc)) (report.excludedChecks ??= []).push({ check: name, doc: f.anchor.doc });
2785
2963
  report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
2786
2964
  report.skippedChecks.push(...skipped);
2787
2965
  }
@@ -2793,6 +2971,8 @@ async function auditCurrent(rootDir, options) {
2793
2971
  var init_audit = __esm({
2794
2972
  "src/audit/audit.ts"() {
2795
2973
  "use strict";
2974
+ init_files();
2975
+ init_policy();
2796
2976
  init_profile();
2797
2977
  init_drift();
2798
2978
  init_snapshot();
@@ -2805,7 +2985,7 @@ var init_audit = __esm({
2805
2985
 
2806
2986
  // src/audit/repair.ts
2807
2987
  import fs12 from "fs/promises";
2808
- import path20 from "path";
2988
+ import path21 from "path";
2809
2989
  import { randomUUID as randomUUID2 } from "crypto";
2810
2990
  import { z as z7 } from "zod";
2811
2991
  function recheckable(finding) {
@@ -2826,7 +3006,7 @@ async function docState(root) {
2826
3006
  docs.push([doc, null]);
2827
3007
  }
2828
3008
  }
2829
- return digest(docs);
3009
+ return digest([docs, (await loadProjectConfig(root)).audit]);
2830
3010
  }
2831
3011
  async function stableAudit(root, checks, options = {}) {
2832
3012
  return profilePhase("repair.audit", () => readStableAudit(root, checks, options));
@@ -2892,8 +3072,8 @@ async function prepareOriginal(rootDir, checks, options, session) {
2892
3072
  }
2893
3073
  async function loadRepairBaseline(rootDir, baselinePath) {
2894
3074
  const root = await fs12.realpath(rootDir);
2895
- const declaredRoot = path20.resolve(rootDir);
2896
- const relative = path20.isAbsolute(baselinePath) ? path20.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
3075
+ const declaredRoot = path21.resolve(rootDir);
3076
+ const relative = path21.isAbsolute(baselinePath) ? path21.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
2897
3077
  const stored = baselineSchema.parse(await readStoreJson(root, relative));
2898
3078
  const { digest: savedDigest, ...payload } = stored;
2899
3079
  if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
@@ -2939,6 +3119,9 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
2939
3119
  if (now && ("confidence" in finding || "confidence" in now)) {
2940
3120
  return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
2941
3121
  }
3122
+ if (current.excludedChecks?.some((excluded) => excluded.check === finding.type && excluded.doc === finding.anchor.doc)) {
3123
+ return { ...base, status: "resolved", reason: "Excluded by project audit policy; this is not a correctness assessment. Original evidence is retained." };
3124
+ }
2942
3125
  const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
2943
3126
  if (!current.checksRun?.includes(finding.type) || skipped.length) {
2944
3127
  return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
@@ -2949,11 +3132,14 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
2949
3132
  if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
2950
3133
  return { ...base, review, status: "review-required", reason: review.reason };
2951
3134
  }
3135
+ if (!("confidence" in finding) && finding.evidence.kind === "doc-behind-manifests" && !finding.evidence.matches?.length && (!now || !("confidence" in now) && now.resolution === "informational")) {
3136
+ return { ...base, status: "resolved", reason: "Manifest recency alone establishes no affected passage. Retained as information; no assessment is required." };
3137
+ }
2952
3138
  if (!("confidence" in finding) && (!recheckable(finding) || now)) {
2953
3139
  return {
2954
3140
  ...base,
2955
3141
  status: "review-required",
2956
- 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."
3142
+ 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."
2957
3143
  };
2958
3144
  }
2959
3145
  return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
@@ -2977,7 +3163,7 @@ async function verifyOriginal(rootDir, baselinePath, options, session) {
2977
3163
  const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
2978
3164
  const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
2979
3165
  for (const f of findings) counts[f.status]++;
2980
- 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)));
3166
+ 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)));
2981
3167
  const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
2982
3168
  return {
2983
3169
  version: 1,
@@ -3020,13 +3206,14 @@ var init_repair = __esm({
3020
3206
  docs: z7.array(z7.object({
3021
3207
  path: z7.string().refine((value) => normalizeRepoPath(value) === value),
3022
3208
  scope: z7.string().optional(),
3023
- kind: z7.enum(["instructions", "readme"]).optional(),
3209
+ kind: z7.enum(["instructions", "readme", "guide"]).optional(),
3024
3210
  lastCommit: commitSchema.nullable(),
3025
3211
  dirty: z7.boolean(),
3026
3212
  lineCount: count2
3027
3213
  })).nonempty(),
3028
3214
  decisionsChecked: z7.boolean(),
3029
3215
  clean: z7.boolean(),
3216
+ excludedChecks: z7.array(z7.object({ check: checkSchema, doc: z7.string() })).optional(),
3030
3217
  issues: z7.array(issueSchema),
3031
3218
  advisories: z7.array(advisorySchema),
3032
3219
  suppressedAdvisories: z7.array(advisorySchema).optional(),
@@ -3052,9 +3239,9 @@ var init_repair = __esm({
3052
3239
 
3053
3240
  // src/automation/evidence.ts
3054
3241
  import fs13 from "fs/promises";
3055
- import path21 from "path";
3242
+ import path22 from "path";
3056
3243
  import { createHash as createHash3 } from "crypto";
3057
- import fg5 from "fast-glob";
3244
+ import fg7 from "fast-glob";
3058
3245
  import { z as z8 } from "zod";
3059
3246
  async function git3(root, ...args) {
3060
3247
  return profilePhase("automation.git", async () => (await execGit(args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout);
@@ -3072,6 +3259,11 @@ async function readInputs(root) {
3072
3259
  }
3073
3260
  async function collectInputs(root) {
3074
3261
  const docPaths = await discoverDocPaths(root);
3262
+ const auditPolicy = (await loadProjectConfig(root)).audit;
3263
+ const allowed = /* @__PURE__ */ new Map();
3264
+ for (const name of ["deleted-reference", "new-module", "stale-count", "dead-command"]) {
3265
+ allowed.set(name, await includedCheckPaths(root, docPaths, auditPolicy?.exclude?.[name] ?? []));
3266
+ }
3075
3267
  const [headText, docStatus, shallowPath, replacements] = await Promise.all([
3076
3268
  git3(root, "rev-parse", "HEAD"),
3077
3269
  docPaths.length ? git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...docPaths.map((p) => `:(literal)${p}`)) : "",
@@ -3081,7 +3273,7 @@ async function collectInputs(root) {
3081
3273
  const head = headText.trim();
3082
3274
  let shallow = null;
3083
3275
  try {
3084
- shallow = await fs13.readFile(path21.resolve(root, shallowPath.trim()), "utf8");
3276
+ shallow = await fs13.readFile(path22.resolve(root, shallowPath.trim()), "utf8");
3085
3277
  } catch (error) {
3086
3278
  if (error.code !== "ENOENT") throw error;
3087
3279
  }
@@ -3100,18 +3292,18 @@ async function collectInputs(root) {
3100
3292
  docContents.push([file, text2]);
3101
3293
  const parsed = { path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") };
3102
3294
  parsedDocs.push(parsed);
3103
- for (const claim of parsed.claims.paths) {
3295
+ for (const claim of allowed.get("deleted-reference").has(file) ? parsed.claims.paths : []) {
3104
3296
  const scope = pathClaimScope(file, claim);
3105
3297
  if (scope?.candidates.some(gitMetadataPath)) continue;
3106
3298
  for (const candidate of scope?.candidates ?? []) {
3107
3299
  if (optionalMasonPath(candidate)) continue;
3108
- const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(path21.posix.dirname(candidate))]);
3300
+ const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(path22.posix.dirname(candidate))]);
3109
3301
  claims.push([candidate, filePresent, parentPresent]);
3110
3302
  }
3111
3303
  }
3112
3304
  }
3113
- const combinedDocs = moduleDocumentation(parsedDocs);
3114
- const countClaims = parsedDocs.flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
3305
+ const combinedDocs = moduleDocumentation(parsedDocs.filter((doc) => allowed.get("new-module").has(doc.path)));
3306
+ const countClaims = parsedDocs.filter((doc) => allowed.get("stale-count").has(doc.path)).flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
3115
3307
  const decisionDirectory = await storePath(root, ".mason/decisions");
3116
3308
  const decisionPresence = await fs13.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
3117
3309
  if (error.code === "ENOENT") return "absent";
@@ -3120,8 +3312,8 @@ async function collectInputs(root) {
3120
3312
  const [modules, counts, commands2, decisionFiles] = await Promise.all([
3121
3313
  combinedDocs ? moduleCandidates(root, combinedDocs) : [],
3122
3314
  Promise.all(countClaims.map(({ doc, claim }) => resolveDocCountSource(root, doc, claim))),
3123
- commandInputs(root, parsedDocs),
3124
- fg5(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
3315
+ commandInputs(root, parsedDocs.filter((doc) => allowed.get("dead-command").has(doc.path))),
3316
+ fg7(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
3125
3317
  ]);
3126
3318
  const decisions = await Promise.all(decisionFiles.sort().map(async (file) => {
3127
3319
  await storePath(root, file);
@@ -3131,7 +3323,7 @@ async function collectInputs(root) {
3131
3323
  git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
3132
3324
  git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
3133
3325
  ]) : ["", ""];
3134
- const common = [5, engineVersion, head, shallow, replacements, docContents];
3326
+ const common = [7, engineVersion, head, shallow, replacements, docContents, auditPolicy];
3135
3327
  const keys = {
3136
3328
  "deleted-reference": hash([common, claims, docStatus]),
3137
3329
  "new-module": hash([common, modules]),
@@ -3178,6 +3370,8 @@ var engineVersion, hash, content, cacheSchema;
3178
3370
  var init_evidence = __esm({
3179
3371
  "src/automation/evidence.ts"() {
3180
3372
  "use strict";
3373
+ init_policy();
3374
+ init_files();
3181
3375
  init_git_read();
3182
3376
  init_profile();
3183
3377
  init_docs();
@@ -3191,7 +3385,7 @@ var init_evidence = __esm({
3191
3385
  init_dead_command();
3192
3386
  init_storage();
3193
3387
  init_advisory_review();
3194
- engineVersion = true ? "0.18.0" : "development";
3388
+ engineVersion = true ? "0.19.1" : "development";
3195
3389
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
3196
3390
  content = readAuditInput;
3197
3391
  cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
@@ -3405,13 +3599,17 @@ async function observeReadOnlyTool(dir, event) {
3405
3599
  }
3406
3600
  function summarize(report) {
3407
3601
  const open = report.findings.filter((f) => f.status !== "resolved");
3602
+ const onlyReview = report.status === "incomplete" && !report.diagnostics.length && !report.counts.unresolved && !report.counts.unverified && report.counts["review-required"] > 0;
3408
3603
  return [
3409
- `Mason: ${report.status}; ${report.counts.unresolved} unresolved, ${report.counts["review-required"]} need review, ${report.counts.unverified} unverified.`,
3410
- ...open.slice(0, 4).map((f) => `[${f.status}] ${cleanText(f.original.anchor.doc)}: ${cleanText(f.original.message)}`),
3604
+ onlyReview ? `Mason: ${report.counts["review-required"]} advisor${report.counts["review-required"] === 1 ? "y" : "ies"} awaiting review; no unresolved issues.` : `Mason: ${report.status}; ${report.counts.unresolved} unresolved, ${report.counts["review-required"]} need review, ${report.counts.unverified} unverified.`,
3605
+ ...open.slice(0, 4).map((f) => {
3606
+ const detail = f.current ? f.current.message : f.status === "review-required" ? `${f.original.type}: current check no longer reports ${f.original.type === "deps-changed" ? "this condition" : "the original finding"}; historical evidence awaits assessment.` : `Original evidence: ${f.original.message}. ${f.reason}`;
3607
+ return `[${f.status}] ${cleanText(f.original.anchor.doc)}: ${cleanText(detail)}`;
3608
+ }),
3411
3609
  ...open.length > 4 ? [`${open.length - 4} more findings in the report.`] : [],
3412
3610
  ...report.diagnostics.slice(0, 2).map(cleanText),
3413
3611
  `Evidence: ${report.reportPath}. Resume/check with mason_automation(action: "check") or mason-auto check.`,
3414
- "Keep original evidence. Address findings relevant to the authorized task; report unrelated findings and unresolved advisories without approving them."
3612
+ "Keep original evidence. Address findings relevant to the authorized task; report unrelated findings with a brief scope reason and suggested follow-up. Use review_advisory for authorized assessments; otherwise leave advisories open without approving them."
3415
3613
  ].join("\n");
3416
3614
  }
3417
3615
  async function loadState(ws) {
@@ -3635,7 +3833,7 @@ async function automate(dir, event) {
3635
3833
  if (event.event === "after_tool" && event.mutating) {
3636
3834
  session.mutationObserved = true;
3637
3835
  if (!event.toolId || !session.pending[event.toolId]) {
3638
- const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
3836
+ const gap = "A tool completed without an observed matching pre-tool capture in this checkout; pre-edit coverage is unknown. The tool may have changed checkouts; captures are kept separately for each worktree.";
3639
3837
  if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
3640
3838
  }
3641
3839
  if (event.toolId) delete session.pending[event.toolId];
@@ -3745,9 +3943,9 @@ var init_runtime = __esm({
3745
3943
 
3746
3944
  // src/automation/hook-root.ts
3747
3945
  import fs15 from "fs/promises";
3748
- import path22 from "path";
3946
+ import path23 from "path";
3749
3947
  async function repositoryRoot(dir) {
3750
- if (!path22.isAbsolute(dir)) throw new Error("Hook input working directory must be absolute.");
3948
+ if (!path23.isAbsolute(dir)) throw new Error("Hook input working directory must be absolute.");
3751
3949
  try {
3752
3950
  return await fs15.realpath((await git3(dir, "rev-parse", "--show-toplevel")).trim());
3753
3951
  } catch (error) {
@@ -3778,11 +3976,39 @@ var init_hook_root = __esm({
3778
3976
 
3779
3977
  // src/setup/model.ts
3780
3978
  import { z as z11 } from "zod";
3979
+ import fs16 from "fs/promises";
3980
+ import path24 from "path";
3781
3981
  async function loadSetup(root) {
3782
3982
  const raw = await readStoreJson(root, SETUP_PATH);
3783
3983
  if (raw === null) return null;
3784
3984
  return setupSchema.parse(raw);
3785
3985
  }
3986
+ async function effectiveSetup(root) {
3987
+ const local = await loadSetup(root);
3988
+ if (local) return { root, setup: local };
3989
+ const common = await fs16.realpath(path24.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim()));
3990
+ const own = await fs16.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
3991
+ if (common === own) return { root, setup: null };
3992
+ const primary = (await git3(root, "worktree", "list", "--porcelain", "-z")).split("\0\0")[0].split("\0");
3993
+ const candidate = primary.find((field) => field.startsWith("worktree "))?.slice(9);
3994
+ if (!candidate || primary.includes("bare")) return { root, setup: null };
3995
+ let inheritedRoot;
3996
+ try {
3997
+ inheritedRoot = await fs16.realpath((await git3(candidate, "rev-parse", "--show-toplevel")).trim());
3998
+ } catch (error) {
3999
+ if (/must be run in a work tree/.test(String(error.stderr))) return { root, setup: null };
4000
+ throw error;
4001
+ }
4002
+ const inheritedCommon = await fs16.realpath(path24.resolve(
4003
+ inheritedRoot,
4004
+ (await git3(inheritedRoot, "rev-parse", "--git-common-dir")).trim()
4005
+ ));
4006
+ if (inheritedCommon !== common) throw new Error("Linked worktree setup does not belong to the same Git repository.");
4007
+ const setup = await loadSetup(inheritedRoot);
4008
+ const inactive = await readStoreJson(root, INACTIVE_HOSTS_PATH);
4009
+ if (inactive && setup) for (const host of inactiveHostsSchema.parse(inactive).hosts) delete setup.hosts[host];
4010
+ return { root: inheritedRoot, setup };
4011
+ }
3786
4012
  async function loadSetupReceipt(root, directory, host) {
3787
4013
  const raw = await readStoreJson(root, directory + "/setup-" + host + ".json");
3788
4014
  if (raw === null) return null;
@@ -3790,10 +4016,11 @@ async function loadSetupReceipt(root, directory, host) {
3790
4016
  if (receipt.root !== root || receipt.host !== host) throw new Error("Setup receipt belongs to another installation.");
3791
4017
  return receipt;
3792
4018
  }
3793
- var setupHostSchema, setupSchema, SETUP_PATH, receiptSchema;
4019
+ var setupHostSchema, setupSchema, SETUP_PATH, INACTIVE_HOSTS_PATH, inactiveHostsSchema, receiptSchema;
3794
4020
  var init_model = __esm({
3795
4021
  "src/setup/model.ts"() {
3796
4022
  "use strict";
4023
+ init_evidence();
3797
4024
  init_storage();
3798
4025
  setupHostSchema = z11.object({
3799
4026
  configuredVersion: z11.string(),
@@ -3807,6 +4034,8 @@ var init_model = __esm({
3807
4034
  claude: setupHostSchema.optional()
3808
4035
  }) });
3809
4036
  SETUP_PATH = ".mason/local/setup.json";
4037
+ INACTIVE_HOSTS_PATH = ".mason/local/inactive-hosts.json";
4038
+ inactiveHostsSchema = z11.object({ version: z11.literal(1), hosts: z11.array(z11.enum(["claude", "codex"])) });
3810
4039
  receiptSchema = z11.object({
3811
4040
  version: z11.literal(1),
3812
4041
  host: z11.enum(["codex", "claude"]),
@@ -3833,7 +4062,7 @@ __export(launcher_exports, {
3833
4062
  });
3834
4063
  import { execFile as execFile2 } from "child_process";
3835
4064
  import { promisify as promisify2 } from "util";
3836
- import fs16 from "fs/promises";
4065
+ import fs17 from "fs/promises";
3837
4066
  function globalCommand(args, platform = process.platform) {
3838
4067
  return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
3839
4068
  }
@@ -3853,8 +4082,8 @@ async function installedCommand() {
3853
4082
  }
3854
4083
  }
3855
4084
  async function prepareLaunch(host, options = {}) {
3856
- const root = await fs16.realpath((await git3(options.dir ?? process.cwd(), "rev-parse", "--show-toplevel")).trim());
3857
- const entry = (await loadSetup(root))?.hosts[host];
4085
+ const root = await fs17.realpath((await git3(options.dir ?? process.cwd(), "rev-parse", "--show-toplevel")).trim());
4086
+ const entry = (await effectiveSetup(root)).setup?.hosts[host];
3858
4087
  process.env.MASON_SETUP_ROOT = root;
3859
4088
  process.env.MASON_SETUP_HOST = host;
3860
4089
  delete process.env.MASON_SETUP_REVISION;
@@ -3876,7 +4105,7 @@ var init_launcher = __esm({
3876
4105
  "use strict";
3877
4106
  init_evidence();
3878
4107
  init_model();
3879
- executingVersion = () => true ? "0.18.0" : "development";
4108
+ executingVersion = () => true ? "0.19.1" : "development";
3880
4109
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
3881
4110
  hookCommand = (host) => `mason --setup-host ${host} auto`;
3882
4111
  }
@@ -3889,7 +4118,7 @@ __export(observations_exports, {
3889
4118
  observeActivation: () => observeActivation,
3890
4119
  readObservation: () => readObservation
3891
4120
  });
3892
- import fs17 from "fs/promises";
4121
+ import fs18 from "fs/promises";
3893
4122
  import { z as z12 } from "zod";
3894
4123
  function observationPath(directory, host, revision, version = executingVersion()) {
3895
4124
  return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
@@ -3906,15 +4135,15 @@ async function observeActivation(dir, event, options = {}) {
3906
4135
  if (host !== "codex" && host !== "claude" || !process.env.MASON_SETUP_ROOT) return null;
3907
4136
  try {
3908
4137
  if (!revision) {
3909
- if ((await loadSetup(process.env.MASON_SETUP_ROOT))?.hosts[host]) {
4138
+ if ((await effectiveSetup(process.env.MASON_SETUP_ROOT)).setup?.hosts[host]) {
3910
4139
  return "Mason setup is now present. Restart the assistant to connect this MCP session to the configured integration; activation has not been established by this session.";
3911
4140
  }
3912
4141
  return inactiveSetupMessage();
3913
4142
  }
3914
4143
  const capturedDirectory = options.directory?.match(/^\.mason\/reports\/automation\/[a-f0-9]{24}$/)?.[0] ?? options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
3915
- const ws = capturedDirectory ? { root: await fs17.realpath(dir), directory: capturedDirectory } : await workspace(dir);
3916
- if (ws.root !== await fs17.realpath(process.env.MASON_SETUP_ROOT)) return null;
3917
- const setup = await loadSetup(ws.root);
4144
+ const ws = capturedDirectory ? { root: await fs18.realpath(dir), directory: capturedDirectory } : await workspace(dir);
4145
+ if (ws.root !== await fs18.realpath(process.env.MASON_SETUP_ROOT)) return null;
4146
+ const { setup } = await effectiveSetup(ws.root);
3918
4147
  if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
3919
4148
  const directory = ws.directory + "/activation";
3920
4149
  await withLock(ws.root, directory, async () => {
@@ -4091,8 +4320,8 @@ __export(files_exports, {
4091
4320
  managedBlock: () => managedBlock,
4092
4321
  readText: () => readText
4093
4322
  });
4094
- import fs18 from "fs/promises";
4095
- import path23 from "path";
4323
+ import fs19 from "fs/promises";
4324
+ import path25 from "path";
4096
4325
  import { randomUUID as randomUUID5 } from "crypto";
4097
4326
  async function readText(root, file) {
4098
4327
  try {
@@ -4121,14 +4350,14 @@ async function applyEdit(root, edit) {
4121
4350
  if (edit.after === null) {
4122
4351
  const file2 = await storePath(root, edit.path);
4123
4352
  if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
4124
- await fs18.unlink(file2);
4353
+ await fs19.unlink(file2);
4125
4354
  return true;
4126
4355
  }
4127
4356
  const file = await storePath(root, edit.path, true);
4128
- const temporary = path23.join(path23.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
4357
+ const temporary = path25.join(path25.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
4129
4358
  try {
4130
- const mode = await fs18.stat(file).then((s) => s.mode & 511, () => 384);
4131
- const handle = await fs18.open(temporary, "wx", mode);
4359
+ const mode = await fs19.stat(file).then((s) => s.mode & 511, () => 384);
4360
+ const handle = await fs19.open(temporary, "wx", mode);
4132
4361
  try {
4133
4362
  await handle.writeFile(edit.after, "utf8");
4134
4363
  await handle.sync();
@@ -4136,10 +4365,10 @@ async function applyEdit(root, edit) {
4136
4365
  await handle.close();
4137
4366
  }
4138
4367
  if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
4139
- await fs18.rename(temporary, file);
4368
+ await fs19.rename(temporary, file);
4140
4369
  return true;
4141
4370
  } finally {
4142
- await fs18.rm(temporary, { force: true });
4371
+ await fs19.rm(temporary, { force: true });
4143
4372
  }
4144
4373
  }
4145
4374
  var init_files2 = __esm({
@@ -4250,9 +4479,11 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
4250
4479
  - For an architectural overview, use \`get_snapshot\` if a map is available. If \`map.status\` is missing or invalid, use available decisions and source evidence; do not start building a map unless requested.
4251
4480
  - \`mason_init\` returns documentation audit and committed-diff review results, plus a short setup guide. Pass \`evidence\` with local CI manifest paths to include test and analysis results; the CLI equivalent is \`mason-review --evidence <manifest>\`. State skipped, unavailable, stale, or unknown checks explicitly. Related accepted decisions identify review context, not proven violations.
4252
4481
 
4253
- - When documentation repair is authorized, use \`mason_repair(action: "prepare")\` before edits, keep its baselinePath, and use \`mason_repair(action: "verify", baselinePath)\` after edits and any final doc commit. Report every original finding's outcome and any new findings. Suppressed advisories remain unresolved; editing a doc does not approve it.
4482
+ - When documentation repair is authorized, use \`mason_repair(action: "prepare")\` before edits, keep its baselinePath, and use \`mason_repair(action: "verify", baselinePath)\` after edits and any final doc commit. Report every original finding's outcome and any new findings. Specific suppressed advisories remain unresolved; editing a doc does not approve it. Informational dependency notices require no review.
4254
4483
  - When advisory assessment is authorized, use \`review_advisory(action: "prepare", baselinePath, findingId)\` to inspect original evidence and history. Record \`addressed\`, \`inapplicable\`, or \`deferred\` with the actual reviewer, reason in note, and returned reviewToken only after relevant edits are committed. Commit the assessment and verify the same baseline again. Relevant evidence changes reopen it; deferral stays outstanding. Decision findings use \`review_decision\`; advisory closure never approves a decision.
4255
4484
 
4485
+ - For a dependency advisory, \`review_advisory(action: "dismiss", baselinePath, findingId, reasonCode, note)\` records a one-step dismissal without reviewer or token. Use \`no-dependency-claims\` for a document with no dependency claims (reopens on document content changes), or \`unrelated-manifest-change\` for reviewed changes that do not affect its guidance (reopens on document or manifest changes). Keep the relevant scope committed; commit the dismissal to share it. This never approves a decision.
4486
+
4256
4487
  - When Mason automation is installed, use \`mason_automation(action: "status")\` to inspect configured hooks and observed events, and \`mason_automation(action: "check")\` to resume its retained repair evidence. CLI fallback: \`mason-auto status\` / \`mason-auto check\`. Preserve existing baselines across sessions. Automatic checks do not authorize unrelated repairs or approve advisories.
4257
4488
 
4258
4489
  Inspect source for what the retrieved context does not answer.
@@ -4547,8 +4778,10 @@ __export(teardown_exports, {
4547
4778
  teardownProject: () => teardownProject
4548
4779
  });
4549
4780
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
4781
+ import fs20 from "fs/promises";
4782
+ import path26 from "path";
4550
4783
  import { parse as parse3, stringify as stringify2 } from "smol-toml";
4551
- import fg6 from "fast-glob";
4784
+ import fg8 from "fast-glob";
4552
4785
  import { z as z18 } from "zod";
4553
4786
  async function planTeardown(root, selected) {
4554
4787
  const edits = [], diagnostics = [];
@@ -4568,6 +4801,11 @@ async function planTeardown(root, selected) {
4568
4801
  const ownership = ownershipBefore === null ? { version: 1, files: {} } : ownershipSchema.parse(JSON.parse(ownershipBefore));
4569
4802
  const setupBefore = await read(SETUP_PATH);
4570
4803
  const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
4804
+ const linked = (await workspace(root)).gitDir !== await fs20.realpath(
4805
+ path26.resolve(root, (await git3(root, "rev-parse", "--git-common-dir")).trim())
4806
+ );
4807
+ const inactiveBefore = linked && !setup ? await read(INACTIVE_HOSTS_PATH) : null;
4808
+ const inactive = inactiveBefore === null ? { version: 1, hosts: [] } : inactiveHostsSchema.parse(JSON.parse(inactiveBefore));
4571
4809
  const automationBefore = await read(AUTOMATION_PATH);
4572
4810
  const automation = automationBefore === null ? null : recordSchema2.parse(JSON.parse(automationBefore));
4573
4811
  const commands2 = (host) => new Set([
@@ -4687,7 +4925,7 @@ async function planTeardown(root, selected) {
4687
4925
  if (diagnostics.length) edits.splice(instructionEditStart);
4688
4926
  if (!diagnostics.length) {
4689
4927
  const cleanup = [];
4690
- const receiptFiles = await fg6(
4928
+ const receiptFiles = await fg8(
4691
4929
  selected.map((host) => ".mason/reports/automation/*/setup-" + host + ".json"),
4692
4930
  { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false }
4693
4931
  );
@@ -4711,7 +4949,16 @@ async function planTeardown(root, selected) {
4711
4949
  delete ownership.files[configPath(host)];
4712
4950
  }
4713
4951
  if (!remainingHost) for (const file of DOC_CANDIDATES) delete ownership.files[file];
4714
- if (setupBefore !== null) cleanup.push({ path: SETUP_PATH, before: setupBefore, after: Object.keys(setup.hosts).length ? json(setup) : null });
4952
+ if (setupBefore !== null) cleanup.push({
4953
+ path: SETUP_PATH,
4954
+ before: setupBefore,
4955
+ after: linked || Object.keys(setup.hosts).length ? json(setup) : null
4956
+ });
4957
+ else if (linked) cleanup.push({
4958
+ path: INACTIVE_HOSTS_PATH,
4959
+ before: inactiveBefore,
4960
+ after: json({ version: 1, hosts: [.../* @__PURE__ */ new Set([...inactive.hosts, ...selected])].sort() })
4961
+ });
4715
4962
  if (automationBefore !== null) cleanup.push({ path: AUTOMATION_PATH, before: automationBefore, after: Object.keys(automation.hosts).length ? json(automation) : null });
4716
4963
  if (ownershipBefore !== null) cleanup.push({ path: OWNERSHIP_PATH, before: ownershipBefore, after: Object.keys(ownership.files).length ? json(ownership) : null });
4717
4964
  edits.push(...cleanup);
@@ -5152,10 +5399,10 @@ var init_check = __esm({
5152
5399
  });
5153
5400
 
5154
5401
  // src/review/evidence.ts
5155
- import path24 from "path";
5402
+ import path27 from "path";
5156
5403
  import { z as z22 } from "zod";
5157
5404
  function relativeArtifact(root, file) {
5158
- const relative = path24.isAbsolute(file) ? path24.relative(root, file) : file;
5405
+ const relative = path27.isAbsolute(file) ? path27.relative(root, file) : file;
5159
5406
  const normalized = normalizeRepoPath(relative);
5160
5407
  if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
5161
5408
  return normalized;
@@ -5252,7 +5499,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
5252
5499
  const report = await readStoreJson(root, file);
5253
5500
  if (report === null) throw new Error(`Report artifact is missing: ${file}`);
5254
5501
  const sourceRoot = check.sourceRoot ?? root;
5255
- if (!path24.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
5502
+ if (!path27.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
5256
5503
  const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
5257
5504
  result.outcome = parsed.outcome;
5258
5505
  result.counts = parsed.counts;
@@ -5373,8 +5620,8 @@ var init_evidence2 = __esm({
5373
5620
  });
5374
5621
 
5375
5622
  // src/review/review.ts
5376
- import fs19 from "fs/promises";
5377
- import path25 from "path";
5623
+ import fs21 from "fs/promises";
5624
+ import path28 from "path";
5378
5625
  import { execFile as execFile4 } from "child_process";
5379
5626
  import { promisify as promisify4 } from "util";
5380
5627
  async function resolveMergeBase(resolvedRoot, base, head) {
@@ -5403,7 +5650,7 @@ function anchorsTouched(record, changedFiles) {
5403
5650
  return matchingPaths(record.files, changedFiles);
5404
5651
  }
5405
5652
  async function computeReview(rootDir, base, options = {}) {
5406
- const resolvedRoot = path25.resolve(rootDir);
5653
+ const resolvedRoot = path28.resolve(rootDir);
5407
5654
  const head = await getCurrentGitHash(resolvedRoot);
5408
5655
  if (head === "unknown") return null;
5409
5656
  const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
@@ -5453,7 +5700,7 @@ async function computeReview(rootDir, base, options = {}) {
5453
5700
  analyzed,
5454
5701
  async (relPath) => {
5455
5702
  try {
5456
- await fs19.access(path25.join(resolvedRoot, relPath));
5703
+ await fs21.access(path28.join(resolvedRoot, relPath));
5457
5704
  return true;
5458
5705
  } catch {
5459
5706
  return false;
@@ -5596,8 +5843,9 @@ __export(status_exports, {
5596
5843
  import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
5597
5844
  async function setupStatus(dir) {
5598
5845
  const ws = await workspace(dir);
5599
- const setup = await loadSetup(ws.root);
5600
- if (!setup) {
5846
+ const activation = await effectiveSetup(ws.root);
5847
+ const setup = activation.setup;
5848
+ if (!setup || !Object.keys(setup.hosts).length) {
5601
5849
  const partial = await Promise.all(["codex", "claude"].map(async (host) => ({
5602
5850
  host,
5603
5851
  receipt: await loadSetupReceipt(ws.root, ws.directory, host)
@@ -5631,7 +5879,7 @@ async function setupStatus(dir) {
5631
5879
  ]));
5632
5880
  const hooks = isDeepStrictEqual3(compatibleHooks, expectedHooks) && !config.disabled;
5633
5881
  const local = await loadSetupReceipt(ws.root, ws.directory, host);
5634
- const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
5882
+ const configured = activation.root !== ws.root || local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
5635
5883
  const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
5636
5884
  const sessions = Object.values(observation?.sessions ?? {}).sort((a, b) => b.at.localeCompare(a.at));
5637
5885
  const complete = sessions.find((session) => events.every((event) => session.events.includes(event)));
@@ -5663,6 +5911,7 @@ async function setupStatus(dir) {
5663
5911
  status: statuses.length && statuses.every((status) => status === "active") ? "active" : statuses.includes("attention") ? "attention" : "pending",
5664
5912
  root: ws.root,
5665
5913
  hosts: hosts2,
5914
+ activationRoot: activation.root,
5666
5915
  decisionRecords: decisions.records.length,
5667
5916
  diagnostics: decisions.diagnostics,
5668
5917
  scope: "Activation receipts record observed use after setup, not complete interception, correct repairs, or measured usefulness. Host trust and higher-priority settings may prevent execution. Receipts are local to this worktree and branch."
@@ -5670,6 +5919,9 @@ async function setupStatus(dir) {
5670
5919
  }
5671
5920
  function summarizeActivation(status) {
5672
5921
  const lines = ["Mason setup: " + status.status + "."];
5922
+ if (status.activationRoot && status.activationRoot !== status.root) {
5923
+ lines.push("Activation inherited from " + status.activationRoot + "; evidence belongs to this worktree.");
5924
+ }
5673
5925
  for (const [host, state] of Object.entries(status.hosts)) {
5674
5926
  lines.push(
5675
5927
  `${host}: ${state.status}`,