mason-context 0.17.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mason-mcp.js CHANGED
@@ -31,6 +31,32 @@ var init_profile = __esm({
31
31
  }
32
32
  });
33
33
 
34
+ // src/utils/git-read.ts
35
+ import { execFile } from "child_process";
36
+ import { promisify } from "util";
37
+ function execGit(args, options) {
38
+ const command = args.find((arg) => !arg.startsWith("-")) ?? "other";
39
+ return profilePhase("git." + (commands.has(command) ? command : "other"), async () => {
40
+ const { input, ...processOptions } = options;
41
+ const operation = execute("git", args, processOptions);
42
+ if (input !== void 0) {
43
+ operation.child.stdin.on("error", () => {
44
+ });
45
+ operation.child.stdin.end(input);
46
+ }
47
+ return operation;
48
+ });
49
+ }
50
+ var execute, commands;
51
+ var init_git_read = __esm({
52
+ "src/utils/git-read.ts"() {
53
+ "use strict";
54
+ init_profile();
55
+ execute = promisify(execFile);
56
+ commands = /* @__PURE__ */ new Set(["rev-parse", "symbolic-ref", "for-each-ref", "ls-files", "check-ignore", "status", "log", "diff", "ls-tree", "show", "merge-base"]);
57
+ }
58
+ });
59
+
34
60
  // src/audit/commands.ts
35
61
  import path from "path";
36
62
  function splitCommands(line) {
@@ -255,14 +281,14 @@ function extractClaims(content2) {
255
281
  const ignored = computeIgnoredLines(lines);
256
282
  const paths = /* @__PURE__ */ new Map();
257
283
  const counts = [];
258
- const commands = /* @__PURE__ */ new Map();
284
+ const commands2 = /* @__PURE__ */ new Map();
259
285
  const addPath = (claim) => {
260
286
  const key = [claim.relativeTo ?? "scope", claim.path].join(":");
261
287
  if (!paths.has(key)) paths.set(key, claim);
262
288
  };
263
289
  const addCommand = (claim) => {
264
290
  const key = JSON.stringify([claim.scriptName, claim.directory, claim.scopeUnknown]);
265
- if (!commands.has(key)) commands.set(key, claim);
291
+ if (!commands2.has(key)) commands2.set(key, claim);
266
292
  };
267
293
  const parsed = markdown.parse(lines.map((line, i) => ignored[i] ? "" : line).join("\n"), {});
268
294
  for (const token of parsed) {
@@ -364,7 +390,7 @@ function extractClaims(content2) {
364
390
  return {
365
391
  paths: [...paths.values()],
366
392
  counts,
367
- commands: [...commands.values()]
393
+ commands: [...commands2.values()]
368
394
  };
369
395
  }
370
396
  var ROOT_FILE_NAMES, SHELL_FENCE_INFOS, markdown, COUNT_RE, COUNT_DENYLIST_RE, IGNORE_LINE, IGNORE_START, IGNORE_END;
@@ -414,97 +440,6 @@ var init_claims = __esm({
414
440
  }
415
441
  });
416
442
 
417
- // src/audit/git.ts
418
- import { execFile } from "child_process";
419
- import { promisify } from "util";
420
- function parseCommitLine(line) {
421
- const parts = line.split(" ");
422
- if (parts.length < 3 || !parts[0]) return null;
423
- return { hash: parts[0], date: parts[1], subject: parts.slice(2).join(" ") };
424
- }
425
- async function lastCommitOf(resolvedRoot, relPath) {
426
- try {
427
- const { stdout } = await exec(
428
- "git",
429
- ["log", "-1", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
430
- { cwd: resolvedRoot }
431
- );
432
- const line = stdout.trim().split("\n")[0];
433
- return line ? parseCommitLine(line) : null;
434
- } catch {
435
- return null;
436
- }
437
- }
438
- async function deletingCommitOf(resolvedRoot, relPath) {
439
- try {
440
- const { stdout } = await exec(
441
- "git",
442
- [
443
- "log",
444
- "-1",
445
- "--diff-filter=D",
446
- `--format=${COMMIT_FORMAT}`,
447
- "--",
448
- `:(literal)${relPath}`
449
- ],
450
- { cwd: resolvedRoot }
451
- );
452
- const line = stdout.trim().split("\n")[0];
453
- return line ? parseCommitLine(line) : null;
454
- } catch {
455
- return null;
456
- }
457
- }
458
- async function firstCommitOf(resolvedRoot, relPath) {
459
- try {
460
- const { stdout } = await exec(
461
- "git",
462
- ["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
463
- { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
464
- );
465
- const line = stdout.trim().split("\n")[0];
466
- return line ? parseCommitLine(line) : null;
467
- } catch {
468
- return null;
469
- }
470
- }
471
- async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
472
- if (!fromHash || fromHash === "unknown") return null;
473
- try {
474
- const { stdout } = await exec(
475
- "git",
476
- [
477
- "log",
478
- `${fromHash}..HEAD`,
479
- `--format=%x01${COMMIT_FORMAT}`,
480
- "--name-only",
481
- "--",
482
- ...pathspecs
483
- ],
484
- { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
485
- );
486
- const commits = [];
487
- for (const block of stdout.split("")) {
488
- if (!block.trim()) continue;
489
- const lines = block.split("\n").filter((l) => l.trim().length > 0);
490
- const ref = parseCommitLine(lines[0]);
491
- if (!ref) continue;
492
- commits.push({ ...ref, files: lines.slice(1).map((l) => l.trim()) });
493
- }
494
- return { commits, total: commits.length };
495
- } catch {
496
- return null;
497
- }
498
- }
499
- var exec, COMMIT_FORMAT;
500
- var init_git = __esm({
501
- "src/audit/git.ts"() {
502
- "use strict";
503
- exec = promisify(execFile);
504
- COMMIT_FORMAT = "%H%x09%cI%x09%s";
505
- }
506
- });
507
-
508
443
  // src/utils/paths.ts
509
444
  import path2 from "path";
510
445
  function normalizeRepoPath(value) {
@@ -539,8 +474,6 @@ var init_paths = __esm({
539
474
  import fs from "fs/promises";
540
475
  import { constants } from "fs";
541
476
  import path3 from "path";
542
- import { execFile as execFile2 } from "child_process";
543
- import { promisify as promisify2 } from "util";
544
477
  import fg from "fast-glob";
545
478
  function isSensitiveFile(file) {
546
479
  return file.split(/[\\/]/).some(
@@ -594,12 +527,12 @@ async function createFileAccess(rootDir) {
594
527
  const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
595
528
  let gitFiles = null;
596
529
  try {
597
- const { stdout } = await exec2("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], { cwd: root, maxBuffer: 50 * 1024 * 1024 });
530
+ const { stdout } = await execGit(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], { cwd: root, maxBuffer: 50 * 1024 * 1024 });
598
531
  gitFiles = new Set(stdout.split("\0").filter(Boolean));
599
532
  } catch {
600
533
  let inGit = false;
601
534
  try {
602
- await exec2("git", ["rev-parse", "--git-dir"], { cwd: root });
535
+ await execGit(["rev-parse", "--git-dir"], { cwd: root });
603
536
  inGit = true;
604
537
  } catch {
605
538
  }
@@ -642,12 +575,12 @@ async function createFileAccess(rootDir) {
642
575
  }
643
576
  return { root, config, list, read };
644
577
  }
645
- var exec2, SOURCE_EXTENSIONS, SOURCE_GLOB, SOURCE_IGNORE, MAX_SOURCE_BYTES;
578
+ var SOURCE_EXTENSIONS, SOURCE_GLOB, SOURCE_IGNORE, MAX_SOURCE_BYTES;
646
579
  var init_files = __esm({
647
580
  "src/utils/files.ts"() {
648
581
  "use strict";
582
+ init_git_read();
649
583
  init_paths();
650
- exec2 = promisify2(execFile2);
651
584
  SOURCE_EXTENSIONS = ["ts", "tsx", "js", "jsx", "mts", "cts", "mjs", "cjs", "vue", "svelte", "kt", "kts", "java", "py", "go", "rs", "swift", "rb", "cs", "cpp", "c", "h", "hpp", "dart", "php"];
652
585
  SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
653
586
  SOURCE_IGNORE = [
@@ -677,9 +610,140 @@ var init_files = __esm({
677
610
  }
678
611
  });
679
612
 
613
+ // src/audit/inspection.ts
614
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
615
+ import path4 from "path";
616
+ function withRepositoryInspection(root, run) {
617
+ return inspections.run({ root: path4.resolve(root), reads: /* @__PURE__ */ new Map() }, run);
618
+ }
619
+ function inspectionRead(root, key, read) {
620
+ const inspection = inspections.getStore();
621
+ if (!inspection || inspection.root !== path4.resolve(root)) return read();
622
+ if (!inspection.reads.has(key)) {
623
+ const pending = read();
624
+ inspection.reads.set(key, pending);
625
+ void pending.catch(() => {
626
+ if (inspection.reads.get(key) === pending) inspection.reads.delete(key);
627
+ });
628
+ }
629
+ return inspection.reads.get(key);
630
+ }
631
+ function inspectionGit(args, options) {
632
+ return inspectionRead(options.cwd, "git:" + JSON.stringify([args, options]), () => execGit(args, options));
633
+ }
634
+ async function auditGitPaths(root, kind) {
635
+ const shared = inspections.getStore()?.root === path4.resolve(root);
636
+ const specs = shared ? [...docSpecs, ...sourceSpecs] : kind === "documents" ? docSpecs : sourceSpecs;
637
+ const limit = kind === "documents" ? 16 * 1024 * 1024 : 32 * 1024 * 1024;
638
+ const { stdout } = await inspectionGit(
639
+ ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...specs],
640
+ { cwd: root, maxBuffer: shared ? 48 * 1024 * 1024 : limit, timeout: 1e4 }
641
+ );
642
+ const paths = [...new Set(stdout.split("\0").filter(Boolean))].filter((file) => kind === "documents" ? /^(readme|agents|claude)\.md$/i.test(path4.posix.basename(file)) : sourceSuffixes.has(file.slice(file.lastIndexOf("."))));
643
+ if (Buffer.byteLength(paths.join("\0")) > limit) throw new Error("Git " + kind + " inventory exceeds its bounded read limit.");
644
+ return paths.sort();
645
+ }
646
+ var inspections, sourceSpecs, docSpecs, sourceSuffixes;
647
+ var init_inspection = __esm({
648
+ "src/audit/inspection.ts"() {
649
+ "use strict";
650
+ init_git_read();
651
+ init_files();
652
+ inspections = new AsyncLocalStorage2();
653
+ sourceSpecs = SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`);
654
+ docSpecs = [":(icase,glob)**/readme.md", ":(icase,glob)**/agents.md", ":(icase,glob)**/claude.md"];
655
+ sourceSuffixes = new Set(SOURCE_EXTENSIONS.map((extension) => "." + extension));
656
+ }
657
+ });
658
+
659
+ // src/audit/git.ts
660
+ function parseCommitLine(line) {
661
+ const parts = line.split(" ");
662
+ if (parts.length < 3 || !parts[0]) return null;
663
+ return { hash: parts[0], date: parts[1], subject: parts.slice(2).join(" ") };
664
+ }
665
+ async function lastCommitOf(resolvedRoot, relPath) {
666
+ try {
667
+ const { stdout } = await inspectionGit(
668
+ ["log", "-1", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
669
+ { cwd: resolvedRoot }
670
+ );
671
+ const line = stdout.trim().split("\n")[0];
672
+ return line ? parseCommitLine(line) : null;
673
+ } catch {
674
+ return null;
675
+ }
676
+ }
677
+ async function deletingCommitOf(resolvedRoot, relPath) {
678
+ try {
679
+ const { stdout } = await inspectionGit(
680
+ [
681
+ "log",
682
+ "-1",
683
+ "--diff-filter=D",
684
+ `--format=${COMMIT_FORMAT}`,
685
+ "--",
686
+ `:(literal)${relPath}`
687
+ ],
688
+ { cwd: resolvedRoot }
689
+ );
690
+ const line = stdout.trim().split("\n")[0];
691
+ return line ? parseCommitLine(line) : null;
692
+ } catch {
693
+ return null;
694
+ }
695
+ }
696
+ async function firstCommitOf(resolvedRoot, relPath) {
697
+ try {
698
+ const { stdout } = await inspectionGit(
699
+ ["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
700
+ { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
701
+ );
702
+ const line = stdout.trim().split("\n")[0];
703
+ return line ? parseCommitLine(line) : null;
704
+ } catch {
705
+ return null;
706
+ }
707
+ }
708
+ async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
709
+ if (!fromHash || fromHash === "unknown") return null;
710
+ try {
711
+ const { stdout } = await inspectionGit(
712
+ [
713
+ "log",
714
+ `${fromHash}..HEAD`,
715
+ `--format=%x01${COMMIT_FORMAT}`,
716
+ "--name-only",
717
+ "--",
718
+ ...pathspecs
719
+ ],
720
+ { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
721
+ );
722
+ const commits = [];
723
+ for (const block of stdout.split("")) {
724
+ if (!block.trim()) continue;
725
+ const lines = block.split("\n").filter((l) => l.trim().length > 0);
726
+ const ref = parseCommitLine(lines[0]);
727
+ if (!ref) continue;
728
+ commits.push({ ...ref, files: lines.slice(1).map((l) => l.trim()) });
729
+ }
730
+ return { commits, total: commits.length };
731
+ } catch {
732
+ return null;
733
+ }
734
+ }
735
+ var COMMIT_FORMAT;
736
+ var init_git = __esm({
737
+ "src/audit/git.ts"() {
738
+ "use strict";
739
+ init_inspection();
740
+ COMMIT_FORMAT = "%H%x09%cI%x09%s";
741
+ }
742
+ });
743
+
680
744
  // src/utils/storage.ts
681
745
  import fs2 from "fs/promises";
682
- import path4 from "path";
746
+ import path5 from "path";
683
747
  import { randomUUID } from "crypto";
684
748
  async function storePath(root, relative, createParents = false) {
685
749
  const normalized = normalizeRepoPath(relative);
@@ -687,7 +751,7 @@ async function storePath(root, relative, createParents = false) {
687
751
  let current = await fs2.realpath(root);
688
752
  const parts = normalized.split("/");
689
753
  for (let i = 0; i < parts.length; i++) {
690
- current = path4.join(current, parts[i]);
754
+ current = path5.join(current, parts[i]);
691
755
  let stat;
692
756
  try {
693
757
  stat = await fs2.lstat(current);
@@ -725,7 +789,7 @@ async function writeStoreJson(root, relative, value) {
725
789
  throw new Error(`Mason store ${relative} exceeds 10 MiB`);
726
790
  }
727
791
  const file = await storePath(root, relative, true);
728
- const temporary = path4.join(path4.dirname(file), `.${path4.basename(file)}.${randomUUID()}.tmp`);
792
+ const temporary = path5.join(path5.dirname(file), `.${path5.basename(file)}.${randomUUID()}.tmp`);
729
793
  try {
730
794
  const handle = await fs2.open(temporary, "wx", 384);
731
795
  try {
@@ -750,9 +814,7 @@ var init_storage = __esm({
750
814
  // src/audit/inputs.ts
751
815
  import fs3 from "fs/promises";
752
816
  import * as nativeFs from "fs";
753
- import path5 from "path";
754
- import { execFile as execFile3 } from "child_process";
755
- import { promisify as promisify3 } from "util";
817
+ import path6 from "path";
756
818
  import fg2 from "fast-glob";
757
819
  async function auditInputPath(root, file) {
758
820
  if (file === ".") return fs3.realpath(root);
@@ -779,7 +841,7 @@ async function auditGlob(root, patterns, options) {
779
841
  if (Array.isArray(patterns) && !patterns.length) return [];
780
842
  const directories = /* @__PURE__ */ new Set();
781
843
  const readdir2 = ((directory, opts, callback) => {
782
- const relative = path5.relative(root, directory).split(path5.sep).join("/") || ".";
844
+ const relative = path6.relative(root, directory).split(path6.sep).join("/") || ".";
783
845
  directories.add(relative);
784
846
  if (directories.size > MAX_INPUTS) {
785
847
  callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
@@ -790,7 +852,7 @@ async function auditGlob(root, patterns, options) {
790
852
  }, callback);
791
853
  });
792
854
  const lstat2 = ((file, callback) => {
793
- const relative = path5.relative(root, file).split(path5.sep).join("/") || ".";
855
+ const relative = path6.relative(root, file).split(path6.sep).join("/") || ".";
794
856
  auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
795
857
  });
796
858
  const stream = fg2.stream(patterns, {
@@ -827,62 +889,48 @@ async function auditGlob(root, patterns, options) {
827
889
  return matches.sort();
828
890
  }
829
891
  async function gitSourcePaths(root) {
830
- const { stdout } = await execute(
831
- "git",
832
- [
833
- "ls-files",
834
- "-z",
835
- "--cached",
836
- "--others",
837
- "--exclude-standard",
838
- "--",
839
- ...SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`)
840
- ],
841
- { cwd: root, maxBuffer: 32 * 1024 * 1024, timeout: 1e4 }
842
- );
843
- return [...new Set(stdout.split("\0").filter(Boolean))].sort();
892
+ return auditGitPaths(root, "sources");
844
893
  }
845
894
  async function gitIgnoredPaths(root, files) {
846
895
  if (!files.length) return /* @__PURE__ */ new Set();
847
- const operation = execute(
848
- "git",
849
- ["check-ignore", "-z", "--stdin"],
850
- { cwd: root, maxBuffer: 4 * 1024 * 1024, timeout: 1e4 }
851
- );
852
- operation.child.stdin.on("error", () => {
853
- });
854
- operation.child.stdin.end(files.join("\0") + "\0");
855
- try {
856
- return new Set((await operation).stdout.split("\0").filter(Boolean));
857
- } catch (error) {
858
- if (error.code === 1) return /* @__PURE__ */ new Set();
859
- throw error;
896
+ const ignored = /* @__PURE__ */ new Set();
897
+ for (let offset = 0; offset < files.length; offset += 1e3) {
898
+ try {
899
+ const { stdout } = await execGit(["check-ignore", "-z", "--stdin"], {
900
+ cwd: root,
901
+ maxBuffer: 4 * 1024 * 1024,
902
+ timeout: 1e4,
903
+ input: files.slice(offset, offset + 1e3).join("\0") + "\0"
904
+ });
905
+ for (const file of stdout.split("\0").filter(Boolean)) ignored.add(file);
906
+ } catch (error) {
907
+ if (error.code !== 1) throw error;
908
+ }
860
909
  }
910
+ return ignored;
861
911
  }
862
- var execute, MAX_INPUTS;
912
+ var MAX_INPUTS;
863
913
  var init_inputs = __esm({
864
914
  "src/audit/inputs.ts"() {
865
915
  "use strict";
866
916
  init_files();
867
917
  init_storage();
868
- execute = promisify3(execFile3);
918
+ init_git_read();
919
+ init_inspection();
869
920
  MAX_INPUTS = 1e5;
870
921
  }
871
922
  });
872
923
 
873
924
  // src/audit/docs.ts
874
925
  import fs4 from "fs/promises";
875
- import path6 from "path";
876
- import { execFile as execFile4 } from "child_process";
877
- import { promisify as promisify4 } from "util";
926
+ import path7 from "path";
878
927
  import fg3 from "fast-glob";
879
928
  async function dirtyDocs(resolvedRoot, files) {
880
929
  if (!files.length) return /* @__PURE__ */ new Set();
881
930
  try {
882
931
  const dirty = /* @__PURE__ */ new Set();
883
932
  for (let offset = 0; offset < files.length; offset += 256) {
884
- const { stdout } = await exec3(
885
- "git",
933
+ const { stdout } = await execGit(
886
934
  ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...files.slice(offset, offset + 256).map((file) => `:(literal)${file}`)],
887
935
  { cwd: resolvedRoot, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 }
888
936
  );
@@ -902,25 +950,10 @@ async function dirtyDocs(resolvedRoot, files) {
902
950
  async function discoverDocPaths(root) {
903
951
  let candidates = [];
904
952
  try {
905
- const { stdout } = await exec3(
906
- "git",
907
- [
908
- "ls-files",
909
- "-z",
910
- "--cached",
911
- "--others",
912
- "--exclude-standard",
913
- "--",
914
- ":(icase,glob)**/readme.md",
915
- ":(icase,glob)**/agents.md",
916
- ":(icase,glob)**/claude.md"
917
- ],
918
- { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 }
919
- );
920
- candidates = stdout.split("\0").filter(Boolean);
953
+ candidates = await auditGitPaths(root, "documents");
921
954
  } catch (error) {
922
955
  try {
923
- await exec3("git", ["rev-parse", "--git-dir"], { cwd: root });
956
+ await execGit(["rev-parse", "--git-dir"], { cwd: root });
924
957
  } catch {
925
958
  return localDocPaths(root);
926
959
  }
@@ -967,14 +1000,14 @@ async function localDocPaths(root) {
967
1000
  throw error;
968
1001
  }
969
1002
  for (const entry of entries) if (/^(?:agents|claude|readme)\.md$/i.test(entry.name)) {
970
- paths.push(path6.posix.join(dir, entry.name));
1003
+ paths.push(path7.posix.join(dir, entry.name));
971
1004
  }
972
1005
  }
973
1006
  return paths;
974
1007
  }
975
1008
  function documentScope(file) {
976
- const parent = path6.posix.dirname(file);
977
- return path6.posix.basename(parent).toLowerCase() === ".claude" ? path6.posix.dirname(parent) : parent;
1009
+ const parent = path7.posix.dirname(file);
1010
+ return path7.posix.basename(parent).toLowerCase() === ".claude" ? path7.posix.dirname(parent) : parent;
978
1011
  }
979
1012
  async function discoverDocs(resolvedRoot) {
980
1013
  return profilePhase("audit.documents", () => readDocs(resolvedRoot));
@@ -994,7 +1027,7 @@ async function readDocs(resolvedRoot) {
994
1027
  path: candidate,
995
1028
  content: content2,
996
1029
  scope: documentScope(candidate),
997
- kind: /^readme\.md$/i.test(path6.posix.basename(candidate)) ? "readme" : "instructions",
1030
+ kind: /^readme\.md$/i.test(path7.posix.basename(candidate)) ? "readme" : "instructions",
998
1031
  lineCount: content2.split("\n").length,
999
1032
  lastCommit,
1000
1033
  dirty: dirty.has(candidate),
@@ -1004,16 +1037,17 @@ async function readDocs(resolvedRoot) {
1004
1037
  }
1005
1038
  return docs;
1006
1039
  }
1007
- var exec3, DOC_CANDIDATES;
1040
+ var DOC_CANDIDATES;
1008
1041
  var init_docs = __esm({
1009
1042
  "src/audit/docs.ts"() {
1010
1043
  "use strict";
1044
+ init_git_read();
1011
1045
  init_profile();
1012
1046
  init_claims();
1013
1047
  init_git();
1014
1048
  init_inputs();
1015
1049
  init_files();
1016
- exec3 = promisify4(execFile4);
1050
+ init_inspection();
1017
1051
  DOC_CANDIDATES = [
1018
1052
  "AGENTS.md",
1019
1053
  "CLAUDE.md",
@@ -1024,14 +1058,14 @@ var init_docs = __esm({
1024
1058
 
1025
1059
  // src/audit/scope.ts
1026
1060
  import fs5 from "fs/promises";
1027
- import path7 from "path";
1061
+ import path8 from "path";
1028
1062
  function scopedPath(directory, value) {
1029
- if (path7.posix.isAbsolute(value) || value.includes("\\") || /[\x00-\x1f]/.test(value)) return null;
1030
- const resolved = path7.posix.normalize(path7.posix.join(directory, value));
1063
+ if (path8.posix.isAbsolute(value) || value.includes("\\") || /[\x00-\x1f]/.test(value)) return null;
1064
+ const resolved = path8.posix.normalize(path8.posix.join(directory, value));
1031
1065
  return resolved === ".." || resolved.startsWith("../") ? null : resolved;
1032
1066
  }
1033
1067
  function pathClaimScope(doc, claim) {
1034
- const directory = claim.relativeTo === "document" ? path7.posix.dirname(doc) : documentScope(doc);
1068
+ const directory = claim.relativeTo === "document" ? path8.posix.dirname(doc) : documentScope(doc);
1035
1069
  const local = scopedPath(directory, claim.path);
1036
1070
  if (!local) return null;
1037
1071
  const candidates = claim.relativeTo === "document" || directory === "." ? [local] : [...new Set([local, scopedPath(".", claim.path)].filter((p) => p !== null))];
@@ -1064,14 +1098,113 @@ var init_scope = __esm({
1064
1098
  }
1065
1099
  });
1066
1100
 
1101
+ // src/utils/store-lock.ts
1102
+ import fs6 from "fs/promises";
1103
+ import os from "os";
1104
+ async function lockDiagnostic(file) {
1105
+ let reason2;
1106
+ try {
1107
+ const owner = JSON.parse(await fs6.readFile(file, "utf8"));
1108
+ if (!owner || typeof owner.host !== "string" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
1109
+ reason2 = "Lock owner data is incomplete or malformed.";
1110
+ } else if (owner.host !== os.hostname()) {
1111
+ reason2 = "The lock belongs to another host; its process cannot be checked locally.";
1112
+ } else {
1113
+ try {
1114
+ process.kill(owner.pid, 0);
1115
+ reason2 = `Local owner PID ${owner.pid} is still running.`;
1116
+ } catch (error) {
1117
+ reason2 = error.code === "ESRCH" ? `Local owner PID ${owner.pid} has exited, but lock recovery has not completed.` : `Local owner PID ${owner.pid} could not be checked.`;
1118
+ }
1119
+ }
1120
+ } catch (error) {
1121
+ reason2 = error instanceof SyntaxError ? "Lock owner data is incomplete or malformed." : "Lock owner data could not be read; the lock may have changed.";
1122
+ }
1123
+ if (await fs6.lstat(file + ".reclaim").then(() => true, () => false)) {
1124
+ reason2 += " A recovery guard also needs inspection: " + file + ".reclaim.";
1125
+ }
1126
+ return reason2 + " Retry if a writer is active. For manual recovery, stop all Mason writers for this checkout, confirm they have exited (including other hosts), then remove only confirmed abandoned lock and lock.reclaim files. See docs/reference.md#snapshot-repairs-and-verification.";
1127
+ }
1128
+ async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason store") {
1129
+ const file = await storePath(root, directory + "/lock", true);
1130
+ const deadline = Date.now() + waitMs;
1131
+ let handle;
1132
+ while (!handle) {
1133
+ try {
1134
+ handle = await fs6.open(file, "wx", 384);
1135
+ try {
1136
+ await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
1137
+ } catch (error) {
1138
+ await handle.close().catch(() => {
1139
+ });
1140
+ handle = void 0;
1141
+ await fs6.rm(file, { force: true }).catch(() => {
1142
+ });
1143
+ throw error;
1144
+ }
1145
+ } catch (error) {
1146
+ if (error.code !== "EEXIST") throw error;
1147
+ try {
1148
+ const owner = JSON.parse(await fs6.readFile(file, "utf8"));
1149
+ if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
1150
+ try {
1151
+ process.kill(owner.pid, 0);
1152
+ } catch (probe) {
1153
+ if (probe.code === "ESRCH") {
1154
+ const reclaim = file + ".reclaim";
1155
+ let guard;
1156
+ try {
1157
+ guard = await fs6.open(reclaim, "wx", 384);
1158
+ const current = JSON.parse(await fs6.readFile(file, "utf8"));
1159
+ if (current.pid === owner.pid && current.host === owner.host) await fs6.unlink(file);
1160
+ } finally {
1161
+ if (guard) {
1162
+ await guard.close();
1163
+ await fs6.rm(reclaim, { force: true });
1164
+ }
1165
+ }
1166
+ }
1167
+ }
1168
+ }
1169
+ } catch {
1170
+ }
1171
+ if (Date.now() >= deadline) throw new Error(label + " is busy or its lock needs inspection: " + file + ". " + await lockDiagnostic(file));
1172
+ await new Promise((resolve2) => setTimeout(resolve2, 40));
1173
+ }
1174
+ }
1175
+ try {
1176
+ return await run();
1177
+ } finally {
1178
+ await handle.close();
1179
+ await fs6.unlink(file);
1180
+ }
1181
+ }
1182
+ var init_store_lock = __esm({
1183
+ "src/utils/store-lock.ts"() {
1184
+ "use strict";
1185
+ init_storage();
1186
+ }
1187
+ });
1188
+
1189
+ // src/snapshot/lock.ts
1190
+ function withSnapshotWrite(root, run, waitMs = 5e3) {
1191
+ return withStoreLock(root, ".mason/local/snapshot-write", run, waitMs, "Snapshot store");
1192
+ }
1193
+ var init_lock = __esm({
1194
+ "src/snapshot/lock.ts"() {
1195
+ "use strict";
1196
+ init_store_lock();
1197
+ }
1198
+ });
1199
+
1067
1200
  // src/test-map.ts
1068
1201
  var test_map_exports = {};
1069
1202
  __export(test_map_exports, {
1070
1203
  buildTestMap: () => buildTestMap
1071
1204
  });
1072
- import path8 from "path";
1205
+ import path9 from "path";
1073
1206
  async function buildTestMap(dir) {
1074
- const rootDir = path8.resolve(dir);
1207
+ const rootDir = path9.resolve(dir);
1075
1208
  const access = await createFileAccess(rootDir);
1076
1209
  const testPatterns = [
1077
1210
  "**/*.test.*",
@@ -1092,7 +1225,7 @@ async function buildTestMap(dir) {
1092
1225
  const sourceByBaseName = /* @__PURE__ */ new Map();
1093
1226
  for (const file of sourceFiles) {
1094
1227
  if (testFiles.includes(file)) continue;
1095
- const baseName = path8.basename(file).replace(/\.[^.]+$/, "");
1228
+ const baseName = path9.basename(file).replace(/\.[^.]+$/, "");
1096
1229
  const existing = sourceByBaseName.get(baseName) ?? [];
1097
1230
  existing.push(file);
1098
1231
  sourceByBaseName.set(baseName, existing);
@@ -1100,7 +1233,7 @@ async function buildTestMap(dir) {
1100
1233
  const paired = [];
1101
1234
  const unmatched = [];
1102
1235
  for (const testFile of testFiles) {
1103
- const testBaseName = path8.basename(testFile).replace(/\.[^.]+$/, "");
1236
+ const testBaseName = path9.basename(testFile).replace(/\.[^.]+$/, "");
1104
1237
  const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
1105
1238
  if (!sourceName) {
1106
1239
  unmatched.push(testFile);
@@ -1108,10 +1241,10 @@ async function buildTestMap(dir) {
1108
1241
  }
1109
1242
  const candidates = sourceByBaseName.get(sourceName);
1110
1243
  if (candidates && candidates.length > 0) {
1111
- const testDir = path8.dirname(testFile);
1244
+ const testDir = path9.dirname(testFile);
1112
1245
  const bestMatch = candidates.reduce((best, candidate) => {
1113
- const candidateDir = path8.dirname(candidate);
1114
- const bestDir = path8.dirname(best);
1246
+ const candidateDir = path9.dirname(candidate);
1247
+ const bestDir = path9.dirname(best);
1115
1248
  const candidateOverlap = commonSegments(testDir, candidateDir);
1116
1249
  const bestOverlap = commonSegments(testDir, bestDir);
1117
1250
  return candidateOverlap > bestOverlap ? candidate : best;
@@ -1145,9 +1278,7 @@ var init_test_map = __esm({
1145
1278
  });
1146
1279
 
1147
1280
  // src/snapshot/snapshot.ts
1148
- import path9 from "path";
1149
- import { execFile as execFile5 } from "child_process";
1150
- import { promisify as promisify5 } from "util";
1281
+ import path10 from "path";
1151
1282
  import { z } from "zod";
1152
1283
  function normalizeFeatureType(value) {
1153
1284
  return value === "infrastructure" ? "infrastructure" : "capability";
@@ -1171,12 +1302,19 @@ async function inspectSnapshot(rootDir) {
1171
1302
  }] };
1172
1303
  }
1173
1304
  }
1174
- async function saveSnapshot(rootDir, snapshot) {
1175
- await writeStoreJson(rootDir, ".mason/snapshot.json", snapshotSchema.parse(snapshot));
1305
+ async function updateSnapshot(rootDir, update) {
1306
+ return withSnapshotWrite(rootDir, async () => {
1307
+ const change = await update(await loadSnapshot(rootDir));
1308
+ if (change.snapshot) {
1309
+ await writeStoreJson(rootDir, ".mason/snapshot.json", snapshotSchema.parse(change.snapshot));
1310
+ await change.afterSave?.();
1311
+ }
1312
+ return change.result;
1313
+ });
1176
1314
  }
1177
1315
  async function getCurrentGitHash(rootDir) {
1178
1316
  try {
1179
- const { stdout } = await exec4("git", ["rev-parse", "HEAD"], {
1317
+ const { stdout } = await execGit(["rev-parse", "HEAD"], {
1180
1318
  cwd: rootDir
1181
1319
  });
1182
1320
  return stdout.trim();
@@ -1188,7 +1326,7 @@ async function listSourceFiles(resolvedRoot) {
1188
1326
  return (await createFileAccess(resolvedRoot)).list();
1189
1327
  }
1190
1328
  async function prepareSnapshotBatch(rootDir, offset, batchSize = DEFAULT_BATCH_SIZE, scopeFiles) {
1191
- const resolvedRoot = path9.resolve(rootDir);
1329
+ const resolvedRoot = path10.resolve(rootDir);
1192
1330
  const access = await createFileAccess(resolvedRoot);
1193
1331
  let allFiles = await access.list();
1194
1332
  if (scopeFiles) {
@@ -1237,21 +1375,23 @@ async function prepareSnapshotBatch(rootDir, offset, batchSize = DEFAULT_BATCH_S
1237
1375
  testPairs
1238
1376
  };
1239
1377
  }
1240
- var exec4, repoPath, verificationFields, featureSchema, flowSchema, snapshotSchema, DEFAULT_BATCH_SIZE, SKELETON_CHARS, DEEP_SAMPLE_CHARS, DEEP_SAMPLES_PER_BATCH;
1378
+ var repoPath, verificationFields, featureSchema, flowSchema, snapshotSchema, DEFAULT_BATCH_SIZE, SKELETON_CHARS, DEEP_SAMPLE_CHARS, DEEP_SAMPLES_PER_BATCH;
1241
1379
  var init_snapshot = __esm({
1242
1380
  "src/snapshot/snapshot.ts"() {
1243
1381
  "use strict";
1382
+ init_git_read();
1244
1383
  init_files();
1245
1384
  init_files();
1246
1385
  init_storage();
1386
+ init_lock();
1247
1387
  init_paths();
1248
1388
  init_test_map();
1249
- exec4 = promisify5(execFile5);
1250
1389
  repoPath = z.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
1251
1390
  verificationFields = {
1252
1391
  refreshedHash: z.string().optional(),
1253
1392
  verifiedAt: z.string().optional(),
1254
1393
  verifiedHash: z.string().optional(),
1394
+ verificationToken: z.string().optional(),
1255
1395
  verificationFailed: z.boolean().optional(),
1256
1396
  verificationNote: z.string().optional()
1257
1397
  };
@@ -1279,10 +1419,8 @@ var init_snapshot = __esm({
1279
1419
  });
1280
1420
 
1281
1421
  // src/drift/drift.ts
1282
- import fs6 from "fs/promises";
1283
- import path10 from "path";
1284
- import { execFile as execFile6 } from "child_process";
1285
- import { promisify as promisify6 } from "util";
1422
+ import fs7 from "fs/promises";
1423
+ import path11 from "path";
1286
1424
  function parseChanges(output) {
1287
1425
  const fields = output.split("\0");
1288
1426
  const changes = [];
@@ -1303,7 +1441,7 @@ function touchedPaths(changes) {
1303
1441
  async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
1304
1442
  if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-")) return null;
1305
1443
  try {
1306
- const { stdout } = await exec5("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
1444
+ const { stdout } = await execGit(["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
1307
1445
  return parseChanges(stdout);
1308
1446
  } catch {
1309
1447
  return null;
@@ -1312,8 +1450,8 @@ async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
1312
1450
  async function getWorkingTree(resolvedRoot) {
1313
1451
  try {
1314
1452
  const [diff, untracked] = await Promise.all([
1315
- exec5("git", ["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
1316
- exec5("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
1453
+ execGit(["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
1454
+ execGit(["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
1317
1455
  ]);
1318
1456
  const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
1319
1457
  return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
@@ -1324,8 +1462,7 @@ async function getWorkingTree(resolvedRoot) {
1324
1462
  async function countCommitsBehind(resolvedRoot, fromHash) {
1325
1463
  if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-")) return null;
1326
1464
  try {
1327
- const { stdout } = await exec5(
1328
- "git",
1465
+ const { stdout } = await execGit(
1329
1466
  ["rev-list", "--count", `${fromHash}..HEAD`],
1330
1467
  { cwd: resolvedRoot }
1331
1468
  );
@@ -1350,7 +1487,7 @@ async function findGhostFiles(resolvedRoot, mappedFiles) {
1350
1487
  const ghosts = [];
1351
1488
  for (const file of mappedFiles) {
1352
1489
  try {
1353
- await fs6.access(path10.join(resolvedRoot, file));
1490
+ await fs7.access(path11.join(resolvedRoot, file));
1354
1491
  } catch {
1355
1492
  ghosts.push(file);
1356
1493
  }
@@ -1358,7 +1495,7 @@ async function findGhostFiles(resolvedRoot, mappedFiles) {
1358
1495
  return ghosts.sort();
1359
1496
  }
1360
1497
  async function computeDrift(rootDir) {
1361
- const root = path10.resolve(rootDir);
1498
+ const root = path11.resolve(rootDir);
1362
1499
  const snapshot = await loadSnapshot(root);
1363
1500
  if (!snapshot) return null;
1364
1501
  const [headHash, workingTree] = await Promise.all([getCurrentGitHash(root), getWorkingTree(root)]);
@@ -1415,7 +1552,7 @@ async function computeDrift(rootDir) {
1415
1552
  const sourceFiles = new Set(await listSourceFiles(root));
1416
1553
  let committedFiles = /* @__PURE__ */ new Set();
1417
1554
  try {
1418
- const { stdout } = await exec5("git", ["ls-tree", "-r", "--name-only", "-z", "HEAD"], { cwd: root, maxBuffer: 50 * 1024 * 1024 });
1555
+ const { stdout } = await execGit(["ls-tree", "-r", "--name-only", "-z", "HEAD"], { cwd: root, maxBuffer: 50 * 1024 * 1024 });
1419
1556
  committedFiles = new Set(stdout.split("\0").filter(Boolean));
1420
1557
  } catch {
1421
1558
  report.historyAvailable = false;
@@ -1435,13 +1572,13 @@ async function computeDrift(rootDir) {
1435
1572
  else report.recommendation = changedMapped.size >= FULL_REBUILD_MIN_CHANGED_MAPPED_FILES && changedMapped.size / Math.max(1, mappedFiles.size) > FULL_REBUILD_FRACTION ? "full-rebuild" : "incremental";
1436
1573
  return report;
1437
1574
  }
1438
- var exec5, FULL_REBUILD_FRACTION, FULL_REBUILD_MIN_CHANGED_MAPPED_FILES;
1575
+ var FULL_REBUILD_FRACTION, FULL_REBUILD_MIN_CHANGED_MAPPED_FILES;
1439
1576
  var init_drift = __esm({
1440
1577
  "src/drift/drift.ts"() {
1441
1578
  "use strict";
1579
+ init_git_read();
1442
1580
  init_snapshot();
1443
1581
  init_paths();
1444
- exec5 = promisify6(execFile6);
1445
1582
  FULL_REBUILD_FRACTION = 0.4;
1446
1583
  FULL_REBUILD_MIN_CHANGED_MAPPED_FILES = 10;
1447
1584
  }
@@ -1464,7 +1601,7 @@ var init_types = __esm({
1464
1601
  });
1465
1602
 
1466
1603
  // src/audit/checks/deleted-reference.ts
1467
- import path11 from "path";
1604
+ import path12 from "path";
1468
1605
  async function checkDeletedReferences(ctx) {
1469
1606
  const result = emptyResult();
1470
1607
  for (const doc of ctx.docs) {
@@ -1493,7 +1630,7 @@ async function checkDeletedReferences(ctx) {
1493
1630
  const candidates = await Promise.all(scope.candidates.map(async (file) => ({
1494
1631
  file,
1495
1632
  tracked: await lastCommitOf(ctx.root, file),
1496
- parent: await pathExists(ctx.root, path11.posix.dirname(file))
1633
+ parent: await pathExists(ctx.root, path12.posix.dirname(file))
1497
1634
  })));
1498
1635
  const candidate = candidates.find((c) => c.tracked || renames.has(c.file)) ?? candidates.find((c) => c.parent);
1499
1636
  if (!candidate && claim.relativeTo !== "document") continue;
@@ -1532,8 +1669,8 @@ var init_deleted_reference = __esm({
1532
1669
 
1533
1670
  // src/audit/checks/new-module.ts
1534
1671
  import fg4 from "fast-glob";
1535
- import fs7 from "fs/promises";
1536
- import path12 from "path";
1672
+ import fs8 from "fs/promises";
1673
+ import path13 from "path";
1537
1674
  function escapeRegExp(text2) {
1538
1675
  return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1539
1676
  }
@@ -1571,23 +1708,36 @@ async function checkNewModules(ctx) {
1571
1708
  return result;
1572
1709
  }
1573
1710
  function moduleDocumentation(docs) {
1574
- return docs.map((doc) => doc.content + "\n" + (path12.posix.dirname(doc.path) === "." ? "" : path12.posix.dirname(doc.path) + "/")).join("\n");
1711
+ return docs.map((doc) => doc.content + "\n" + (path13.posix.dirname(doc.path) === "." ? "" : path13.posix.dirname(doc.path) + "/")).join("\n");
1575
1712
  }
1576
- async function moduleCandidates(root, combinedDocs) {
1713
+ function moduleCandidates(root, combinedDocs) {
1714
+ return inspectionRead(root, "modules:" + combinedDocs, () => collectModuleCandidates(root, combinedDocs));
1715
+ }
1716
+ async function collectModuleCandidates(root, combinedDocs) {
1577
1717
  const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
1578
1718
  const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
1579
1719
  const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
1580
- const listSubdirs = async (dir) => {
1581
- const entries = await fs7.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1582
- 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));
1583
- const ignored = await gitIgnoredPaths(root, dirs);
1584
- const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
1585
- return (await auditGlob(root, dir === "." ? "*" : `${fg4.escapePath(dir)}/*`, {
1586
- ignore,
1587
- onlyDirectories: true,
1588
- label: "Module directory discovery",
1589
- select: (file) => visible.has(file)
1590
- })).map((file) => path12.posix.basename(file));
1720
+ const listSubdirs = async (parents) => {
1721
+ const byParent = /* @__PURE__ */ new Map();
1722
+ for (let offset = 0; offset < parents.length; offset += 16) {
1723
+ await Promise.all(parents.slice(offset, offset + 16).map(async (dir) => {
1724
+ const entries = await fs8.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1725
+ const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink() && sources.some((file) => file.startsWith(path13.posix.join(dir, entry.name) + "/"))) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path13.posix.join(dir, entry.name));
1726
+ byParent.set(dir, dirs);
1727
+ }));
1728
+ }
1729
+ const ignored = await gitIgnoredPaths(root, [...byParent.values()].flat());
1730
+ const result = /* @__PURE__ */ new Map();
1731
+ for (const dir of parents) {
1732
+ const visible = new Set(byParent.get(dir).filter((candidate) => !ignored.has(candidate) || sources.some((file) => file.startsWith(candidate + "/"))));
1733
+ result.set(dir, (await auditGlob(root, dir === "." ? "*" : `${fg4.escapePath(dir)}/*`, {
1734
+ ignore,
1735
+ onlyDirectories: true,
1736
+ label: "Module directory discovery",
1737
+ select: (file) => visible.has(file)
1738
+ })).map((file) => path13.posix.basename(file)));
1739
+ }
1740
+ return result;
1591
1741
  };
1592
1742
  const countSourceFiles = async (dir) => {
1593
1743
  const files = sources.filter((file) => file.startsWith(dir + "/"));
@@ -1603,14 +1753,16 @@ async function moduleCandidates(root, combinedDocs) {
1603
1753
  return count4;
1604
1754
  };
1605
1755
  const candidates = [];
1606
- for (const topDir of await listSubdirs(".")) {
1756
+ const topDirs = (await listSubdirs(["."])).get(".");
1757
+ const nested = await listSubdirs(topDirs.filter((dir) => isMentioned(combinedDocs, dir)));
1758
+ for (const topDir of topDirs) {
1607
1759
  const topMentioned = isMentioned(combinedDocs, topDir);
1608
1760
  if (!topMentioned) {
1609
1761
  const count4 = await countSourceFiles(topDir);
1610
1762
  if (count4 >= 1) candidates.push({ dir: topDir, sourceFileCount: count4 });
1611
1763
  continue;
1612
1764
  }
1613
- const subdirs = await listSubdirs(topDir);
1765
+ const subdirs = nested.get(topDir);
1614
1766
  const mentioned = subdirs.filter((s) => isMentioned(combinedDocs, s));
1615
1767
  if (mentioned.length < ENUMERATION_THRESHOLD) continue;
1616
1768
  for (const sub of subdirs) {
@@ -1627,6 +1779,7 @@ var DIR_DENYLIST, SECOND_LEVEL_MIN_SOURCE_FILES, ENUMERATION_THRESHOLD;
1627
1779
  var init_new_module = __esm({
1628
1780
  "src/audit/checks/new-module.ts"() {
1629
1781
  "use strict";
1782
+ init_inspection();
1630
1783
  init_files();
1631
1784
  init_inputs();
1632
1785
  init_git();
@@ -1656,7 +1809,7 @@ var init_new_module = __esm({
1656
1809
  });
1657
1810
 
1658
1811
  // src/audit/checks/stale-count.ts
1659
- import path13 from "path";
1812
+ import path14 from "path";
1660
1813
  async function countGradleModules(root) {
1661
1814
  for (const name of ["settings.gradle.kts", "settings.gradle"]) {
1662
1815
  const content2 = await readAuditInput(root, name);
@@ -1690,7 +1843,7 @@ async function countNpmWorkspaces(root) {
1690
1843
  return {
1691
1844
  actual: matched.length,
1692
1845
  countedFrom: "package.json workspaces",
1693
- members: matched.map((m) => path13.dirname(m)).sort()
1846
+ members: matched.map((m) => path14.dirname(m)).sort()
1694
1847
  };
1695
1848
  }
1696
1849
  }
@@ -1721,7 +1874,7 @@ async function countNpmWorkspaces(root) {
1721
1874
  return {
1722
1875
  actual: matched.length,
1723
1876
  countedFrom: "pnpm-workspace.yaml",
1724
- members: matched.map((m) => path13.dirname(m)).sort()
1877
+ members: matched.map((m) => path14.dirname(m)).sort()
1725
1878
  };
1726
1879
  }
1727
1880
  }
@@ -1743,8 +1896,8 @@ async function countCargoCrates(root) {
1743
1896
  ignore: ["**/target/**"],
1744
1897
  label: "Cargo workspace discovery"
1745
1898
  });
1746
- for (const m of matched) members.add(path13.dirname(m));
1747
- } else if (await readAuditInput(root, path13.posix.join(entry, "Cargo.toml")) !== null) {
1899
+ for (const m of matched) members.add(path14.dirname(m));
1900
+ } else if (await readAuditInput(root, path14.posix.join(entry, "Cargo.toml")) !== null) {
1748
1901
  members.add(entry);
1749
1902
  }
1750
1903
  }
@@ -1764,7 +1917,7 @@ async function resolveCountSource(root, claim) {
1764
1917
  }
1765
1918
  async function resolveDocCountSource(root, doc, claim) {
1766
1919
  const scope = documentScope(doc);
1767
- const source = await resolveCountSource(path13.join(root, scope), claim);
1920
+ const source = await resolveCountSource(path14.join(root, scope), claim);
1768
1921
  if (!source || scope === ".") return source;
1769
1922
  return {
1770
1923
  ...source,
@@ -1816,7 +1969,7 @@ var init_stale_count = __esm({
1816
1969
  });
1817
1970
 
1818
1971
  // src/audit/checks/dead-command.ts
1819
- import path14 from "path";
1972
+ import path15 from "path";
1820
1973
  function scriptsOf(raw) {
1821
1974
  try {
1822
1975
  const pkg = JSON.parse(raw);
@@ -1852,7 +2005,7 @@ async function commandInputs(root, docs) {
1852
2005
  }
1853
2006
  let cursor = directory;
1854
2007
  while (true) {
1855
- const manifest = path14.posix.join(cursor, "package.json");
2008
+ const manifest = path15.posix.join(cursor, "package.json");
1856
2009
  scope.candidates.push(manifest);
1857
2010
  const raw = await read(manifest);
1858
2011
  if (raw !== null) {
@@ -1863,7 +2016,7 @@ async function commandInputs(root, docs) {
1863
2016
  break;
1864
2017
  }
1865
2018
  if (claim.directory !== void 0 || cursor === ".") break;
1866
- cursor = path14.posix.dirname(cursor);
2019
+ cursor = path15.posix.dirname(cursor);
1867
2020
  }
1868
2021
  if (!resolution.manifest && !resolution.problem) {
1869
2022
  resolution.problem = `No package.json for the command's scope (${directory}).`;
@@ -1926,10 +2079,8 @@ var init_dead_command = __esm({
1926
2079
  });
1927
2080
 
1928
2081
  // src/audit/release-metadata.ts
1929
- import { execFile as execFile7 } from "child_process";
1930
- import { promisify as promisify7 } from "util";
1931
2082
  async function git(root, args) {
1932
- return (await exec6("git", args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
2083
+ return (await execGit(args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
1933
2084
  }
1934
2085
  function withoutAndroidReleaseValues(text2) {
1935
2086
  if (/\/\*|"""|'''/.test(text2) || !/id\s*\(?\s*["']com\.android\.(application|library)["']/.test(text2)) return null;
@@ -1975,11 +2126,10 @@ async function releaseMetadataOnly(root, commit) {
1975
2126
  return false;
1976
2127
  }
1977
2128
  }
1978
- var exec6;
1979
2129
  var init_release_metadata = __esm({
1980
2130
  "src/audit/release-metadata.ts"() {
1981
2131
  "use strict";
1982
- exec6 = promisify7(execFile7);
2132
+ init_git_read();
1983
2133
  }
1984
2134
  });
1985
2135
 
@@ -2075,7 +2225,7 @@ var init_deps_changed = __esm({
2075
2225
  });
2076
2226
 
2077
2227
  // src/context/lexical.ts
2078
- import path15 from "path";
2228
+ import path16 from "path";
2079
2229
  function tokenize(text2) {
2080
2230
  return text2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2 && !STOPWORDS.has(t));
2081
2231
  }
@@ -2088,7 +2238,7 @@ function tokenSet(text2) {
2088
2238
  function scoreEntry(taskTokens, entry) {
2089
2239
  const nameTokens = tokenSet(entry.name);
2090
2240
  const descTokens = tokenSet(entry.description);
2091
- const fileTokens = tokenSet(entry.files.map((f) => path15.basename(f)).join(" "));
2241
+ const fileTokens = tokenSet(entry.files.map((f) => path16.basename(f)).join(" "));
2092
2242
  let score = 0;
2093
2243
  for (const token of taskTokens) {
2094
2244
  if (nameTokens.has(token)) score += 3;
@@ -2194,7 +2344,9 @@ function assessTrust(entry, freshness) {
2194
2344
  }
2195
2345
  function trustHint(states) {
2196
2346
  const parts = [];
2197
- if (states.some((s) => s.verification === "failed")) parts.push("Verification failed for returned entries; do not rely on those descriptions until corrected.");
2347
+ if (states.some((s) => s.verification === "failed" || s.recordedVerdict === "failed")) parts.push("Verification failed for returned entries; do not rely on those descriptions until corrected.");
2348
+ if (states.some((s) => s.verification === "stale")) parts.push("Recorded verification evidence changed; use verify_snapshot and review the current evidence before reusing those verdicts.");
2349
+ if (states.some((s) => s.verification === "unknown")) parts.push("Recorded verification evidence cannot be confirmed; inspect the files and obtain a fresh review before relying on those verdicts.");
2198
2350
  if (states.some((s) => s.freshness === "unknown")) parts.push("Freshness is unknown for some returned entries; inspect their files before relying on them.");
2199
2351
  if (states.some((s) => s.freshness === "changed")) parts.push("Some returned entries have changed files, including possible local edits; verify against the current code.");
2200
2352
  if (!parts.length) parts.push("No changes detected in the returned anchors. This does not prove the descriptions are correct.");
@@ -2419,8 +2571,8 @@ __export(decisions_exports, {
2419
2571
  upsertDecision: () => upsertDecision,
2420
2572
  withDecisionWrite: () => withDecisionWrite
2421
2573
  });
2422
- import fs8 from "fs/promises";
2423
- import path16 from "path";
2574
+ import fs9 from "fs/promises";
2575
+ import path17 from "path";
2424
2576
  import { createHash } from "crypto";
2425
2577
  import { z as z3 } from "zod";
2426
2578
  async function loadDecisionStore(rootDir) {
@@ -2428,7 +2580,7 @@ async function loadDecisionStore(rootDir) {
2428
2580
  const diagnostics = [];
2429
2581
  let entries;
2430
2582
  try {
2431
- entries = await fs8.readdir(await storePath(rootDir, ".mason/decisions"));
2583
+ entries = await fs9.readdir(await storePath(rootDir, ".mason/decisions"));
2432
2584
  } catch (error) {
2433
2585
  if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
2434
2586
  return { records, diagnostics };
@@ -2487,7 +2639,7 @@ async function withDecisionWrite(root, operation) {
2487
2639
  const lockPath = await storePath(root, ".mason/decisions/.write-lock", true);
2488
2640
  let lock;
2489
2641
  try {
2490
- lock = await fs8.open(lockPath, "wx", 384);
2642
+ lock = await fs9.open(lockPath, "wx", 384);
2491
2643
  } catch (error) {
2492
2644
  if (error.code === "EEXIST") return { status: "error", error: "Decision store is locked by another write. Retry after it finishes; an abandoned .mason/decisions/.write-lock must be removed only after confirming no writer is running." };
2493
2645
  throw error;
@@ -2496,7 +2648,7 @@ async function withDecisionWrite(root, operation) {
2496
2648
  return await operation();
2497
2649
  } finally {
2498
2650
  await lock.close();
2499
- await fs8.unlink(lockPath);
2651
+ await fs9.unlink(lockPath);
2500
2652
  }
2501
2653
  }
2502
2654
  async function upsertDecision(rootDir, input) {
@@ -2518,7 +2670,7 @@ async function upsertDecision(rootDir, input) {
2518
2670
  if (input.files && files.length < input.files.length) warnings.push("some anchor paths were outside the repo or duplicated and were dropped");
2519
2671
  for (const file of files) {
2520
2672
  try {
2521
- await fs8.access(path16.join(rootDir, file));
2673
+ await fs9.access(path17.join(rootDir, file));
2522
2674
  } catch {
2523
2675
  warnings.push(`anchor file does not exist on disk: ${file}`);
2524
2676
  }
@@ -2643,9 +2795,9 @@ var init_decisions = __esm({
2643
2795
  });
2644
2796
 
2645
2797
  // src/decisions/drift.ts
2646
- import path17 from "path";
2798
+ import path18 from "path";
2647
2799
  async function computeDecisionDrift(rootDir, decisions) {
2648
- const resolvedRoot = path17.resolve(rootDir);
2800
+ const resolvedRoot = path18.resolve(rootDir);
2649
2801
  const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
2650
2802
  const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
2651
2803
  const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
@@ -2868,8 +3020,6 @@ var init_findings = __esm({
2868
3020
  });
2869
3021
 
2870
3022
  // src/automation/store.ts
2871
- import fs9 from "fs/promises";
2872
- import os from "os";
2873
3023
  import { z as z5 } from "zod";
2874
3024
  function parseState(raw) {
2875
3025
  try {
@@ -2878,65 +3028,14 @@ function parseState(raw) {
2878
3028
  throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
2879
3029
  }
2880
3030
  }
2881
- async function withLock(root, directory, run, waitMs = 5e3) {
2882
- const file = await storePath(root, directory + "/lock", true);
2883
- const deadline = Date.now() + waitMs;
2884
- let handle;
2885
- while (!handle) {
2886
- try {
2887
- handle = await fs9.open(file, "wx", 384);
2888
- try {
2889
- await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
2890
- } catch (error) {
2891
- await handle.close().catch(() => {
2892
- });
2893
- handle = void 0;
2894
- await fs9.rm(file, { force: true }).catch(() => {
2895
- });
2896
- throw error;
2897
- }
2898
- } catch (error) {
2899
- if (error.code !== "EEXIST") throw error;
2900
- try {
2901
- const owner = JSON.parse(await fs9.readFile(file, "utf8"));
2902
- if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
2903
- try {
2904
- process.kill(owner.pid, 0);
2905
- } catch (probe) {
2906
- if (probe.code === "ESRCH") {
2907
- const reclaim = file + ".reclaim";
2908
- let guard;
2909
- try {
2910
- guard = await fs9.open(reclaim, "wx", 384);
2911
- const current = JSON.parse(await fs9.readFile(file, "utf8"));
2912
- if (current.pid === owner.pid && current.host === owner.host) await fs9.unlink(file);
2913
- } finally {
2914
- if (guard) {
2915
- await guard.close();
2916
- await fs9.rm(reclaim, { force: true });
2917
- }
2918
- }
2919
- }
2920
- }
2921
- }
2922
- } catch {
2923
- }
2924
- if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
2925
- await new Promise((resolve2) => setTimeout(resolve2, 40));
2926
- }
2927
- }
2928
- try {
2929
- return await run();
2930
- } finally {
2931
- await handle.close();
2932
- await fs9.unlink(file);
2933
- }
3031
+ function withLock(root, directory, run, waitMs = 5e3) {
3032
+ return withStoreLock(root, directory, run, waitMs, "Automation");
2934
3033
  }
2935
3034
  var hostSchema, events, stateSchema;
2936
3035
  var init_store = __esm({
2937
3036
  "src/automation/store.ts"() {
2938
3037
  "use strict";
2939
- init_storage();
3038
+ init_store_lock();
2940
3039
  hostSchema = z5.enum(["claude", "codex"]);
2941
3040
  events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
2942
3041
  stateSchema = z5.object({
@@ -2967,8 +3066,6 @@ var init_store = __esm({
2967
3066
 
2968
3067
  // src/audit/advisory-review.ts
2969
3068
  import fs10 from "fs/promises";
2970
- import { execFile as execFile8 } from "child_process";
2971
- import { promisify as promisify8 } from "util";
2972
3069
  import { z as z6 } from "zod";
2973
3070
  async function readRecord(root, id) {
2974
3071
  const raw = await readStoreJson(root, recordPath(id));
@@ -3163,10 +3260,12 @@ async function reviewAdvisory(rootDir, input) {
3163
3260
  };
3164
3261
  });
3165
3262
  }
3166
- var execute2, git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
3263
+ var git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
3167
3264
  var init_advisory_review = __esm({
3168
3265
  "src/audit/advisory-review.ts"() {
3169
3266
  "use strict";
3267
+ init_inspection();
3268
+ init_git_read();
3170
3269
  init_findings();
3171
3270
  init_deps_changed();
3172
3271
  init_inputs();
@@ -3176,8 +3275,7 @@ var init_advisory_review = __esm({
3176
3275
  init_store();
3177
3276
  init_decisions();
3178
3277
  init_drift2();
3179
- execute2 = promisify8(execFile8);
3180
- git2 = async (root, ...args) => (await execute2("git", args, { cwd: root, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 })).stdout;
3278
+ git2 = async (root, ...args) => (await (["log", "ls-tree", "ls-files", "merge-base"].includes(args[0]) ? inspectionGit : execGit)(args, { cwd: root, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 })).stdout;
3181
3279
  hashSchema = z6.string().regex(/^[a-f0-9]{64}$/);
3182
3280
  commitSchema2 = z6.string().regex(/^[a-f0-9]{40,64}$/);
3183
3281
  evidenceSchema2 = z6.object({
@@ -3221,12 +3319,12 @@ var init_advisory_review = __esm({
3221
3319
 
3222
3320
  // src/audit/audit.ts
3223
3321
  import fs11 from "fs/promises";
3224
- import path18 from "path";
3322
+ import path19 from "path";
3225
3323
  async function computeAudit(rootDir, options = {}) {
3226
3324
  return profilePhase("audit.current", () => auditCurrent(rootDir, options));
3227
3325
  }
3228
3326
  async function auditCurrent(rootDir, options) {
3229
- const resolvedRoot = path18.resolve(rootDir);
3327
+ const resolvedRoot = path19.resolve(rootDir);
3230
3328
  const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
3231
3329
  if (docs.length === 0) return null;
3232
3330
  const report = {
@@ -3260,7 +3358,7 @@ async function auditCurrent(rootDir, options) {
3260
3358
  }
3261
3359
  let decisionsPresent = false;
3262
3360
  try {
3263
- await fs11.access(path18.join(resolvedRoot, ".mason", "decisions"));
3361
+ await fs11.access(path19.join(resolvedRoot, ".mason", "decisions"));
3264
3362
  decisionsPresent = true;
3265
3363
  } catch {
3266
3364
  }
@@ -3315,7 +3413,7 @@ __export(repair_exports, {
3315
3413
  withRepairSession: () => withRepairSession
3316
3414
  });
3317
3415
  import fs12 from "fs/promises";
3318
- import path19 from "path";
3416
+ import path20 from "path";
3319
3417
  import { randomUUID as randomUUID2 } from "crypto";
3320
3418
  import { z as z7 } from "zod";
3321
3419
  function recheckable(finding) {
@@ -3353,8 +3451,8 @@ async function readStableAudit(root, checks, options) {
3353
3451
  async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
3354
3452
  return prepareOriginal(rootDir, checks, options);
3355
3453
  }
3356
- async function withRepairSession(root, options, run, cachedAudit) {
3357
- const before = await Promise.all([getCurrentGitHash(root), docState(root), advisoryReviewInventory(root).then(digest)]);
3454
+ async function withRepairSession(root, options, run, validate, cachedAudit) {
3455
+ const head = await getCurrentGitHash(root);
3358
3456
  const audits = /* @__PURE__ */ new Map();
3359
3457
  const documents = /* @__PURE__ */ new Map();
3360
3458
  const history = /* @__PURE__ */ new Map();
@@ -3365,9 +3463,9 @@ async function withRepairSession(root, options, run, cachedAudit) {
3365
3463
  };
3366
3464
  const session = {
3367
3465
  audit: (checks) => memo(audits, [...checks].sort().join(","), async () => {
3368
- if (cachedAudit && cachedAudit.headHash === before[0] && cachedAudit.root === root && digest([...checks].sort()) === digest([...cachedAudit.checksRun ?? []].sort())) {
3466
+ if (cachedAudit && cachedAudit.headHash === head && cachedAudit.root === root && digest([...checks].sort()) === digest([...cachedAudit.checksRun ?? []].sort())) {
3369
3467
  const current = structuredClone(cachedAudit);
3370
- const reviews = (await Promise.all(allFindings(current).map((f) => assessAdvisory(root, f, before[0])))).filter((r) => r !== null);
3468
+ const reviews = (await Promise.all(allFindings(current).map((f) => assessAdvisory(root, f, head)))).filter((r) => r !== null);
3371
3469
  delete current.advisoryReviews;
3372
3470
  if (reviews.length) current.advisoryReviews = reviews;
3373
3471
  return current;
@@ -3375,16 +3473,15 @@ async function withRepairSession(root, options, run, cachedAudit) {
3375
3473
  return computeAudit(root, { ...options, checks });
3376
3474
  }),
3377
3475
  document: (file) => memo(documents, file, async () => readBoundedFile(await storePath(root, file), 10 * 1024 * 1024)),
3378
- history: (head) => memo(history, head, () => getChangesWithStatus(root, head)),
3379
- assessment: (dir, finding, head) => memo(assessments, digest([dir, finding, head]), () => assessAdvisory(dir, finding, head))
3476
+ history: (head2) => memo(history, head2, () => getChangesWithStatus(root, head2)),
3477
+ assessment: (dir, finding, head2) => memo(assessments, digest([dir, finding, head2]), () => assessAdvisory(dir, finding, head2))
3380
3478
  };
3381
3479
  const result = await run({
3382
3480
  prepare: () => prepareOriginal(root, ALL_CHECKS, options, session),
3383
3481
  verify: (baseline) => profilePhase("repair.verify", () => verifyOriginal(root, baseline, options, session)),
3384
3482
  currentAudit: () => session.audit(ALL_CHECKS)
3385
3483
  });
3386
- const after = await Promise.all([getCurrentGitHash(root), docState(root), advisoryReviewInventory(root).then(digest)]);
3387
- if (digest(before) !== digest(after)) throw new Error("HEAD, context files or advisory review records changed during verification; retry against stable evidence.");
3484
+ await validate();
3388
3485
  return result;
3389
3486
  }
3390
3487
  async function prepareOriginal(rootDir, checks, options, session) {
@@ -3406,8 +3503,8 @@ async function prepareOriginal(rootDir, checks, options, session) {
3406
3503
  }
3407
3504
  async function loadRepairBaseline(rootDir, baselinePath) {
3408
3505
  const root = await fs12.realpath(rootDir);
3409
- const declaredRoot = path19.resolve(rootDir);
3410
- const relative = path19.isAbsolute(baselinePath) ? path19.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
3506
+ const declaredRoot = path20.resolve(rootDir);
3507
+ const relative = path20.isAbsolute(baselinePath) ? path20.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
3411
3508
  const stored = baselineSchema.parse(await readStoreJson(root, relative));
3412
3509
  const { digest: savedDigest, ...payload } = stored;
3413
3510
  if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
@@ -3582,14 +3679,12 @@ var init_repair = __esm({
3582
3679
 
3583
3680
  // src/automation/evidence.ts
3584
3681
  import fs13 from "fs/promises";
3585
- import path20 from "path";
3682
+ import path21 from "path";
3586
3683
  import { createHash as createHash3 } from "crypto";
3587
- import { execFile as execFile9 } from "child_process";
3588
- import { promisify as promisify9 } from "util";
3589
3684
  import fg5 from "fast-glob";
3590
3685
  import { z as z8 } from "zod";
3591
3686
  async function git3(root, ...args) {
3592
- return profilePhase("automation.git", async () => (await exec7("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout);
3687
+ return profilePhase("automation.git", async () => (await execGit(args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout);
3593
3688
  }
3594
3689
  async function workspace(dir) {
3595
3690
  const root = await fs13.realpath((await git3(dir, "rev-parse", "--show-toplevel")).trim());
@@ -3613,7 +3708,7 @@ async function collectInputs(root) {
3613
3708
  const head = headText.trim();
3614
3709
  let shallow = null;
3615
3710
  try {
3616
- shallow = await fs13.readFile(path20.resolve(root, shallowPath.trim()), "utf8");
3711
+ shallow = await fs13.readFile(path21.resolve(root, shallowPath.trim()), "utf8");
3617
3712
  } catch (error) {
3618
3713
  if (error.code !== "ENOENT") throw error;
3619
3714
  }
@@ -3637,7 +3732,7 @@ async function collectInputs(root) {
3637
3732
  if (scope?.candidates.some(gitMetadataPath)) continue;
3638
3733
  for (const candidate of scope?.candidates ?? []) {
3639
3734
  if (optionalMasonPath(candidate)) continue;
3640
- const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(path20.posix.dirname(candidate))]);
3735
+ const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(path21.posix.dirname(candidate))]);
3641
3736
  claims.push([candidate, filePresent, parentPresent]);
3642
3737
  }
3643
3738
  }
@@ -3649,7 +3744,7 @@ async function collectInputs(root) {
3649
3744
  if (error.code === "ENOENT") return "absent";
3650
3745
  throw error;
3651
3746
  });
3652
- const [modules, counts, commands, decisionFiles] = await Promise.all([
3747
+ const [modules, counts, commands2, decisionFiles] = await Promise.all([
3653
3748
  combinedDocs ? moduleCandidates(root, combinedDocs) : [],
3654
3749
  Promise.all(countClaims.map(({ doc, claim }) => resolveDocCountSource(root, doc, claim))),
3655
3750
  commandInputs(root, parsedDocs),
@@ -3668,7 +3763,7 @@ async function collectInputs(root) {
3668
3763
  "deleted-reference": hash([common, claims, docStatus]),
3669
3764
  "new-module": hash([common, modules]),
3670
3765
  "stale-count": hash([common, counts]),
3671
- "dead-command": hash([common, commands]),
3766
+ "dead-command": hash([common, commands2]),
3672
3767
  "deps-changed": hash([common, docStatus]),
3673
3768
  "decision-anchor-drift": hash([common, decisionPresence, decisions, status, index2])
3674
3769
  };
@@ -3706,10 +3801,11 @@ function checkCache(raw, inputs) {
3706
3801
  }
3707
3802
  };
3708
3803
  }
3709
- var exec7, engineVersion, hash, content, cacheSchema;
3804
+ var engineVersion, hash, content, cacheSchema;
3710
3805
  var init_evidence = __esm({
3711
3806
  "src/automation/evidence.ts"() {
3712
3807
  "use strict";
3808
+ init_git_read();
3713
3809
  init_profile();
3714
3810
  init_docs();
3715
3811
  init_scope();
@@ -3722,8 +3818,7 @@ var init_evidence = __esm({
3722
3818
  init_dead_command();
3723
3819
  init_storage();
3724
3820
  init_advisory_review();
3725
- exec7 = promisify9(execFile9);
3726
- engineVersion = true ? "0.17.3" : "development";
3821
+ engineVersion = true ? "0.18.0" : "development";
3727
3822
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
3728
3823
  content = readAuditInput;
3729
3824
  cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
@@ -3783,8 +3878,8 @@ var init_model = __esm({
3783
3878
  });
3784
3879
 
3785
3880
  // src/setup/launcher.ts
3786
- import { execFile as execFile10 } from "child_process";
3787
- import { promisify as promisify10 } from "util";
3881
+ import { execFile as execFile2 } from "child_process";
3882
+ import { promisify as promisify2 } from "util";
3788
3883
  import fs14 from "fs/promises";
3789
3884
  function globalCommand(args, platform = process.platform) {
3790
3885
  return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
@@ -3792,7 +3887,7 @@ function globalCommand(args, platform = process.platform) {
3792
3887
  async function installedCommand() {
3793
3888
  const invocation = globalCommand(["internal-integration-version"]);
3794
3889
  try {
3795
- const { stdout } = await promisify10(execFile10)(
3890
+ const { stdout } = await promisify2(execFile2)(
3796
3891
  invocation.command,
3797
3892
  invocation.args,
3798
3893
  { timeout: 5e3, maxBuffer: 8192, windowsHide: true }
@@ -3815,12 +3910,124 @@ var init_launcher = __esm({
3815
3910
  "use strict";
3816
3911
  init_evidence();
3817
3912
  init_model();
3818
- executingVersion = () => true ? "0.17.3" : "development";
3913
+ executingVersion = () => true ? "0.18.0" : "development";
3819
3914
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
3820
3915
  hookCommand = (host) => `mason --setup-host ${host} auto`;
3821
3916
  }
3822
3917
  });
3823
3918
 
3919
+ // src/snapshot/review.ts
3920
+ import { createHash as createHash5 } from "crypto";
3921
+ function snapshotEntryContent(entry) {
3922
+ return {
3923
+ description: entry.description,
3924
+ files: "files" in entry ? entry.files : void 0,
3925
+ chain: "chain" in entry ? entry.chain : void 0,
3926
+ tests: "files" in entry ? entry.tests : void 0,
3927
+ type: "files" in entry ? normalizeFeatureType(entry.type) : void 0
3928
+ };
3929
+ }
3930
+ function reviewToken(kind, name, entry, evidence) {
3931
+ return createHash5("sha256").update(JSON.stringify({
3932
+ version: 1,
3933
+ kind,
3934
+ name,
3935
+ entry: snapshotEntryContent(entry),
3936
+ evidence
3937
+ })).digest("hex");
3938
+ }
3939
+ async function prepareSnapshotReview(access, kind, name, entry) {
3940
+ const files = "files" in entry ? entry.files : entry.chain;
3941
+ const evidence = [];
3942
+ const skeletons = [];
3943
+ for (const file of files.slice(0, VERIFY_MAX_FILES_PER_ENTRY)) {
3944
+ const full = await access.read(file);
3945
+ evidence.push({ path: file, hash: full ? sourceHash(full.content) : null });
3946
+ skeletons.push(full ? { path: full.path, content: full.content.slice(0, VERIFY_SKELETON_CHARS) } : { path: file, missing: true });
3947
+ }
3948
+ return { reviewToken: reviewToken(kind, name, entry, evidence), skeletons, truncated: files.length > VERIFY_MAX_FILES_PER_ENTRY };
3949
+ }
3950
+ function createSnapshotEvidenceReader(access) {
3951
+ const hashes = /* @__PURE__ */ new Map();
3952
+ return async (kind, name, entry) => {
3953
+ const files = "files" in entry ? entry.files : entry.chain;
3954
+ const evidence = [];
3955
+ for (const file of files.slice(0, VERIFY_MAX_FILES_PER_ENTRY)) {
3956
+ let hash2 = hashes.get(file);
3957
+ if (!hash2) {
3958
+ hash2 = access.read(file).then((full) => full ? sourceHash(full.content) : null);
3959
+ hashes.set(file, hash2);
3960
+ }
3961
+ evidence.push({ path: file, hash: await hash2 });
3962
+ }
3963
+ return {
3964
+ reviewToken: reviewToken(kind, name, entry, evidence),
3965
+ evidenceAvailable: evidence.length > 0 && evidence.every((file) => file.hash !== null)
3966
+ };
3967
+ };
3968
+ }
3969
+ var VERIFY_MAX_FILES_PER_ENTRY, VERIFY_SKELETON_CHARS, sourceHash;
3970
+ var init_review = __esm({
3971
+ "src/snapshot/review.ts"() {
3972
+ "use strict";
3973
+ init_snapshot();
3974
+ VERIFY_MAX_FILES_PER_ENTRY = 8;
3975
+ VERIFY_SKELETON_CHARS = 500;
3976
+ sourceHash = (content2) => createHash5("sha256").update(content2).digest("hex");
3977
+ }
3978
+ });
3979
+
3980
+ // src/snapshot/trust.ts
3981
+ function createSnapshotTrustReader(root) {
3982
+ let reader;
3983
+ return async (kind, name, entry, freshness) => {
3984
+ const trust = assessTrust(entry, freshness);
3985
+ const verdict = entry.verificationFailed ? "failed" : entry.verifiedAt ? "passed" : void 0;
3986
+ if (!verdict) return trust;
3987
+ trust.recordedVerdict = verdict;
3988
+ const unknown = (reason2) => {
3989
+ trust.verification = "unknown";
3990
+ trust.reasons.push(reason2);
3991
+ return trust;
3992
+ };
3993
+ if (!entry.verificationToken || !/^[a-f0-9]{64}$/.test(entry.verificationToken)) return unknown("The recorded verdict has no supported evidence token; obtain a fresh verify_snapshot review.");
3994
+ try {
3995
+ reader ??= createFileAccess(root).then(createSnapshotEvidenceReader).catch(() => null);
3996
+ const readReview = await reader;
3997
+ if (!readReview) return unknown("Verification source evidence could not be read under the current file policy.");
3998
+ const review = await readReview(kind, name, entry);
3999
+ if (!review.evidenceAvailable) {
4000
+ return unknown("Verification source evidence is missing, excluded, unreadable, or empty; the recorded verdict cannot be confirmed.");
4001
+ }
4002
+ if (review.reviewToken !== entry.verificationToken) {
4003
+ trust.verification = "stale";
4004
+ trust.reasons.push("The entry or sampled source contents differ from the recorded verification evidence; obtain a fresh review.");
4005
+ }
4006
+ return trust;
4007
+ } catch {
4008
+ return unknown("Verification source evidence could not be read under the current file policy.");
4009
+ }
4010
+ };
4011
+ }
4012
+ async function readSnapshotTrustIndex(readTrust, snapshot, drift) {
4013
+ const features = /* @__PURE__ */ Object.create(null), flows = /* @__PURE__ */ Object.create(null);
4014
+ for (const [name, entry] of Object.entries(snapshot.features)) {
4015
+ features[name] = await readTrust("feature", name, entry, drift?.featureFreshness?.[name] ?? "unknown");
4016
+ }
4017
+ for (const [name, entry] of Object.entries(snapshot.flows)) {
4018
+ flows[name] = await readTrust("flow", name, entry, drift?.flowFreshness?.[name] ?? "unknown");
4019
+ }
4020
+ return { features, flows };
4021
+ }
4022
+ var init_trust2 = __esm({
4023
+ "src/snapshot/trust.ts"() {
4024
+ "use strict";
4025
+ init_trust();
4026
+ init_files();
4027
+ init_review();
4028
+ }
4029
+ });
4030
+
3824
4031
  // src/setup/files.ts
3825
4032
  var files_exports = {};
3826
4033
  __export(files_exports, {
@@ -3829,7 +4036,7 @@ __export(files_exports, {
3829
4036
  readText: () => readText
3830
4037
  });
3831
4038
  import fs17 from "fs/promises";
3832
- import path24 from "path";
4039
+ import path25 from "path";
3833
4040
  import { randomUUID as randomUUID3 } from "crypto";
3834
4041
  async function readText(root, file) {
3835
4042
  try {
@@ -3862,7 +4069,7 @@ async function applyEdit(root, edit) {
3862
4069
  return true;
3863
4070
  }
3864
4071
  const file = await storePath(root, edit.path, true);
3865
- const temporary = path24.join(path24.dirname(file), ".mason-setup-" + randomUUID3() + ".tmp");
4072
+ const temporary = path25.join(path25.dirname(file), ".mason-setup-" + randomUUID3() + ".tmp");
3866
4073
  try {
3867
4074
  const mode = await fs17.stat(file).then((s) => s.mode & 511, () => 384);
3868
4075
  const handle = await fs17.open(temporary, "wx", mode);
@@ -4164,12 +4371,12 @@ async function matchingAnalysis(ws, state, inputs) {
4164
4371
  return { analysis: a };
4165
4372
  }
4166
4373
  async function verifyInputs(ws, inputs) {
4167
- return profilePhase("automation.revalidate", async () => {
4374
+ return profilePhase("automation.revalidate", () => withRepositoryInspection(ws.root, async () => {
4168
4375
  const [after, currentWs] = await Promise.all([readInputs(ws.root), workspace(ws.root)]);
4169
4376
  if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
4170
4377
  throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
4171
4378
  }
4172
- });
4379
+ }));
4173
4380
  }
4174
4381
  async function computeAnalysis(ws, state, inputs, event, previousAudit, cacheDiagnostic) {
4175
4382
  const baselines = [...state.baselines];
@@ -4189,7 +4396,10 @@ async function computeAnalysis(ws, state, inputs, event, previousAudit, cacheDia
4189
4396
  capture: "unknown",
4190
4397
  scope: SCOPE
4191
4398
  };
4192
- if (!baselines.length && Object.values(inputs.docs).every((value) => value === null)) return { baselines, report: empty, cache: null };
4399
+ if (!baselines.length && Object.values(inputs.docs).every((value) => value === null)) {
4400
+ await verifyInputs(ws, inputs);
4401
+ return { baselines, report: empty, cache: null };
4402
+ }
4193
4403
  if (ws.branch === "detached" && state.latest) {
4194
4404
  const previous = await readStoreJson(ws.root, state.latest);
4195
4405
  if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
@@ -4227,7 +4437,7 @@ async function computeAnalysis(ws, state, inputs, event, previousAudit, cacheDia
4227
4437
  }
4228
4438
  currentAudit = await repairs.currentAudit();
4229
4439
  return verifications2;
4230
- }, cachedAudit);
4440
+ }, () => verifyInputs(ws, inputs), cachedAudit);
4231
4441
  const merged = /* @__PURE__ */ new Map();
4232
4442
  for (const verification of verifications) {
4233
4443
  for (const finding of verification.findings) {
@@ -4262,7 +4472,6 @@ async function analyze(ws, inputs, event, startedAt) {
4262
4472
  if (previous && previous.completedAt >= startedAt) return { analysis: previous, shared: true };
4263
4473
  const originalDigest = await baselineDigest(ws, before);
4264
4474
  const computed = await computeAnalysis(ws, before, inputs, event, previous?.currentAudit, diagnostic);
4265
- await verifyInputs(ws, inputs);
4266
4475
  if (originalDigest !== await baselineDigest(ws, before)) throw new Error("Repair baseline was modified during automation; original evidence must be inspected.");
4267
4476
  const id = randomUUID5();
4268
4477
  const analysis = analysisSchema.parse({
@@ -4291,7 +4500,7 @@ async function analyze(ws, inputs, event, startedAt) {
4291
4500
  async function automate(dir, event) {
4292
4501
  const startedAt = Date.now();
4293
4502
  const ws = await profilePhase("automation.workspace", () => workspace(dir));
4294
- return recordExecution(ws.root, ws.directory, event.event, async () => {
4503
+ return recordExecution(ws.root, ws.directory, event.event, () => withRepositoryInspection(ws.root, async () => {
4295
4504
  const inputs = await readInputs(ws.root);
4296
4505
  const result = await profilePhase("automation.analysis", () => analyze(ws, inputs, event, startedAt));
4297
4506
  if (result.shared) {
@@ -4364,7 +4573,7 @@ async function automate(dir, event) {
4364
4573
  await writeStoreJson(ws.root, ws.directory + "/state.json", state);
4365
4574
  return { report, message: notify ? summarize(report) : null, continueOnce };
4366
4575
  }));
4367
- });
4576
+ }));
4368
4577
  }
4369
4578
  async function automationStatus(dir) {
4370
4579
  const ws = await profilePhase("automation.workspace", () => workspace(dir));
@@ -4402,6 +4611,7 @@ var init_runtime = __esm({
4402
4611
  "src/automation/runtime.ts"() {
4403
4612
  "use strict";
4404
4613
  init_profile();
4614
+ init_inspection();
4405
4615
  init_execution();
4406
4616
  init_repair();
4407
4617
  init_types();
@@ -4447,7 +4657,7 @@ var init_runtime = __esm({
4447
4657
 
4448
4658
  // src/automation/hook-root.ts
4449
4659
  import fs19 from "fs/promises";
4450
- import path25 from "path";
4660
+ import path26 from "path";
4451
4661
  var init_hook_root = __esm({
4452
4662
  "src/automation/hook-root.ts"() {
4453
4663
  "use strict";
@@ -4852,7 +5062,7 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
4852
5062
  - Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
4853
5063
  - 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.
4854
5064
  - 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.
4855
- - 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.
5065
+ - Consult trust metadata before relying on entries: unknown or changed freshness requires inspection, stale or unknown map verification requires a fresh evidence review, and a recorded failed verdict 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.
4856
5066
  - 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.
4857
5067
  - 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.
4858
5068
  - \`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.
@@ -4916,13 +5126,13 @@ If a build is interrupted, its partials remain in \`.mason/partial-snapshots/\`.
4916
5126
  });
4917
5127
 
4918
5128
  // src/review/cochange.ts
4919
- import { execFile as execFile15 } from "child_process";
4920
- import { promisify as promisify15 } from "util";
5129
+ import { execFile as execFile6 } from "child_process";
5130
+ import { promisify as promisify6 } from "util";
4921
5131
  async function buildMatrix(resolvedRoot) {
4922
5132
  try {
4923
- const { stdout: shallow } = await exec12("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
5133
+ const { stdout: shallow } = await exec4("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
4924
5134
  if (shallow.trim() === "true") return null;
4925
- const { stdout } = await exec12(
5135
+ const { stdout } = await exec4(
4926
5136
  "git",
4927
5137
  ["log", `-n${HISTORY_COMMITS}`, "--format=%x01", "--name-only", "-M"],
4928
5138
  { cwd: resolvedRoot, maxBuffer: 100 * 1024 * 1024 }
@@ -4978,11 +5188,11 @@ async function findMissingPartners(resolvedRoot, changedFiles, existsOnDisk, all
4978
5188
  findings.sort((a, b) => b.rate - a.rate || b.sharedCommits - a.sharedCommits);
4979
5189
  return findings;
4980
5190
  }
4981
- var exec12, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_RATE, MAX_COMMIT_FILES;
5191
+ var exec4, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_RATE, MAX_COMMIT_FILES;
4982
5192
  var init_cochange = __esm({
4983
5193
  "src/review/cochange.ts"() {
4984
5194
  "use strict";
4985
- exec12 = promisify15(execFile15);
5195
+ exec4 = promisify6(execFile6);
4986
5196
  HISTORY_COMMITS = 1500;
4987
5197
  MIN_FILE_COMMITS = 5;
4988
5198
  MIN_SHARED_COMMITS = 4;
@@ -5290,10 +5500,10 @@ var init_check = __esm({
5290
5500
  });
5291
5501
 
5292
5502
  // src/review/evidence.ts
5293
- import path26 from "path";
5503
+ import path27 from "path";
5294
5504
  import { z as z22 } from "zod";
5295
5505
  function relativeArtifact(root, file) {
5296
- const relative = path26.isAbsolute(file) ? path26.relative(root, file) : file;
5506
+ const relative = path27.isAbsolute(file) ? path27.relative(root, file) : file;
5297
5507
  const normalized = normalizeRepoPath(relative);
5298
5508
  if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
5299
5509
  return normalized;
@@ -5390,7 +5600,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
5390
5600
  const report = await readStoreJson(root, file);
5391
5601
  if (report === null) throw new Error(`Report artifact is missing: ${file}`);
5392
5602
  const sourceRoot = check.sourceRoot ?? root;
5393
- if (!path26.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
5603
+ if (!path27.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
5394
5604
  const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
5395
5605
  result.outcome = parsed.outcome;
5396
5606
  result.counts = parsed.counts;
@@ -5512,12 +5722,12 @@ var init_evidence2 = __esm({
5512
5722
 
5513
5723
  // src/review/review.ts
5514
5724
  import fs20 from "fs/promises";
5515
- import path27 from "path";
5516
- import { execFile as execFile16 } from "child_process";
5517
- import { promisify as promisify16 } from "util";
5725
+ import path28 from "path";
5726
+ import { execFile as execFile7 } from "child_process";
5727
+ import { promisify as promisify7 } from "util";
5518
5728
  async function resolveMergeBase(resolvedRoot, base, head) {
5519
5729
  try {
5520
- const { stdout } = await exec13("git", ["merge-base", base, head], {
5730
+ const { stdout } = await exec5("git", ["merge-base", base, head], {
5521
5731
  cwd: resolvedRoot
5522
5732
  });
5523
5733
  return stdout.trim() || null;
@@ -5528,7 +5738,7 @@ async function resolveMergeBase(resolvedRoot, base, head) {
5528
5738
  async function defaultBase(resolvedRoot) {
5529
5739
  for (const ref of ["origin/HEAD", "origin/main", "origin/master", "main"]) {
5530
5740
  try {
5531
- await exec13("git", ["rev-parse", "--verify", "--quiet", ref], {
5741
+ await exec5("git", ["rev-parse", "--verify", "--quiet", ref], {
5532
5742
  cwd: resolvedRoot
5533
5743
  });
5534
5744
  return ref;
@@ -5541,7 +5751,7 @@ function anchorsTouched(record, changedFiles) {
5541
5751
  return matchingPaths(record.files, changedFiles);
5542
5752
  }
5543
5753
  async function computeReview(rootDir, base, options = {}) {
5544
- const resolvedRoot = path27.resolve(rootDir);
5754
+ const resolvedRoot = path28.resolve(rootDir);
5545
5755
  const head = await getCurrentGitHash(resolvedRoot);
5546
5756
  if (head === "unknown") return null;
5547
5757
  const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
@@ -5591,7 +5801,7 @@ async function computeReview(rootDir, base, options = {}) {
5591
5801
  analyzed,
5592
5802
  async (relPath) => {
5593
5803
  try {
5594
- await fs20.access(path27.join(resolvedRoot, relPath));
5804
+ await fs20.access(path28.join(resolvedRoot, relPath));
5595
5805
  return true;
5596
5806
  } catch {
5597
5807
  return false;
@@ -5624,8 +5834,8 @@ async function computeReview(rootDir, base, options = {}) {
5624
5834
  }
5625
5835
  return finalize();
5626
5836
  }
5627
- var exec13, MAX_ANALYZED_FILES;
5628
- var init_review = __esm({
5837
+ var exec5, MAX_ANALYZED_FILES;
5838
+ var init_review2 = __esm({
5629
5839
  "src/review/review.ts"() {
5630
5840
  "use strict";
5631
5841
  init_drift();
@@ -5636,7 +5846,7 @@ var init_review = __esm({
5636
5846
  init_cochange();
5637
5847
  init_evidence2();
5638
5848
  init_snapshot();
5639
- exec13 = promisify16(execFile16);
5849
+ exec5 = promisify7(execFile7);
5640
5850
  MAX_ANALYZED_FILES = 50;
5641
5851
  }
5642
5852
  });
@@ -5714,7 +5924,7 @@ var init_onboarding = __esm({
5714
5924
  "src/mcp/onboarding.ts"() {
5715
5925
  "use strict";
5716
5926
  init_audit();
5717
- init_review();
5927
+ init_review2();
5718
5928
  init_drift();
5719
5929
  init_snapshot();
5720
5930
  init_provenance();
@@ -5726,7 +5936,7 @@ var init_onboarding = __esm({
5726
5936
  });
5727
5937
 
5728
5938
  // src/impact/references.ts
5729
- import path28 from "path";
5939
+ import path29 from "path";
5730
5940
  function templateEnd(content2, start) {
5731
5941
  let i = start + 1, interpolation = 0;
5732
5942
  while (i < content2.length) {
@@ -5852,10 +6062,10 @@ function jvmImports(content2) {
5852
6062
  }
5853
6063
  function resolveImport(file, specifier, available) {
5854
6064
  if (!specifier.startsWith("./") && !specifier.startsWith("../")) return null;
5855
- const relative = path28.posix.normalize(path28.posix.join(path28.posix.dirname(file), specifier));
6065
+ const relative = path29.posix.normalize(path29.posix.join(path29.posix.dirname(file), specifier));
5856
6066
  if (relative.startsWith("../")) return null;
5857
6067
  if (available.has(relative)) return relative;
5858
- const ext = path28.posix.extname(relative);
6068
+ const ext = path29.posix.extname(relative);
5859
6069
  const replacements = { ".js": [".ts", ".tsx"], ".mjs": [".mts"], ".cjs": [".cts"] };
5860
6070
  const candidates = ext ? (replacements[ext] ?? []).map((e) => relative.slice(0, -ext.length) + e) : jsExtensions.flatMap((e) => [relative + e, relative + "/index" + e]);
5861
6071
  const found = candidates.filter((f) => available.has(f));
@@ -5867,13 +6077,13 @@ function mentionsPath(content2, target) {
5867
6077
  function createReferenceMatcher(targets, available, targetContents = /* @__PURE__ */ new Map()) {
5868
6078
  const counts = /* @__PURE__ */ new Map();
5869
6079
  for (const file of available) {
5870
- const name = path28.posix.basename(file, path28.posix.extname(file));
6080
+ const name = path29.posix.basename(file, path29.posix.extname(file));
5871
6081
  counts.set(name, (counts.get(name) ?? 0) + 1);
5872
6082
  }
5873
6083
  const jvmTargets = new Map([...targetContents].filter(([file]) => /\.(?:kt|kts|java)$/.test(file)).map(([file, content2]) => [file, declaredSymbols(content2)]));
5874
6084
  return (source) => {
5875
6085
  if (targets.includes(source.path)) return [];
5876
- const resolved = new Set(jsExtensions.includes(path28.posix.extname(source.path)) ? imports(source.content).map((s) => resolveImport(source.path, s, available)).filter(Boolean) : []);
6086
+ const resolved = new Set(jsExtensions.includes(path29.posix.extname(source.path)) ? imports(source.content).map((s) => resolveImport(source.path, s, available)).filter(Boolean) : []);
5877
6087
  if (/\.(?:kt|kts|java)$/.test(source.path)) {
5878
6088
  const imported = jvmImports(source.content);
5879
6089
  for (const [target, symbols] of jvmTargets) if (symbols.some((symbol) => imported.some((name) => name === symbol || name.startsWith(symbol + ".")))) resolved.add(target);
@@ -5889,7 +6099,7 @@ function createReferenceMatcher(targets, available, targetContents = /* @__PURE_
5889
6099
  add("explicit-path", target);
5890
6100
  continue;
5891
6101
  }
5892
- const ext = path28.posix.extname(target), name = path28.posix.basename(target, ext);
6102
+ const ext = path29.posix.extname(target), name = path29.posix.basename(target, ext);
5893
6103
  if (!/\.(?:[cm]?[jt]sx?|py|kt|java|go|rs|swift|rb|cs|cpp|c|h|hpp|dart|php)$/.test(ext) || name.startsWith(".") || name.length < 3 || genericNames.has(name.toLowerCase()) || counts.get(name) !== 1) continue;
5894
6104
  if (word(name).test(source.content)) {
5895
6105
  add("name-candidate", name);
@@ -5918,11 +6128,11 @@ var impact_exports = {};
5918
6128
  __export(impact_exports, {
5919
6129
  analyzeImpact: () => analyzeImpact
5920
6130
  });
5921
- import path29 from "path";
5922
- import { execFile as execFile17 } from "child_process";
5923
- import { promisify as promisify17 } from "util";
6131
+ import path30 from "path";
6132
+ import { execFile as execFile8 } from "child_process";
6133
+ import { promisify as promisify8 } from "util";
5924
6134
  async function analyzeImpact(rootDir, targetFiles) {
5925
- const resolvedRoot = path29.resolve(rootDir);
6135
+ const resolvedRoot = path30.resolve(rootDir);
5926
6136
  const resolvedTargets = await resolveTargetFiles(resolvedRoot, targetFiles);
5927
6137
  const [cochange, references, tests] = await Promise.all([
5928
6138
  getCochangeFiles(resolvedRoot, resolvedTargets),
@@ -5965,7 +6175,7 @@ async function getCochangeFiles(rootDir, targetFiles) {
5965
6175
  let totalTargetCommits = 0;
5966
6176
  for (const targetFile of targetFiles) {
5967
6177
  try {
5968
- const { stdout: commitLog } = await exec14(
6178
+ const { stdout: commitLog } = await exec6(
5969
6179
  "git",
5970
6180
  ["log", "--format=%H", "-n", "500", "--", targetFile],
5971
6181
  { cwd: rootDir, maxBuffer: 5e6 }
@@ -5975,7 +6185,7 @@ async function getCochangeFiles(rootDir, targetFiles) {
5975
6185
  if (commits.length === 0) continue;
5976
6186
  for (const commit of commits) {
5977
6187
  try {
5978
- const { stdout: filesInCommit } = await exec14(
6188
+ const { stdout: filesInCommit } = await exec6(
5979
6189
  "git",
5980
6190
  ["diff-tree", "--no-commit-id", "--name-only", "-r", commit],
5981
6191
  { cwd: rootDir }
@@ -6036,9 +6246,9 @@ async function getRelatedTests(rootDir, targetFiles) {
6036
6246
  const testFiles = await (await createFileAccess(rootDir)).list(testPatterns);
6037
6247
  const results = [];
6038
6248
  for (const target of targetFiles) {
6039
- const targetBaseName = path29.basename(target).replace(/\.[^.]+$/, "");
6249
+ const targetBaseName = path30.basename(target).replace(/\.[^.]+$/, "");
6040
6250
  for (const testFile of testFiles) {
6041
- const testBaseName = path29.basename(testFile).replace(/\.[^.]+$/, "");
6251
+ const testBaseName = path30.basename(testFile).replace(/\.[^.]+$/, "");
6042
6252
  const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
6043
6253
  if (sourceName === targetBaseName) {
6044
6254
  results.push({
@@ -6050,14 +6260,14 @@ async function getRelatedTests(rootDir, targetFiles) {
6050
6260
  }
6051
6261
  return results;
6052
6262
  }
6053
- var exec14;
6263
+ var exec6;
6054
6264
  var init_impact = __esm({
6055
6265
  "src/impact/impact.ts"() {
6056
6266
  "use strict";
6057
6267
  init_files();
6058
6268
  init_paths();
6059
6269
  init_references();
6060
- exec14 = promisify17(execFile17);
6270
+ exec6 = promisify8(execFile8);
6061
6271
  }
6062
6272
  });
6063
6273
 
@@ -6066,13 +6276,13 @@ var assemble_exports = {};
6066
6276
  __export(assemble_exports, {
6067
6277
  assembleContext: () => assembleContext
6068
6278
  });
6069
- import path30 from "path";
6279
+ import path31 from "path";
6070
6280
  import fs21 from "fs/promises";
6071
6281
  async function collectImpact(root, candidates) {
6072
6282
  const targets = /* @__PURE__ */ new Set();
6073
6283
  let sourceFiles;
6074
6284
  for (const candidate of sanitizeRepoPaths(candidates)) {
6075
- const stat = await fs21.stat(path30.join(root, candidate)).catch(() => null);
6285
+ const stat = await fs21.stat(path31.join(root, candidate)).catch(() => null);
6076
6286
  if (stat?.isDirectory()) {
6077
6287
  sourceFiles ??= await (await createFileAccess(root)).list();
6078
6288
  for (const file of sourceFiles) {
@@ -6098,7 +6308,7 @@ async function collectImpact(root, candidates) {
6098
6308
  };
6099
6309
  }
6100
6310
  async function assembleContext(rootDir, task, files) {
6101
- const resolvedRoot = path30.resolve(rootDir);
6311
+ const resolvedRoot = path31.resolve(rootDir);
6102
6312
  const mapState = await inspectSnapshot(resolvedRoot);
6103
6313
  const snapshot = mapState.snapshot;
6104
6314
  const store = await loadDecisionStore(resolvedRoot);
@@ -6130,6 +6340,7 @@ async function assembleContext(rootDir, task, files) {
6130
6340
  };
6131
6341
  }
6132
6342
  const drift = await computeDrift(resolvedRoot);
6343
+ const readTrust = createSnapshotTrustReader(resolvedRoot);
6133
6344
  const featureScores = Object.entries(snapshot.features).map(([name, feat]) => ({
6134
6345
  name,
6135
6346
  feat,
@@ -6156,14 +6367,7 @@ async function assembleContext(rootDir, task, files) {
6156
6367
  Object.assign(bundle, await collectImpact(resolvedRoot, [...anchorFiles, ...Object.values(decisions).flatMap((d) => [...d.files, ...d.pendingProposal?.files ?? []])]));
6157
6368
  bundle.diagnostics = store.diagnostics;
6158
6369
  bundle.freshness = drift;
6159
- bundle.trust = {
6160
- features: Object.fromEntries(Object.entries(snapshot.features).map(
6161
- ([name, entry]) => [name, assessTrust(entry, drift?.featureFreshness?.[name] ?? "unknown")]
6162
- )),
6163
- flows: Object.fromEntries(Object.entries(snapshot.flows).map(
6164
- ([name, entry]) => [name, assessTrust(entry, drift?.flowFreshness?.[name] ?? "unknown")]
6165
- ))
6166
- };
6370
+ bundle.trust = await readSnapshotTrustIndex(readTrust, snapshot, drift);
6167
6371
  bundle.hint += " " + trustHint([
6168
6372
  ...Object.values(bundle.trust.features),
6169
6373
  ...Object.values(bundle.trust.flows),
@@ -6176,7 +6380,7 @@ async function assembleContext(rootDir, task, files) {
6176
6380
  const features = {};
6177
6381
  const staleMatches = [];
6178
6382
  for (const { name, feat, score } of featureScores) {
6179
- const trust = assessTrust(feat, drift?.featureFreshness?.[name] ?? "unknown");
6383
+ const trust = await readTrust("feature", name, feat, drift?.featureFreshness?.[name] ?? "unknown");
6180
6384
  const stale2 = trust.freshness !== "current";
6181
6385
  if (stale2) staleMatches.push(name);
6182
6386
  features[name] = {
@@ -6191,7 +6395,7 @@ async function assembleContext(rootDir, task, files) {
6191
6395
  }
6192
6396
  const flows = {};
6193
6397
  for (const { name, flow, score } of flowScores) {
6194
- const trust = assessTrust(flow, drift?.flowFreshness?.[name] ?? "unknown");
6398
+ const trust = await readTrust("flow", name, flow, drift?.flowFreshness?.[name] ?? "unknown");
6195
6399
  const stale2 = trust.freshness !== "current";
6196
6400
  if (stale2) staleMatches.push(name);
6197
6401
  flows[name] = {
@@ -6276,6 +6480,7 @@ var init_assemble = __esm({
6276
6480
  "use strict";
6277
6481
  init_snapshot();
6278
6482
  init_files();
6483
+ init_trust2();
6279
6484
  init_drift();
6280
6485
  init_impact();
6281
6486
  init_lexical();
@@ -6641,8 +6846,8 @@ __export(client_exports, {
6641
6846
  function createConfluenceClient(config, fetchFn = fetch) {
6642
6847
  const baseUrl = config.baseUrl.replace(/\/+$/, "");
6643
6848
  const auth = "Basic " + Buffer.from(`${config.email}:${config.apiToken}`).toString("base64");
6644
- async function call(method, path34, body) {
6645
- const res = await fetchFn(`${baseUrl}${path34}`, {
6849
+ async function call(method, path35, body) {
6850
+ const res = await fetchFn(`${baseUrl}${path35}`, {
6646
6851
  method,
6647
6852
  headers: {
6648
6853
  Authorization: auth,
@@ -6654,7 +6859,7 @@ function createConfluenceClient(config, fetchFn = fetch) {
6654
6859
  if (!res.ok) {
6655
6860
  const text2 = await res.text();
6656
6861
  throw new Error(
6657
- `Confluence ${method} ${path34} failed: ${res.status} ${res.statusText} \u2014 ${text2}`
6862
+ `Confluence ${method} ${path35} failed: ${res.status} ${res.statusText} \u2014 ${text2}`
6658
6863
  );
6659
6864
  }
6660
6865
  if (res.status === 204) return null;
@@ -6756,15 +6961,15 @@ __export(config_exports2, {
6756
6961
  validateProvider: () => validateProvider
6757
6962
  });
6758
6963
  import fs23 from "fs/promises";
6759
- import path31 from "path";
6964
+ import path32 from "path";
6760
6965
  import os3 from "os";
6761
- import { execFile as execFile18 } from "child_process";
6762
- import { promisify as promisify18 } from "util";
6966
+ import { execFile as execFile9 } from "child_process";
6967
+ import { promisify as promisify9 } from "util";
6763
6968
  function configDir() {
6764
- return path31.join(os3.homedir(), ".mason");
6969
+ return path32.join(os3.homedir(), ".mason");
6765
6970
  }
6766
6971
  function configFile() {
6767
- return path31.join(configDir(), "config.json");
6972
+ return path32.join(configDir(), "config.json");
6768
6973
  }
6769
6974
  async function loadConfig() {
6770
6975
  try {
@@ -6794,7 +6999,7 @@ async function detectCLI(provider) {
6794
6999
  const cliName = provider === "claude" ? "claude" : provider === "gemini" ? "gemini" : provider === "ollama" ? "ollama" : null;
6795
7000
  if (!cliName) return { available: false };
6796
7001
  try {
6797
- const { stdout } = await exec15(cliName, ["--version"]);
7002
+ const { stdout } = await exec7(cliName, ["--version"]);
6798
7003
  return { available: true, version: stdout.trim() };
6799
7004
  } catch {
6800
7005
  return { available: false };
@@ -6811,11 +7016,11 @@ async function loadConfluenceConfig() {
6811
7016
  const config = await loadConfig();
6812
7017
  return config?.confluence ?? null;
6813
7018
  }
6814
- var exec15, DEFAULT_MODELS;
7019
+ var exec7, DEFAULT_MODELS;
6815
7020
  var init_config2 = __esm({
6816
7021
  "src/llm/config.ts"() {
6817
7022
  "use strict";
6818
- exec15 = promisify18(execFile18);
7023
+ exec7 = promisify9(execFile9);
6819
7024
  DEFAULT_MODELS = {
6820
7025
  claude: "claude-sonnet-4-20250514",
6821
7026
  gemini: "gemini-2.5-flash",
@@ -6937,16 +7142,16 @@ var init_renderer = __esm({
6937
7142
 
6938
7143
  // src/confluence/diff.ts
6939
7144
  import fs24 from "fs/promises";
6940
- import path32 from "path";
6941
- import { createHash as createHash5 } from "crypto";
7145
+ import path33 from "path";
7146
+ import { createHash as createHash6 } from "crypto";
6942
7147
  function hashDescription(description) {
6943
- return createHash5("sha256").update(description, "utf8").digest("hex");
7148
+ return createHash6("sha256").update(description, "utf8").digest("hex");
6944
7149
  }
6945
7150
  function syncStateDir(rootDir) {
6946
- return path32.join(rootDir, ".mason");
7151
+ return path33.join(rootDir, ".mason");
6947
7152
  }
6948
7153
  function syncStatePath(rootDir) {
6949
- return path32.join(syncStateDir(rootDir), "confluence-sync.json");
7154
+ return path33.join(syncStateDir(rootDir), "confluence-sync.json");
6950
7155
  }
6951
7156
  async function loadSyncState(rootDir) {
6952
7157
  try {
@@ -7014,8 +7219,8 @@ var init_diff = __esm({
7014
7219
  });
7015
7220
 
7016
7221
  // src/llm/providers.ts
7017
- import { execFile as execFile19, spawn } from "child_process";
7018
- import { promisify as promisify19 } from "util";
7222
+ import { execFile as execFile10, spawn } from "child_process";
7223
+ import { promisify as promisify10 } from "util";
7019
7224
  async function callLLM(config, userMessage, systemPrompt) {
7020
7225
  const model = config.model ?? getDefaultModel(config.provider);
7021
7226
  const system = systemPrompt ?? CLAUDE_MD_SYSTEM_PROMPT;
@@ -7172,12 +7377,12 @@ async function callOpenAIAPI(apiKey, model, system, userMessage) {
7172
7377
  });
7173
7378
  return response.choices[0]?.message?.content ?? "";
7174
7379
  }
7175
- var exec16, CLAUDE_MD_SYSTEM_PROMPT;
7380
+ var exec8, CLAUDE_MD_SYSTEM_PROMPT;
7176
7381
  var init_providers = __esm({
7177
7382
  "src/llm/providers.ts"() {
7178
7383
  "use strict";
7179
7384
  init_config2();
7180
- exec16 = promisify19(execFile19);
7385
+ exec8 = promisify10(execFile10);
7181
7386
  CLAUDE_MD_SYSTEM_PROMPT = `You are Mason, a context engineering tool. You've been given a comprehensive analysis of a codebase including:
7182
7387
  - Git history stats (commit patterns, frequently changed files, stale directories)
7183
7388
  - Project structure (directory layout, file counts by type)
@@ -7519,20 +7724,20 @@ import { z as z24 } from "zod";
7519
7724
  init_repair();
7520
7725
  init_advisory_review();
7521
7726
  import fs25 from "fs/promises";
7522
- import path33 from "path";
7523
- import { execFile as execFile20 } from "child_process";
7524
- import { promisify as promisify20 } from "util";
7727
+ import path34 from "path";
7728
+ import { execFile as execFile11 } from "child_process";
7729
+ import { promisify as promisify11 } from "util";
7525
7730
 
7526
7731
  // src/audit/cli.ts
7527
7732
  init_audit();
7528
7733
  init_types();
7529
7734
  init_repair();
7530
7735
  init_findings();
7531
- import path22 from "path";
7736
+ import path23 from "path";
7532
7737
 
7533
7738
  // src/audit/advisory-cli.ts
7534
7739
  init_advisory_review();
7535
- import path21 from "path";
7740
+ import path22 from "path";
7536
7741
  import { stripVTControlCharacters } from "util";
7537
7742
 
7538
7743
  // src/audit/cli.ts
@@ -7625,8 +7830,8 @@ function formatFixPrompt(report, baselinePath) {
7625
7830
  }
7626
7831
 
7627
7832
  // src/analyzers/git-history.ts
7628
- import { execFile as execFile11 } from "child_process";
7629
- import { promisify as promisify11 } from "util";
7833
+ import { execFile as execFile3 } from "child_process";
7834
+ import { promisify as promisify3 } from "util";
7630
7835
 
7631
7836
  // src/analyzers/base.ts
7632
7837
  import fs15 from "fs/promises";
@@ -7663,7 +7868,7 @@ var BaseAnalyzer = class {
7663
7868
  };
7664
7869
 
7665
7870
  // src/analyzers/git-history.ts
7666
- var exec8 = promisify11(execFile11);
7871
+ var exec = promisify3(execFile3);
7667
7872
  var GitHistoryAnalyzer = class extends BaseAnalyzer {
7668
7873
  name = "git-history";
7669
7874
  async analyze(context) {
@@ -7684,7 +7889,7 @@ var GitHistoryAnalyzer = class extends BaseAnalyzer {
7684
7889
  }
7685
7890
  async git(args, cwd) {
7686
7891
  try {
7687
- const { stdout } = await exec8("git", args, { cwd, maxBuffer: 1e7 });
7892
+ const { stdout } = await exec("git", args, { cwd, maxBuffer: 1e7 });
7688
7893
  return stdout.trim();
7689
7894
  } catch {
7690
7895
  return "";
@@ -7833,12 +8038,10 @@ async function runAll(context) {
7833
8038
  }
7834
8039
 
7835
8040
  // src/utils/git.ts
7836
- import { execFile as execFile12 } from "child_process";
7837
- import { promisify as promisify12 } from "util";
7838
- var exec9 = promisify12(execFile12);
8041
+ init_git_read();
7839
8042
  async function isGitRepo(dir) {
7840
8043
  try {
7841
- await exec9("git", ["rev-parse", "--git-dir"], { cwd: dir });
8044
+ await execGit(["rev-parse", "--git-dir"], { cwd: dir });
7842
8045
  return true;
7843
8046
  } catch {
7844
8047
  return false;
@@ -7847,10 +8050,10 @@ async function isGitRepo(dir) {
7847
8050
 
7848
8051
  // src/mcp/sampler.ts
7849
8052
  init_files();
7850
- import path23 from "path";
7851
- import { execFile as execFile13 } from "child_process";
7852
- import { promisify as promisify13 } from "util";
7853
- var exec10 = promisify13(execFile13);
8053
+ import path24 from "path";
8054
+ import { execFile as execFile4 } from "child_process";
8055
+ import { promisify as promisify4 } from "util";
8056
+ var exec2 = promisify4(execFile4);
7854
8057
  var CONFIG_FILES = [
7855
8058
  // Build & project config
7856
8059
  "package.json",
@@ -7955,8 +8158,8 @@ async function sampleFiles(rootDir, maxFiles = 25) {
7955
8158
  const projectConfig = access.config;
7956
8159
  for (const filePath of projectConfig.alwaysInclude ?? []) {
7957
8160
  if (selected.size >= maxFiles) break;
7958
- const resolvedPath = path23.resolve(rootDir, filePath);
7959
- if (!resolvedPath.startsWith(path23.resolve(rootDir))) continue;
8161
+ const resolvedPath = path24.resolve(rootDir, filePath);
8162
+ if (!resolvedPath.startsWith(path24.resolve(rootDir))) continue;
7960
8163
  selected.set(filePath, "always-include (project config)");
7961
8164
  }
7962
8165
  let configCount = 0;
@@ -8012,7 +8215,7 @@ async function sampleFiles(rootDir, maxFiles = 25) {
8012
8215
  }
8013
8216
  }
8014
8217
  try {
8015
- const { stdout } = await exec10(
8218
+ const { stdout } = await exec2(
8016
8219
  "git",
8017
8220
  ["log", "--since=3 months ago", "--format=", "--name-only"],
8018
8221
  { cwd: rootDir, maxBuffer: 5e6 }
@@ -8022,7 +8225,7 @@ async function sampleFiles(rootDir, maxFiles = 25) {
8022
8225
  if (!line) continue;
8023
8226
  if (line.includes("node_modules") || line.includes("/build/") || line.includes(".gradle") || line.includes("/generated/"))
8024
8227
  continue;
8025
- const ext = path23.extname(line).slice(1);
8228
+ const ext = path24.extname(line).slice(1);
8026
8229
  if (!SOURCE_EXTENSIONS.includes(ext)) continue;
8027
8230
  fileCounts.set(line, (fileCounts.get(line) ?? 0) + 1);
8028
8231
  }
@@ -8142,10 +8345,10 @@ init_drift();
8142
8345
  init_paths();
8143
8346
  init_files();
8144
8347
  import { createHash as createHash4 } from "crypto";
8145
- import { execFile as execFile14 } from "child_process";
8146
- import { promisify as promisify14 } from "util";
8348
+ import { execFile as execFile5 } from "child_process";
8349
+ import { promisify as promisify5 } from "util";
8147
8350
  import { z as z10 } from "zod";
8148
- var exec11 = promisify14(execFile14);
8351
+ var exec3 = promisify5(execFile5);
8149
8352
  var requestSchema = z10.object({
8150
8353
  id: z10.string().regex(/^[a-zA-Z0-9_-]+$/),
8151
8354
  action: z10.enum(["prepare", "accept", "reaffirm", "retire"]).default("prepare"),
@@ -8168,8 +8371,8 @@ async function reviewState(root, record) {
8168
8371
  changedFiles: touchedPaths(committed),
8169
8372
  localChanges: matchingPaths(anchors, workingTree.changedFiles)
8170
8373
  };
8171
- const reviewToken = createHash4("sha256").update(JSON.stringify({ record, evidence, workingTreeAvailable: workingTree.available })).digest("hex");
8172
- return { reviewToken, evidence, committed, workingTreeAvailable: workingTree.available };
8374
+ const reviewToken2 = createHash4("sha256").update(JSON.stringify({ record, evidence, workingTreeAvailable: workingTree.available })).digest("hex");
8375
+ return { reviewToken: reviewToken2, evidence, committed, workingTreeAvailable: workingTree.available };
8173
8376
  }
8174
8377
  async function previews(root, record, state) {
8175
8378
  const access = await createFileAccess(root);
@@ -8200,7 +8403,7 @@ async function previews(root, record, state) {
8200
8403
  let diffUnavailable;
8201
8404
  if (state.evidence.historyAvailable && diffPaths.length) {
8202
8405
  try {
8203
- const { stdout } = await exec11("git", ["--literal-pathspecs", "diff", "--no-ext-diff", "--no-textconv", "--no-color", "--no-renames", "-U3", record.refreshedHash, state.evidence.headHash, "--", ...diffPaths], { cwd: root, maxBuffer: 1024 * 1024 });
8406
+ const { stdout } = await exec3("git", ["--literal-pathspecs", "diff", "--no-ext-diff", "--no-textconv", "--no-color", "--no-renames", "-U3", record.refreshedHash, state.evidence.headHash, "--", ...diffPaths], { cwd: root, maxBuffer: 1024 * 1024 });
8204
8407
  diff = stdout.slice(0, 16e3);
8205
8408
  if (stdout.length > diff.length) diffUnavailable = "Diff preview was truncated; inspect the full diff before reviewing.";
8206
8409
  } catch {
@@ -8275,7 +8478,9 @@ async function reviewDecision(root, input) {
8275
8478
  // src/mcp/tools.ts
8276
8479
  init_drift2();
8277
8480
  init_snapshot();
8481
+ init_trust2();
8278
8482
  init_drift();
8483
+ init_review();
8279
8484
 
8280
8485
  // src/snapshot/prompt.ts
8281
8486
  var BATCH_SYSTEM_PROMPT = `You are Mason, building one piece of a larger concept-to-files map via a Map-Reduce pattern.
@@ -8372,6 +8577,7 @@ ${JSON.stringify({ partials }, null, 2)}`;
8372
8577
  }
8373
8578
 
8374
8579
  // src/snapshot/partials.ts
8580
+ init_lock();
8375
8581
  init_storage();
8376
8582
  init_snapshot();
8377
8583
  import fs16 from "fs/promises";
@@ -8387,7 +8593,7 @@ var partialSchema = z11.object({
8387
8593
  var scopeSchema2 = z11.object({ files: z11.array(z11.string()), savedAt: z11.string() });
8388
8594
  async function savePartial(rootDir, partial) {
8389
8595
  if (!/^[a-zA-Z0-9_-]+$/.test(partial.batchId)) throw new Error(`Invalid batchId: ${partial.batchId}`);
8390
- await writeStoreJson(rootDir, `${DIRECTORY}/${partial.batchId}.json`, partialSchema.parse(partial));
8596
+ await withSnapshotWrite(rootDir, () => writeStoreJson(rootDir, `${DIRECTORY}/${partial.batchId}.json`, partialSchema.parse(partial)));
8391
8597
  }
8392
8598
  async function loadAllPartials(rootDir) {
8393
8599
  let entries;
@@ -8407,14 +8613,16 @@ async function loadAllPartials(rootDir) {
8407
8613
  return partials.sort((a, b) => a.offset - b.offset);
8408
8614
  }
8409
8615
  async function saveScope(rootDir, files) {
8410
- await writeStoreJson(rootDir, `${DIRECTORY}/scope.json`, { files, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
8616
+ await withSnapshotWrite(rootDir, () => writeStoreJson(rootDir, `${DIRECTORY}/scope.json`, { files, savedAt: (/* @__PURE__ */ new Date()).toISOString() }));
8411
8617
  }
8412
8618
  async function loadScope(rootDir) {
8413
8619
  const raw = await readStoreJson(rootDir, `${DIRECTORY}/scope.json`);
8414
8620
  return raw === null ? null : scopeSchema2.parse(raw).files;
8415
8621
  }
8416
8622
  async function clearScope(rootDir) {
8417
- await fs16.rm(await storePath(rootDir, `${DIRECTORY}/scope.json`), { force: true });
8623
+ await withSnapshotWrite(rootDir, async () => {
8624
+ await fs16.rm(await storePath(rootDir, `${DIRECTORY}/scope.json`), { force: true });
8625
+ });
8418
8626
  }
8419
8627
  async function clearAllPartials(rootDir) {
8420
8628
  await fs16.rm(await storePath(rootDir, DIRECTORY), { recursive: true, force: true });
@@ -8426,7 +8634,7 @@ function batchIdFor(offset) {
8426
8634
  // src/mcp/tools.ts
8427
8635
  init_init();
8428
8636
  init_onboarding();
8429
- var exec17 = promisify20(execFile20);
8637
+ var exec9 = promisify11(execFile11);
8430
8638
  async function buildContext(dir) {
8431
8639
  return {
8432
8640
  rootDir: dir,
@@ -8434,7 +8642,7 @@ async function buildContext(dir) {
8434
8642
  };
8435
8643
  }
8436
8644
  async function analyzeProject(dir) {
8437
- const rootDir = path33.resolve(dir);
8645
+ const rootDir = path34.resolve(dir);
8438
8646
  const context = await buildContext(rootDir);
8439
8647
  const results = await runAll(context);
8440
8648
  const projectSnapshot = await detectProjectSnapshot(rootDir);
@@ -8489,7 +8697,7 @@ async function detectProjectSnapshot(rootDir) {
8489
8697
  const present = [];
8490
8698
  for (const file of buildFiles) {
8491
8699
  try {
8492
- await fs25.access(path33.join(rootDir, file));
8700
+ await fs25.access(path34.join(rootDir, file));
8493
8701
  present.push(file);
8494
8702
  } catch {
8495
8703
  }
@@ -8531,7 +8739,7 @@ async function detectProjectSnapshot(rootDir) {
8531
8739
  const sourceFiles = await access.list();
8532
8740
  const fileCounts = {};
8533
8741
  for (const file of sourceFiles) {
8534
- const ext = path33.extname(file).slice(1);
8742
+ const ext = path34.extname(file).slice(1);
8535
8743
  fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
8536
8744
  }
8537
8745
  return {
@@ -8542,7 +8750,7 @@ async function detectProjectSnapshot(rootDir) {
8542
8750
  };
8543
8751
  }
8544
8752
  async function getCodeSamples(dir, count4 = 15) {
8545
- const rootDir = path33.resolve(dir);
8753
+ const rootDir = path34.resolve(dir);
8546
8754
  const samples = await sampleFiles(rootDir, count4);
8547
8755
  const output = {
8548
8756
  note: "These are previews (first ~60 lines). Read the file directly with your own tools to see it in full.",
@@ -8585,7 +8793,7 @@ async function unmappedContextResponse(rootDir) {
8585
8793
  });
8586
8794
  }
8587
8795
  async function getProjectStructure(dir) {
8588
- const rootDir = path33.resolve(dir);
8796
+ const rootDir = path34.resolve(dir);
8589
8797
  const allFiles = await (await createFileAccess(rootDir)).list("**/*");
8590
8798
  const dirInfo = /* @__PURE__ */ new Map();
8591
8799
  for (const file of allFiles) {
@@ -8597,7 +8805,7 @@ async function getProjectStructure(dir) {
8597
8805
  }
8598
8806
  const info = dirInfo.get(dirPath);
8599
8807
  info.fileCount++;
8600
- const ext = path33.extname(file).slice(1);
8808
+ const ext = path34.extname(file).slice(1);
8601
8809
  if (ext) {
8602
8810
  info.extensions.set(ext, (info.extensions.get(ext) ?? 0) + 1);
8603
8811
  }
@@ -8642,7 +8850,7 @@ async function buildChangedFilePreviews(rootDir, changedFiles) {
8642
8850
  return previews2;
8643
8851
  }
8644
8852
  async function getSnapshot(dir) {
8645
- const rootDir = path33.resolve(dir);
8853
+ const rootDir = path34.resolve(dir);
8646
8854
  const snapshot = await loadSnapshot(rootDir);
8647
8855
  if (!snapshot) {
8648
8856
  return unmappedContextResponse(rootDir);
@@ -8680,9 +8888,9 @@ async function getSnapshot(dir) {
8680
8888
  const store = await loadDecisionStore2(rootDir);
8681
8889
  const decisionRecords = store.records;
8682
8890
  const decisionDrift = await computeDecisionDrift(rootDir, decisionRecords);
8891
+ const readTrust = createSnapshotTrustReader(rootDir);
8683
8892
  const trust = {
8684
- features: Object.fromEntries(Object.entries(snapshot.features).map(([name, entry]) => [name, assessTrust(entry, drift?.featureFreshness?.[name] ?? "unknown")])),
8685
- flows: Object.fromEntries(Object.entries(snapshot.flows).map(([name, entry]) => [name, assessTrust(entry, drift?.flowFreshness?.[name] ?? "unknown")])),
8893
+ ...await readSnapshotTrustIndex(readTrust, snapshot, drift),
8686
8894
  decisions: Object.fromEntries(decisionRecords.filter((d) => d.status === "active").map((d) => [d.id, decisionTrust(effectiveDecision(d), decisionDrift.freshness?.[d.id] ?? "unknown")]))
8687
8895
  };
8688
8896
  output.trust = trust;
@@ -8743,7 +8951,7 @@ function driftHint(report) {
8743
8951
  return "Read the changed files under staleFeatures/staleFlows, update those entries (fold unmappedFiles into the right features), and call save_snapshot with only the affected entries \u2014 unchanged entries are preserved. Drop ghostFiles from any entries that reference them, and delete features/flows that no longer exist via save_snapshot's removeFeatures/removeFlows.";
8744
8952
  }
8745
8953
  async function checkDrift(dir) {
8746
- const rootDir = path33.resolve(dir);
8954
+ const rootDir = path34.resolve(dir);
8747
8955
  const report = await computeDrift(rootDir);
8748
8956
  if (!report) {
8749
8957
  return JSON.stringify({
@@ -8774,7 +8982,7 @@ async function checkDrift(dir) {
8774
8982
  return JSON.stringify({ exists: true, ...report, verification, hint });
8775
8983
  }
8776
8984
  async function generateSnapshotBatch(dir, offset = 0, batchSize = DEFAULT_BATCH_SIZE, files) {
8777
- const rootDir = path33.resolve(dir);
8985
+ const rootDir = path34.resolve(dir);
8778
8986
  const scoped = files !== void 0 && files.length > 0;
8779
8987
  const scopeFiles = scoped ? sanitizePaths(rootDir, files) : void 0;
8780
8988
  const batch = await prepareSnapshotBatch(dir, offset, batchSize, scopeFiles);
@@ -8821,7 +9029,7 @@ async function generateSnapshotBatch(dir, offset = 0, batchSize = DEFAULT_BATCH_
8821
9029
  );
8822
9030
  }
8823
9031
  async function saveSnapshotPartial(dir, batchId, offset, features, flows) {
8824
- const rootDir = path33.resolve(dir);
9032
+ const rootDir = path34.resolve(dir);
8825
9033
  for (const feat of Object.values(features)) {
8826
9034
  feat.files = sanitizePaths(rootDir, feat.files);
8827
9035
  if (feat.tests) feat.tests = sanitizePaths(rootDir, feat.tests);
@@ -8850,7 +9058,7 @@ async function saveSnapshotPartial(dir, batchId, offset, features, flows) {
8850
9058
  );
8851
9059
  }
8852
9060
  async function reduceSnapshot(dir) {
8853
- const rootDir = path33.resolve(dir);
9061
+ const rootDir = path34.resolve(dir);
8854
9062
  const partials = await loadAllPartials(rootDir);
8855
9063
  if (partials.length === 0) {
8856
9064
  return JSON.stringify(
@@ -8912,7 +9120,7 @@ async function reduceSnapshot(dir) {
8912
9120
  );
8913
9121
  }
8914
9122
  async function fullAnalysis(dir) {
8915
- const rootDir = path33.resolve(dir);
9123
+ const rootDir = path34.resolve(dir);
8916
9124
  const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
8917
9125
  analyzeProject(dir),
8918
9126
  getProjectStructure(dir),
@@ -8941,100 +9149,94 @@ function sanitizePaths(rootDir, files) {
8941
9149
  return sanitizeRepoPaths(files);
8942
9150
  }
8943
9151
  async function saveSnapshotData(dir, features, flows, removeFeatures = [], removeFlows = []) {
8944
- const rootDir = path33.resolve(dir);
8945
- const gitHash = await getCurrentGitHash(rootDir);
8946
- const now = (/* @__PURE__ */ new Date()).toISOString();
8947
- for (const feat of Object.values(features)) {
8948
- feat.files = sanitizePaths(rootDir, feat.files);
8949
- if (feat.tests) feat.tests = sanitizePaths(rootDir, feat.tests);
8950
- feat.type = normalizeFeatureType(feat.type);
8951
- }
8952
- for (const flow of Object.values(flows)) {
8953
- flow.chain = sanitizePaths(rootDir, flow.chain);
8954
- }
8955
- const partials = await loadAllPartials(rootDir);
8956
- const replaceMode = partials.length > 0;
8957
- const previous = await loadSnapshot(rootDir);
8958
- const existing = replaceMode ? null : previous;
8959
- const preserveVerification = (next, old) => {
8960
- if (!old) return;
8961
- const semantic = (entry) => JSON.stringify({
8962
- description: entry.description,
8963
- files: "files" in entry ? entry.files : void 0,
8964
- chain: "chain" in entry ? entry.chain : void 0,
8965
- tests: "files" in entry ? entry.tests : void 0,
8966
- type: "files" in entry ? normalizeFeatureType(entry.type) : void 0
8967
- });
8968
- if (semantic(next) !== semantic(old)) return;
8969
- next.verifiedAt = old.verifiedAt;
8970
- next.verifiedHash = old.verifiedHash;
8971
- next.verificationFailed = old.verificationFailed;
8972
- next.verificationNote = old.verificationNote;
8973
- };
8974
- for (const [name, entry] of Object.entries(features)) preserveVerification(entry, previous?.features[name]);
8975
- for (const [name, entry] of Object.entries(flows)) preserveVerification(entry, previous?.flows[name]);
8976
- if (existing) {
8977
- if (existing.gitHash !== "unknown") {
8978
- for (const feat of Object.values(existing.features)) {
8979
- feat.refreshedHash ??= existing.gitHash;
9152
+ const rootDir = path34.resolve(dir);
9153
+ return updateSnapshot(rootDir, async (previous) => {
9154
+ const gitHash = await getCurrentGitHash(rootDir);
9155
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9156
+ for (const feat of Object.values(features)) {
9157
+ feat.files = sanitizePaths(rootDir, feat.files);
9158
+ if (feat.tests) feat.tests = sanitizePaths(rootDir, feat.tests);
9159
+ feat.type = normalizeFeatureType(feat.type);
9160
+ }
9161
+ for (const flow of Object.values(flows)) {
9162
+ flow.chain = sanitizePaths(rootDir, flow.chain);
9163
+ }
9164
+ const partials = await loadAllPartials(rootDir);
9165
+ const replaceMode = partials.length > 0;
9166
+ const existing = replaceMode ? null : previous;
9167
+ const preserveVerification = (next, old) => {
9168
+ delete next.verifiedAt;
9169
+ delete next.verifiedHash;
9170
+ delete next.verificationToken;
9171
+ delete next.verificationFailed;
9172
+ delete next.verificationNote;
9173
+ if (!old) return;
9174
+ if (JSON.stringify(snapshotEntryContent(next)) !== JSON.stringify(snapshotEntryContent(old))) return;
9175
+ next.verificationToken = old.verificationToken;
9176
+ next.verifiedAt = old.verifiedAt;
9177
+ next.verifiedHash = old.verifiedHash;
9178
+ next.verificationFailed = old.verificationFailed;
9179
+ next.verificationNote = old.verificationNote;
9180
+ };
9181
+ for (const [name, entry] of Object.entries(features)) preserveVerification(entry, previous?.features[name]);
9182
+ for (const [name, entry] of Object.entries(flows)) preserveVerification(entry, previous?.flows[name]);
9183
+ if (existing) {
9184
+ if (existing.gitHash !== "unknown") {
9185
+ for (const feat of Object.values(existing.features)) {
9186
+ feat.refreshedHash ??= existing.gitHash;
9187
+ }
9188
+ for (const flow of Object.values(existing.flows)) {
9189
+ flow.refreshedHash ??= existing.gitHash;
9190
+ }
8980
9191
  }
8981
- for (const flow of Object.values(existing.flows)) {
8982
- flow.refreshedHash ??= existing.gitHash;
9192
+ const removedFeatures = removeFeatures.filter(
9193
+ (name) => name in existing.features
9194
+ );
9195
+ const removedFlows = removeFlows.filter((name) => name in existing.flows);
9196
+ for (const name of removedFeatures) delete existing.features[name];
9197
+ for (const name of removedFlows) delete existing.flows[name];
9198
+ if (gitHash !== "unknown") {
9199
+ for (const feat of Object.values(features)) feat.refreshedHash = gitHash;
9200
+ for (const flow of Object.values(flows)) flow.refreshedHash = gitHash;
8983
9201
  }
8984
- }
8985
- const removedFeatures = removeFeatures.filter(
8986
- (name) => name in existing.features
8987
- );
8988
- const removedFlows = removeFlows.filter((name) => name in existing.flows);
8989
- for (const name of removedFeatures) delete existing.features[name];
8990
- for (const name of removedFlows) delete existing.flows[name];
8991
- if (gitHash !== "unknown") {
8992
- for (const feat of Object.values(features)) feat.refreshedHash = gitHash;
8993
- for (const flow of Object.values(flows)) flow.refreshedHash = gitHash;
8994
- }
8995
- existing.features = { ...existing.features, ...features };
8996
- existing.flows = { ...existing.flows, ...flows };
8997
- existing.updatedAt = now;
8998
- existing.gitHash = gitHash;
8999
- await saveSnapshot(rootDir, existing);
9000
- await clearAllPartials(rootDir);
9001
- return JSON.stringify({
9002
- status: "updated",
9003
- mode: "merged",
9004
- features: Object.keys(existing.features).length,
9005
- flows: Object.keys(existing.flows).length,
9006
- removedFeatures: removedFeatures.length,
9007
- removedFlows: removedFlows.length
9008
- });
9009
- }
9010
- const snapshot = {
9011
- version: 2,
9012
- createdAt: now,
9013
- updatedAt: now,
9014
- gitHash,
9015
- features,
9016
- flows
9017
- };
9018
- await saveSnapshot(rootDir, snapshot);
9019
- await clearAllPartials(rootDir);
9020
- return JSON.stringify({
9021
- status: replaceMode ? "replaced" : "created",
9022
- mode: replaceMode ? "replaced-from-partials" : "fresh",
9023
- features: Object.keys(features).length,
9024
- flows: Object.keys(flows).length
9202
+ existing.features = { ...existing.features, ...features };
9203
+ existing.flows = { ...existing.flows, ...flows };
9204
+ existing.updatedAt = now;
9205
+ existing.gitHash = gitHash;
9206
+ return { snapshot: existing, afterSave: () => clearAllPartials(rootDir), result: JSON.stringify({
9207
+ status: "updated",
9208
+ mode: "merged",
9209
+ features: Object.keys(existing.features).length,
9210
+ flows: Object.keys(existing.flows).length,
9211
+ removedFeatures: removedFeatures.length,
9212
+ removedFlows: removedFlows.length
9213
+ }) };
9214
+ }
9215
+ const snapshot = {
9216
+ version: 2,
9217
+ createdAt: now,
9218
+ updatedAt: now,
9219
+ gitHash,
9220
+ features,
9221
+ flows
9222
+ };
9223
+ return { snapshot, afterSave: () => clearAllPartials(rootDir), result: JSON.stringify({
9224
+ status: replaceMode ? "replaced" : "created",
9225
+ mode: replaceMode ? "replaced-from-partials" : "fresh",
9226
+ features: Object.keys(features).length,
9227
+ flows: Object.keys(flows).length
9228
+ }) };
9025
9229
  });
9026
9230
  }
9027
9231
  async function getImpact(dir, files) {
9028
- const rootDir = path33.resolve(dir);
9232
+ const rootDir = path34.resolve(dir);
9029
9233
  const { analyzeImpact: analyzeImpact2 } = await Promise.resolve().then(() => (init_impact(), impact_exports));
9030
9234
  const result = await analyzeImpact2(rootDir, files);
9031
9235
  return JSON.stringify(result, null, 2);
9032
9236
  }
9033
9237
  var VERIFY_DEFAULT_SAMPLE = 5;
9034
- var VERIFY_MAX_FILES_PER_ENTRY = 8;
9035
- var VERIFY_SKELETON_CHARS = 500;
9036
9238
  async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
9037
- const rootDir = path33.resolve(dir);
9239
+ const rootDir = path34.resolve(dir);
9038
9240
  const snapshot = await loadSnapshot(rootDir);
9039
9241
  if (!snapshot) {
9040
9242
  return JSON.stringify({
@@ -9068,25 +9270,13 @@ async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
9068
9270
  const picked = entries.slice(0, Math.max(1, sample));
9069
9271
  const toVerify = [];
9070
9272
  for (const entry of picked) {
9071
- const skeletons = [];
9072
- for (const filePath of entry.files.slice(0, VERIFY_MAX_FILES_PER_ENTRY)) {
9073
- const full = await access.read(filePath);
9074
- if (full) {
9075
- skeletons.push({
9076
- path: full.path,
9077
- content: full.content.slice(0, VERIFY_SKELETON_CHARS)
9078
- });
9079
- } else {
9080
- skeletons.push({ path: filePath, missing: true });
9081
- }
9082
- }
9273
+ const stored = entry.kind === "feature" ? snapshot.features[entry.name] : snapshot.flows[entry.name];
9083
9274
  toVerify.push({
9084
9275
  name: entry.name,
9085
9276
  kind: entry.kind,
9086
9277
  description: entry.description,
9087
9278
  lastVerified: entry.verifiedAt ?? "never",
9088
- skeletons,
9089
- truncated: entry.files.length > VERIFY_MAX_FILES_PER_ENTRY
9279
+ ...await prepareSnapshotReview(access, entry.kind, entry.name, stored)
9090
9280
  });
9091
9281
  }
9092
9282
  const neverVerified = entries.filter((e) => !e.verifiedAt).length;
@@ -9095,58 +9285,94 @@ async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
9095
9285
  totalEntries: entries.length,
9096
9286
  neverVerified,
9097
9287
  entries: toVerify,
9098
- instructions: 'For each entry, judge from the skeletons whether the listed files actually implement the claimed feature/flow (missing files count against it). Then call save_verification with verdicts: {"<entry name>": {"ok": true|false, "note": "<one line, required when ok is false>"}}. Be skeptical \u2014 a plausible description is not evidence; the files must show it.'
9288
+ instructions: 'Judge whether the files implement each claimed feature/flow, then call save_verification with verdicts: {"<entry name>": {"kind": "<feature|flow from the entry>", "reviewToken": "<token from the entry>", "ok": true|false, "note": "<required when ok is false>"}}. Tokens cover the entry and full contents of the previewed files (at most 8), including unavailable-file markers; previews themselves are truncated. This is a spot-check, not proof of unshown files. A conflict requires verify_snapshot and a fresh review; never reuse a verdict for changed content. If a feature and flow share a name, submit their verdicts in separate calls.'
9099
9289
  });
9100
9290
  }
9101
9291
  async function saveVerification(dir, verdicts) {
9102
- const rootDir = path33.resolve(dir);
9103
- const snapshot = await loadSnapshot(rootDir);
9104
- if (!snapshot) {
9105
- return JSON.stringify({ exists: false, hint: "No concept map exists." });
9106
- }
9107
- const now = (/* @__PURE__ */ new Date()).toISOString();
9108
- const verifiedHash = await getCurrentGitHash(rootDir);
9109
- const stamped = [];
9110
- const unknown = [];
9111
- const failed = [];
9112
- for (const [name, verdict] of Object.entries(verdicts)) {
9113
- const entry = snapshot.features[name] ?? snapshot.flows[name];
9114
- if (!entry) {
9115
- unknown.push(name);
9116
- continue;
9117
- }
9118
- entry.verifiedAt = now;
9119
- entry.verifiedHash = verifiedHash;
9120
- if (verdict.ok) {
9121
- delete entry.verificationFailed;
9122
- delete entry.verificationNote;
9123
- } else {
9124
- entry.verificationFailed = true;
9125
- entry.verificationNote = verdict.note ?? "verification failed";
9126
- failed.push(name);
9127
- }
9128
- stamped.push(name);
9129
- }
9130
- snapshot.updatedAt = now;
9131
- await saveSnapshot(rootDir, snapshot);
9132
- return JSON.stringify({
9133
- stamped,
9134
- unknown,
9135
- failed,
9136
- hint: failed.length > 0 ? `Entries [${failed.join(", ")}] are mis-mapped. Re-map them: read their actual files, correct the entries, and call save_snapshot with only those entries (plus removeFeatures/removeFlows if a concept no longer exists).` : "All sampled entries verified. Re-run verify_snapshot periodically \u2014 it always picks the least-recently-verified entries next."
9292
+ const rootDir = path34.resolve(dir);
9293
+ return updateSnapshot(rootDir, async (snapshot) => {
9294
+ if (!snapshot) {
9295
+ return { snapshot: null, result: JSON.stringify({ exists: false, stamped: [], hint: "No concept map exists. Build it and run verify_snapshot before recording verdicts." }) };
9296
+ }
9297
+ const stamped = [], unknown = [], failed = [];
9298
+ const conflicts = [], reviewRequired = [], invalid = [];
9299
+ const candidates = [];
9300
+ let access;
9301
+ for (const [name, verdict] of Object.entries(verdicts)) {
9302
+ if (!verdict.kind || !["feature", "flow"].includes(verdict.kind) || !/^[a-f0-9]{64}$/.test(verdict.reviewToken ?? "")) {
9303
+ reviewRequired.push(name);
9304
+ continue;
9305
+ }
9306
+ if (typeof verdict.ok !== "boolean" || !verdict.ok && !verdict.note?.trim()) {
9307
+ invalid.push(name);
9308
+ continue;
9309
+ }
9310
+ const collection = verdict.kind === "feature" ? snapshot.features : snapshot.flows;
9311
+ if (!Object.hasOwn(collection, name)) {
9312
+ unknown.push(name);
9313
+ conflicts.push(name);
9314
+ continue;
9315
+ }
9316
+ const entry = collection[name];
9317
+ access ??= await createFileAccess(rootDir);
9318
+ const current = await prepareSnapshotReview(access, verdict.kind, name, entry);
9319
+ if (current.reviewToken !== verdict.reviewToken) conflicts.push(name);
9320
+ else candidates.push({ name, verdict, entry });
9321
+ }
9322
+ const verifiedHash = candidates.length ? await getCurrentGitHash(rootDir) : "unknown";
9323
+ const freshAccess = candidates.length ? await createFileAccess(rootDir) : null;
9324
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9325
+ for (const { name, verdict, entry } of candidates) {
9326
+ const current = await prepareSnapshotReview(freshAccess, verdict.kind, name, entry);
9327
+ if (current.reviewToken !== verdict.reviewToken) {
9328
+ conflicts.push(name);
9329
+ continue;
9330
+ }
9331
+ entry.verifiedAt = now;
9332
+ entry.verifiedHash = verifiedHash;
9333
+ entry.verificationToken = verdict.reviewToken;
9334
+ if (verdict.ok) {
9335
+ delete entry.verificationFailed;
9336
+ delete entry.verificationNote;
9337
+ } else {
9338
+ entry.verificationFailed = true;
9339
+ entry.verificationNote = verdict.note.trim();
9340
+ failed.push(name);
9341
+ }
9342
+ stamped.push(name);
9343
+ }
9344
+ const rejected = conflicts.length + reviewRequired.length + invalid.length;
9345
+ const status = rejected ? stamped.length ? "partial" : conflicts.length ? "conflict" : reviewRequired.length ? "review_required" : "invalid" : stamped.length ? "saved" : "unchanged";
9346
+ const hints = [];
9347
+ if (conflicts.length) hints.push(`Entries [${conflicts.join(", ")}] changed or were deleted after review. Run verify_snapshot and review current evidence before retrying.`);
9348
+ if (reviewRequired.length) hints.push(`Entries [${reviewRequired.join(", ")}] need kind and reviewToken from verify_snapshot. Inspect that evidence before submitting a verdict.`);
9349
+ if (invalid.length) hints.push(`Entries [${invalid.join(", ")}] need a boolean ok and a non-empty note when ok is false.`);
9350
+ if (failed.length) hints.push(`Entries [${failed.join(", ")}] are mis-mapped. Re-map them: read their actual files, correct the entries, and call save_snapshot with only those entries (plus removeFeatures/removeFlows if a concept no longer exists). Then run verify_snapshot for fresh review tokens.`);
9351
+ if (!hints.length) hints.push(stamped.length ? "All submitted entries verified against the reviewed evidence. Re-run verify_snapshot periodically." : "No verdicts submitted; nothing changed.");
9352
+ if (stamped.length) snapshot.updatedAt = now;
9353
+ return { snapshot: stamped.length ? snapshot : null, result: JSON.stringify({
9354
+ status,
9355
+ stamped,
9356
+ unknown,
9357
+ failed,
9358
+ conflicts,
9359
+ reviewRequired,
9360
+ invalid,
9361
+ hint: hints.join(" ")
9362
+ }) };
9137
9363
  });
9138
9364
  }
9139
9365
  async function saveDecision(dir, input) {
9140
- const rootDir = path33.resolve(dir);
9366
+ const rootDir = path34.resolve(dir);
9141
9367
  const { upsertDecision: upsertDecision2 } = await Promise.resolve().then(() => (init_decisions(), decisions_exports));
9142
9368
  const result = await upsertDecision2(rootDir, input);
9143
9369
  return JSON.stringify(result);
9144
9370
  }
9145
9371
  async function reviewDecision2(dir, input) {
9146
- return JSON.stringify(await reviewDecision(path33.resolve(dir), input));
9372
+ return JSON.stringify(await reviewDecision(path34.resolve(dir), input));
9147
9373
  }
9148
9374
  async function getContext(dir, task, files) {
9149
- const rootDir = path33.resolve(dir);
9375
+ const rootDir = path34.resolve(dir);
9150
9376
  const { assembleContext: assembleContext2 } = await Promise.resolve().then(() => (init_assemble(), assemble_exports));
9151
9377
  const bundle = await assembleContext2(rootDir, task, files);
9152
9378
  return JSON.stringify(bundle);
@@ -9195,7 +9421,7 @@ async function masonInit(dir, options = {}) {
9195
9421
  return JSON.stringify(await setupProject2(dir, options), null, 2);
9196
9422
  }
9197
9423
  if (options.host) throw new Error("host applies only to mode: setup.");
9198
- const rootDir = path33.resolve(dir);
9424
+ const rootDir = path34.resolve(dir);
9199
9425
  const marker = await loadProjectMarker(rootDir);
9200
9426
  const { loadSetup: loadSetup2 } = await Promise.resolve().then(() => (init_model(), model_exports));
9201
9427
  const setup = await loadSetup2(rootDir);
@@ -9215,7 +9441,7 @@ async function masonInit(dir, options = {}) {
9215
9441
  );
9216
9442
  }
9217
9443
  async function masonCompleteInit(dir, options = {}) {
9218
- const rootDir = path33.resolve(dir);
9444
+ const rootDir = path34.resolve(dir);
9219
9445
  const existing = await loadProjectMarker(rootDir);
9220
9446
  const features = await loadProjectFeatures(rootDir);
9221
9447
  const marker = {
@@ -9324,7 +9550,7 @@ async function masonSetConfluence(input) {
9324
9550
  );
9325
9551
  }
9326
9552
  async function exportToConfluenceTool(dir, overrides) {
9327
- const rootDir = path33.resolve(dir);
9553
+ const rootDir = path34.resolve(dir);
9328
9554
  const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports2));
9329
9555
  const { exportToConfluence: exportToConfluence2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
9330
9556
  const config = await loadConfig2();
@@ -9363,10 +9589,10 @@ function createMcpServer() {
9363
9589
  const server = new McpServer(
9364
9590
  {
9365
9591
  name: "mason",
9366
- version: "0.17.3"
9592
+ version: "0.18.0"
9367
9593
  },
9368
9594
  {
9369
- instructions: (setupNotice ? setupNotice + "\n\n" : "") + 'Mason retrieves recorded decisions, file impact, and optional feature/flow maps. Use get_context with the task and known files, and get_impact before editing. Save learned rationale and constraints with save_decision; review and commit the local records through the project workflow. These tools work without initialization or a concept map. Consult trust and diagnostics: changed or unknown freshness needs source inspection, failed verification needs correction, and proposals are suggestions and legacy records are unreviewed. Accepted decisions are recorded team constraints, still subject to freshness checks. Use review_decision to prepare code evidence and record only authorized acceptance, reaffirmation, or retirement. Never invent a reviewer or treat these recorded identities as authenticated approval. mason_init returns documentation audit and committed-diff review findings with a quickstart guide. Use mode: "map" only when a full architecture map is requested. A missing map is not a setup failure; use decisions and source evidence. get_snapshot provides architecture navigation when a map is available. The mason-audit and mason-review CLIs also work without setup.'
9595
+ instructions: (setupNotice ? setupNotice + "\n\n" : "") + 'Mason retrieves recorded decisions, file impact, and optional feature/flow maps. Use get_context with the task and known files, and get_impact before editing. Save learned rationale and constraints with save_decision; review and commit the local records through the project workflow. These tools work without initialization or a concept map. Consult trust and diagnostics: changed or unknown freshness needs source inspection, stale or unknown map verification needs a fresh review, recorded failed verdicts need correction, and proposals are suggestions and legacy records are unreviewed. Accepted decisions are recorded team constraints, still subject to freshness checks. Use review_decision to prepare code evidence and record only authorized acceptance, reaffirmation, or retirement. Never invent a reviewer or treat these recorded identities as authenticated approval. mason_init returns documentation audit and committed-diff review findings with a quickstart guide. Use mode: "map" only when a full architecture map is requested. A missing map is not a setup failure; use decisions and source evidence. get_snapshot provides architecture navigation when a map is available. The mason-audit and mason-review CLIs also work without setup.'
9370
9596
  }
9371
9597
  );
9372
9598
  server.tool(
@@ -9488,7 +9714,7 @@ function createMcpServer() {
9488
9714
  );
9489
9715
  server.tool(
9490
9716
  "get_snapshot",
9491
- "Return the optional feature-to-file architecture map with drift and trust evidence. If no map exists, returns exists:false plus project structure, Git signals, and test pairs. Decision capture, get_context, and get_impact still work. No initialization required.",
9717
+ "Return the optional feature-to-file architecture map with drift and trust evidence. Map verification is rechecked against sampled source contents; stale or unknown verification requires a fresh review, and recordedVerdict preserves the historical verdict. If no map exists, returns exists:false plus project structure, Git signals, and test pairs. Decision capture, get_context, and get_impact still work. No initialization required.",
9492
9718
  {
9493
9719
  dir: z24.string().describe("Absolute path to the project root directory")
9494
9720
  },
@@ -9501,7 +9727,7 @@ function createMcpServer() {
9501
9727
  );
9502
9728
  server.tool(
9503
9729
  "get_context",
9504
- "Assemble task context: matching decisions with rationale, approval, owner, sources, last review, and freshness, plus related tests, file impact, and any available map entries. Proposals are suggestions; legacy records are unreviewed; accepted decisions are constraints subject to freshness. No initialization or map required. Pass task and optional files. map.status and diagnostics preserve missing or invalid knowledge. Impact covers up to three unique files, expanding directory anchors.",
9730
+ "Assemble task context: matching decisions with rationale, approval, owner, sources, last review, and freshness, plus related tests, file impact, and any available map entries. Map verification is rechecked against sampled source contents; stale or unknown verification requires a fresh review, and recordedVerdict preserves the historical verdict. Proposals are suggestions; legacy records are unreviewed; accepted decisions are constraints subject to freshness. No initialization or map required. Pass task and optional files. map.status and diagnostics preserve missing or invalid knowledge. Impact covers up to three unique files, expanding directory anchors.",
9505
9731
  {
9506
9732
  dir: z24.string().describe("Absolute path to the project root directory"),
9507
9733
  task: z24.string().describe("The task, bug, or change request in natural language \u2014 e.g. 'add rate limiting to the API client' or a ticket description"),
@@ -9677,7 +9903,7 @@ function createMcpServer() {
9677
9903
  );
9678
9904
  server.tool(
9679
9905
  "verify_snapshot",
9680
- "Spot-check the concept map's CORRECTNESS (drift checks freshness; this checks entries were right to begin with). Returns a sample of entries \u2014 always the never-verified and least-recently-verified first \u2014 with skeletons of their claimed files, for you to judge whether the files actually implement what the entry claims. Report verdicts back via save_verification. Run periodically, or after an automated refresh wrote entries no human reviewed.",
9906
+ "Spot-check the concept map's CORRECTNESS (drift checks freshness; this checks entries were right to begin with). Returns a sample of entries \u2014 always the never-verified and least-recently-verified first \u2014 with skeletons of their claimed files, for you to judge whether the files actually implement what the entry claims. Report verdicts back via save_verification with each entry\u2019s kind and reviewToken. Tokens bind the entry and bounded source evidence; changed evidence requires a fresh review. Run periodically, or after an automated refresh wrote entries no human reviewed.",
9681
9907
  {
9682
9908
  dir: z24.string().describe("Absolute path to the project root directory"),
9683
9909
  sample: z24.number().int().optional().describe("Entries to sample (default 5)")
@@ -9689,11 +9915,13 @@ function createMcpServer() {
9689
9915
  );
9690
9916
  server.tool(
9691
9917
  "save_verification",
9692
- "Record verify_snapshot verdicts. Entries judged ok are stamped verifiedAt; failures are flagged verificationFailed with your note and surface in get_context, get_snapshot, and mason_check_drift until corrected. Verdict notes are required for failures.",
9918
+ "Record verify_snapshot verdicts using each entry\u2019s kind and reviewToken. Missing tokens request a fresh review; changed or deleted entries return conflicts without being stamped. Entries judged ok are stamped verifiedAt; failures are flagged verificationFailed with your note and surface in get_context, get_snapshot, and mason_check_drift until corrected. Verdict notes are required for failures.",
9693
9919
  {
9694
9920
  dir: z24.string().describe("Absolute path to the project root directory"),
9695
9921
  verdicts: z24.record(
9696
9922
  z24.object({
9923
+ kind: z24.enum(["feature", "flow"]).optional().describe("Entry kind returned by verify_snapshot; required to record a verdict"),
9924
+ reviewToken: z24.string().optional().describe("Copy the reviewToken from verify_snapshot after inspecting its evidence; required to record a verdict"),
9697
9925
  ok: z24.boolean(),
9698
9926
  note: z24.string().optional().describe("One line on what's wrong \u2014 required when ok is false")
9699
9927
  })