mason-context 0.16.3 → 0.17.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.
@@ -346,6 +346,89 @@ var init_store = __esm({
346
346
  }
347
347
  });
348
348
 
349
+ // src/audit/commands.ts
350
+ import path4 from "path";
351
+ function splitCommands(line) {
352
+ const parts = [];
353
+ let quote = "", start = 0;
354
+ for (let i = 0; i < line.length; i++) {
355
+ const char = line[i];
356
+ if (char === "\\") {
357
+ i++;
358
+ continue;
359
+ }
360
+ if (quote) {
361
+ if (char === quote) quote = "";
362
+ continue;
363
+ }
364
+ if (char === "'" || char === '"') {
365
+ quote = char;
366
+ continue;
367
+ }
368
+ if (char === ";" || line.slice(i, i + 2) === "&&") {
369
+ parts.push(line.slice(start, i));
370
+ if (char === "&") i++;
371
+ start = i + 1;
372
+ }
373
+ }
374
+ parts.push(line.slice(start));
375
+ return parts;
376
+ }
377
+ function commandClaims(line, lineNo, cwd, shell = false) {
378
+ const claims = [];
379
+ const segments = splitCommands(line);
380
+ for (const segment of segments) {
381
+ const text2 = segment.trim().replace(/^\$\s+/, "");
382
+ if (text2.startsWith("#") || /^(?:echo|printf)\b/.test(text2)) continue;
383
+ const cd = text2.match(/^cd\s+(.+)$/);
384
+ if (cd) {
385
+ const directory = literal(cd[1]);
386
+ cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path4.posix.join(cwd ?? ".", directory);
387
+ continue;
388
+ }
389
+ for (const match of text2.matchAll(COMMAND)) {
390
+ const flags = match[2].trim();
391
+ const selector = flags.match(/^(?:--prefix|--dir|-C)(?:\s+|=)(.+)$/);
392
+ let directory = cwd;
393
+ let scopeUnknown;
394
+ if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
395
+ if (selector) {
396
+ const selected = literal(selector[1]);
397
+ directory = selected === null || cwd === null ? null : path4.posix.join(cwd ?? ".", selected);
398
+ } else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
399
+ const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
400
+ if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
401
+ scopeUnknown = "Package selection or conditional options after the script need review.";
402
+ }
403
+ if (/[|()]/.test(text2) || /\b(?:pushd|popd|if|for|while)\b/.test(text2) || directory === null) {
404
+ scopeUnknown = "The command's working directory or shell control flow could not be resolved.";
405
+ }
406
+ claims.push({
407
+ scriptName: match[3],
408
+ invocation: match[0],
409
+ excerpt: match[0],
410
+ line: lineNo,
411
+ ...directory !== void 0 && directory !== null ? { directory } : {},
412
+ ...scopeUnknown ? { scopeUnknown } : {}
413
+ });
414
+ }
415
+ if (/\b(?:cd|pushd|popd)\b/.test(text2)) cwd = null;
416
+ }
417
+ return { claims, cwd };
418
+ }
419
+ var TOKEN, COMMAND, literal;
420
+ var init_commands = __esm({
421
+ "src/audit/commands.ts"() {
422
+ "use strict";
423
+ TOKEN = `(?:"[^"\\n]*"|'[^'\\n]*'|[^\\s\`;&|]+)`;
424
+ COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
425
+ literal = (value) => {
426
+ const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
427
+ return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path4.posix.isAbsolute(unquoted) ? null : unquoted;
428
+ };
429
+ }
430
+ });
431
+
349
432
  // src/audit/tree.ts
350
433
  function glyphIndex(line) {
351
434
  for (const glyph of GLYPHS) {
@@ -425,6 +508,7 @@ var init_tree = __esm({
425
508
  });
426
509
 
427
510
  // src/audit/claims.ts
511
+ import MarkdownIt from "markdown-it";
428
512
  function normalizePathToken(token) {
429
513
  let t = token.trim();
430
514
  if (!t) return null;
@@ -488,11 +572,34 @@ function extractClaims(content2) {
488
572
  const counts = [];
489
573
  const commands = /* @__PURE__ */ new Map();
490
574
  const addPath = (claim) => {
491
- if (!paths.has(claim.path)) paths.set(claim.path, claim);
575
+ const key = [claim.relativeTo ?? "scope", claim.path].join(":");
576
+ if (!paths.has(key)) paths.set(key, claim);
492
577
  };
493
578
  const addCommand = (claim) => {
494
- if (!commands.has(claim.scriptName)) commands.set(claim.scriptName, claim);
579
+ const key = JSON.stringify([claim.scriptName, claim.directory, claim.scopeUnknown]);
580
+ if (!commands.has(key)) commands.set(key, claim);
495
581
  };
582
+ const parsed = markdown.parse(lines.map((line, i) => ignored[i] ? "" : line).join("\n"), {});
583
+ for (const token of parsed) {
584
+ if (token.type !== "inline" || !token.map) continue;
585
+ let line = token.map[0] + 1;
586
+ for (const child of token.children ?? []) {
587
+ const target = child.type === "link_open" ? child.attrGet("href") : child.type === "image" ? child.attrGet("src") : null;
588
+ if (target && !/^(?:[a-z][a-z0-9+.-]*:|[\/#~])/i.test(target)) {
589
+ let pathname = null;
590
+ try {
591
+ pathname = decodeURIComponent(target.split(/[?#]/)[0]);
592
+ } catch {
593
+ }
594
+ if (pathname && !/[$*?{}<>\\\x00-\x1f]/.test(pathname)) {
595
+ addPath({ path: pathname, line, excerpt: target, relativeTo: "document" });
596
+ }
597
+ }
598
+ if (child.type === "softbreak" || child.type === "hardbreak") line++;
599
+ else line += child.content.split("\n").length - 1;
600
+ }
601
+ }
602
+ let shellCwd;
496
603
  let inFence = false;
497
604
  let fenceInfo = "";
498
605
  let fenceMarker = "";
@@ -523,6 +630,7 @@ function extractClaims(content2) {
523
630
  fenceMarker = fenceMatch[1][0];
524
631
  fenceInfo = fenceMatch[2].trim().toLowerCase();
525
632
  blockLines = [];
633
+ shellCwd = void 0;
526
634
  blockStartLine = lineNo + 1;
527
635
  } else if (fenceMatch[1][0] === fenceMarker) {
528
636
  inFence = false;
@@ -533,14 +641,9 @@ function extractClaims(content2) {
533
641
  if (inFence) {
534
642
  blockLines.push(ignored[i] ? "" : line);
535
643
  if (!ignored[i] && SHELL_FENCE_INFOS.has(fenceInfo)) {
536
- for (const m of line.matchAll(COMMAND_RE)) {
537
- addCommand({
538
- scriptName: m[2],
539
- invocation: m[0],
540
- line: lineNo,
541
- excerpt: m[0]
542
- });
543
- }
644
+ const scanned = commandClaims(line, lineNo, shellCwd, true);
645
+ shellCwd = scanned.cwd;
646
+ for (const claim of scanned.claims) addCommand(claim);
544
647
  }
545
648
  continue;
546
649
  }
@@ -567,14 +670,10 @@ function extractClaims(content2) {
567
670
  excerpt: m[0]
568
671
  });
569
672
  }
570
- for (const m of line.matchAll(COMMAND_RE)) {
571
- addCommand({
572
- scriptName: m[2],
573
- invocation: m[0],
574
- line: lineNo,
575
- excerpt: m[0]
576
- });
673
+ for (const match of line.matchAll(/`([^`]+)`/g)) {
674
+ for (const claim of commandClaims(match[1], lineNo, void 0, true).claims) addCommand(claim);
577
675
  }
676
+ for (const claim of commandClaims(line.replace(/`[^`]+`/g, ""), lineNo).claims) addCommand(claim);
578
677
  }
579
678
  if (inFence) processBlock();
580
679
  return {
@@ -583,10 +682,11 @@ function extractClaims(content2) {
583
682
  commands: [...commands.values()]
584
683
  };
585
684
  }
586
- var ROOT_FILE_NAMES, SHELL_FENCE_INFOS, COMMAND_RE, COUNT_RE, COUNT_DENYLIST_RE, IGNORE_LINE, IGNORE_START, IGNORE_END;
685
+ var ROOT_FILE_NAMES, SHELL_FENCE_INFOS, markdown, COUNT_RE, COUNT_DENYLIST_RE, IGNORE_LINE, IGNORE_START, IGNORE_END;
587
686
  var init_claims = __esm({
588
687
  "src/audit/claims.ts"() {
589
688
  "use strict";
689
+ init_commands();
590
690
  init_tree();
591
691
  ROOT_FILE_NAMES = /* @__PURE__ */ new Set([
592
692
  "package.json",
@@ -620,7 +720,7 @@ var init_claims = __esm({
620
720
  ".env.example"
621
721
  ]);
622
722
  SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
623
- COMMAND_RE = /\b(npm|pnpm|yarn)\s+run\s+([A-Za-z0-9:_.-]+)/g;
723
+ markdown = new MarkdownIt("commonmark", { html: false });
624
724
  COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
625
725
  COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
626
726
  IGNORE_LINE = "<!-- mason:ignore -->";
@@ -641,7 +741,7 @@ async function lastCommitOf(resolvedRoot, relPath) {
641
741
  try {
642
742
  const { stdout } = await exec2(
643
743
  "git",
644
- ["log", "-1", `--format=${COMMIT_FORMAT}`, "--", relPath],
744
+ ["log", "-1", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
645
745
  { cwd: resolvedRoot }
646
746
  );
647
747
  const line = stdout.trim().split("\n")[0];
@@ -660,7 +760,7 @@ async function deletingCommitOf(resolvedRoot, relPath) {
660
760
  "--diff-filter=D",
661
761
  `--format=${COMMIT_FORMAT}`,
662
762
  "--",
663
- relPath
763
+ `:(literal)${relPath}`
664
764
  ],
665
765
  { cwd: resolvedRoot }
666
766
  );
@@ -674,7 +774,7 @@ async function firstCommitOf(resolvedRoot, relPath) {
674
774
  try {
675
775
  const { stdout } = await exec2(
676
776
  "git",
677
- ["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", relPath],
777
+ ["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
678
778
  { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
679
779
  );
680
780
  const line = stdout.trim().split("\n")[0];
@@ -720,42 +820,242 @@ var init_git = __esm({
720
820
  }
721
821
  });
722
822
 
723
- // src/audit/docs.ts
823
+ // src/audit/inputs.ts
724
824
  import fs4 from "fs/promises";
725
- import path4 from "path";
825
+ import * as nativeFs from "fs";
826
+ import path5 from "path";
726
827
  import { execFile as execFile3 } from "child_process";
727
828
  import { promisify as promisify3 } from "util";
829
+ import fg2 from "fast-glob";
830
+ async function auditInputPath(root, file) {
831
+ if (file === ".") return fs4.realpath(root);
832
+ try {
833
+ return await storePath(root, file);
834
+ } catch (error) {
835
+ if (error instanceof Error && error.message.startsWith("Symlink")) {
836
+ throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
837
+ }
838
+ throw error;
839
+ }
840
+ }
841
+ async function readAuditInput(root, file) {
842
+ try {
843
+ const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
844
+ if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
845
+ return value;
846
+ } catch (error) {
847
+ if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
848
+ throw error;
849
+ }
850
+ }
851
+ async function auditGlob(root, patterns, options) {
852
+ if (Array.isArray(patterns) && !patterns.length) return [];
853
+ const directories = /* @__PURE__ */ new Set();
854
+ const readdir2 = ((directory, opts, callback) => {
855
+ const relative = path5.relative(root, directory).split(path5.sep).join("/") || ".";
856
+ directories.add(relative);
857
+ if (directories.size > MAX_INPUTS) {
858
+ callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
859
+ return;
860
+ }
861
+ auditInputPath(root, relative).then(() => {
862
+ nativeFs.readdir(directory, opts, callback);
863
+ }, callback);
864
+ });
865
+ const lstat2 = ((file, callback) => {
866
+ const relative = path5.relative(root, file).split(path5.sep).join("/") || ".";
867
+ auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
868
+ });
869
+ const stream = fg2.stream(patterns, {
870
+ cwd: root,
871
+ ignore: options.ignore,
872
+ onlyFiles: false,
873
+ onlyDirectories: options.onlyDirectories,
874
+ objectMode: true,
875
+ followSymbolicLinks: options.followSymbolicLinks ?? true,
876
+ fs: { readdir: readdir2, lstat: lstat2 },
877
+ concurrency: 16
878
+ });
879
+ const matches = [];
880
+ try {
881
+ for await (const value of stream) {
882
+ const entry = value;
883
+ const file = entry.path;
884
+ if (options.select && !options.select(file)) continue;
885
+ await auditInputPath(root, file);
886
+ if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
887
+ matches.push(file);
888
+ if (matches.length > MAX_INPUTS) {
889
+ const largest = [...matches.reduce((counts, item) => {
890
+ const dir = item.split("/")[0];
891
+ counts.set(dir, (counts.get(dir) ?? 0) + 1);
892
+ return counts;
893
+ }, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
894
+ 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.`);
895
+ }
896
+ }
897
+ } finally {
898
+ stream.destroy();
899
+ }
900
+ return matches.sort();
901
+ }
902
+ async function gitSourcePaths(root) {
903
+ const { stdout } = await execute(
904
+ "git",
905
+ [
906
+ "ls-files",
907
+ "-z",
908
+ "--cached",
909
+ "--others",
910
+ "--exclude-standard",
911
+ "--",
912
+ ...SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`)
913
+ ],
914
+ { cwd: root, maxBuffer: 32 * 1024 * 1024, timeout: 1e4 }
915
+ );
916
+ return [...new Set(stdout.split("\0").filter(Boolean))].sort();
917
+ }
918
+ async function gitIgnoredPaths(root, files) {
919
+ if (!files.length) return /* @__PURE__ */ new Set();
920
+ const operation = execute(
921
+ "git",
922
+ ["check-ignore", "-z", "--stdin"],
923
+ { cwd: root, maxBuffer: 4 * 1024 * 1024, timeout: 1e4 }
924
+ );
925
+ operation.child.stdin.on("error", () => {
926
+ });
927
+ operation.child.stdin.end(files.join("\0") + "\0");
928
+ try {
929
+ return new Set((await operation).stdout.split("\0").filter(Boolean));
930
+ } catch (error) {
931
+ if (error.code === 1) return /* @__PURE__ */ new Set();
932
+ throw error;
933
+ }
934
+ }
935
+ var execute, MAX_INPUTS;
936
+ var init_inputs = __esm({
937
+ "src/audit/inputs.ts"() {
938
+ "use strict";
939
+ init_files();
940
+ init_storage();
941
+ execute = promisify3(execFile3);
942
+ MAX_INPUTS = 1e5;
943
+ }
944
+ });
945
+
946
+ // src/audit/docs.ts
947
+ import fs5 from "fs/promises";
948
+ import path6 from "path";
949
+ import { execFile as execFile4 } from "child_process";
950
+ import { promisify as promisify4 } from "util";
951
+ import fg3 from "fast-glob";
728
952
  async function isDirty(resolvedRoot, relPath) {
729
953
  try {
730
954
  const { stdout } = await exec3(
731
955
  "git",
732
- ["status", "--porcelain", "--", relPath],
956
+ ["status", "--porcelain", "--", `:(literal)${relPath}`],
733
957
  { cwd: resolvedRoot }
734
958
  );
735
959
  return stdout.trim().length > 0;
736
960
  } catch {
737
- return false;
961
+ return true;
738
962
  }
739
963
  }
740
- async function discoverDocs(resolvedRoot) {
741
- const docs = await Promise.all(DOC_CANDIDATES.map(async (candidate) => {
742
- let content2;
964
+ async function discoverDocPaths(root) {
965
+ let candidates = [];
966
+ try {
967
+ const { stdout } = await exec3(
968
+ "git",
969
+ [
970
+ "ls-files",
971
+ "-z",
972
+ "--cached",
973
+ "--others",
974
+ "--exclude-standard",
975
+ "--",
976
+ ":(icase,glob)**/readme.md",
977
+ ":(icase,glob)**/agents.md",
978
+ ":(icase,glob)**/claude.md"
979
+ ],
980
+ { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 }
981
+ );
982
+ candidates = stdout.split("\0").filter(Boolean);
983
+ } catch (error) {
743
984
  try {
744
- content2 = await fs4.readFile(path4.join(resolvedRoot, candidate), "utf-8");
985
+ await exec3("git", ["rev-parse", "--git-dir"], { cwd: root });
745
986
  } catch {
746
- return null;
987
+ return localDocPaths(root);
747
988
  }
989
+ throw error;
990
+ }
991
+ candidates.push(...await localDocPaths(root));
992
+ const config = await loadProjectConfig(root);
993
+ const excluded = [
994
+ "**/node_modules/**",
995
+ "**/vendor/**",
996
+ "**/dist/**",
997
+ "**/build/**",
998
+ "**/target/**",
999
+ "**/.git/**",
1000
+ "**/.venv/**",
1001
+ "**/venv/**",
1002
+ ".mason/**",
1003
+ ...config.ignore ?? []
1004
+ ];
1005
+ const paths = [];
1006
+ const unique = [...new Set(candidates)].sort();
1007
+ for (let offset = 0; offset < unique.length; offset += 256) {
1008
+ paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) => fg3.escapePath(p)), {
1009
+ ignore: excluded,
1010
+ followSymbolicLinks: false,
1011
+ label: "Documentation discovery"
1012
+ }));
1013
+ if (paths.length > 1e4) throw new Error("Documentation discovery exceeds 10,000 relevant files; narrow .mason/config.json ignore patterns.");
1014
+ }
1015
+ const priority2 = (file) => {
1016
+ const index2 = DOC_CANDIDATES.findIndex((p) => p.toLowerCase() === file.toLowerCase());
1017
+ return index2 < 0 ? DOC_CANDIDATES.length : index2;
1018
+ };
1019
+ return paths.sort((a, b) => priority2(a) - priority2(b) || a.localeCompare(b));
1020
+ }
1021
+ async function localDocPaths(root) {
1022
+ const paths = [];
1023
+ for (const dir of [".", ".claude"]) {
1024
+ let entries;
1025
+ try {
1026
+ entries = await fs5.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1027
+ } catch (error) {
1028
+ if (error.code === "ENOENT") continue;
1029
+ throw error;
1030
+ }
1031
+ for (const entry of entries) if (/^(?:agents|claude|readme)\.md$/i.test(entry.name)) {
1032
+ paths.push(path6.posix.join(dir, entry.name));
1033
+ }
1034
+ }
1035
+ return paths;
1036
+ }
1037
+ function documentScope(file) {
1038
+ const parent = path6.posix.dirname(file);
1039
+ return path6.posix.basename(parent).toLowerCase() === ".claude" ? path6.posix.dirname(parent) : parent;
1040
+ }
1041
+ async function discoverDocs(resolvedRoot) {
1042
+ const docs = [];
1043
+ for (const candidate of await discoverDocPaths(resolvedRoot)) {
1044
+ const content2 = await readAuditInput(resolvedRoot, candidate);
1045
+ if (content2 === null) throw new Error("Document disappeared during discovery: " + candidate);
748
1046
  const [lastCommit, dirty] = await Promise.all([lastCommitOf(resolvedRoot, candidate), isDirty(resolvedRoot, candidate)]);
749
- return {
1047
+ docs.push({
750
1048
  path: candidate,
751
1049
  content: content2,
1050
+ scope: documentScope(candidate),
1051
+ kind: /^readme\.md$/i.test(path6.posix.basename(candidate)) ? "readme" : "instructions",
752
1052
  lineCount: content2.split("\n").length,
753
1053
  lastCommit,
754
1054
  dirty,
755
1055
  claims: extractClaims(content2)
756
- };
757
- }));
758
- return docs.filter((doc) => doc !== null);
1056
+ });
1057
+ }
1058
+ return docs;
759
1059
  }
760
1060
  var exec3, DOC_CANDIDATES;
761
1061
  var init_docs = __esm({
@@ -763,7 +1063,9 @@ var init_docs = __esm({
763
1063
  "use strict";
764
1064
  init_claims();
765
1065
  init_git();
766
- exec3 = promisify3(execFile3);
1066
+ init_inputs();
1067
+ init_files();
1068
+ exec3 = promisify4(execFile4);
767
1069
  DOC_CANDIDATES = [
768
1070
  "AGENTS.md",
769
1071
  "CLAUDE.md",
@@ -772,10 +1074,52 @@ var init_docs = __esm({
772
1074
  }
773
1075
  });
774
1076
 
1077
+ // src/audit/scope.ts
1078
+ import fs6 from "fs/promises";
1079
+ import path7 from "path";
1080
+ function scopedPath(directory, value) {
1081
+ if (path7.posix.isAbsolute(value) || value.includes("\\") || /[\x00-\x1f]/.test(value)) return null;
1082
+ const resolved = path7.posix.normalize(path7.posix.join(directory, value));
1083
+ return resolved === ".." || resolved.startsWith("../") ? null : resolved;
1084
+ }
1085
+ function pathClaimScope(doc, claim) {
1086
+ const directory = claim.relativeTo === "document" ? path7.posix.dirname(doc) : documentScope(doc);
1087
+ const local = scopedPath(directory, claim.path);
1088
+ if (!local) return null;
1089
+ const candidates = claim.relativeTo === "document" || directory === "." ? [local] : [...new Set([local, scopedPath(".", claim.path)].filter((p) => p !== null))];
1090
+ return {
1091
+ basis: claim.relativeTo === "document" ? "document-link" : directory === "." ? "repository" : "document",
1092
+ directory,
1093
+ candidates
1094
+ };
1095
+ }
1096
+ async function pathExists(root, file) {
1097
+ try {
1098
+ await fs6.access(await auditInputPath(root, file));
1099
+ return true;
1100
+ } catch (error) {
1101
+ if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return false;
1102
+ throw error;
1103
+ }
1104
+ }
1105
+ function gitMetadataPath(file) {
1106
+ return file.split("/").includes(".git");
1107
+ }
1108
+ function optionalMasonPath(file) {
1109
+ return file.split("/").includes(".mason");
1110
+ }
1111
+ var init_scope = __esm({
1112
+ "src/audit/scope.ts"() {
1113
+ "use strict";
1114
+ init_inputs();
1115
+ init_docs();
1116
+ }
1117
+ });
1118
+
775
1119
  // src/test-map.ts
776
- import path5 from "path";
1120
+ import path8 from "path";
777
1121
  async function buildTestMap(dir) {
778
- const rootDir = path5.resolve(dir);
1122
+ const rootDir = path8.resolve(dir);
779
1123
  const access = await createFileAccess(rootDir);
780
1124
  const testPatterns = [
781
1125
  "**/*.test.*",
@@ -796,7 +1140,7 @@ async function buildTestMap(dir) {
796
1140
  const sourceByBaseName = /* @__PURE__ */ new Map();
797
1141
  for (const file of sourceFiles) {
798
1142
  if (testFiles.includes(file)) continue;
799
- const baseName = path5.basename(file).replace(/\.[^.]+$/, "");
1143
+ const baseName = path8.basename(file).replace(/\.[^.]+$/, "");
800
1144
  const existing = sourceByBaseName.get(baseName) ?? [];
801
1145
  existing.push(file);
802
1146
  sourceByBaseName.set(baseName, existing);
@@ -804,7 +1148,7 @@ async function buildTestMap(dir) {
804
1148
  const paired = [];
805
1149
  const unmatched = [];
806
1150
  for (const testFile of testFiles) {
807
- const testBaseName = path5.basename(testFile).replace(/\.[^.]+$/, "");
1151
+ const testBaseName = path8.basename(testFile).replace(/\.[^.]+$/, "");
808
1152
  const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
809
1153
  if (!sourceName) {
810
1154
  unmatched.push(testFile);
@@ -812,10 +1156,10 @@ async function buildTestMap(dir) {
812
1156
  }
813
1157
  const candidates = sourceByBaseName.get(sourceName);
814
1158
  if (candidates && candidates.length > 0) {
815
- const testDir = path5.dirname(testFile);
1159
+ const testDir = path8.dirname(testFile);
816
1160
  const bestMatch = candidates.reduce((best, candidate) => {
817
- const candidateDir = path5.dirname(candidate);
818
- const bestDir = path5.dirname(best);
1161
+ const candidateDir = path8.dirname(candidate);
1162
+ const bestDir = path8.dirname(best);
819
1163
  const candidateOverlap = commonSegments(testDir, candidateDir);
820
1164
  const bestOverlap = commonSegments(testDir, bestDir);
821
1165
  return candidateOverlap > bestOverlap ? candidate : best;
@@ -834,12 +1178,12 @@ async function buildTestMap(dir) {
834
1178
  function commonSegments(pathA, pathB) {
835
1179
  const segsA = pathA.split("/");
836
1180
  const segsB = pathB.split("/");
837
- let count3 = 0;
1181
+ let count4 = 0;
838
1182
  for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
839
- if (segsA[i] === segsB[i]) count3++;
1183
+ if (segsA[i] === segsB[i]) count4++;
840
1184
  else break;
841
1185
  }
842
- return count3;
1186
+ return count4;
843
1187
  }
844
1188
  var init_test_map = __esm({
845
1189
  "src/test-map.ts"() {
@@ -849,9 +1193,9 @@ var init_test_map = __esm({
849
1193
  });
850
1194
 
851
1195
  // src/snapshot/snapshot.ts
852
- import path6 from "path";
853
- import { execFile as execFile4 } from "child_process";
854
- import { promisify as promisify4 } from "util";
1196
+ import path9 from "path";
1197
+ import { execFile as execFile5 } from "child_process";
1198
+ import { promisify as promisify5 } from "util";
855
1199
  import { z as z2 } from "zod";
856
1200
  async function inspectSnapshot(rootDir) {
857
1201
  try {
@@ -884,7 +1228,7 @@ var init_snapshot = __esm({
884
1228
  init_storage();
885
1229
  init_paths();
886
1230
  init_test_map();
887
- exec4 = promisify4(execFile4);
1231
+ exec4 = promisify5(execFile5);
888
1232
  repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
889
1233
  verificationFields = {
890
1234
  refreshedHash: z2.string().optional(),
@@ -913,10 +1257,10 @@ var init_snapshot = __esm({
913
1257
  });
914
1258
 
915
1259
  // src/drift/drift.ts
916
- import fs5 from "fs/promises";
917
- import path7 from "path";
918
- import { execFile as execFile5 } from "child_process";
919
- import { promisify as promisify5 } from "util";
1260
+ import fs7 from "fs/promises";
1261
+ import path10 from "path";
1262
+ import { execFile as execFile6 } from "child_process";
1263
+ import { promisify as promisify6 } from "util";
920
1264
  function parseChanges(output) {
921
1265
  const fields = output.split("\0");
922
1266
  const changes = [];
@@ -961,7 +1305,7 @@ var init_drift = __esm({
961
1305
  "use strict";
962
1306
  init_snapshot();
963
1307
  init_paths();
964
- exec5 = promisify5(execFile5);
1308
+ exec5 = promisify6(execFile6);
965
1309
  }
966
1310
  });
967
1311
 
@@ -982,230 +1326,76 @@ var init_types = __esm({
982
1326
  });
983
1327
 
984
1328
  // src/audit/checks/deleted-reference.ts
985
- import fs6 from "fs/promises";
986
- import path8 from "path";
987
- async function exists(absPath) {
988
- try {
989
- await fs6.access(absPath);
990
- return true;
991
- } catch {
992
- return false;
993
- }
994
- }
995
- async function checkDeletedReferences(ctx) {
996
- const result = emptyResult();
997
- for (const doc of ctx.docs) {
998
- const changes = ctx.changesSinceDoc.get(doc.path);
999
- const renames = /* @__PURE__ */ new Map();
1000
- for (const change of changes ?? []) {
1001
- if (change.status === "renamed" && change.previousPath) {
1002
- renames.set(change.previousPath, change.path);
1003
- }
1004
- }
1005
- for (const claim of doc.claims.paths) {
1006
- if (claim.path === ".mason" || claim.path.startsWith(".mason/")) {
1007
- continue;
1008
- }
1009
- if (await exists(path8.join(ctx.root, claim.path))) continue;
1010
- const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
1011
- const renamedTo = renames.get(claim.path) ?? null;
1012
- if (renamedTo) {
1013
- result.issues.push({
1014
- type: "deleted-reference",
1015
- message: `\`${claim.path}\` was renamed to \`${renamedTo}\``,
1016
- anchor,
1017
- confidence: "certain",
1018
- evidence: {
1019
- kind: "missing-path",
1020
- claimed: claim.path,
1021
- renamedTo,
1022
- deletedInCommit: null,
1023
- everTracked: true,
1024
- parentDirExists: true
1025
- }
1026
- });
1027
- continue;
1028
- }
1029
- const tracked = await lastCommitOf(ctx.root, claim.path);
1030
- if (tracked) {
1031
- const deleted = await deletingCommitOf(ctx.root, claim.path);
1032
- const detail = deleted ? ` \u2013 deleted in ${deleted.hash.slice(0, 7)} "${deleted.subject}" (${deleted.date.slice(0, 10)})` : "";
1033
- result.issues.push({
1034
- type: "deleted-reference",
1035
- message: `\`${claim.path}\` no longer exists${detail}`,
1036
- anchor,
1037
- confidence: "certain",
1038
- evidence: {
1039
- kind: "missing-path",
1040
- claimed: claim.path,
1041
- renamedTo: null,
1042
- deletedInCommit: deleted,
1043
- everTracked: true,
1044
- parentDirExists: await exists(
1045
- path8.join(ctx.root, path8.dirname(claim.path))
1046
- )
1047
- }
1048
- });
1049
- continue;
1050
- }
1051
- const parentDirExists = await exists(
1052
- path8.join(ctx.root, path8.dirname(claim.path))
1053
- );
1054
- if (!parentDirExists) continue;
1055
- const issue = {
1056
- type: "deleted-reference",
1057
- message: `\`${claim.path}\` does not exist (never tracked in git \u2013 possible typo or invented path)`,
1058
- anchor,
1059
- confidence: "likely",
1060
- evidence: {
1061
- kind: "missing-path",
1062
- claimed: claim.path,
1063
- renamedTo: null,
1064
- deletedInCommit: null,
1065
- everTracked: false,
1066
- parentDirExists: true
1067
- }
1068
- };
1069
- result.issues.push(issue);
1070
- }
1071
- }
1072
- return result;
1073
- }
1074
- var init_deleted_reference = __esm({
1075
- "src/audit/checks/deleted-reference.ts"() {
1076
- "use strict";
1077
- init_git();
1078
- init_checks();
1079
- }
1080
- });
1081
-
1082
- // src/audit/inputs.ts
1083
- import fs7 from "fs/promises";
1084
- import * as nativeFs from "fs";
1085
- import path9 from "path";
1086
- import { execFile as execFile6 } from "child_process";
1087
- import { promisify as promisify6 } from "util";
1088
- import fg2 from "fast-glob";
1089
- async function auditInputPath(root, file) {
1090
- if (file === ".") return fs7.realpath(root);
1091
- try {
1092
- return await storePath(root, file);
1093
- } catch (error) {
1094
- if (error instanceof Error && error.message.startsWith("Symlink")) {
1095
- throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
1096
- }
1097
- throw error;
1098
- }
1099
- }
1100
- async function readAuditInput(root, file) {
1101
- try {
1102
- const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
1103
- if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
1104
- return value;
1105
- } catch (error) {
1106
- if (error.code === "ENOENT") return null;
1107
- throw error;
1108
- }
1109
- }
1110
- async function auditGlob(root, patterns, options) {
1111
- if (Array.isArray(patterns) && !patterns.length) return [];
1112
- const directories = /* @__PURE__ */ new Set();
1113
- const readdir2 = ((directory, opts, callback) => {
1114
- const relative = path9.relative(root, directory).split(path9.sep).join("/") || ".";
1115
- directories.add(relative);
1116
- if (directories.size > MAX_INPUTS) {
1117
- callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
1118
- return;
1119
- }
1120
- auditInputPath(root, relative).then(() => {
1121
- nativeFs.readdir(directory, opts, callback);
1122
- }, callback);
1123
- });
1124
- const lstat2 = ((file, callback) => {
1125
- const relative = path9.relative(root, file).split(path9.sep).join("/") || ".";
1126
- auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
1127
- });
1128
- const stream = fg2.stream(patterns, {
1129
- cwd: root,
1130
- ignore: options.ignore,
1131
- onlyFiles: false,
1132
- onlyDirectories: options.onlyDirectories,
1133
- objectMode: true,
1134
- followSymbolicLinks: options.followSymbolicLinks ?? true,
1135
- fs: { readdir: readdir2, lstat: lstat2 },
1136
- concurrency: 16
1137
- });
1138
- const matches = [];
1139
- try {
1140
- for await (const value of stream) {
1141
- const entry = value;
1142
- const file = entry.path;
1143
- if (options.select && !options.select(file)) continue;
1144
- await auditInputPath(root, file);
1145
- if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
1146
- matches.push(file);
1147
- if (matches.length > MAX_INPUTS) {
1148
- const largest = [...matches.reduce((counts, item) => {
1149
- const dir = item.split("/")[0];
1150
- counts.set(dir, (counts.get(dir) ?? 0) + 1);
1151
- return counts;
1152
- }, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
1153
- 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.`);
1329
+ import path11 from "path";
1330
+ async function checkDeletedReferences(ctx) {
1331
+ const result = emptyResult();
1332
+ for (const doc of ctx.docs) {
1333
+ const changes = ctx.changesSinceDoc.get(doc.path);
1334
+ const renames = new Map((changes ?? []).filter((c) => c.status === "renamed" && c.previousPath).map((c) => [c.previousPath, c.path]));
1335
+ for (const claim of doc.claims.paths) {
1336
+ const scope = pathClaimScope(doc.path, claim);
1337
+ if (!scope) {
1338
+ result.skipped.push({
1339
+ check: "deleted-reference",
1340
+ doc: doc.path,
1341
+ reason: `Reference ${claim.excerpt} is outside the repository or has an unsupported path.`
1342
+ });
1343
+ continue;
1344
+ }
1345
+ if (scope.candidates.some(optionalMasonPath)) continue;
1346
+ if (scope.candidates.some(gitMetadataPath)) {
1347
+ result.skipped.push({
1348
+ check: "deleted-reference",
1349
+ doc: doc.path,
1350
+ reason: `Reference ${claim.path} names Git metadata, whose layout varies between checkouts; it was not verified as a repository file.`
1351
+ });
1352
+ continue;
1353
+ }
1354
+ if ((await Promise.all(scope.candidates.map((file) => pathExists(ctx.root, file)))).some(Boolean)) continue;
1355
+ const candidates = await Promise.all(scope.candidates.map(async (file) => ({
1356
+ file,
1357
+ tracked: await lastCommitOf(ctx.root, file),
1358
+ parent: await pathExists(ctx.root, path11.posix.dirname(file))
1359
+ })));
1360
+ const candidate = candidates.find((c) => c.tracked || renames.has(c.file)) ?? candidates.find((c) => c.parent);
1361
+ if (!candidate && claim.relativeTo !== "document") continue;
1362
+ const resolvedPath = candidate?.file ?? scope.candidates[0];
1363
+ const renamedTo = renames.get(resolvedPath) ?? null;
1364
+ const deletedInCommit = candidate?.tracked && !renamedTo ? await deletingCommitOf(ctx.root, resolvedPath) : null;
1365
+ const evidence = {
1366
+ kind: "missing-path",
1367
+ claimed: claim.path,
1368
+ resolvedPath,
1369
+ scope,
1370
+ renamedTo,
1371
+ deletedInCommit,
1372
+ everTracked: !!candidate?.tracked || !!renamedTo,
1373
+ parentDirExists: candidate?.parent ?? false
1374
+ };
1375
+ const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
1376
+ const message2 = renamedTo ? `\`${resolvedPath}\` was renamed to \`${renamedTo}\`` : `\`${resolvedPath}\` does not exist` + (deletedInCommit ? ` \u2013 deleted in ${deletedInCommit.hash.slice(0, 7)} "${deletedInCommit.subject}" (${deletedInCommit.date.slice(0, 10)})` : "");
1377
+ if (scope.basis !== "document" && (renamedTo || candidate?.tracked)) {
1378
+ result.issues.push({ type: "deleted-reference", message: message2, anchor, confidence: "certain", evidence });
1379
+ } else {
1380
+ result.advisories.push({ resolution: "recheck", type: "deleted-reference", message: message2 + (scope.basis === "document" ? "; the bare path's document/repository scope needs review." : "; no tracked history establishes it as a required file. Review for a typo, generated output, or example."), anchor, evidence });
1154
1381
  }
1155
1382
  }
1156
- } finally {
1157
- stream.destroy();
1158
- }
1159
- return matches.sort();
1160
- }
1161
- async function gitSourcePaths(root) {
1162
- const { stdout } = await execute(
1163
- "git",
1164
- [
1165
- "ls-files",
1166
- "-z",
1167
- "--cached",
1168
- "--others",
1169
- "--exclude-standard",
1170
- "--",
1171
- ...SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`)
1172
- ],
1173
- { cwd: root, maxBuffer: 32 * 1024 * 1024, timeout: 1e4 }
1174
- );
1175
- return [...new Set(stdout.split("\0").filter(Boolean))].sort();
1176
- }
1177
- async function gitIgnoredPaths(root, files) {
1178
- if (!files.length) return /* @__PURE__ */ new Set();
1179
- const operation = execute(
1180
- "git",
1181
- ["check-ignore", "-z", "--stdin"],
1182
- { cwd: root, maxBuffer: 4 * 1024 * 1024, timeout: 1e4 }
1183
- );
1184
- operation.child.stdin.on("error", () => {
1185
- });
1186
- operation.child.stdin.end(files.join("\0") + "\0");
1187
- try {
1188
- return new Set((await operation).stdout.split("\0").filter(Boolean));
1189
- } catch (error) {
1190
- if (error.code === 1) return /* @__PURE__ */ new Set();
1191
- throw error;
1192
1383
  }
1384
+ return result;
1193
1385
  }
1194
- var execute, MAX_INPUTS;
1195
- var init_inputs = __esm({
1196
- "src/audit/inputs.ts"() {
1386
+ var init_deleted_reference = __esm({
1387
+ "src/audit/checks/deleted-reference.ts"() {
1197
1388
  "use strict";
1198
- init_files();
1199
- init_storage();
1200
- execute = promisify6(execFile6);
1201
- MAX_INPUTS = 1e5;
1389
+ init_git();
1390
+ init_scope();
1391
+ init_checks();
1202
1392
  }
1203
1393
  });
1204
1394
 
1205
1395
  // src/audit/checks/new-module.ts
1206
- import fg3 from "fast-glob";
1396
+ import fg4 from "fast-glob";
1207
1397
  import fs8 from "fs/promises";
1208
- import path10 from "path";
1398
+ import path12 from "path";
1209
1399
  function escapeRegExp(text2) {
1210
1400
  return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1211
1401
  }
@@ -1219,15 +1409,15 @@ function isMentioned(combinedDocs, name) {
1219
1409
  async function checkNewModules(ctx) {
1220
1410
  const result = emptyResult();
1221
1411
  if (ctx.docs.length === 0) return result;
1222
- const combinedDocs = ctx.docs.map((d) => d.content).join("\n");
1412
+ const combinedDocs = moduleDocumentation(ctx.docs);
1223
1413
  const primaryDoc = ctx.docs[0].path;
1224
1414
  const checkedDocs = ctx.docs.map((d) => d.path);
1225
1415
  const flag = async (dir, sourceFileCount) => {
1226
- result.issues.push({
1416
+ result.advisories.push({
1417
+ resolution: "recheck",
1227
1418
  type: "new-module",
1228
- message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in any context file`,
1419
+ message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in the discovered documentation; review whether it needs documenting`,
1229
1420
  anchor: { doc: primaryDoc, line: null, excerpt: dir },
1230
- confidence: "likely",
1231
1421
  evidence: {
1232
1422
  kind: "unmentioned-dir",
1233
1423
  dir,
@@ -1242,41 +1432,44 @@ async function checkNewModules(ctx) {
1242
1432
  }
1243
1433
  return result;
1244
1434
  }
1435
+ function moduleDocumentation(docs) {
1436
+ return docs.map((doc) => doc.content + "\n" + (path12.posix.dirname(doc.path) === "." ? "" : path12.posix.dirname(doc.path) + "/")).join("\n");
1437
+ }
1245
1438
  async function moduleCandidates(root, combinedDocs) {
1246
1439
  const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
1247
1440
  const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
1248
1441
  const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
1249
1442
  const listSubdirs = async (dir) => {
1250
1443
  const entries = await fs8.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1251
- const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path10.posix.join(dir, entry.name));
1444
+ const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink() && sources.some((file) => file.startsWith(path12.posix.join(dir, entry.name) + "/"))) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path12.posix.join(dir, entry.name));
1252
1445
  const ignored = await gitIgnoredPaths(root, dirs);
1253
1446
  const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
1254
- return (await auditGlob(root, dir === "." ? "*" : `${fg3.escapePath(dir)}/*`, {
1447
+ return (await auditGlob(root, dir === "." ? "*" : `${fg4.escapePath(dir)}/*`, {
1255
1448
  ignore,
1256
1449
  onlyDirectories: true,
1257
1450
  label: "Module directory discovery",
1258
1451
  select: (file) => visible.has(file)
1259
- })).map((file) => path10.posix.basename(file));
1452
+ })).map((file) => path12.posix.basename(file));
1260
1453
  };
1261
1454
  const countSourceFiles = async (dir) => {
1262
1455
  const files = sources.filter((file) => file.startsWith(dir + "/"));
1263
- let count3 = 0;
1456
+ let count4 = 0;
1264
1457
  for (let offset = 0; offset < files.length; offset += 256) {
1265
- count3 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg3.escapePath(file)), {
1458
+ count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg4.escapePath(file)), {
1266
1459
  ignore,
1267
1460
  followSymbolicLinks: false,
1268
1461
  label: `Source discovery in ${dir}`
1269
1462
  })).length;
1270
- if (count3 > 1e5) throw new Error(`Source discovery in ${dir} exceeds 100,000 relevant files. Exclude generated source with .mason/config.json ignore patterns or narrow this audit's checks.`);
1463
+ if (count4 > 1e5) throw new Error(`Source discovery in ${dir} exceeds 100,000 relevant files. Exclude generated source with .mason/config.json ignore patterns or narrow this audit's checks.`);
1271
1464
  }
1272
- return count3;
1465
+ return count4;
1273
1466
  };
1274
1467
  const candidates = [];
1275
1468
  for (const topDir of await listSubdirs(".")) {
1276
1469
  const topMentioned = isMentioned(combinedDocs, topDir);
1277
1470
  if (!topMentioned) {
1278
- const count3 = await countSourceFiles(topDir);
1279
- if (count3 >= 1) candidates.push({ dir: topDir, sourceFileCount: count3 });
1471
+ const count4 = await countSourceFiles(topDir);
1472
+ if (count4 >= 1) candidates.push({ dir: topDir, sourceFileCount: count4 });
1280
1473
  continue;
1281
1474
  }
1282
1475
  const subdirs = await listSubdirs(topDir);
@@ -1284,9 +1477,9 @@ async function moduleCandidates(root, combinedDocs) {
1284
1477
  if (mentioned.length < ENUMERATION_THRESHOLD) continue;
1285
1478
  for (const sub of subdirs) {
1286
1479
  if (isMentioned(combinedDocs, sub)) continue;
1287
- const count3 = await countSourceFiles(`${topDir}/${sub}`);
1288
- if (count3 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
1289
- candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count3 });
1480
+ const count4 = await countSourceFiles(`${topDir}/${sub}`);
1481
+ if (count4 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
1482
+ candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count4 });
1290
1483
  }
1291
1484
  }
1292
1485
  }
@@ -1325,7 +1518,7 @@ var init_new_module = __esm({
1325
1518
  });
1326
1519
 
1327
1520
  // src/audit/checks/stale-count.ts
1328
- import path11 from "path";
1521
+ import path13 from "path";
1329
1522
  async function countGradleModules(root) {
1330
1523
  for (const name of ["settings.gradle.kts", "settings.gradle"]) {
1331
1524
  const content2 = await readAuditInput(root, name);
@@ -1359,7 +1552,7 @@ async function countNpmWorkspaces(root) {
1359
1552
  return {
1360
1553
  actual: matched.length,
1361
1554
  countedFrom: "package.json workspaces",
1362
- members: matched.map((m) => path11.dirname(m)).sort()
1555
+ members: matched.map((m) => path13.dirname(m)).sort()
1363
1556
  };
1364
1557
  }
1365
1558
  }
@@ -1390,7 +1583,7 @@ async function countNpmWorkspaces(root) {
1390
1583
  return {
1391
1584
  actual: matched.length,
1392
1585
  countedFrom: "pnpm-workspace.yaml",
1393
- members: matched.map((m) => path11.dirname(m)).sort()
1586
+ members: matched.map((m) => path13.dirname(m)).sort()
1394
1587
  };
1395
1588
  }
1396
1589
  }
@@ -1412,8 +1605,8 @@ async function countCargoCrates(root) {
1412
1605
  ignore: ["**/target/**"],
1413
1606
  label: "Cargo workspace discovery"
1414
1607
  });
1415
- for (const m of matched) members.add(path11.dirname(m));
1416
- } else if (await readAuditInput(root, path11.posix.join(entry, "Cargo.toml")) !== null) {
1608
+ for (const m of matched) members.add(path13.dirname(m));
1609
+ } else if (await readAuditInput(root, path13.posix.join(entry, "Cargo.toml")) !== null) {
1417
1610
  members.add(entry);
1418
1611
  }
1419
1612
  }
@@ -1431,11 +1624,21 @@ async function resolveCountSource(root, claim) {
1431
1624
  if (unit === "crate") return countCargoCrates(root);
1432
1625
  return await countNpmWorkspaces(root) ?? await countCargoCrates(root) ?? await countGradleModules(root);
1433
1626
  }
1627
+ async function resolveDocCountSource(root, doc, claim) {
1628
+ const scope = documentScope(doc);
1629
+ const source = await resolveCountSource(path13.join(root, scope), claim);
1630
+ if (!source || scope === ".") return source;
1631
+ return {
1632
+ ...source,
1633
+ countedFrom: scope + "/" + source.countedFrom,
1634
+ members: source.members.map((member) => scope + "/" + member)
1635
+ };
1636
+ }
1434
1637
  async function checkStaleCounts(ctx) {
1435
1638
  const result = emptyResult();
1436
1639
  for (const doc of ctx.docs) {
1437
1640
  for (const claim of doc.claims.counts) {
1438
- const source = await resolveCountSource(ctx.root, claim);
1641
+ const source = await resolveDocCountSource(ctx.root, doc.path, claim);
1439
1642
  if (source === null) {
1440
1643
  result.skipped.push({
1441
1644
  check: "stale-count",
@@ -1467,6 +1670,7 @@ var MEMBERS_CAP;
1467
1670
  var init_stale_count = __esm({
1468
1671
  "src/audit/checks/stale-count.ts"() {
1469
1672
  "use strict";
1673
+ init_docs();
1470
1674
  init_inputs();
1471
1675
  init_checks();
1472
1676
  MEMBERS_CAP = 50;
@@ -1474,61 +1678,94 @@ var init_stale_count = __esm({
1474
1678
  });
1475
1679
 
1476
1680
  // src/audit/checks/dead-command.ts
1477
- async function scriptsOf(root, manifest) {
1478
- const raw = await readAuditInput(root, manifest);
1479
- if (raw === null) return null;
1681
+ import path14 from "path";
1682
+ function scriptsOf(raw) {
1480
1683
  try {
1481
1684
  const pkg = JSON.parse(raw);
1482
- return pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? Object.keys(pkg.scripts) : [];
1685
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return null;
1686
+ if (pkg.scripts === void 0) return [];
1687
+ return pkg.scripts && typeof pkg.scripts === "object" && !Array.isArray(pkg.scripts) && Object.values(pkg.scripts).every((value) => typeof value === "string") ? Object.keys(pkg.scripts) : null;
1483
1688
  } catch {
1484
1689
  return null;
1485
1690
  }
1486
1691
  }
1692
+ async function commandInputs(root, docs) {
1693
+ const contents = /* @__PURE__ */ new Map();
1694
+ const read = async (file) => {
1695
+ if (!contents.has(file)) contents.set(file, await readAuditInput(root, file));
1696
+ return contents.get(file);
1697
+ };
1698
+ await read("package.json");
1699
+ let inventory;
1700
+ const resolutions = [];
1701
+ for (const doc of docs) for (const claim of doc.claims.commands) {
1702
+ const base = documentScope(doc.path);
1703
+ const directory = scopedPath(base, claim.directory ?? ".");
1704
+ const scope = {
1705
+ basis: claim.directory === void 0 ? "document" : "explicit",
1706
+ directory: directory ?? base,
1707
+ candidates: []
1708
+ };
1709
+ const resolution = { doc: doc.path, claim, scope, manifest: null, scripts: [], elsewhere: [] };
1710
+ resolutions.push(resolution);
1711
+ if (claim.scopeUnknown || directory === null) {
1712
+ resolution.problem = claim.scopeUnknown ?? "The command's directory leaves the repository.";
1713
+ continue;
1714
+ }
1715
+ let cursor = directory;
1716
+ while (true) {
1717
+ const manifest = path14.posix.join(cursor, "package.json");
1718
+ scope.candidates.push(manifest);
1719
+ const raw = await read(manifest);
1720
+ if (raw !== null) {
1721
+ resolution.manifest = manifest;
1722
+ const scripts = scriptsOf(raw);
1723
+ if (scripts === null) resolution.problem = "Invalid package manifest: " + manifest;
1724
+ else resolution.scripts = scripts;
1725
+ break;
1726
+ }
1727
+ if (claim.directory !== void 0 || cursor === ".") break;
1728
+ cursor = path14.posix.dirname(cursor);
1729
+ }
1730
+ if (!resolution.manifest && !resolution.problem) {
1731
+ resolution.problem = `No package.json for the command's scope (${directory}).`;
1732
+ }
1733
+ if (resolution.problem || resolution.scripts.includes(claim.scriptName) || claim.directory !== void 0) continue;
1734
+ inventory ??= await commandManifests(root);
1735
+ for (const manifest of [.../* @__PURE__ */ new Set(["package.json", ...inventory])].sort()) {
1736
+ if (scope.candidates.includes(manifest)) continue;
1737
+ scope.candidates.push(manifest);
1738
+ const raw = await read(manifest);
1739
+ if (raw === null) continue;
1740
+ const scripts = scriptsOf(raw);
1741
+ if (scripts === null) resolution.problem = "Cannot establish script availability: invalid manifest " + manifest;
1742
+ else if (scripts.includes(claim.scriptName)) resolution.elsewhere.push(manifest);
1743
+ }
1744
+ }
1745
+ return { manifests: [...contents].sort(([a], [b]) => a.localeCompare(b)), resolutions };
1746
+ }
1487
1747
  async function checkDeadCommands(ctx) {
1488
1748
  const result = emptyResult();
1489
- const commandClaims = ctx.docs.flatMap(
1490
- (doc) => doc.claims.commands.map((claim) => ({ doc, claim }))
1491
- );
1492
- if (commandClaims.length === 0) return result;
1493
- const rootScripts = await scriptsOf(ctx.root, "package.json");
1494
- if (rootScripts === null) {
1495
- result.skipped.push({
1496
- check: "dead-command",
1497
- reason: "no package.json at the repo root"
1498
- });
1499
- return result;
1500
- }
1501
- const rootSet = new Set(rootScripts);
1502
- let workspaceScripts = null;
1503
- let manifestsChecked = ["package.json"];
1504
- const loadWorkspaceScripts = async () => {
1505
- if (workspaceScripts !== null) return workspaceScripts;
1506
- workspaceScripts = /* @__PURE__ */ new Set();
1507
- const manifests = await commandManifests(ctx.root);
1508
- manifestsChecked = ["package.json", ...manifests.sort()];
1509
- for (const manifest of manifests) {
1510
- const scripts = await scriptsOf(ctx.root, manifest);
1511
- for (const name of scripts ?? []) workspaceScripts.add(name);
1749
+ const { resolutions } = await commandInputs(ctx.root, ctx.docs);
1750
+ for (const { doc, claim, scope, scripts, elsewhere, problem, manifest } of resolutions) {
1751
+ if (problem) {
1752
+ result.skipped.push({ check: "dead-command", doc, reason: problem });
1753
+ continue;
1512
1754
  }
1513
- return workspaceScripts;
1514
- };
1515
- for (const { doc, claim } of commandClaims) {
1516
- if (rootSet.has(claim.scriptName)) continue;
1517
- const elsewhere = await loadWorkspaceScripts();
1518
- if (elsewhere.has(claim.scriptName)) continue;
1519
- result.issues.push({
1520
- type: "dead-command",
1521
- message: `\`${claim.invocation}\` refers to script "${claim.scriptName}", which exists in no package.json`,
1522
- anchor: { doc: doc.path, line: claim.line, excerpt: claim.excerpt },
1523
- confidence: "certain",
1524
- evidence: {
1525
- kind: "missing-script",
1526
- scriptName: claim.scriptName,
1527
- invocation: claim.invocation,
1528
- manifestsChecked,
1529
- availableScripts: rootScripts.slice(0, AVAILABLE_SCRIPTS_CAP)
1530
- }
1531
- });
1755
+ if (scripts.includes(claim.scriptName)) continue;
1756
+ const evidence = {
1757
+ kind: "missing-script",
1758
+ scriptName: claim.scriptName,
1759
+ invocation: claim.invocation,
1760
+ manifestsChecked: scope.candidates,
1761
+ availableScripts: scripts.slice(0, AVAILABLE_SCRIPTS_CAP),
1762
+ scope
1763
+ };
1764
+ const anchor = { doc, line: claim.line, excerpt: claim.excerpt };
1765
+ const message2 = `\`${claim.invocation}\` names script "${claim.scriptName}", which is absent from ${manifest}`;
1766
+ const certain = documentScope(doc) === "." && (scope.basis === "explicit" || !elsewhere.length);
1767
+ if (certain) result.issues.push({ type: "dead-command", message: message2, anchor, confidence: "certain", evidence });
1768
+ else result.advisories.push({ resolution: "recheck", type: "dead-command", message: message2 + (elsewhere.length ? `; it exists in ${elsewhere.join(", ")}. Clarify the intended working directory.` : "; the intended working directory needs review."), anchor, evidence });
1532
1769
  }
1533
1770
  return result;
1534
1771
  }
@@ -1543,6 +1780,8 @@ var init_dead_command = __esm({
1543
1780
  "src/audit/checks/dead-command.ts"() {
1544
1781
  "use strict";
1545
1782
  init_inputs();
1783
+ init_docs();
1784
+ init_scope();
1546
1785
  init_checks();
1547
1786
  AVAILABLE_SCRIPTS_CAP = 30;
1548
1787
  }
@@ -1556,13 +1795,13 @@ async function git(root, args) {
1556
1795
  }
1557
1796
  function withoutAndroidReleaseValues(text2) {
1558
1797
  if (/\/\*|"""|'''/.test(text2) || !/id\s*\(?\s*["']com\.android\.(application|library)["']/.test(text2)) return null;
1559
- const scopes = [];
1798
+ const scopes2 = [];
1560
1799
  const normalized = [];
1561
1800
  let assignments = 0;
1562
1801
  for (const line of text2.split("\n")) {
1563
1802
  const code = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/.*$/g, (token) => token.startsWith("//") ? "" : '""');
1564
1803
  const assignment = line.match(/^(\s*)(versionName|versionCode)(\s*(?:=\s*|\s+))("[A-Za-z0-9._+-]+"|'[A-Za-z0-9._+-]+'|\d+)(\s*)$/);
1565
- if (assignment && scopes.join("/") === "android/defaultConfig" && (assignment[2] === "versionCode" ? /^\d+$/.test(assignment[4]) : /^["']/.test(assignment[4]))) {
1804
+ if (assignment && scopes2.join("/") === "android/defaultConfig" && (assignment[2] === "versionCode" ? /^\d+$/.test(assignment[4]) : /^["']/.test(assignment[4]))) {
1566
1805
  normalized.push(assignment[1] + assignment[2] + assignment[3] + "<release-value>" + assignment[5]);
1567
1806
  assignments++;
1568
1807
  } else {
@@ -1571,12 +1810,12 @@ function withoutAndroidReleaseValues(text2) {
1571
1810
  }
1572
1811
  const named = code.match(/^\s*(android|defaultConfig)\s*\{\s*$/)?.[1];
1573
1812
  for (const brace of code.matchAll(/[{}]/g)) {
1574
- if (brace[0] === "{") scopes.push(named ?? "unknown");
1575
- else if (!scopes.length) return null;
1576
- else scopes.pop();
1813
+ if (brace[0] === "{") scopes2.push(named ?? "unknown");
1814
+ else if (!scopes2.length) return null;
1815
+ else scopes2.pop();
1577
1816
  }
1578
1817
  }
1579
- return assignments && !scopes.length ? normalized.join("\n") : null;
1818
+ return assignments && !scopes2.length ? normalized.join("\n") : null;
1580
1819
  }
1581
1820
  async function releaseMetadataOnly(root, commit) {
1582
1821
  if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file))) return false;
@@ -1607,6 +1846,12 @@ var init_release_metadata = __esm({
1607
1846
  });
1608
1847
 
1609
1848
  // src/audit/checks/deps-changed.ts
1849
+ function manifestPathspecs(doc) {
1850
+ const scope = documentScope(doc);
1851
+ if (scope === ".") return MANIFEST_PATHSPECS;
1852
+ const escaped = scope.replace(/[\\*?\[\]]/g, "\\$&");
1853
+ return MANIFEST_PATHSPECS.map((spec) => spec.startsWith(":(glob)") ? ":(glob)" + escaped + "/" + spec.slice(":(glob)".length) : ":(literal)" + scope + "/" + spec);
1854
+ }
1610
1855
  async function checkDepsChanged(ctx) {
1611
1856
  const result = emptyResult();
1612
1857
  result.suppressedAdvisories = [];
@@ -1630,7 +1875,7 @@ async function checkDepsChanged(ctx) {
1630
1875
  const range = await commitsTouchingSince(
1631
1876
  ctx.root,
1632
1877
  doc.lastCommit.hash,
1633
- MANIFEST_PATHSPECS
1878
+ manifestPathspecs(doc.path)
1634
1879
  );
1635
1880
  if (range === null) {
1636
1881
  result.skipped.push({
@@ -1669,6 +1914,7 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
1669
1914
  var init_deps_changed = __esm({
1670
1915
  "src/audit/checks/deps-changed.ts"() {
1671
1916
  "use strict";
1917
+ init_docs();
1672
1918
  init_release_metadata();
1673
1919
  init_git();
1674
1920
  init_checks();
@@ -1691,7 +1937,7 @@ var init_deps_changed = __esm({
1691
1937
  });
1692
1938
 
1693
1939
  // src/context/lexical.ts
1694
- import path12 from "path";
1940
+ import path15 from "path";
1695
1941
  var init_lexical = __esm({
1696
1942
  "src/context/lexical.ts"() {
1697
1943
  "use strict";
@@ -1875,7 +2121,7 @@ var init_provenance = __esm({
1875
2121
 
1876
2122
  // src/decisions/decisions.ts
1877
2123
  import fs9 from "fs/promises";
1878
- import path13 from "path";
2124
+ import path16 from "path";
1879
2125
  import { createHash } from "crypto";
1880
2126
  import { z as z4 } from "zod";
1881
2127
  async function loadDecisionStore(rootDir) {
@@ -1926,9 +2172,9 @@ var init_decisions = __esm({
1926
2172
  });
1927
2173
 
1928
2174
  // src/decisions/drift.ts
1929
- import path14 from "path";
2175
+ import path17 from "path";
1930
2176
  async function computeDecisionDrift(rootDir, decisions) {
1931
- const resolvedRoot = path14.resolve(rootDir);
2177
+ const resolvedRoot = path17.resolve(rootDir);
1932
2178
  const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
1933
2179
  const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
1934
2180
  const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
@@ -2017,38 +2263,330 @@ var init_decision_anchor = __esm({
2017
2263
  init_provenance();
2018
2264
  init_checks();
2019
2265
  }
2020
- });
2021
-
2022
- // src/audit/checks/index.ts
2023
- function emptyResult() {
2024
- return { issues: [], advisories: [], skipped: [] };
2266
+ });
2267
+
2268
+ // src/audit/checks/index.ts
2269
+ function emptyResult() {
2270
+ return { issues: [], advisories: [], skipped: [] };
2271
+ }
2272
+ var CHECKS;
2273
+ var init_checks = __esm({
2274
+ "src/audit/checks/index.ts"() {
2275
+ "use strict";
2276
+ init_deleted_reference();
2277
+ init_new_module();
2278
+ init_stale_count();
2279
+ init_dead_command();
2280
+ init_deps_changed();
2281
+ init_decision_anchor();
2282
+ CHECKS = {
2283
+ "deleted-reference": checkDeletedReferences,
2284
+ "new-module": checkNewModules,
2285
+ "stale-count": checkStaleCounts,
2286
+ "dead-command": checkDeadCommands,
2287
+ "deps-changed": checkDepsChanged,
2288
+ "decision-anchor-drift": checkDecisionAnchors
2289
+ };
2290
+ }
2291
+ });
2292
+
2293
+ // src/audit/findings.ts
2294
+ import { createHash as createHash2 } from "crypto";
2295
+ import { z as z5 } from "zod";
2296
+ function findingId(finding) {
2297
+ const e = finding.evidence;
2298
+ let key;
2299
+ switch (e.kind) {
2300
+ case "missing-path":
2301
+ key = e.resolvedPath && e.resolvedPath !== e.claimed ? [e.claimed, e.resolvedPath] : e.claimed;
2302
+ break;
2303
+ case "unmentioned-dir":
2304
+ key = e.dir;
2305
+ break;
2306
+ case "count-mismatch":
2307
+ key = [e.unit.replace(/s$/, ""), e.countedFrom];
2308
+ break;
2309
+ case "missing-script":
2310
+ key = e.scope && e.scope.directory !== "." ? [e.scriptName, e.scope.directory] : e.scriptName;
2311
+ break;
2312
+ case "doc-behind-manifests":
2313
+ key = null;
2314
+ break;
2315
+ case "decision-anchor":
2316
+ key = [e.decisionId, e.provenance?.revision, e.provenance?.approval];
2317
+ break;
2318
+ }
2319
+ return digest([finding.type, finding.anchor.doc, key]);
2320
+ }
2321
+ var digest, checkSchema, commitSchema, anchorSchema, count, scopeSchema, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema;
2322
+ var init_findings = __esm({
2323
+ "src/audit/findings.ts"() {
2324
+ "use strict";
2325
+ digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
2326
+ checkSchema = z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
2327
+ commitSchema = z5.object({ hash: z5.string().regex(/^[a-f0-9]{40,64}$/), date: z5.string(), subject: z5.string() });
2328
+ anchorSchema = z5.object({ doc: z5.string(), line: z5.number().int().positive().nullable(), excerpt: z5.string().nullable() });
2329
+ count = z5.number().int().nonnegative();
2330
+ scopeSchema = z5.object({
2331
+ basis: z5.enum(["repository", "document", "document-link", "explicit"]),
2332
+ directory: z5.string(),
2333
+ candidates: z5.array(z5.string())
2334
+ });
2335
+ evidenceSchema = z5.discriminatedUnion("kind", [
2336
+ z5.object({
2337
+ kind: z5.literal("missing-path"),
2338
+ claimed: z5.string(),
2339
+ renamedTo: z5.string().nullable(),
2340
+ deletedInCommit: commitSchema.nullable(),
2341
+ everTracked: z5.boolean(),
2342
+ parentDirExists: z5.boolean(),
2343
+ resolvedPath: z5.string().optional(),
2344
+ scope: scopeSchema.optional()
2345
+ }),
2346
+ z5.object({
2347
+ kind: z5.literal("unmentioned-dir"),
2348
+ dir: z5.string(),
2349
+ sourceFileCount: count,
2350
+ firstCommit: commitSchema.nullable(),
2351
+ checkedDocs: z5.array(z5.string())
2352
+ }),
2353
+ z5.object({
2354
+ kind: z5.literal("count-mismatch"),
2355
+ claimed: count,
2356
+ actual: count,
2357
+ unit: z5.string(),
2358
+ countedFrom: z5.string(),
2359
+ members: z5.array(z5.string())
2360
+ }),
2361
+ z5.object({
2362
+ kind: z5.literal("missing-script"),
2363
+ scriptName: z5.string(),
2364
+ invocation: z5.string(),
2365
+ manifestsChecked: z5.array(z5.string()),
2366
+ availableScripts: z5.array(z5.string()),
2367
+ scope: scopeSchema.optional()
2368
+ }),
2369
+ z5.object({
2370
+ kind: z5.literal("doc-behind-manifests"),
2371
+ docLastCommit: commitSchema,
2372
+ manifestCommits: z5.array(commitSchema.extend({ files: z5.array(z5.string()) })),
2373
+ totalCommits: count
2374
+ }),
2375
+ z5.object({
2376
+ kind: z5.literal("decision-anchor"),
2377
+ decisionId: z5.string(),
2378
+ title: z5.string(),
2379
+ changedFiles: z5.array(z5.string()),
2380
+ refreshedHash: z5.string(),
2381
+ provenance: z5.object({}).passthrough().optional()
2382
+ })
2383
+ ]);
2384
+ findingSchema = z5.object({ message: z5.string(), anchor: anchorSchema, evidence: evidenceSchema });
2385
+ issueSchema = findingSchema.extend({
2386
+ type: z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
2387
+ confidence: z5.enum(["certain", "likely"])
2388
+ });
2389
+ advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z5.literal("recheck").optional() });
2390
+ checkResultSchema = z5.object({
2391
+ issues: z5.array(issueSchema),
2392
+ advisories: z5.array(advisorySchema),
2393
+ suppressedAdvisories: z5.array(advisorySchema).optional(),
2394
+ skipped: z5.array(z5.object({ check: z5.string(), reason: z5.string(), doc: z5.string().optional() }))
2395
+ });
2396
+ }
2397
+ });
2398
+
2399
+ // src/audit/advisory-review.ts
2400
+ import fs10 from "fs/promises";
2401
+ import { execFile as execFile8 } from "child_process";
2402
+ import { promisify as promisify8 } from "util";
2403
+ import { z as z6 } from "zod";
2404
+ async function readRecord(root, id) {
2405
+ const raw = await readStoreJson(root, recordPath(id));
2406
+ if (raw === null) return null;
2407
+ const record = recordSchema.parse(raw);
2408
+ const { digest: stored, ...payload } = record;
2409
+ 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]))) {
2410
+ throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
2411
+ }
2412
+ return record;
2413
+ }
2414
+ function scopes(finding) {
2415
+ const files = [finding.anchor.doc], e = finding.evidence;
2416
+ if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
2417
+ if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
2418
+ if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
2419
+ else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
2420
+ else if (e.kind === "unmentioned-dir") files.push(e.dir, ...e.checkedDocs);
2421
+ else throw new Error("This advisory has no supported review scope.");
2422
+ return [...new Set(files)].map((file) => {
2423
+ if (normalizeRepoPath(file) !== file) throw new Error("Advisory contains an invalid scoped path: " + file);
2424
+ return `:(literal)${file}`;
2425
+ }).sort();
2426
+ }
2427
+ async function reviewEvidence(root, finding) {
2428
+ if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
2429
+ const scope = scopes(finding);
2430
+ const head = (await git2(root, "rev-parse", "HEAD")).trim();
2431
+ const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
2432
+ if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
2433
+ const [tree, status, lastScopeCommit, document] = await Promise.all([
2434
+ tracked.length ? git2(root, "ls-tree", "-r", "-z", "HEAD", "--", ...tracked.map((file) => `:(literal)${file}`)) : "",
2435
+ git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...scope),
2436
+ git2(root, "log", "-1", "--format=%H", "HEAD", "--", ...scope).then((value) => value.trim()),
2437
+ readAuditInput(root, finding.anchor.doc)
2438
+ ]);
2439
+ if (!document?.trim()) throw new Error("Original advisory document is missing, empty or unreadable.");
2440
+ if (tree.length > 2 * 1024 * 1024 || tree.split("\0").length > 2e4) throw new Error("Advisory scope is too large for a complete review; narrow the documentation scope.");
2441
+ for (const entry of tree.split("\0").filter(Boolean)) {
2442
+ const tab = entry.indexOf(" "), file = entry.slice(tab + 1);
2443
+ if (entry.startsWith("120000 ") || entry.startsWith("160000 ")) throw new Error("Symlink or submodule in advisory review scope: " + file);
2444
+ await storePath(root, file);
2445
+ }
2446
+ const paths = finding.evidence.kind === "missing-path" ? await Promise.all(
2447
+ (finding.evidence.scope?.candidates ?? [finding.evidence.resolvedPath ?? finding.evidence.claimed]).map(async (file) => ({ path: file, exists: await pathExists(root, file) }))
2448
+ ) : [];
2449
+ let dirty = status.length > 0;
2450
+ if (finding.evidence.kind === "missing-script") {
2451
+ for (const file of [...finding.evidence.manifestsChecked, ...finding.evidence.scope?.candidates ?? []]) {
2452
+ if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
2453
+ }
2454
+ }
2455
+ const fingerprint = digest([scope, tree, lastScopeCommit, paths]);
2456
+ if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
2457
+ return {
2458
+ ...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
2459
+ dirty,
2460
+ documentDigest: digest(document)
2461
+ };
2462
+ }
2463
+ async function decisionAssessment(root, finding, baselineHead) {
2464
+ const evidence = finding.evidence;
2465
+ if (evidence.kind !== "decision-anchor") return null;
2466
+ const id = findingId(finding);
2467
+ const store = await loadDecisionStore(root);
2468
+ if (store.diagnostics.length) throw new Error("Decision records have unavailable or malformed evidence; review cannot be confirmed.");
2469
+ const record = store.records.find((record2) => record2.id === evidence.decisionId);
2470
+ if (!record || record.version !== 2) return null;
2471
+ const events2 = record.history.filter((event2) => ["accepted", "reaffirmed", "retired"].includes(event2.kind));
2472
+ const event = events2.at(-1);
2473
+ if (!event?.evidence || !event.actor || !event.note || !event.evidence.historyAvailable || event.evidence.localChanges.length) return null;
2474
+ try {
2475
+ await git2(root, "merge-base", "--is-ancestor", baselineHead, event.evidence.headHash);
2476
+ } catch {
2477
+ return null;
2478
+ }
2479
+ const drift = await computeDecisionDrift(root, [record]);
2480
+ if (!drift.historyAvailable) throw new Error("Decision review history is unavailable.");
2481
+ const retired = record.status === "retired" && event.kind === "retired";
2482
+ const current = retired || record.status === "active" && record.approval === "accepted" && drift.freshness?.[record.id] === "current" && !drift.pendingProposals?.[record.id];
2483
+ return {
2484
+ id,
2485
+ status: current ? "current" : "reopened",
2486
+ outcome: event.kind,
2487
+ reviewer: event.actor,
2488
+ note: event.note,
2489
+ reviewedHead: event.evidence.headHash,
2490
+ recordPath: `.mason/decisions/${record.id}.json`,
2491
+ reason: current ? "The existing decision review records acceptance, reaffirmation or retirement covering the original finding." : "Decision content or anchor evidence changed after its review; review the decision again."
2492
+ };
2493
+ }
2494
+ async function assessAdvisory(root, finding, baselineHead) {
2495
+ if ("confidence" in finding) return null;
2496
+ const id = findingId(finding);
2497
+ try {
2498
+ if (finding.evidence.kind === "decision-anchor") return await decisionAssessment(root, finding, baselineHead);
2499
+ const record = await readRecord(root, id);
2500
+ if (!record) return null;
2501
+ const event = record.events.at(-1);
2502
+ const current = await reviewEvidence(root, event.finding);
2503
+ await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
2504
+ const base = {
2505
+ id,
2506
+ recordPath: recordPath(id),
2507
+ reviewer: event.reviewer,
2508
+ note: event.note,
2509
+ outcome: event.outcome,
2510
+ reviewedHead: event.evidence.head
2511
+ };
2512
+ 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." };
2513
+ 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." };
2514
+ } catch (error) {
2515
+ return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
2516
+ }
2025
2517
  }
2026
- var CHECKS;
2027
- var init_checks = __esm({
2028
- "src/audit/checks/index.ts"() {
2518
+ async function advisoryReviewInventory(root) {
2519
+ let names;
2520
+ try {
2521
+ names = await fs10.readdir(await storePath(root, ".mason/reviews/advisories"));
2522
+ } catch (error) {
2523
+ if (error.code === "ENOENT") return [];
2524
+ throw error;
2525
+ }
2526
+ if (names.length > 5e3) throw new Error("Advisory review inventory exceeds 5,000 records.");
2527
+ return Promise.all(names.filter((name) => name.endsWith(".json")).sort().map(async (name) => [name, await readAuditInput(root, ".mason/reviews/advisories/" + name)]));
2528
+ }
2529
+ var execute2, git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
2530
+ var init_advisory_review = __esm({
2531
+ "src/audit/advisory-review.ts"() {
2029
2532
  "use strict";
2030
- init_deleted_reference();
2031
- init_new_module();
2032
- init_stale_count();
2033
- init_dead_command();
2533
+ init_findings();
2034
2534
  init_deps_changed();
2035
- init_decision_anchor();
2036
- CHECKS = {
2037
- "deleted-reference": checkDeletedReferences,
2038
- "new-module": checkNewModules,
2039
- "stale-count": checkStaleCounts,
2040
- "dead-command": checkDeadCommands,
2041
- "deps-changed": checkDepsChanged,
2042
- "decision-anchor-drift": checkDecisionAnchors
2043
- };
2535
+ init_inputs();
2536
+ init_scope();
2537
+ init_storage();
2538
+ init_paths();
2539
+ init_store();
2540
+ init_decisions();
2541
+ init_drift2();
2542
+ execute2 = promisify8(execFile8);
2543
+ git2 = async (root, ...args) => (await execute2("git", args, { cwd: root, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 })).stdout;
2544
+ hashSchema = z6.string().regex(/^[a-f0-9]{64}$/);
2545
+ commitSchema2 = z6.string().regex(/^[a-f0-9]{40,64}$/);
2546
+ evidenceSchema2 = z6.object({
2547
+ head: commitSchema2,
2548
+ fingerprint: hashSchema,
2549
+ scope: z6.array(z6.string()).min(1).max(1e3),
2550
+ tree: z6.string().max(2 * 1024 * 1024),
2551
+ lastScopeCommit: commitSchema2,
2552
+ paths: z6.array(z6.object({ path: z6.string(), exists: z6.boolean() })).max(1e3)
2553
+ });
2554
+ eventSchema2 = z6.object({
2555
+ at: z6.string().datetime(),
2556
+ reviewer: z6.string().trim().min(1).max(200),
2557
+ note: z6.string().trim().min(1).max(2500),
2558
+ outcome: z6.enum(["addressed", "inapplicable", "deferred"]),
2559
+ finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
2560
+ evidence: evidenceSchema2
2561
+ });
2562
+ recordSchema = z6.object({ version: z6.literal(1), id: hashSchema, events: z6.array(eventSchema2).min(1).max(200), digest: hashSchema });
2563
+ reviewSummarySchema = z6.object({
2564
+ id: hashSchema,
2565
+ status: z6.enum(["current", "reopened", "deferred", "unverified"]),
2566
+ reason: z6.string(),
2567
+ recordPath: z6.string().optional(),
2568
+ reviewer: z6.string().optional(),
2569
+ note: z6.string().optional(),
2570
+ outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
2571
+ reviewedHead: z6.string().optional()
2572
+ });
2573
+ advisoryReviewRequest = z6.object({
2574
+ baselinePath: z6.string().min(1),
2575
+ findingId: hashSchema,
2576
+ action: z6.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
2577
+ reviewer: eventSchema2.shape.reviewer.optional(),
2578
+ note: eventSchema2.shape.note.optional(),
2579
+ reviewToken: hashSchema.optional()
2580
+ });
2581
+ recordPath = (id) => `.mason/reviews/advisories/${hashSchema.parse(id)}.json`;
2044
2582
  }
2045
2583
  });
2046
2584
 
2047
2585
  // src/audit/audit.ts
2048
- import fs10 from "fs/promises";
2049
- import path15 from "path";
2586
+ import fs11 from "fs/promises";
2587
+ import path18 from "path";
2050
2588
  async function computeAudit(rootDir, options = {}) {
2051
- const resolvedRoot = path15.resolve(rootDir);
2589
+ const resolvedRoot = path18.resolve(rootDir);
2052
2590
  const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
2053
2591
  if (docs.length === 0) return null;
2054
2592
  const report = {
@@ -2059,6 +2597,8 @@ async function computeAudit(rootDir, options = {}) {
2059
2597
  checksRun: [],
2060
2598
  docs: docs.map((d) => ({
2061
2599
  path: d.path,
2600
+ scope: d.scope,
2601
+ kind: d.kind,
2062
2602
  lastCommit: d.lastCommit,
2063
2603
  dirty: d.dirty,
2064
2604
  lineCount: d.lineCount
@@ -2080,7 +2620,7 @@ async function computeAudit(rootDir, options = {}) {
2080
2620
  }
2081
2621
  let decisionsPresent = false;
2082
2622
  try {
2083
- await fs10.access(path15.join(resolvedRoot, ".mason", "decisions"));
2623
+ await fs11.access(path18.join(resolvedRoot, ".mason", "decisions"));
2084
2624
  decisionsPresent = true;
2085
2625
  } catch {
2086
2626
  }
@@ -2103,6 +2643,8 @@ async function computeAudit(rootDir, options = {}) {
2103
2643
  report.skippedChecks.push(...skipped);
2104
2644
  }
2105
2645
  report.clean = report.issues.length === 0;
2646
+ const reviews = (await Promise.all([...report.advisories, ...report.suppressedAdvisories].map((finding) => assessAdvisory(resolvedRoot, finding, headHash)))).filter((review) => review !== null);
2647
+ if (reviews.length) report.advisoryReviews = reviews;
2106
2648
  return report;
2107
2649
  }
2108
2650
  var init_audit = __esm({
@@ -2113,45 +2655,24 @@ var init_audit = __esm({
2113
2655
  init_docs();
2114
2656
  init_types();
2115
2657
  init_checks();
2658
+ init_advisory_review();
2116
2659
  }
2117
2660
  });
2118
2661
 
2119
2662
  // src/audit/repair.ts
2120
- import fs11 from "fs/promises";
2121
- import path16 from "path";
2122
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
2123
- import { z as z5 } from "zod";
2124
- function findingId(finding) {
2125
- const e = finding.evidence;
2126
- let key;
2127
- switch (e.kind) {
2128
- case "missing-path":
2129
- key = e.claimed;
2130
- break;
2131
- case "unmentioned-dir":
2132
- key = e.dir;
2133
- break;
2134
- case "count-mismatch":
2135
- key = [e.unit.replace(/s$/, ""), e.countedFrom];
2136
- break;
2137
- case "missing-script":
2138
- key = e.scriptName;
2139
- break;
2140
- case "doc-behind-manifests":
2141
- key = null;
2142
- break;
2143
- case "decision-anchor":
2144
- key = [e.decisionId, e.provenance?.revision, e.provenance?.approval];
2145
- break;
2146
- }
2147
- return digest([finding.type, finding.anchor.doc, key]);
2663
+ import fs12 from "fs/promises";
2664
+ import path19 from "path";
2665
+ import { randomUUID as randomUUID2 } from "crypto";
2666
+ import { z as z7 } from "zod";
2667
+ function recheckable(finding) {
2668
+ return !("confidence" in finding) && finding.resolution === "recheck" && ["deleted-reference", "dead-command", "new-module"].includes(finding.type) && ["missing-path", "missing-script", "unmentioned-dir"].includes(finding.evidence.kind);
2148
2669
  }
2149
2670
  function allFindings(report) {
2150
2671
  return [...report.issues, ...report.advisories, ...report.suppressedAdvisories ?? []];
2151
2672
  }
2152
2673
  async function docState(root) {
2153
2674
  const docs = [];
2154
- for (const doc of DOC_CANDIDATES) {
2675
+ for (const doc of await discoverDocPaths(root)) {
2155
2676
  try {
2156
2677
  const content2 = await readBoundedFile(await storePath(root, doc), 10 * 1024 * 1024);
2157
2678
  if (content2 === null) throw new Error("Context file is not regular or exceeds 10 MiB: " + doc);
@@ -2173,8 +2694,8 @@ async function stableAudit(root, checks, options = {}) {
2173
2694
  return report;
2174
2695
  }
2175
2696
  async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
2176
- const root = await fs11.realpath(rootDir);
2177
- const selected = z5.array(checkSchema).nonempty().parse(checks);
2697
+ const root = await fs12.realpath(rootDir);
2698
+ const selected = z7.array(checkSchema).nonempty().parse(checks);
2178
2699
  const report = await stableAudit(root, selected, options);
2179
2700
  if (!report) throw new Error("No context files found to prepare a repair.");
2180
2701
  if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
@@ -2189,23 +2710,29 @@ async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
2189
2710
  await writeStoreJson(root, baselinePath, { ...payload, digest: digest(payload) });
2190
2711
  return { version: 1, action: "prepare", baselinePath, report };
2191
2712
  }
2192
- async function verifyRepair(rootDir, baselinePath, options = {}) {
2193
- const root = await fs11.realpath(rootDir);
2194
- const declaredRoot = path16.resolve(rootDir);
2195
- const relative = path16.isAbsolute(baselinePath) ? path16.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
2713
+ async function loadRepairBaseline(rootDir, baselinePath) {
2714
+ const root = await fs12.realpath(rootDir);
2715
+ const declaredRoot = path19.resolve(rootDir);
2716
+ const relative = path19.isAbsolute(baselinePath) ? path19.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
2196
2717
  const stored = baselineSchema.parse(await readStoreJson(root, relative));
2197
2718
  const { digest: savedDigest, ...payload } = stored;
2198
2719
  if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
2199
2720
  if (stored.report.root !== root) throw new Error("Repair baseline belongs to a different repository.");
2200
2721
  const original = stored.report;
2722
+ return { root, relative, original };
2723
+ }
2724
+ async function verifyRepair(rootDir, baselinePath, options = {}) {
2725
+ const { root, relative, original } = await loadRepairBaseline(rootDir, baselinePath);
2201
2726
  const diagnostics = [];
2727
+ let reviewDigest;
2202
2728
  let current = null;
2203
2729
  try {
2730
+ reviewDigest = digest(await advisoryReviewInventory(root));
2204
2731
  current = await stableAudit(root, original.checksRun, options);
2205
2732
  if (!current) diagnostics.push("No context files remain available to audit.");
2206
2733
  else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
2207
2734
  for (const doc of original.docs) {
2208
- if (!original.issues.some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
2735
+ if (!allFindings(original).some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
2209
2736
  const content2 = await readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024);
2210
2737
  if (content2 === null || !content2.trim()) {
2211
2738
  diagnostics.push("Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair.");
@@ -2222,21 +2749,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
2222
2749
  const originalIds = new Set(originalFindings.map(findingId));
2223
2750
  const missingDocs = original.docs.filter((doc) => !current?.docs.some((d) => d.path === doc.path));
2224
2751
  for (const doc of missingDocs) diagnostics.push("Original context file " + doc.path + " is unavailable; removing it does not verify a repair.");
2225
- const findings = originalFindings.map((finding) => {
2752
+ const findings = await Promise.all(originalFindings.map(async (finding) => {
2226
2753
  const id = findingId(finding);
2227
2754
  const now = currentById.get(id);
2228
2755
  const base = { id, original: finding, ...now ? { current: now } : {} };
2229
2756
  if (diagnostics.length || !current) {
2230
2757
  return { ...base, status: "unverified", reason: "The original audit scope could not be verified. See diagnostics." };
2231
2758
  }
2232
- if ("confidence" in finding && now) {
2759
+ if (now && ("confidence" in finding || "confidence" in now)) {
2233
2760
  return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
2234
2761
  }
2235
2762
  const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
2236
2763
  if (!current.checksRun?.includes(finding.type) || skipped.length) {
2237
2764
  return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
2238
2765
  }
2239
- if (!("confidence" in finding)) {
2766
+ const review = await assessAdvisory(root, now ?? finding, original.headHash);
2767
+ if (review) {
2768
+ if (review.status === "unverified") return { ...base, review, status: "unverified", reason: review.reason };
2769
+ if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
2770
+ return { ...base, review, status: "review-required", reason: review.reason };
2771
+ }
2772
+ if (!("confidence" in finding) && (!recheckable(finding) || now)) {
2240
2773
  return {
2241
2774
  ...base,
2242
2775
  status: "review-required",
@@ -2244,11 +2777,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
2244
2777
  };
2245
2778
  }
2246
2779
  return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
2247
- });
2780
+ }));
2781
+ try {
2782
+ if (reviewDigest !== digest(await advisoryReviewInventory(root)) || current?.headHash !== await getCurrentGitHash(root)) {
2783
+ diagnostics.push("HEAD or advisory review records changed during verification; repeat against stable evidence.");
2784
+ for (const finding of findings) {
2785
+ finding.status = "unverified";
2786
+ finding.reason = diagnostics.at(-1);
2787
+ }
2788
+ }
2789
+ } catch (error) {
2790
+ diagnostics.push(String(error));
2791
+ for (const finding of findings) {
2792
+ finding.status = "unverified";
2793
+ finding.reason = "Advisory review evidence became unavailable during verification.";
2794
+ }
2795
+ }
2248
2796
  const newFindings = [...currentById].filter(([id]) => !originalIds.has(id)).map(([, f]) => f);
2797
+ const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
2249
2798
  const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
2250
2799
  for (const f of findings) counts[f.status]++;
2251
- const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f));
2800
+ 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)));
2252
2801
  const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
2253
2802
  return {
2254
2803
  version: 1,
@@ -2262,10 +2811,10 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
2262
2811
  diagnostics,
2263
2812
  currentAudit: current,
2264
2813
  counts,
2265
- scope: "Original audit checks over current context files and repository evidence. Resolved means no longer detected by that check. Advisories require separate review; this is not a certification of documentation or application correctness."
2814
+ scope: "Original audit checks over current context files and repository evidence. Resolved means no longer detected by the check or covered by an explicit assessment of current evidence; inspect each finding's reason and review. This is not a certification of documentation or application correctness."
2266
2815
  };
2267
2816
  }
2268
- var checkSchema, commitSchema, anchorSchema, count, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema, reportSchema, baselineSchema, digest, repairFindingSchema;
2817
+ var count2, reportSchema, baselineSchema, repairFindingSchema;
2269
2818
  var init_repair = __esm({
2270
2819
  "src/audit/repair.ts"() {
2271
2820
  "use strict";
@@ -2277,205 +2826,134 @@ var init_repair = __esm({
2277
2826
  init_files();
2278
2827
  init_paths();
2279
2828
  init_types();
2280
- checkSchema = z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
2281
- commitSchema = z5.object({ hash: z5.string().regex(/^[a-f0-9]{40,64}$/), date: z5.string(), subject: z5.string() });
2282
- anchorSchema = z5.object({ doc: z5.string(), line: z5.number().int().positive().nullable(), excerpt: z5.string().nullable() });
2283
- count = z5.number().int().nonnegative();
2284
- evidenceSchema = z5.discriminatedUnion("kind", [
2285
- z5.object({
2286
- kind: z5.literal("missing-path"),
2287
- claimed: z5.string(),
2288
- renamedTo: z5.string().nullable(),
2289
- deletedInCommit: commitSchema.nullable(),
2290
- everTracked: z5.boolean(),
2291
- parentDirExists: z5.boolean()
2292
- }),
2293
- z5.object({
2294
- kind: z5.literal("unmentioned-dir"),
2295
- dir: z5.string(),
2296
- sourceFileCount: count,
2297
- firstCommit: commitSchema.nullable(),
2298
- checkedDocs: z5.array(z5.string())
2299
- }),
2300
- z5.object({
2301
- kind: z5.literal("count-mismatch"),
2302
- claimed: count,
2303
- actual: count,
2304
- unit: z5.string(),
2305
- countedFrom: z5.string(),
2306
- members: z5.array(z5.string())
2307
- }),
2308
- z5.object({
2309
- kind: z5.literal("missing-script"),
2310
- scriptName: z5.string(),
2311
- invocation: z5.string(),
2312
- manifestsChecked: z5.array(z5.string()),
2313
- availableScripts: z5.array(z5.string())
2314
- }),
2315
- z5.object({
2316
- kind: z5.literal("doc-behind-manifests"),
2317
- docLastCommit: commitSchema,
2318
- manifestCommits: z5.array(commitSchema.extend({ files: z5.array(z5.string()) })),
2319
- totalCommits: count
2320
- }),
2321
- z5.object({
2322
- kind: z5.literal("decision-anchor"),
2323
- decisionId: z5.string(),
2324
- title: z5.string(),
2325
- changedFiles: z5.array(z5.string()),
2326
- refreshedHash: z5.string(),
2327
- provenance: z5.object({}).passthrough().optional()
2328
- })
2329
- ]);
2330
- findingSchema = z5.object({ message: z5.string(), anchor: anchorSchema, evidence: evidenceSchema });
2331
- issueSchema = findingSchema.extend({
2332
- type: z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
2333
- confidence: z5.enum(["certain", "likely"])
2334
- });
2335
- advisorySchema = findingSchema.extend({ type: z5.enum(["deps-changed", "decision-anchor-drift"]) });
2336
- checkResultSchema = z5.object({
2337
- issues: z5.array(issueSchema),
2338
- advisories: z5.array(advisorySchema),
2339
- suppressedAdvisories: z5.array(advisorySchema).optional(),
2340
- skipped: z5.array(z5.object({ check: z5.string(), reason: z5.string(), doc: z5.string().optional() }))
2341
- });
2342
- reportSchema = z5.object({
2343
- version: z5.literal(1),
2344
- root: z5.string(),
2345
- gitAvailable: z5.literal(true),
2829
+ init_findings();
2830
+ init_advisory_review();
2831
+ init_findings();
2832
+ count2 = z7.number().int().nonnegative();
2833
+ reportSchema = z7.object({
2834
+ version: z7.literal(1),
2835
+ root: z7.string(),
2836
+ gitAvailable: z7.literal(true),
2346
2837
  headHash: commitSchema.shape.hash,
2347
- checksRun: z5.array(checkSchema).nonempty(),
2348
- docs: z5.array(z5.object({
2349
- path: z5.enum(DOC_CANDIDATES),
2838
+ checksRun: z7.array(checkSchema).nonempty(),
2839
+ docs: z7.array(z7.object({
2840
+ path: z7.string().refine((value) => normalizeRepoPath(value) === value),
2841
+ scope: z7.string().optional(),
2842
+ kind: z7.enum(["instructions", "readme"]).optional(),
2350
2843
  lastCommit: commitSchema.nullable(),
2351
- dirty: z5.boolean(),
2352
- lineCount: count
2844
+ dirty: z7.boolean(),
2845
+ lineCount: count2
2353
2846
  })).nonempty(),
2354
- decisionsChecked: z5.boolean(),
2355
- clean: z5.boolean(),
2356
- issues: z5.array(issueSchema),
2357
- advisories: z5.array(advisorySchema),
2358
- suppressedAdvisories: z5.array(advisorySchema).optional(),
2359
- skippedChecks: z5.array(z5.object({ check: z5.string(), reason: z5.string(), doc: z5.string().optional() }))
2847
+ decisionsChecked: z7.boolean(),
2848
+ clean: z7.boolean(),
2849
+ issues: z7.array(issueSchema),
2850
+ advisories: z7.array(advisorySchema),
2851
+ suppressedAdvisories: z7.array(advisorySchema).optional(),
2852
+ skippedChecks: z7.array(z7.object({ check: z7.string(), reason: z7.string(), doc: z7.string().optional() }))
2360
2853
  });
2361
- baselineSchema = z5.object({
2362
- kind: z5.literal("mason-audit-repair"),
2363
- version: z5.literal(1),
2364
- createdAt: z5.string().datetime(),
2854
+ baselineSchema = z7.object({
2855
+ kind: z7.literal("mason-audit-repair"),
2856
+ version: z7.literal(1),
2857
+ createdAt: z7.string().datetime(),
2365
2858
  report: reportSchema,
2366
- digest: z5.string().regex(/^[a-f0-9]{64}$/)
2859
+ digest: z7.string().regex(/^[a-f0-9]{64}$/)
2367
2860
  });
2368
- digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
2369
- repairFindingSchema = z5.object({
2370
- id: z5.string(),
2371
- original: z5.union([issueSchema, advisorySchema]),
2372
- status: z5.enum(["resolved", "unresolved", "review-required", "unverified"]),
2373
- reason: z5.string(),
2374
- current: z5.union([issueSchema, advisorySchema]).optional()
2861
+ repairFindingSchema = z7.object({
2862
+ id: z7.string(),
2863
+ original: z7.union([issueSchema, advisorySchema]),
2864
+ status: z7.enum(["resolved", "unresolved", "review-required", "unverified"]),
2865
+ reason: z7.string(),
2866
+ current: z7.union([issueSchema, advisorySchema]).optional(),
2867
+ review: reviewSummarySchema.optional()
2375
2868
  });
2376
2869
  }
2377
2870
  });
2378
2871
 
2379
2872
  // src/automation/evidence.ts
2380
- var evidence_exports = {};
2381
- __export(evidence_exports, {
2382
- checkCache: () => checkCache,
2383
- git: () => git2,
2384
- hash: () => hash,
2385
- readInputs: () => readInputs,
2386
- workspace: () => workspace
2387
- });
2388
- import fs12 from "fs/promises";
2389
- import path17 from "path";
2873
+ import fs13 from "fs/promises";
2874
+ import path20 from "path";
2390
2875
  import { createHash as createHash3 } from "crypto";
2391
- import { execFile as execFile8 } from "child_process";
2392
- import { promisify as promisify8 } from "util";
2393
- import fg4 from "fast-glob";
2394
- import { z as z6 } from "zod";
2395
- async function git2(root, ...args) {
2876
+ import { execFile as execFile9 } from "child_process";
2877
+ import { promisify as promisify9 } from "util";
2878
+ import fg5 from "fast-glob";
2879
+ import { z as z8 } from "zod";
2880
+ async function git3(root, ...args) {
2396
2881
  return (await exec7("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout;
2397
2882
  }
2398
2883
  async function workspace(dir) {
2399
- const root = await fs12.realpath((await git2(dir, "rev-parse", "--show-toplevel")).trim());
2400
- const gitDir = await fs12.realpath((await git2(root, "rev-parse", "--absolute-git-dir")).trim());
2884
+ const root = await fs13.realpath((await git3(dir, "rev-parse", "--show-toplevel")).trim());
2885
+ const gitDir = await fs13.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
2401
2886
  let branch;
2402
2887
  try {
2403
- branch = (await git2(root, "symbolic-ref", "--quiet", "HEAD")).trim();
2888
+ branch = (await git3(root, "symbolic-ref", "--quiet", "HEAD")).trim();
2404
2889
  } catch {
2405
2890
  branch = "detached";
2406
2891
  }
2407
2892
  return { root, gitDir, branch, directory: ".mason/reports/automation/" + hash([root, gitDir, branch]).slice(0, 24) };
2408
2893
  }
2409
2894
  async function readInputs(root) {
2895
+ const docPaths = await discoverDocPaths(root);
2410
2896
  const [headText, docStatus, shallowPath, replacements] = await Promise.all([
2411
- git2(root, "rev-parse", "HEAD"),
2412
- git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...DOC_CANDIDATES),
2413
- git2(root, "rev-parse", "--git-path", "shallow"),
2414
- git2(root, "for-each-ref", "--format=%(refname) %(objectname)", "refs/replace")
2897
+ git3(root, "rev-parse", "HEAD"),
2898
+ docPaths.length ? git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...docPaths.map((p) => `:(literal)${p}`)) : "",
2899
+ git3(root, "rev-parse", "--git-path", "shallow"),
2900
+ git3(root, "for-each-ref", "--format=%(refname) %(objectname)", "refs/replace")
2415
2901
  ]);
2416
2902
  const head = headText.trim();
2417
2903
  let shallow = null;
2418
2904
  try {
2419
- shallow = await fs12.readFile(path17.resolve(root, shallowPath.trim()), "utf8");
2905
+ shallow = await fs13.readFile(path20.resolve(root, shallowPath.trim()), "utf8");
2420
2906
  } catch (error) {
2421
2907
  if (error.code !== "ENOENT") throw error;
2422
2908
  }
2423
2909
  const docs = {};
2424
2910
  const docContents = [];
2425
2911
  const claims = [];
2426
- for (const file of DOC_CANDIDATES) {
2912
+ for (const file of docPaths) {
2427
2913
  const text2 = await content(root, file);
2428
2914
  docs[file] = text2 === null ? null : hash(text2);
2429
2915
  docContents.push([file, text2]);
2430
2916
  for (const claim of text2 ? extractClaims(text2).paths : []) {
2431
- if (internal(claim.path) || claim.path.startsWith(".mason/")) continue;
2432
- const exists2 = async (p) => fs12.access(path17.resolve(root, p)).then(() => true, () => false);
2433
- claims.push([claim.path, await exists2(claim.path), await exists2(path17.dirname(claim.path))]);
2434
- }
2435
- }
2436
- const combinedDocs = docContents.map(([, text2]) => text2 ?? "").join("\n");
2437
- const countClaims = docContents.flatMap(([, text2]) => text2 ? extractClaims(text2).counts : []);
2438
- const commandClaims = docContents.flatMap(([, text2]) => text2 ? extractClaims(text2).commands : []);
2439
- const rootPackage = await content(root, "package.json");
2440
- let rootScripts = null;
2441
- if (rootPackage !== null) {
2442
- try {
2443
- const pkg = JSON.parse(rootPackage);
2444
- rootScripts = pkg?.scripts && typeof pkg.scripts === "object" ? Object.keys(pkg.scripts) : [];
2445
- } catch {
2917
+ const scope = pathClaimScope(file, claim);
2918
+ if (scope?.candidates.some(gitMetadataPath)) continue;
2919
+ for (const candidate of scope?.candidates ?? []) {
2920
+ if (optionalMasonPath(candidate)) continue;
2921
+ claims.push([candidate, await pathExists(root, candidate), await pathExists(root, path20.posix.dirname(candidate))]);
2922
+ }
2446
2923
  }
2447
2924
  }
2448
- const needsWorkspaceScripts = rootScripts !== null && commandClaims.some((claim) => !rootScripts.includes(claim.scriptName));
2925
+ const parsedDocs = docContents.map(([file, text2]) => ({ path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") }));
2926
+ const combinedDocs = moduleDocumentation(parsedDocs);
2927
+ const countClaims = parsedDocs.flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
2449
2928
  const decisionDirectory = await storePath(root, ".mason/decisions");
2450
- const decisionPresence = await fs12.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
2929
+ const decisionPresence = await fs13.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
2451
2930
  if (error.code === "ENOENT") return "absent";
2452
2931
  throw error;
2453
2932
  });
2454
- const [modules, counts, manifests, decisionFiles] = await Promise.all([
2933
+ const [modules, counts, commands, decisionFiles] = await Promise.all([
2455
2934
  combinedDocs ? moduleCandidates(root, combinedDocs) : [],
2456
- Promise.all(countClaims.map((claim) => resolveCountSource(root, claim))),
2457
- needsWorkspaceScripts ? commandManifests(root) : [],
2458
- fg4(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
2935
+ Promise.all(countClaims.map(({ doc, claim }) => resolveDocCountSource(root, doc, claim))),
2936
+ commandInputs(root, parsedDocs),
2937
+ fg5(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
2459
2938
  ]);
2460
- const packages = [["package.json", rootPackage], ...await Promise.all(manifests.sort().map(async (file) => [file, await content(root, file)]))];
2461
2939
  const decisions = await Promise.all(decisionFiles.sort().map(async (file) => {
2462
2940
  await storePath(root, file);
2463
2941
  return [file, await content(root, file)];
2464
2942
  }));
2465
2943
  const [status, index2] = decisions.length ? await Promise.all([
2466
- git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
2467
- git2(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
2944
+ git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
2945
+ git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
2468
2946
  ]) : ["", ""];
2469
- const common = [3, engineVersion, head, shallow, replacements, docContents];
2947
+ const common = [4, engineVersion, head, shallow, replacements, docContents];
2470
2948
  const keys = {
2471
2949
  "deleted-reference": hash([common, claims, docStatus]),
2472
2950
  "new-module": hash([common, modules]),
2473
2951
  "stale-count": hash([common, counts]),
2474
- "dead-command": hash([common, packages]),
2952
+ "dead-command": hash([common, commands]),
2475
2953
  "deps-changed": hash([common, docStatus]),
2476
2954
  "decision-anchor-drift": hash([common, decisionPresence, decisions, status, index2])
2477
2955
  };
2478
- return { fingerprint: hash(keys), head, docs, keys };
2956
+ return { fingerprint: hash([keys, await advisoryReviewInventory(root)]), head, docs, keys };
2479
2957
  }
2480
2958
  function checkCache(raw, inputs) {
2481
2959
  let entries = {};
@@ -2502,11 +2980,12 @@ function checkCache(raw, inputs) {
2502
2980
  return { version: 1, entries: canonical, digest: hash(canonical) };
2503
2981
  } };
2504
2982
  }
2505
- var exec7, engineVersion, hash, content, internal, cacheSchema;
2983
+ var exec7, engineVersion, hash, content, cacheSchema;
2506
2984
  var init_evidence = __esm({
2507
2985
  "src/automation/evidence.ts"() {
2508
2986
  "use strict";
2509
2987
  init_docs();
2988
+ init_scope();
2510
2989
  init_claims();
2511
2990
  init_repair();
2512
2991
  init_checks();
@@ -2515,20 +2994,20 @@ var init_evidence = __esm({
2515
2994
  init_stale_count();
2516
2995
  init_dead_command();
2517
2996
  init_storage();
2518
- exec7 = promisify8(execFile8);
2519
- engineVersion = true ? "0.16.3" : "development";
2997
+ init_advisory_review();
2998
+ exec7 = promisify9(execFile9);
2999
+ engineVersion = true ? "0.17.1" : "development";
2520
3000
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
2521
3001
  content = readAuditInput;
2522
- internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
2523
- cacheSchema = z6.object({ version: z6.literal(1), entries: z6.record(z6.object({ key: z6.string(), result: checkResultSchema })), digest: z6.string() });
3002
+ cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
2524
3003
  }
2525
3004
  });
2526
3005
 
2527
3006
  // src/automation/execution.ts
2528
3007
  import os2 from "os";
2529
- import fs13 from "fs/promises";
3008
+ import fs14 from "fs/promises";
2530
3009
  import { randomUUID as randomUUID3 } from "crypto";
2531
- import { z as z7 } from "zod";
3010
+ import { z as z9 } from "zod";
2532
3011
  function parseExecution(raw) {
2533
3012
  try {
2534
3013
  return executionSchema.parse(raw);
@@ -2546,7 +3025,7 @@ function automationFailure(error) {
2546
3025
  codes.add(String(cause.code));
2547
3026
  cause = cause.cause;
2548
3027
  }
2549
- const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z7.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
3028
+ const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z9.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
2550
3029
  return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
2551
3030
  }
2552
3031
  function failureMessage(error) {
@@ -2654,7 +3133,7 @@ async function recordExecution(root, directory, event, run) {
2654
3133
  throw Object.assign(new Error(failure.message, { cause: error }), { failure });
2655
3134
  }
2656
3135
  } finally {
2657
- await fs13.rm(await storePath(root, owner), { force: true }).catch(() => {
3136
+ await fs14.rm(await storePath(root, owner), { force: true }).catch(() => {
2658
3137
  });
2659
3138
  }
2660
3139
  }
@@ -2675,33 +3154,33 @@ var init_execution = __esm({
2675
3154
  init_store();
2676
3155
  init_evidence();
2677
3156
  init_storage();
2678
- failureSchema = z7.object({
2679
- code: z7.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
2680
- message: z7.string(),
2681
- retryable: z7.boolean(),
2682
- receiptRecorded: z7.boolean()
3157
+ failureSchema = z9.object({
3158
+ code: z9.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
3159
+ message: z9.string(),
3160
+ retryable: z9.boolean(),
3161
+ receiptRecorded: z9.boolean()
2683
3162
  });
2684
- attemptSchema = z7.object({
2685
- id: z7.string().regex(/^[a-zA-Z0-9_-]+$/),
2686
- lease: z7.literal(true).optional(),
2687
- event: z7.string(),
2688
- startedAt: z7.string(),
2689
- finishedAt: z7.string().optional(),
2690
- durationMs: z7.number().nonnegative().optional(),
2691
- pid: z7.number().int().positive(),
2692
- host: z7.string(),
2693
- status: z7.enum(["running", "completed", "failed", "unknown"]),
2694
- verificationStatus: z7.string().optional(),
2695
- reportPath: z7.string().optional(),
3163
+ attemptSchema = z9.object({
3164
+ id: z9.string().regex(/^[a-zA-Z0-9_-]+$/),
3165
+ lease: z9.literal(true).optional(),
3166
+ event: z9.string(),
3167
+ startedAt: z9.string(),
3168
+ finishedAt: z9.string().optional(),
3169
+ durationMs: z9.number().nonnegative().optional(),
3170
+ pid: z9.number().int().positive(),
3171
+ host: z9.string(),
3172
+ status: z9.enum(["running", "completed", "failed", "unknown"]),
3173
+ verificationStatus: z9.string().optional(),
3174
+ reportPath: z9.string().optional(),
2696
3175
  failure: failureSchema.optional()
2697
3176
  });
2698
- executionSchema = z7.object({ version: z7.literal(1), attempts: z7.array(attemptSchema).max(160), discardedAttempts: z7.number().int().nonnegative().default(0) });
2699
- notificationsSchema = z7.object({ version: z7.literal(1), sessions: z7.record(z7.object({
2700
- signature: z7.string(),
3177
+ executionSchema = z9.object({ version: z9.literal(1), attempts: z9.array(attemptSchema).max(160), discardedAttempts: z9.number().int().nonnegative().default(0) });
3178
+ notificationsSchema = z9.object({ version: z9.literal(1), sessions: z9.record(z9.object({
3179
+ signature: z9.string(),
2701
3180
  code: failureSchema.shape.code,
2702
- message: z7.string(),
2703
- count: z7.number().int().positive(),
2704
- lastAt: z7.string()
3181
+ message: z9.string(),
3182
+ count: z9.number().int().positive(),
3183
+ lastAt: z9.string()
2705
3184
  })) });
2706
3185
  leasePath = (directory, id) => `${directory}/executions/${id}.json`;
2707
3186
  }
@@ -2709,7 +3188,7 @@ var init_execution = __esm({
2709
3188
 
2710
3189
  // src/automation/runtime.ts
2711
3190
  import { randomUUID as randomUUID4 } from "crypto";
2712
- import { z as z8 } from "zod";
3191
+ import { z as z10 } from "zod";
2713
3192
  async function observeReadOnlyTool(dir, event) {
2714
3193
  const ws = await workspace(dir);
2715
3194
  await withLock(ws.root, ws.directory, async () => {
@@ -2787,7 +3266,7 @@ async function computeAnalysis(ws, state, inputs, event) {
2787
3266
  baselinePaths: [],
2788
3267
  reportPath: "",
2789
3268
  findings: [],
2790
- diagnostics: ["No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md exists. Documentation capture is unavailable; other Mason tools remain usable."],
3269
+ diagnostics: ["No README.md or agent instruction files were discovered. Documentation capture is unavailable; other Mason tools remain usable."],
2791
3270
  checks: { ran: [], reused: [], skipped: [] },
2792
3271
  counts: { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 },
2793
3272
  capture: "unknown",
@@ -2798,7 +3277,7 @@ async function computeAnalysis(ws, state, inputs, event) {
2798
3277
  const previous = await readStoreJson(ws.root, state.latest);
2799
3278
  if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
2800
3279
  try {
2801
- await git2(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
3280
+ await git3(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
2802
3281
  } catch {
2803
3282
  throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
2804
3283
  }
@@ -3003,43 +3482,76 @@ var init_runtime = __esm({
3003
3482
  init_storage();
3004
3483
  init_evidence();
3005
3484
  init_store();
3006
- SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved claims no longer fail their checks; advisories need separate review. Repair only within the user's task authorization.";
3485
+ SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved findings no longer fail their checks or have a recorded assessment covering current evidence. Repair and review only within the user's task authorization.";
3007
3486
  priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
3008
3487
  cleanText = (text2) => text2.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
3009
- analysisSchema = z8.object({
3010
- id: z8.string(),
3011
- fingerprint: z8.string(),
3012
- completedAt: z8.number(),
3013
- baselineDigest: z8.string(),
3014
- report: z8.object({
3015
- version: z8.literal(1),
3016
- status: z8.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
3017
- root: z8.string(),
3018
- branch: z8.string(),
3019
- head: z8.string(),
3020
- baselinePaths: z8.array(z8.string()),
3021
- reportPath: z8.string(),
3022
- findings: z8.array(repairFindingSchema),
3023
- diagnostics: z8.array(z8.string()),
3024
- checks: z8.object({
3025
- ran: z8.array(z8.string()),
3026
- reused: z8.array(z8.string()),
3027
- skipped: z8.array(z8.object({ check: z8.string(), reason: z8.string(), doc: z8.string().optional() }))
3488
+ analysisSchema = z10.object({
3489
+ id: z10.string(),
3490
+ fingerprint: z10.string(),
3491
+ completedAt: z10.number(),
3492
+ baselineDigest: z10.string(),
3493
+ report: z10.object({
3494
+ version: z10.literal(1),
3495
+ status: z10.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
3496
+ root: z10.string(),
3497
+ branch: z10.string(),
3498
+ head: z10.string(),
3499
+ baselinePaths: z10.array(z10.string()),
3500
+ reportPath: z10.string(),
3501
+ findings: z10.array(repairFindingSchema),
3502
+ diagnostics: z10.array(z10.string()),
3503
+ checks: z10.object({
3504
+ ran: z10.array(z10.string()),
3505
+ reused: z10.array(z10.string()),
3506
+ skipped: z10.array(z10.object({ check: z10.string(), reason: z10.string(), doc: z10.string().optional() }))
3028
3507
  }),
3029
- counts: z8.object({ resolved: z8.number(), unresolved: z8.number(), "review-required": z8.number(), unverified: z8.number() }),
3030
- capture: z8.enum(["observed", "unknown"]),
3031
- scope: z8.string()
3508
+ counts: z10.object({ resolved: z10.number(), unresolved: z10.number(), "review-required": z10.number(), unverified: z10.number() }),
3509
+ capture: z10.enum(["observed", "unknown"]),
3510
+ scope: z10.string()
3032
3511
  })
3033
3512
  });
3034
- sharedSchema = z8.object({ analysis: analysisSchema, digest: z8.string() });
3513
+ sharedSchema = z10.object({ analysis: analysisSchema, digest: z10.string() });
3035
3514
  baselineDigest = async (ws, state) => hash(await Promise.all(
3036
3515
  state.baselines.map(async (b) => [b.path, await readStoreJson(ws.root, b.path)])
3037
3516
  ));
3038
3517
  }
3039
3518
  });
3040
3519
 
3520
+ // src/automation/hook-root.ts
3521
+ import fs15 from "fs/promises";
3522
+ import path21 from "path";
3523
+ async function repositoryRoot(dir) {
3524
+ if (!path21.isAbsolute(dir)) throw new Error("Hook input working directory must be absolute.");
3525
+ try {
3526
+ return await fs15.realpath((await git3(dir, "rev-parse", "--show-toplevel")).trim());
3527
+ } catch (error) {
3528
+ if (error.code === "ENOENT" || /not a git repository/.test(String(error.stderr))) return null;
3529
+ throw error;
3530
+ }
3531
+ }
3532
+ async function resolveHookRoot(host, cwd, options = {}) {
3533
+ const current = await repositoryRoot(cwd);
3534
+ if (current) return current;
3535
+ const projectDir = options.projectDir ?? (host === "claude" ? process.env.CLAUDE_PROJECT_DIR : void 0);
3536
+ const projectRoot = projectDir ? await repositoryRoot(projectDir) : null;
3537
+ if (projectDir && !projectRoot) throw new Error("Hook input is outside Git and the host project is no longer a Git repository. Restore its history before running mason check.");
3538
+ const launchRoot = await repositoryRoot(options.launchCwd ?? process.cwd());
3539
+ const roots = [...new Set([launchRoot, projectRoot].filter((root) => root !== null))];
3540
+ if (!roots.length) throw new Error("Hook input is outside a Git repository and no project root is available. Return to the project and run mason check.");
3541
+ if (roots.length > 1) throw new Error("Hook input is outside a Git repository and project roots disagree. Return to the intended checkout and run mason check; verification was not established.");
3542
+ const worktrees = (await git3(roots[0], "worktree", "list", "--porcelain", "-z")).split("\0").filter((field) => field.startsWith("worktree "));
3543
+ if (worktrees.length !== 1) throw new Error("Hook input is outside a Git repository with multiple worktrees. Return to the intended worktree and run mason check; the active checkout is unknown.");
3544
+ return roots[0];
3545
+ }
3546
+ var init_hook_root = __esm({
3547
+ "src/automation/hook-root.ts"() {
3548
+ "use strict";
3549
+ init_evidence();
3550
+ }
3551
+ });
3552
+
3041
3553
  // src/setup/model.ts
3042
- import { z as z9 } from "zod";
3554
+ import { z as z11 } from "zod";
3043
3555
  async function loadSetup(root) {
3044
3556
  const raw = await readStoreJson(root, SETUP_PATH);
3045
3557
  if (raw === null) return null;
@@ -3057,42 +3569,52 @@ var init_model = __esm({
3057
3569
  "src/setup/model.ts"() {
3058
3570
  "use strict";
3059
3571
  init_storage();
3060
- setupHostSchema = z9.object({
3061
- configuredVersion: z9.string(),
3062
- revision: z9.string().uuid(),
3063
- fingerprint: z9.string(),
3064
- mcpFingerprint: z9.string(),
3065
- instructions: z9.array(z9.string())
3572
+ setupHostSchema = z11.object({
3573
+ configuredVersion: z11.string(),
3574
+ revision: z11.string().uuid(),
3575
+ fingerprint: z11.string(),
3576
+ mcpFingerprint: z11.string(),
3577
+ instructions: z11.array(z11.string())
3066
3578
  });
3067
- setupSchema = z9.object({ version: z9.literal(2), hosts: z9.object({
3579
+ setupSchema = z11.object({ version: z11.literal(2), hosts: z11.object({
3068
3580
  codex: setupHostSchema.optional(),
3069
3581
  claude: setupHostSchema.optional()
3070
3582
  }) });
3071
3583
  SETUP_PATH = ".mason/local/setup.json";
3072
- receiptSchema = z9.object({
3073
- version: z9.literal(1),
3074
- host: z9.enum(["codex", "claude"]),
3075
- status: z9.enum(["installing", "configured"]),
3076
- initialReportPath: z9.string(),
3077
- initialBaselinePaths: z9.array(z9.string()),
3078
- root: z9.string(),
3079
- revision: z9.string().uuid(),
3080
- configuredAt: z9.string().optional()
3584
+ receiptSchema = z11.object({
3585
+ version: z11.literal(1),
3586
+ host: z11.enum(["codex", "claude"]),
3587
+ status: z11.enum(["installing", "configured"]),
3588
+ initialReportPath: z11.string(),
3589
+ initialBaselinePaths: z11.array(z11.string()),
3590
+ root: z11.string(),
3591
+ revision: z11.string().uuid(),
3592
+ configuredAt: z11.string().optional()
3081
3593
  });
3082
3594
  }
3083
3595
  });
3084
3596
 
3085
3597
  // src/setup/launcher.ts
3086
- import { execFile as execFile9 } from "child_process";
3087
- import { promisify as promisify9 } from "util";
3088
- import fs14 from "fs/promises";
3598
+ var launcher_exports = {};
3599
+ __export(launcher_exports, {
3600
+ executingVersion: () => executingVersion,
3601
+ globalCommand: () => globalCommand,
3602
+ hookCommand: () => hookCommand,
3603
+ inactiveSetupMessage: () => inactiveSetupMessage,
3604
+ installedCommand: () => installedCommand,
3605
+ mcpCommand: () => mcpCommand,
3606
+ prepareLaunch: () => prepareLaunch
3607
+ });
3608
+ import { execFile as execFile10 } from "child_process";
3609
+ import { promisify as promisify10 } from "util";
3610
+ import fs16 from "fs/promises";
3089
3611
  function globalCommand(args, platform = process.platform) {
3090
3612
  return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
3091
3613
  }
3092
3614
  async function installedCommand() {
3093
3615
  const invocation = globalCommand(["internal-integration-version"]);
3094
3616
  try {
3095
- const { stdout } = await promisify9(execFile9)(
3617
+ const { stdout } = await promisify10(execFile10)(
3096
3618
  invocation.command,
3097
3619
  invocation.args,
3098
3620
  { timeout: 5e3, maxBuffer: 8192, windowsHide: true }
@@ -3104,13 +3626,31 @@ async function installedCommand() {
3104
3626
  return { available: false, version: null, message: "An up-to-date mason command is not available on this process's PATH. Install or upgrade Mason, then reopen your terminal or desktop assistant and rerun mason setup." };
3105
3627
  }
3106
3628
  }
3629
+ async function prepareLaunch(host, options = {}) {
3630
+ const root = await fs16.realpath((await git3(options.dir ?? process.cwd(), "rev-parse", "--show-toplevel")).trim());
3631
+ const entry = (await loadSetup(root))?.hosts[host];
3632
+ process.env.MASON_SETUP_ROOT = root;
3633
+ process.env.MASON_SETUP_HOST = host;
3634
+ delete process.env.MASON_SETUP_REVISION;
3635
+ if (!entry) {
3636
+ if (options.allowInactive) return false;
3637
+ throw new Error(`Mason is not configured locally for ${host}. Run mason setup --host ${host} in this checkout.`);
3638
+ }
3639
+ process.env.MASON_SETUP_REVISION = entry.revision;
3640
+ return true;
3641
+ }
3642
+ function inactiveSetupMessage() {
3643
+ const host = process.env.MASON_SETUP_HOST;
3644
+ if (!process.env.MASON_SETUP_ROOT || process.env.MASON_SETUP_REVISION || host !== "claude" && host !== "codex") return null;
3645
+ return `Mason MCP is available, but automatic hooks are inactive in ${process.env.MASON_SETUP_ROOT}. Run mason setup --host ${host} in that checkout, review the host configuration, then restart the assistant. Setup has not been performed automatically.`;
3646
+ }
3107
3647
  var executingVersion, mcpCommand, hookCommand;
3108
3648
  var init_launcher = __esm({
3109
3649
  "src/setup/launcher.ts"() {
3110
3650
  "use strict";
3111
3651
  init_evidence();
3112
3652
  init_model();
3113
- executingVersion = () => true ? "0.16.3" : "development";
3653
+ executingVersion = () => true ? "0.17.1" : "development";
3114
3654
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
3115
3655
  hookCommand = (host) => `mason --setup-host ${host} auto`;
3116
3656
  }
@@ -3123,8 +3663,8 @@ __export(observations_exports, {
3123
3663
  observeActivation: () => observeActivation,
3124
3664
  readObservation: () => readObservation
3125
3665
  });
3126
- import fs15 from "fs/promises";
3127
- import { z as z10 } from "zod";
3666
+ import fs17 from "fs/promises";
3667
+ import { z as z12 } from "zod";
3128
3668
  function observationPath(directory, host, revision, version = executingVersion()) {
3129
3669
  return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
3130
3670
  }
@@ -3137,11 +3677,17 @@ async function readObservation(root, directory, host, revision, version = execut
3137
3677
  }
3138
3678
  async function observeActivation(dir, event, options = {}) {
3139
3679
  const host = process.env.MASON_SETUP_HOST, revision = process.env.MASON_SETUP_REVISION;
3140
- if (host !== "codex" && host !== "claude" || !revision || !process.env.MASON_SETUP_ROOT) return null;
3680
+ if (host !== "codex" && host !== "claude" || !process.env.MASON_SETUP_ROOT) return null;
3141
3681
  try {
3682
+ if (!revision) {
3683
+ if ((await loadSetup(process.env.MASON_SETUP_ROOT))?.hosts[host]) {
3684
+ 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.";
3685
+ }
3686
+ return inactiveSetupMessage();
3687
+ }
3142
3688
  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];
3143
- const ws = capturedDirectory ? { root: await fs15.realpath(dir), directory: capturedDirectory } : await workspace(dir);
3144
- if (ws.root !== await fs15.realpath(process.env.MASON_SETUP_ROOT)) return null;
3689
+ const ws = capturedDirectory ? { root: await fs17.realpath(dir), directory: capturedDirectory } : await workspace(dir);
3690
+ if (ws.root !== await fs17.realpath(process.env.MASON_SETUP_ROOT)) return null;
3145
3691
  const setup = await loadSetup(ws.root);
3146
3692
  if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
3147
3693
  const directory = ws.directory + "/activation";
@@ -3189,26 +3735,26 @@ var init_observations = __esm({
3189
3735
  init_storage();
3190
3736
  init_model();
3191
3737
  init_launcher();
3192
- observationSchema = z10.object({
3193
- version: z10.literal(1),
3194
- masonVersion: z10.string(),
3195
- root: z10.string(),
3196
- revision: z10.string(),
3197
- host: z10.enum(["codex", "claude"]),
3198
- contextCalls: z10.number().int().nonnegative(),
3199
- lastContextAt: z10.string().optional(),
3200
- sessions: z10.record(z10.object({
3201
- events: z10.array(z10.enum(events)),
3202
- at: z10.string(),
3203
- verificationStatus: z10.string().optional(),
3204
- reportPath: z10.string().optional()
3738
+ observationSchema = z12.object({
3739
+ version: z12.literal(1),
3740
+ masonVersion: z12.string(),
3741
+ root: z12.string(),
3742
+ revision: z12.string(),
3743
+ host: z12.enum(["codex", "claude"]),
3744
+ contextCalls: z12.number().int().nonnegative(),
3745
+ lastContextAt: z12.string().optional(),
3746
+ sessions: z12.record(z12.object({
3747
+ events: z12.array(z12.enum(events)),
3748
+ at: z12.string(),
3749
+ verificationStatus: z12.string().optional(),
3750
+ reportPath: z12.string().optional()
3205
3751
  }))
3206
3752
  });
3207
3753
  }
3208
3754
  });
3209
3755
 
3210
3756
  // src/automation/adapters.ts
3211
- import { z as z11 } from "zod";
3757
+ import { z as z13 } from "zod";
3212
3758
  function normalizeHook(host, raw) {
3213
3759
  hostSchema.parse(host);
3214
3760
  const input2 = inputSchema.parse(raw);
@@ -3222,7 +3768,7 @@ function normalizeHook(host, raw) {
3222
3768
  stopHookActive: input2.stop_hook_active || input2.permission_mode === "plan"
3223
3769
  } };
3224
3770
  }
3225
- async function runAutomationHook(host, stdin) {
3771
+ async function runAutomationHook(host, stdin, options = {}) {
3226
3772
  let name = "";
3227
3773
  let hook;
3228
3774
  try {
@@ -3236,6 +3782,11 @@ async function runAutomationHook(host, stdin) {
3236
3782
  const input2 = normalizeHook(host, raw);
3237
3783
  hook = input2;
3238
3784
  name = input2.name;
3785
+ if (options.managed) {
3786
+ input2.cwd = await resolveHookRoot(host, input2.cwd);
3787
+ const { prepareLaunch: prepareLaunch2 } = await Promise.resolve().then(() => (init_launcher(), launcher_exports));
3788
+ if (!await prepareLaunch2(host, { dir: input2.cwd, allowInactive: true })) return null;
3789
+ }
3239
3790
  if (["before_tool", "after_tool"].includes(input2.event.event) && input2.readOnly) {
3240
3791
  const observed = await observeReadOnlyTool(input2.cwd, input2.event);
3241
3792
  const { observeActivation: observeActivation3 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
@@ -3268,6 +3819,9 @@ function managedHookCommand(host, platform = process.platform) {
3268
3819
  function knownManagedHookCommands(host) {
3269
3820
  return [managedHookCommand(host, "linux"), managedHookCommand(host, "win32"), `mason --setup-host ${host} auto hook --host ${host}`];
3270
3821
  }
3822
+ function compatibleManagedHookCommands(host, platform = process.platform) {
3823
+ return [managedHookCommand(host, platform), `mason --setup-host ${host} auto hook --host ${host}`];
3824
+ }
3271
3825
  function hookConfig(host, command = "npx --no-install --package mason-context mason-auto") {
3272
3826
  const handler = { type: "command", command: command === `mason --setup-host ${host} auto` ? managedHookCommand(host) : command + " hook --host " + host, timeout: 30 };
3273
3827
  return { hooks: Object.fromEntries(HOOK_EVENTS.map((name) => [
@@ -3282,15 +3836,16 @@ var init_adapters = __esm({
3282
3836
  init_execution();
3283
3837
  init_runtime();
3284
3838
  init_store();
3285
- inputSchema = z11.object({
3286
- cwd: z11.string().min(1),
3287
- session_id: z11.string().min(1).max(500),
3288
- hook_event_name: z11.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
3289
- tool_name: z11.string().optional(),
3290
- tool_use_id: z11.string().max(500).optional(),
3291
- tool_input: z11.unknown().optional(),
3292
- stop_hook_active: z11.boolean().optional(),
3293
- permission_mode: z11.string().optional()
3839
+ init_hook_root();
3840
+ inputSchema = z13.object({
3841
+ cwd: z13.string().min(1),
3842
+ session_id: z13.string().min(1).max(500),
3843
+ hook_event_name: z13.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
3844
+ tool_name: z13.string().optional(),
3845
+ tool_use_id: z13.string().max(500).optional(),
3846
+ tool_input: z13.unknown().optional(),
3847
+ stop_hook_active: z13.boolean().optional(),
3848
+ permission_mode: z13.string().optional()
3294
3849
  });
3295
3850
  lifecycle = {
3296
3851
  SessionStart: "session_start",
@@ -3310,8 +3865,8 @@ __export(files_exports, {
3310
3865
  managedBlock: () => managedBlock,
3311
3866
  readText: () => readText
3312
3867
  });
3313
- import fs16 from "fs/promises";
3314
- import path18 from "path";
3868
+ import fs18 from "fs/promises";
3869
+ import path22 from "path";
3315
3870
  import { randomUUID as randomUUID5 } from "crypto";
3316
3871
  async function readText(root, file) {
3317
3872
  try {
@@ -3340,14 +3895,14 @@ async function applyEdit(root, edit) {
3340
3895
  if (edit.after === null) {
3341
3896
  const file2 = await storePath(root, edit.path);
3342
3897
  if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
3343
- await fs16.unlink(file2);
3898
+ await fs18.unlink(file2);
3344
3899
  return true;
3345
3900
  }
3346
3901
  const file = await storePath(root, edit.path, true);
3347
- const temporary = path18.join(path18.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3902
+ const temporary = path22.join(path22.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3348
3903
  try {
3349
- const mode = await fs16.stat(file).then((s) => s.mode & 511, () => 384);
3350
- const handle = await fs16.open(temporary, "wx", mode);
3904
+ const mode = await fs18.stat(file).then((s) => s.mode & 511, () => 384);
3905
+ const handle = await fs18.open(temporary, "wx", mode);
3351
3906
  try {
3352
3907
  await handle.writeFile(edit.after, "utf8");
3353
3908
  await handle.sync();
@@ -3355,10 +3910,10 @@ async function applyEdit(root, edit) {
3355
3910
  await handle.close();
3356
3911
  }
3357
3912
  if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
3358
- await fs16.rename(temporary, file);
3913
+ await fs18.rename(temporary, file);
3359
3914
  return true;
3360
3915
  } finally {
3361
- await fs16.rm(temporary, { force: true });
3916
+ await fs18.rm(temporary, { force: true });
3362
3917
  }
3363
3918
  }
3364
3919
  var init_files2 = __esm({
@@ -3379,7 +3934,7 @@ __export(ownership_exports, {
3379
3934
  ownershipEdit: () => ownershipEdit,
3380
3935
  ownershipSchema: () => ownershipSchema
3381
3936
  });
3382
- import { z as z12 } from "zod";
3937
+ import { z as z14 } from "zod";
3383
3938
  import { parse } from "smol-toml";
3384
3939
  function blockSpan(text2, start, end) {
3385
3940
  const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
@@ -3430,30 +3985,30 @@ var init_ownership = __esm({
3430
3985
  ["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
3431
3986
  ["# mason:mcp:start", "# mason:mcp:end"]
3432
3987
  ];
3433
- fileSchema = z12.object({
3434
- created: z12.boolean(),
3435
- initialHash: z12.string(),
3436
- blocks: z12.record(z12.string()),
3437
- mcpHash: z12.string().optional(),
3438
- hookCommands: z12.array(z12.string()).optional(),
3439
- hooks: z12.record(z12.array(z12.string())).optional()
3988
+ fileSchema = z14.object({
3989
+ created: z14.boolean(),
3990
+ initialHash: z14.string(),
3991
+ blocks: z14.record(z14.string()),
3992
+ mcpHash: z14.string().optional(),
3993
+ hookCommands: z14.array(z14.string()).optional(),
3994
+ hooks: z14.record(z14.array(z14.string())).optional()
3440
3995
  });
3441
- ownershipSchema = z12.object({ version: z12.literal(1), files: z12.record(fileSchema) });
3996
+ ownershipSchema = z14.object({ version: z14.literal(1), files: z14.record(fileSchema) });
3442
3997
  }
3443
3998
  });
3444
3999
 
3445
4000
  // src/mcp/init.ts
3446
- import { z as z13 } from "zod";
4001
+ import { z as z15 } from "zod";
3447
4002
  var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
3448
4003
  var init_init = __esm({
3449
4004
  "src/mcp/init.ts"() {
3450
4005
  "use strict";
3451
4006
  init_storage();
3452
4007
  init_files2();
3453
- markerSchema = z13.object({
3454
- version: z13.literal(1),
3455
- initializedAt: z13.string(),
3456
- features: z13.object({ confluence: z13.boolean().optional() }).passthrough().optional()
4008
+ markerSchema = z15.object({
4009
+ version: z15.literal(1),
4010
+ initializedAt: z15.string(),
4011
+ features: z15.object({ confluence: z15.boolean().optional() }).passthrough().optional()
3457
4012
  }).passthrough();
3458
4013
  CLAUDE_MD_SECTION = `<!-- mason:start -->
3459
4014
  ## Mason project knowledge
@@ -3463,12 +4018,14 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
3463
4018
  - Task, bug, change request, or investigation of why the project works this way \u2192 \`get_context\` with the task text and known files: matching decisions, related tests, impact, and any available map entries.
3464
4019
  - Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
3465
4020
  - Investigation or correction reveals a reusable project lesson \u2192 \`save_decision\` before finishing. Capture only the lasting constraint and reason, with anchors and known sources, owner, and recorder. This creates a proposal; never infer approval. Preserve uncertain causes and unknown attribution. Route project lessons here; keep personal preferences in agent memory. Skip code summaries, current checkout status, temporary task permissions, session trivia, and secrets. No qualifying lesson means no record.
4021
+ - Before saving, compare the lesson with matching \`get_context\` records and any pending proposal. If new evidence changes the same lesson's assumptions, scope, or recommended action, revise it with \`save_decision(id: existingId)\`, including when it is accepted. Preserve supported rationale and sources, replace obsolete instructions, and add the new evidence. Create a separate record for a genuinely distinct lesson; skip unchanged restatements. Revision preserves history and leaves the accepted version operative until authorized review.
3466
4022
  - Consult trust metadata before relying on entries: unknown or changed freshness requires inspection, and failed verification means the description must be corrected. Check approval too: proposals are suggestions, legacy records are unreviewed, and accepted decisions are recorded constraints subject to freshness checks. An accepted revision remains operative while a pending proposal is reviewed; keep both versions and their freshness distinct.
3467
4023
  - Asked to review or re-verify a decision \u2192 \`review_decision\` first to inspect content, sources, history, and code changes. Record acceptance, reaffirmation, or retirement only when authorized by the user or a cited team review, with the actual reviewer and reason. Never infer approval from unchanged code. Review and commit the local record through the normal project workflow.
3468
4024
  - 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.
3469
4025
  - \`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.
3470
4026
 
3471
4027
  - 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.
4028
+ - 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.
3472
4029
 
3473
4030
  - 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.
3474
4031
 
@@ -3484,7 +4041,7 @@ ${CLAUDE_MD_SECTION}`;
3484
4041
 
3485
4042
  1. Explain the actionable findings with their source evidence. Separate audit issues, advisories, and skipped checks. The review covers committed changes from the merge base to HEAD; workingTree paths are not included in that review. An unavailable or empty check is not proof that the project is correct. Use the CLI for full output if a summary is truncated.
3486
4043
  2. Address findings within the user's requested scope. Setup alone authorizes Mason host configuration, hooks, instructions, and ignore rules; rewriting existing project claims requires repair scope. If repair is authorized, call \`mason_repair(dir, action: "prepare")\` before editing; it saves the full original findings even when this summary is truncated. Inspect relevant source, make grounded edits, then call \`mason_repair(dir, action: "verify", baselinePath)\` with that same baseline, including after any final doc commit. Report resolved, unresolved, review-required, unverified, and new findings. Keep suppressed advisories visible even when setup has already dirtied a doc. Do not invent a decision just to populate the store.
3487
- 3. When the task reveals a real lesson or constraint, call \`save_decision\` with title, body, category, anchors, and known owner/source/actor information. Missing attribution can be added later. The tool writes a local proposal; editing it preserves revision history and requires a new acceptance, while any earlier accepted revision remains operative. An unchanged save never refreshes its evidence. When decision review is requested, \`review_decision\` prepares the record and code evidence before any authorized verdict. Review and commit records through the normal workflow. Retrieve it on the next relevant task with \`get_context(dir, task, files)\`.
4044
+ 3. When the task reveals a real lesson or constraint, compare matching \`get_context\` records first. If its assumptions, scope, or recommended action changed, revise that record using \`save_decision(id: existingId)\`; create a separate record for a genuinely distinct lesson and skip unchanged restatements. Include title, body, category, anchors, and known owner/source/actor information. Preserve supported rationale and sources, replace obsolete instructions, and add new evidence. Missing attribution can be added later. The tool writes a local proposal; editing it preserves revision history and requires a new acceptance, while any earlier accepted revision remains operative. An unchanged save never refreshes its evidence. When decision review is requested, \`review_decision\` prepares the record and code evidence before any authorized verdict. Review and commit records through the normal workflow. Retrieve it on the next relevant task with \`get_context(dir, task, files)\`.
3488
4045
 
3489
4046
  ${ASSISTANT_SETUP}
3490
4047
 
@@ -3525,29 +4082,6 @@ If a build is interrupted, its partials remain in \`.mason/partial-snapshots/\`.
3525
4082
  }
3526
4083
  });
3527
4084
 
3528
- // src/utils/git.ts
3529
- var git_exports = {};
3530
- __export(git_exports, {
3531
- isGitRepo: () => isGitRepo
3532
- });
3533
- import { execFile as execFile10 } from "child_process";
3534
- import { promisify as promisify10 } from "util";
3535
- async function isGitRepo(dir) {
3536
- try {
3537
- await exec8("git", ["rev-parse", "--git-dir"], { cwd: dir });
3538
- return true;
3539
- } catch {
3540
- return false;
3541
- }
3542
- }
3543
- var exec8;
3544
- var init_git2 = __esm({
3545
- "src/utils/git.ts"() {
3546
- "use strict";
3547
- exec8 = promisify10(execFile10);
3548
- }
3549
- });
3550
-
3551
4085
  // src/setup/config.ts
3552
4086
  var config_exports = {};
3553
4087
  __export(config_exports, {
@@ -3560,7 +4094,7 @@ __export(config_exports, {
3560
4094
  });
3561
4095
  import { isDeepStrictEqual } from "util";
3562
4096
  import { parse as parse2, stringify } from "smol-toml";
3563
- import { z as z14 } from "zod";
4097
+ import { z as z16 } from "zod";
3564
4098
  async function instructionEdits(root, host) {
3565
4099
  const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
3566
4100
  const existing = found.find((f) => f.text !== null);
@@ -3592,7 +4126,7 @@ async function instructionEdits(root, host) {
3592
4126
  function managedMcp(previous, host) {
3593
4127
  const options = previous === void 0 ? {} : object.parse(previous);
3594
4128
  for (const key of ["command", "args", "url", "type", "headers", "http_headers", "env_http_headers", "bearer_token_env_var"]) delete options[key];
3595
- return { ...options, ...mcpCommand(host) };
4129
+ return { ...options, ...host === "claude" ? { type: "stdio" } : {}, ...mcpCommand(host) };
3596
4130
  }
3597
4131
  async function mcpEdit(root, host) {
3598
4132
  const file = mcpPath(host), before = await readText(root, file);
@@ -3629,19 +4163,13 @@ async function hookEdits(root, host) {
3629
4163
  }
3630
4164
  async function ancillaryEdits(root) {
3631
4165
  const ignore = await readText(root, ".gitignore");
3632
- const { git: git3 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
3633
- const { isGitRepo: isGitRepo2 } = await Promise.resolve().then(() => (init_git2(), git_exports));
3634
- let parentIgnored = false;
3635
- try {
3636
- if (await isGitRepo2(root)) parentIgnored = !!(await git3(root, "check-ignore", "--no-index", ".mason")).trim();
3637
- } catch (error) {
3638
- if (error.code !== 1) throw error;
3639
- }
3640
- const retainParentRule = parentIgnored || !!ignore?.replace(/\r\n/g, "\n").includes("# mason:ignore:start\n!/.mason/\n/.mason/*");
3641
4166
  const rules = [
3642
- ...retainParentRule ? ["!/.mason/", "/.mason/*"] : [],
4167
+ "!/.mason/",
4168
+ "/.mason/*",
3643
4169
  "!/.mason/decisions/",
3644
4170
  "!/.mason/decisions/**",
4171
+ "!/.mason/reviews/",
4172
+ "!/.mason/reviews/**",
3645
4173
  "!/.mason/config.json",
3646
4174
  "!/.mason/snapshot.json",
3647
4175
  "/.mason/local/",
@@ -3655,13 +4183,14 @@ async function inspectHostConfig(root, host, plannedMcp) {
3655
4183
  const servers = host === "codex" ? config.mcp_servers : config.mcpServers;
3656
4184
  const hooks = automationConfigSchema.parse(JSON.parse(await readText(root, configPath(host)) ?? "{}"));
3657
4185
  const expected = hookConfig(host, hookCommand(host));
4186
+ const compatible = compatibleManagedHookCommands(host);
3658
4187
  const mcp = servers?.mason === void 0 ? null : object.parse(servers.mason);
3659
4188
  return {
3660
4189
  mcp,
3661
4190
  mcpDisabled: mcp?.enabled === false,
3662
4191
  hooks: Object.fromEntries(Object.keys(expected.hooks).map((event) => [
3663
4192
  event,
3664
- (hooks.hooks?.[event] ?? []).flatMap((group) => group.hooks.filter((handler) => handler.command === expected.hooks.SessionStart[0].hooks[0].command).map((handler) => ({ ...group, hooks: [handler] })) ?? [])
4193
+ (hooks.hooks?.[event] ?? []).flatMap((group) => group.hooks.filter((handler) => compatible.includes(handler.command ?? "")).map((handler) => ({ ...group, hooks: [handler] })) ?? [])
3665
4194
  ])),
3666
4195
  disabled: hooks.disableAllHooks === true || config.features?.hooks === false || config.features?.codex_hooks === false
3667
4196
  };
@@ -3676,7 +4205,7 @@ var init_config = __esm({
3676
4205
  init_install();
3677
4206
  init_files2();
3678
4207
  init_launcher();
3679
- object = z14.record(z14.unknown());
4208
+ object = z16.record(z16.unknown());
3680
4209
  mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
3681
4210
  TOML_START = "# mason:mcp:start";
3682
4211
  TOML_END = "# mason:mcp:end";
@@ -3694,11 +4223,11 @@ __export(install_exports, {
3694
4223
  installAutomation: () => installAutomation,
3695
4224
  installedAutomation: () => installedAutomation,
3696
4225
  planAutomationInstall: () => planAutomationInstall,
3697
- recordSchema: () => recordSchema
4226
+ recordSchema: () => recordSchema2
3698
4227
  });
3699
- import { z as z15 } from "zod";
4228
+ import { z as z17 } from "zod";
3700
4229
  async function installationRecord(root) {
3701
- return recordSchema.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
4230
+ return recordSchema2.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
3702
4231
  }
3703
4232
  async function installAutomation(dir, host, command) {
3704
4233
  const ws = await workspace(dir);
@@ -3767,7 +4296,7 @@ async function installedAutomation(dir) {
3767
4296
  }
3768
4297
  return result;
3769
4298
  }
3770
- var groupSchema, automationConfigSchema, recordSchema, AUTOMATION_PATH, configPath;
4299
+ var groupSchema, automationConfigSchema, recordSchema2, AUTOMATION_PATH, configPath;
3771
4300
  var init_install = __esm({
3772
4301
  "src/automation/install.ts"() {
3773
4302
  "use strict";
@@ -3777,9 +4306,9 @@ var init_install = __esm({
3777
4306
  init_store();
3778
4307
  init_files2();
3779
4308
  init_ownership();
3780
- groupSchema = z15.object({ hooks: z15.array(z15.object({ type: z15.string(), command: z15.string().optional() }).passthrough()) }).passthrough();
3781
- automationConfigSchema = z15.object({ hooks: z15.record(z15.array(groupSchema)).optional() }).passthrough();
3782
- recordSchema = z15.object({ version: z15.literal(1), hosts: z15.record(z15.object({ command: z15.string() })) });
4309
+ groupSchema = z17.object({ hooks: z17.array(z17.object({ type: z17.string(), command: z17.string().optional() }).passthrough()) }).passthrough();
4310
+ automationConfigSchema = z17.object({ hooks: z17.record(z17.array(groupSchema)).optional() }).passthrough();
4311
+ recordSchema2 = z17.object({ version: z17.literal(1), hosts: z17.record(z17.object({ command: z17.string() })) });
3783
4312
  AUTOMATION_PATH = ".mason/local/automation.json";
3784
4313
  configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
3785
4314
  }
@@ -3793,8 +4322,8 @@ __export(teardown_exports, {
3793
4322
  });
3794
4323
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
3795
4324
  import { parse as parse3, stringify as stringify2 } from "smol-toml";
3796
- import fg5 from "fast-glob";
3797
- import { z as z16 } from "zod";
4325
+ import fg6 from "fast-glob";
4326
+ import { z as z18 } from "zod";
3798
4327
  async function planTeardown(root, selected) {
3799
4328
  const edits = [], diagnostics = [];
3800
4329
  const inputs = /* @__PURE__ */ new Map();
@@ -3814,7 +4343,7 @@ async function planTeardown(root, selected) {
3814
4343
  const setupBefore = await read(SETUP_PATH);
3815
4344
  const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
3816
4345
  const automationBefore = await read(AUTOMATION_PATH);
3817
- const automation = automationBefore === null ? null : recordSchema.parse(JSON.parse(automationBefore));
4346
+ const automation = automationBefore === null ? null : recordSchema2.parse(JSON.parse(automationBefore));
3818
4347
  const commands = (host) => new Set([
3819
4348
  automation?.hosts[host]?.command,
3820
4349
  ...ownership.files[configPath(host)]?.hookCommands ?? [],
@@ -3851,8 +4380,12 @@ async function planTeardown(root, selected) {
3851
4380
  if (host === "codex" && BLOCKS[2].some((marker) => before.includes(marker))) throw new Error("The marked Mason MCP entry was removed or renamed; inspect the retained block manually.");
3852
4381
  return;
3853
4382
  }
3854
- const expected = ownership.files[mcpFile]?.mcpHash ?? hash(mcpCommand(host));
3855
- if (hash(servers.mason) !== expected) throw new Error("The Mason MCP entry was edited or its ownership is uncertain; retained for manual cleanup.");
4383
+ const ownedHash = ownership.files[mcpFile]?.mcpHash;
4384
+ const expected = ownedHash ? [ownedHash] : [
4385
+ hash(mcpCommand(host)),
4386
+ ...host === "claude" ? [hash({ type: "stdio", ...mcpCommand(host) })] : []
4387
+ ];
4388
+ if (!expected.includes(hash(servers.mason))) throw new Error("The Mason MCP entry was edited or its ownership is uncertain; retained for manual cleanup.");
3856
4389
  delete servers.mason;
3857
4390
  const desired = { ...config, [key]: servers };
3858
4391
  if (!Object.keys(servers).length) delete desired[key];
@@ -3928,7 +4461,7 @@ async function planTeardown(root, selected) {
3928
4461
  if (diagnostics.length) edits.splice(instructionEditStart);
3929
4462
  if (!diagnostics.length) {
3930
4463
  const cleanup = [];
3931
- const receiptFiles = await fg5(
4464
+ const receiptFiles = await fg6(
3932
4465
  selected.map((host) => ".mason/reports/automation/*/setup-" + host + ".json"),
3933
4466
  { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false }
3934
4467
  );
@@ -3941,7 +4474,7 @@ async function planTeardown(root, selected) {
3941
4474
  if (!remainingHost) await attempt(".mason/local/project.json", async () => {
3942
4475
  const before = await read(".mason/local/project.json");
3943
4476
  if (before === null) return;
3944
- z16.object({ version: z16.literal(1), initializedAt: z16.string() }).strict().parse(JSON.parse(before));
4477
+ z18.object({ version: z18.literal(1), initializedAt: z18.string() }).strict().parse(JSON.parse(before));
3945
4478
  cleanup.push({ path: ".mason/local/project.json", before, after: null });
3946
4479
  });
3947
4480
  if (!diagnostics.length) {
@@ -4013,7 +4546,7 @@ var init_teardown = __esm({
4013
4546
  init_model();
4014
4547
  init_ownership();
4015
4548
  hosts = ["codex", "claude"];
4016
- object2 = z16.record(z16.unknown());
4549
+ object2 = z18.record(z18.unknown());
4017
4550
  json = (value) => JSON.stringify(value, null, 2) + "\n";
4018
4551
  }
4019
4552
  });
@@ -4023,9 +4556,9 @@ import { execFile as execFile11 } from "child_process";
4023
4556
  import { promisify as promisify11 } from "util";
4024
4557
  async function buildMatrix(resolvedRoot) {
4025
4558
  try {
4026
- const { stdout: shallow } = await exec9("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
4559
+ const { stdout: shallow } = await exec8("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
4027
4560
  if (shallow.trim() === "true") return null;
4028
- const { stdout } = await exec9(
4561
+ const { stdout } = await exec8(
4029
4562
  "git",
4030
4563
  ["log", `-n${HISTORY_COMMITS}`, "--format=%x01", "--name-only", "-M"],
4031
4564
  { cwd: resolvedRoot, maxBuffer: 100 * 1024 * 1024 }
@@ -4081,11 +4614,11 @@ async function findMissingPartners(resolvedRoot, changedFiles, existsOnDisk, all
4081
4614
  findings.sort((a, b) => b.rate - a.rate || b.sharedCommits - a.sharedCommits);
4082
4615
  return findings;
4083
4616
  }
4084
- var exec9, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_RATE, MAX_COMMIT_FILES;
4617
+ var exec8, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_RATE, MAX_COMMIT_FILES;
4085
4618
  var init_cochange = __esm({
4086
4619
  "src/review/cochange.ts"() {
4087
4620
  "use strict";
4088
- exec9 = promisify11(execFile11);
4621
+ exec8 = promisify11(execFile11);
4089
4622
  HISTORY_COMMITS = 1500;
4090
4623
  MIN_FILE_COMMITS = 5;
4091
4624
  MIN_SHARED_COMMITS = 4;
@@ -4140,7 +4673,7 @@ var init_types2 = __esm({
4140
4673
  });
4141
4674
 
4142
4675
  // src/review/evidence/vitest.ts
4143
- import { z as z17 } from "zod";
4676
+ import { z as z19 } from "zod";
4144
4677
  function parseVitest(raw, sourceRoot) {
4145
4678
  const report = schema.parse(raw);
4146
4679
  const findings = [], diagnostics = [];
@@ -4183,30 +4716,30 @@ function parseVitest(raw, sourceRoot) {
4183
4716
  diagnostics
4184
4717
  };
4185
4718
  }
4186
- var count2, schema;
4719
+ var count3, schema;
4187
4720
  var init_vitest = __esm({
4188
4721
  "src/review/evidence/vitest.ts"() {
4189
4722
  "use strict";
4190
4723
  init_paths2();
4191
4724
  init_types2();
4192
- count2 = z17.number().int().nonnegative();
4193
- schema = z17.object({
4194
- success: z17.boolean(),
4195
- numTotalTests: count2,
4196
- numPassedTests: count2,
4197
- numFailedTests: count2,
4198
- numPendingTests: count2,
4199
- numTodoTests: count2,
4200
- numFailedTestSuites: count2,
4201
- testResults: z17.array(z17.object({
4202
- name: z17.string().min(1),
4203
- status: z17.enum(["passed", "failed"]),
4204
- message: z17.string().optional(),
4205
- assertionResults: z17.array(z17.object({
4206
- fullName: z17.string(),
4207
- status: z17.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
4208
- failureMessages: z17.array(z17.string()).nullable().optional(),
4209
- location: z17.object({ line: count2, column: count2 }).nullable().optional()
4725
+ count3 = z19.number().int().nonnegative();
4726
+ schema = z19.object({
4727
+ success: z19.boolean(),
4728
+ numTotalTests: count3,
4729
+ numPassedTests: count3,
4730
+ numFailedTests: count3,
4731
+ numPendingTests: count3,
4732
+ numTodoTests: count3,
4733
+ numFailedTestSuites: count3,
4734
+ testResults: z19.array(z19.object({
4735
+ name: z19.string().min(1),
4736
+ status: z19.enum(["passed", "failed"]),
4737
+ message: z19.string().optional(),
4738
+ assertionResults: z19.array(z19.object({
4739
+ fullName: z19.string(),
4740
+ status: z19.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
4741
+ failureMessages: z19.array(z19.string()).nullable().optional(),
4742
+ location: z19.object({ line: count3, column: count3 }).nullable().optional()
4210
4743
  }))
4211
4744
  }))
4212
4745
  });
@@ -4214,7 +4747,7 @@ var init_vitest = __esm({
4214
4747
  });
4215
4748
 
4216
4749
  // src/review/evidence/sarif.ts
4217
- import { z as z18 } from "zod";
4750
+ import { z as z20 } from "zod";
4218
4751
  function parseSarif(raw, sourceRoot) {
4219
4752
  const report = schema2.parse(raw), findings = [], diagnostics = [];
4220
4753
  let active = 0, suppressed = 0, absent = 0, unresolved = 0;
@@ -4305,40 +4838,98 @@ var init_sarif = __esm({
4305
4838
  "use strict";
4306
4839
  init_paths2();
4307
4840
  init_types2();
4308
- index = z18.number().int().nonnegative();
4309
- artifact = z18.object({ uri: z18.string().optional(), uriBaseId: z18.string().optional(), index: index.optional() });
4310
- message = z18.object({ text: z18.string().optional(), markdown: z18.string().optional(), id: z18.string().optional(), arguments: z18.array(z18.string()).optional() });
4311
- level = z18.enum(["error", "warning", "note", "none"]);
4312
- location = z18.object({ physicalLocation: z18.object({ artifactLocation: artifact.optional(), region: z18.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
4313
- schema2 = z18.object({
4314
- version: z18.literal("2.1.0"),
4315
- runs: z18.array(z18.object({
4316
- tool: z18.object({ driver: z18.object({ name: z18.string().min(1), rules: z18.array(z18.object({ id: z18.string(), defaultConfiguration: z18.object({ level: level.optional() }).optional(), messageStrings: z18.record(message).optional() })).optional(), globalMessageStrings: z18.record(message).optional() }) }),
4317
- invocations: z18.array(z18.object({ executionSuccessful: z18.boolean(), commandLine: z18.string().optional(), toolExecutionNotifications: z18.array(z18.object({ level: level.optional(), message })).optional() })).optional(),
4318
- versionControlProvenance: z18.array(z18.object({ revisionId: z18.string().optional() })).optional(),
4319
- originalUriBaseIds: z18.record(artifact).optional(),
4320
- artifacts: z18.array(z18.object({ location: artifact.optional() })).optional(),
4321
- results: z18.array(z18.object({
4322
- ruleId: z18.string().optional(),
4841
+ index = z20.number().int().nonnegative();
4842
+ artifact = z20.object({ uri: z20.string().optional(), uriBaseId: z20.string().optional(), index: index.optional() });
4843
+ message = z20.object({ text: z20.string().optional(), markdown: z20.string().optional(), id: z20.string().optional(), arguments: z20.array(z20.string()).optional() });
4844
+ level = z20.enum(["error", "warning", "note", "none"]);
4845
+ location = z20.object({ physicalLocation: z20.object({ artifactLocation: artifact.optional(), region: z20.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
4846
+ schema2 = z20.object({
4847
+ version: z20.literal("2.1.0"),
4848
+ runs: z20.array(z20.object({
4849
+ tool: z20.object({ driver: z20.object({ name: z20.string().min(1), rules: z20.array(z20.object({ id: z20.string(), defaultConfiguration: z20.object({ level: level.optional() }).optional(), messageStrings: z20.record(message).optional() })).optional(), globalMessageStrings: z20.record(message).optional() }) }),
4850
+ invocations: z20.array(z20.object({ executionSuccessful: z20.boolean(), commandLine: z20.string().optional(), toolExecutionNotifications: z20.array(z20.object({ level: level.optional(), message })).optional() })).optional(),
4851
+ versionControlProvenance: z20.array(z20.object({ revisionId: z20.string().optional() })).optional(),
4852
+ originalUriBaseIds: z20.record(artifact).optional(),
4853
+ artifacts: z20.array(z20.object({ location: artifact.optional() })).optional(),
4854
+ results: z20.array(z20.object({
4855
+ ruleId: z20.string().optional(),
4323
4856
  ruleIndex: index.optional(),
4324
4857
  message,
4325
4858
  level: level.optional(),
4326
- kind: z18.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
4327
- baselineState: z18.enum(["new", "unchanged", "updated", "absent"]).optional(),
4328
- suppressions: z18.array(z18.object({ kind: z18.enum(["inSource", "external"]), status: z18.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
4329
- locations: z18.array(location).optional(),
4330
- relatedLocations: z18.array(location).optional()
4859
+ kind: z20.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
4860
+ baselineState: z20.enum(["new", "unchanged", "updated", "absent"]).optional(),
4861
+ suppressions: z20.array(z20.object({ kind: z20.enum(["inSource", "external"]), status: z20.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
4862
+ locations: z20.array(location).optional(),
4863
+ relatedLocations: z20.array(location).optional()
4331
4864
  })).optional()
4332
4865
  }))
4333
4866
  });
4334
4867
  }
4335
4868
  });
4336
4869
 
4870
+ // src/review/evidence/check.ts
4871
+ import { z as z21 } from "zod";
4872
+ function parseCheck(raw, sourceRoot) {
4873
+ const report = schema3.parse(raw);
4874
+ const diagnostics = [], findings = [];
4875
+ const counts = { total: report.results.length, passed: 0, failed: 0, skipped: 0 };
4876
+ for (const [index2, result] of report.results.entries()) {
4877
+ counts[result.status]++;
4878
+ const locations = result.locations.flatMap((location2) => {
4879
+ const file = evidencePath(location2.file, sourceRoot);
4880
+ if (!file) {
4881
+ diagnostics.push(`Check location is outside the declared checkout or invalid: ${location2.file}`);
4882
+ return [];
4883
+ }
4884
+ return [{ ...location2, file }];
4885
+ });
4886
+ if (result.status === "failed") findings.push({
4887
+ id: String(index2),
4888
+ ...messagePreview([result.name, result.message].filter(Boolean).join("\n")),
4889
+ severity: "error",
4890
+ state: "active",
4891
+ locations
4892
+ });
4893
+ }
4894
+ if (!counts.total) diagnostics.push("No check results were recorded; an empty report is not passing evidence.");
4895
+ if (counts.skipped) diagnostics.push(`${counts.skipped} checks were skipped; verification is incomplete.`);
4896
+ return {
4897
+ outcome: counts.failed ? "failed" : !counts.total ? "unavailable" : !counts.passed ? "skipped" : "passed",
4898
+ counts,
4899
+ findings,
4900
+ incomplete: diagnostics.length > 0,
4901
+ diagnostics,
4902
+ ...report.commit ? { reportedCommits: [report.commit] } : {}
4903
+ };
4904
+ }
4905
+ var schema3;
4906
+ var init_check = __esm({
4907
+ "src/review/evidence/check.ts"() {
4908
+ "use strict";
4909
+ init_paths2();
4910
+ init_types2();
4911
+ schema3 = z21.object({
4912
+ version: z21.literal(1),
4913
+ commit: z21.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).optional(),
4914
+ results: z21.array(z21.object({
4915
+ name: z21.string().min(1).max(1e3),
4916
+ status: z21.enum(["passed", "failed", "skipped"]),
4917
+ message: z21.string().max(1e5).optional(),
4918
+ locations: z21.array(z21.object({
4919
+ file: z21.string().min(1).max(4e3),
4920
+ line: z21.number().int().positive().optional(),
4921
+ column: z21.number().int().positive().optional()
4922
+ })).max(100).default([])
4923
+ })).max(1e5)
4924
+ });
4925
+ }
4926
+ });
4927
+
4337
4928
  // src/review/evidence.ts
4338
- import path19 from "path";
4339
- import { z as z19 } from "zod";
4929
+ import path23 from "path";
4930
+ import { z as z22 } from "zod";
4340
4931
  function relativeArtifact(root, file) {
4341
- const relative = path19.isAbsolute(file) ? path19.relative(root, file) : file;
4932
+ const relative = path23.isAbsolute(file) ? path23.relative(root, file) : file;
4342
4933
  const normalized = normalizeRepoPath(relative);
4343
4934
  if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
4344
4935
  return normalized;
@@ -4377,7 +4968,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4377
4968
  let manifest, raw;
4378
4969
  try {
4379
4970
  manifest = relativeArtifact(root, supplied);
4380
- raw = z19.object({ version: z19.literal(1), checks: z19.array(z19.unknown()) }).parse(await readStoreJson(root, manifest));
4971
+ raw = z22.object({ version: z22.literal(1), checks: z22.array(z22.unknown()) }).parse(await readStoreJson(root, manifest));
4381
4972
  } catch (error) {
4382
4973
  output.diagnostics.push(`${supplied}: ${String(error)}`);
4383
4974
  continue;
@@ -4416,7 +5007,8 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4416
5007
  counts: {},
4417
5008
  incomplete: false,
4418
5009
  diagnostics: [],
4419
- truncated: false
5010
+ truncated: false,
5011
+ ...check.environment ? { environment: check.environment } : {}
4420
5012
  };
4421
5013
  output.checks.push(result);
4422
5014
  if (check.workingTreeClean !== true) result.diagnostics.push("The check's working tree was dirty or not recorded; its results cannot be attributed to the claimed commit alone.");
@@ -4429,13 +5021,13 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4429
5021
  }
4430
5022
  try {
4431
5023
  if (!check.report) throw new Error("Completed check has no report artifact.");
4432
- if (check.kind === "tests" !== (check.report.format === "vitest-json")) throw new Error("Test checks require vitest-json; analysis checks require sarif.");
5024
+ if (check.report.format !== "mason-check-json" && check.kind === "tests" !== (check.report.format === "vitest-json")) throw new Error("Test checks require vitest-json or mason-check-json; analysis checks require sarif or mason-check-json.");
4433
5025
  const file = relativeArtifact(root, check.report.path);
4434
5026
  const report = await readStoreJson(root, file);
4435
5027
  if (report === null) throw new Error(`Report artifact is missing: ${file}`);
4436
5028
  const sourceRoot = check.sourceRoot ?? root;
4437
- if (!path19.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
4438
- const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : parseSarif(report, sourceRoot);
5029
+ if (!path23.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
5030
+ const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
4439
5031
  result.outcome = parsed.outcome;
4440
5032
  result.counts = parsed.counts;
4441
5033
  result.incomplete = parsed.incomplete;
@@ -4455,7 +5047,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4455
5047
  result.outcome = "failed";
4456
5048
  result.diagnostics.push(`Check command exited ${check.exitCode} despite a report with no active failures.`);
4457
5049
  }
4458
- if (check.report.format === "vitest-json" && pairs === void 0) {
5050
+ if (check.kind === "tests" && pairs === void 0) {
4459
5051
  try {
4460
5052
  pairs = (await buildTestMap(root)).paired;
4461
5053
  } catch (error) {
@@ -4529,32 +5121,39 @@ var init_evidence2 = __esm({
4529
5121
  init_provenance();
4530
5122
  init_vitest();
4531
5123
  init_sarif();
5124
+ init_check();
4532
5125
  init_types2();
4533
- checkSchema2 = z19.object({
4534
- id: z19.string().min(1).max(100),
4535
- kind: z19.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
4536
- tool: z19.string().min(1).max(200),
4537
- command: z19.string().min(1).max(2e3),
4538
- commit: z19.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
4539
- workingTreeClean: z19.boolean().optional(),
4540
- source: z19.string().max(2e3).optional(),
4541
- sourceRoot: z19.string().min(1).max(4e3).optional(),
4542
- status: z19.enum(["completed", "skipped", "unavailable"]).default("completed"),
4543
- reason: z19.string().min(1).max(2e3).optional(),
4544
- exitCode: z19.number().int().nullable().optional(),
4545
- report: z19.object({ format: z19.enum(["vitest-json", "sarif"]), path: z19.string().min(1).max(4e3) }).optional()
5126
+ checkSchema2 = z22.object({
5127
+ id: z22.string().min(1).max(100),
5128
+ kind: z22.enum(["tests", "static-analysis", "security", "complexity", "duplication", "documentation"]),
5129
+ tool: z22.string().min(1).max(200),
5130
+ command: z22.string().min(1).max(2e3),
5131
+ commit: z22.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
5132
+ workingTreeClean: z22.boolean().optional(),
5133
+ source: z22.string().max(2e3).optional(),
5134
+ sourceRoot: z22.string().min(1).max(4e3).optional(),
5135
+ status: z22.enum(["completed", "skipped", "unavailable"]).default("completed"),
5136
+ reason: z22.string().min(1).max(2e3).optional(),
5137
+ exitCode: z22.number().int().nullable().optional(),
5138
+ environment: z22.object({
5139
+ toolVersion: z22.string().min(1).max(200),
5140
+ runtime: z22.string().min(1).max(200).optional(),
5141
+ platform: z22.string().min(1).max(200).optional(),
5142
+ configuration: z22.array(z22.string().min(1).max(4e3)).max(100).optional()
5143
+ }).optional(),
5144
+ report: z22.object({ format: z22.enum(["vitest-json", "sarif", "mason-check-json"]), path: z22.string().min(1).max(4e3) }).optional()
4546
5145
  });
4547
5146
  }
4548
5147
  });
4549
5148
 
4550
5149
  // src/review/review.ts
4551
- import fs17 from "fs/promises";
4552
- import path20 from "path";
5150
+ import fs19 from "fs/promises";
5151
+ import path24 from "path";
4553
5152
  import { execFile as execFile12 } from "child_process";
4554
5153
  import { promisify as promisify12 } from "util";
4555
5154
  async function resolveMergeBase(resolvedRoot, base, head) {
4556
5155
  try {
4557
- const { stdout } = await exec10("git", ["merge-base", base, head], {
5156
+ const { stdout } = await exec9("git", ["merge-base", base, head], {
4558
5157
  cwd: resolvedRoot
4559
5158
  });
4560
5159
  return stdout.trim() || null;
@@ -4565,7 +5164,7 @@ async function resolveMergeBase(resolvedRoot, base, head) {
4565
5164
  async function defaultBase(resolvedRoot) {
4566
5165
  for (const ref of ["origin/HEAD", "origin/main", "origin/master", "main"]) {
4567
5166
  try {
4568
- await exec10("git", ["rev-parse", "--verify", "--quiet", ref], {
5167
+ await exec9("git", ["rev-parse", "--verify", "--quiet", ref], {
4569
5168
  cwd: resolvedRoot
4570
5169
  });
4571
5170
  return ref;
@@ -4578,7 +5177,7 @@ function anchorsTouched(record, changedFiles) {
4578
5177
  return matchingPaths(record.files, changedFiles);
4579
5178
  }
4580
5179
  async function computeReview(rootDir, base, options = {}) {
4581
- const resolvedRoot = path20.resolve(rootDir);
5180
+ const resolvedRoot = path24.resolve(rootDir);
4582
5181
  const head = await getCurrentGitHash(resolvedRoot);
4583
5182
  if (head === "unknown") return null;
4584
5183
  const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
@@ -4628,7 +5227,7 @@ async function computeReview(rootDir, base, options = {}) {
4628
5227
  analyzed,
4629
5228
  async (relPath) => {
4630
5229
  try {
4631
- await fs17.access(path20.join(resolvedRoot, relPath));
5230
+ await fs19.access(path24.join(resolvedRoot, relPath));
4632
5231
  return true;
4633
5232
  } catch {
4634
5233
  return false;
@@ -4661,7 +5260,7 @@ async function computeReview(rootDir, base, options = {}) {
4661
5260
  }
4662
5261
  return finalize();
4663
5262
  }
4664
- var exec10, MAX_ANALYZED_FILES;
5263
+ var exec9, MAX_ANALYZED_FILES;
4665
5264
  var init_review = __esm({
4666
5265
  "src/review/review.ts"() {
4667
5266
  "use strict";
@@ -4673,7 +5272,7 @@ var init_review = __esm({
4673
5272
  init_cochange();
4674
5273
  init_evidence2();
4675
5274
  init_snapshot();
4676
- exec10 = promisify12(execFile12);
5275
+ exec9 = promisify12(execFile12);
4677
5276
  MAX_ANALYZED_FILES = 50;
4678
5277
  }
4679
5278
  });
@@ -4682,7 +5281,7 @@ var init_review = __esm({
4682
5281
  async function auditSummary(root) {
4683
5282
  try {
4684
5283
  const report = await computeAudit(root);
4685
- if (!report) return { status: "no-context-files", reason: "No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md found to audit." };
5284
+ if (!report) return { status: "no-context-files", reason: "No README.md or agent instruction files found to audit." };
4686
5285
  if (!report.gitAvailable) return { status: "unavailable", reason: "Audit needs readable Git history to verify documentation claims.", docs: report.docs };
4687
5286
  return {
4688
5287
  ...report,
@@ -4795,7 +5394,16 @@ async function setupStatus(dir) {
4795
5394
  const instructionsCurrent = instructions.every((edit) => edit.before === edit.after);
4796
5395
  const config = await inspectHostConfig(ws.root, host, void 0);
4797
5396
  const mcp = !config.mcpDisabled && hash(config.mcp) === entry.mcpFingerprint && isDeepStrictEqual3({ command: config.mcp?.command, args: config.mcp?.args }, mcpCommand(host));
4798
- const hooks = isDeepStrictEqual3(config.hooks, hookConfig(host, hookCommand(host)).hooks) && !config.disabled;
5397
+ const expectedHooks = hookConfig(host, hookCommand(host)).hooks;
5398
+ const currentHooks = isDeepStrictEqual3(config.hooks, expectedHooks);
5399
+ const compatibleHooks = Object.fromEntries(Object.entries(config.hooks).map(([event, groups]) => [
5400
+ event,
5401
+ groups.map((group) => ({ ...group, hooks: group.hooks.map((handler) => ({
5402
+ ...handler,
5403
+ command: expectedHooks.SessionStart[0].hooks[0].command
5404
+ })) }))
5405
+ ]));
5406
+ const hooks = isDeepStrictEqual3(compatibleHooks, expectedHooks) && !config.disabled;
4799
5407
  const local = await loadSetupReceipt(ws.root, ws.directory, host);
4800
5408
  const configured = local?.status === "configured" && local.root === ws.root && local.revision === entry.revision;
4801
5409
  const observation = await readObservation(ws.root, ws.directory, host, entry.revision, installed.version ?? "unavailable");
@@ -4814,11 +5422,12 @@ async function setupStatus(dir) {
4814
5422
  runtime: installed.available ? "global (" + installed.version + ")" : "unavailable-on-path",
4815
5423
  mcp: mcp ? "configured" : "changed",
4816
5424
  instructions: instructionsCurrent ? "current" : "changed",
4817
- hookConfiguration: hooks ? "configured" : "disabled-or-changed",
5425
+ hookConfiguration: hooks ? currentHooks ? "configured" : "compatible" : "disabled-or-changed",
4818
5426
  observedEvents,
4819
5427
  contextCalls: observation?.contextCalls ?? 0,
4820
5428
  verificationStatus: automation.status === "current" ? automation.verificationStatus ?? "unavailable" : "unavailable",
4821
- pending
5429
+ pending,
5430
+ updates: hooks && !currentHooks ? ["A newer hook template is available. Run mason setup --host " + host + " to update it when convenient; review and commit the configuration changes."] : []
4822
5431
  };
4823
5432
  }
4824
5433
  const statuses = Object.values(hosts2).map((host) => host.status);
@@ -4843,6 +5452,7 @@ function summarizeActivation(status) {
4843
5452
  ` Task context requests: ${state.contextCalls}; verification: ${state.verificationStatus}.`,
4844
5453
  ...state.pending.map((message2) => " Next: " + message2)
4845
5454
  );
5455
+ lines.push(...state.updates.map((message2) => " Optional: " + message2));
4846
5456
  }
4847
5457
  if ("decisionRecords" in status) lines.push(`Decision records: ${status.decisionRecords}.`);
4848
5458
  if (status.next) lines.push(status.next);
@@ -5048,7 +5658,7 @@ function isHookCommand(argv2) {
5048
5658
  async function runAutomationCli(argv2, stdin = "", io = {
5049
5659
  out: (s) => process.stdout.write(s + "\n"),
5050
5660
  err: (s) => process.stderr.write(s + "\n")
5051
- }) {
5661
+ }, options = {}) {
5052
5662
  let action = "";
5053
5663
  try {
5054
5664
  const { values, positionals } = parseCli(argv2);
@@ -5068,7 +5678,9 @@ async function runAutomationCli(argv2, stdin = "", io = {
5068
5678
  return result.status === "complete" ? 0 : 2;
5069
5679
  }
5070
5680
  if (action === "hook") {
5071
- const output = await runAutomationHook(hostSchema.parse(values.host), stdin);
5681
+ const host = hostSchema.parse(values.host);
5682
+ if (options.managedHost && options.managedHost !== host) throw new Error("Managed hook --host does not match its launcher.");
5683
+ const output = await runAutomationHook(host, stdin, { managed: !!options.managedHost });
5072
5684
  if (output) io.out(JSON.stringify(output));
5073
5685
  return 0;
5074
5686
  }