mason-context 0.16.3 → 0.17.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.
@@ -1,30 +1,13 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/audit/cli.ts
4
- import path17 from "path";
5
-
6
- // src/audit/audit.ts
7
- import fs9 from "fs/promises";
8
- import path15 from "path";
9
-
10
- // src/drift/drift.ts
11
- import fs3 from "fs/promises";
12
- import path6 from "path";
13
- import { execFile as execFile3 } from "child_process";
14
- import { promisify as promisify3 } from "util";
15
-
16
- // src/snapshot/snapshot.ts
17
- import path5 from "path";
18
- import { execFile as execFile2 } from "child_process";
19
- import { promisify as promisify2 } from "util";
20
-
21
- // src/utils/files.ts
22
- import fs from "fs/promises";
23
- import { constants } from "fs";
24
- import path2 from "path";
25
- import { execFile } from "child_process";
26
- import { promisify } from "util";
27
- import fg from "fast-glob";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
28
11
 
29
12
  // src/utils/paths.ts
30
13
  import path from "path";
@@ -47,35 +30,19 @@ function anchorMatches(anchor, file) {
47
30
  function matchingPaths(anchors, files) {
48
31
  return [...new Set(files)].filter((file) => anchors.some((anchor) => anchorMatches(anchor, file)));
49
32
  }
33
+ var init_paths = __esm({
34
+ "src/utils/paths.ts"() {
35
+ "use strict";
36
+ }
37
+ });
50
38
 
51
39
  // src/utils/files.ts
52
- var exec = promisify(execFile);
53
- var 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"];
54
- var SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
55
- var SOURCE_IGNORE = [
56
- "**/node_modules/**",
57
- "**/dist/**",
58
- "**/build/**",
59
- "**/.gradle/**",
60
- "**/target/**",
61
- "**/.git/**",
62
- "**/.mason/**",
63
- "**/vendor/**",
64
- "**/__pycache__/**",
65
- "**/venv/**",
66
- "**/.venv/**",
67
- "**/*.min.*",
68
- "**/*.map",
69
- "**/*.lock",
70
- "**/generated/**",
71
- "**/*.generated.*",
72
- "**/R.java",
73
- "**/BuildConfig.java",
74
- "**/package-lock.json",
75
- "**/yarn.lock",
76
- "**/pnpm-lock.yaml"
77
- ];
78
- var MAX_SOURCE_BYTES = 1024 * 1024;
40
+ import fs from "fs/promises";
41
+ import { constants } from "fs";
42
+ import path2 from "path";
43
+ import { execFile } from "child_process";
44
+ import { promisify } from "util";
45
+ import fg from "fast-glob";
79
46
  async function readBoundedFile(file, maxBytes) {
80
47
  const handle = await fs.open(file, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
81
48
  try {
@@ -116,6 +83,40 @@ async function loadProjectConfig(root) {
116
83
  throw new Error(`Cannot apply project file policy: ${error instanceof Error ? error.message : String(error)}`);
117
84
  }
118
85
  }
86
+ var exec, SOURCE_EXTENSIONS, SOURCE_GLOB, SOURCE_IGNORE, MAX_SOURCE_BYTES;
87
+ var init_files = __esm({
88
+ "src/utils/files.ts"() {
89
+ "use strict";
90
+ init_paths();
91
+ exec = promisify(execFile);
92
+ 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"];
93
+ SOURCE_GLOB = `**/*.{${SOURCE_EXTENSIONS.join(",")}}`;
94
+ SOURCE_IGNORE = [
95
+ "**/node_modules/**",
96
+ "**/dist/**",
97
+ "**/build/**",
98
+ "**/.gradle/**",
99
+ "**/target/**",
100
+ "**/.git/**",
101
+ "**/.mason/**",
102
+ "**/vendor/**",
103
+ "**/__pycache__/**",
104
+ "**/venv/**",
105
+ "**/.venv/**",
106
+ "**/*.min.*",
107
+ "**/*.map",
108
+ "**/*.lock",
109
+ "**/generated/**",
110
+ "**/*.generated.*",
111
+ "**/R.java",
112
+ "**/BuildConfig.java",
113
+ "**/package-lock.json",
114
+ "**/yarn.lock",
115
+ "**/pnpm-lock.yaml"
116
+ ];
117
+ MAX_SOURCE_BYTES = 1024 * 1024;
118
+ }
119
+ });
119
120
 
120
121
  // src/utils/storage.ts
121
122
  import fs2 from "fs/promises";
@@ -179,39 +180,28 @@ async function writeStoreJson(root, relative, value) {
179
180
  await fs2.rm(temporary, { force: true });
180
181
  }
181
182
  }
182
-
183
- // src/snapshot/snapshot.ts
184
- import { z } from "zod";
183
+ var init_storage = __esm({
184
+ "src/utils/storage.ts"() {
185
+ "use strict";
186
+ init_paths();
187
+ init_files();
188
+ }
189
+ });
185
190
 
186
191
  // src/test-map.ts
187
192
  import path4 from "path";
193
+ var init_test_map = __esm({
194
+ "src/test-map.ts"() {
195
+ "use strict";
196
+ init_files();
197
+ }
198
+ });
188
199
 
189
200
  // src/snapshot/snapshot.ts
190
- var exec2 = promisify2(execFile2);
191
- var repoPath = z.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
192
- var verificationFields = {
193
- refreshedHash: z.string().optional(),
194
- verifiedAt: z.string().optional(),
195
- verifiedHash: z.string().optional(),
196
- verificationFailed: z.boolean().optional(),
197
- verificationNote: z.string().optional()
198
- };
199
- var featureSchema = z.object({
200
- description: z.string(),
201
- files: z.array(repoPath),
202
- tests: z.array(repoPath).optional(),
203
- type: z.enum(["capability", "infrastructure"]).optional(),
204
- ...verificationFields
205
- }).passthrough();
206
- var flowSchema = z.object({ description: z.string(), chain: z.array(repoPath), ...verificationFields }).passthrough();
207
- var snapshotSchema = z.object({
208
- version: z.literal(2),
209
- createdAt: z.string(),
210
- updatedAt: z.string(),
211
- gitHash: z.string(),
212
- features: z.record(featureSchema),
213
- flows: z.record(flowSchema)
214
- }).passthrough();
201
+ import path5 from "path";
202
+ import { execFile as execFile2 } from "child_process";
203
+ import { promisify as promisify2 } from "util";
204
+ import { z } from "zod";
215
205
  async function getCurrentGitHash(rootDir) {
216
206
  try {
217
207
  const { stdout } = await exec2("git", ["rev-parse", "HEAD"], {
@@ -222,9 +212,48 @@ async function getCurrentGitHash(rootDir) {
222
212
  return "unknown";
223
213
  }
224
214
  }
215
+ var exec2, repoPath, verificationFields, featureSchema, flowSchema, snapshotSchema;
216
+ var init_snapshot = __esm({
217
+ "src/snapshot/snapshot.ts"() {
218
+ "use strict";
219
+ init_files();
220
+ init_files();
221
+ init_storage();
222
+ init_paths();
223
+ init_test_map();
224
+ exec2 = promisify2(execFile2);
225
+ repoPath = z.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
226
+ verificationFields = {
227
+ refreshedHash: z.string().optional(),
228
+ verifiedAt: z.string().optional(),
229
+ verifiedHash: z.string().optional(),
230
+ verificationFailed: z.boolean().optional(),
231
+ verificationNote: z.string().optional()
232
+ };
233
+ featureSchema = z.object({
234
+ description: z.string(),
235
+ files: z.array(repoPath),
236
+ tests: z.array(repoPath).optional(),
237
+ type: z.enum(["capability", "infrastructure"]).optional(),
238
+ ...verificationFields
239
+ }).passthrough();
240
+ flowSchema = z.object({ description: z.string(), chain: z.array(repoPath), ...verificationFields }).passthrough();
241
+ snapshotSchema = z.object({
242
+ version: z.literal(2),
243
+ createdAt: z.string(),
244
+ updatedAt: z.string(),
245
+ gitHash: z.string(),
246
+ features: z.record(featureSchema),
247
+ flows: z.record(flowSchema)
248
+ }).passthrough();
249
+ }
250
+ });
225
251
 
226
252
  // src/drift/drift.ts
227
- var exec3 = promisify3(execFile3);
253
+ import fs3 from "fs/promises";
254
+ import path6 from "path";
255
+ import { execFile as execFile3 } from "child_process";
256
+ import { promisify as promisify3 } from "util";
228
257
  function parseChanges(output) {
229
258
  const fields = output.split("\0");
230
259
  const changes = [];
@@ -263,16 +292,100 @@ async function getWorkingTree(resolvedRoot) {
263
292
  return { available: false, changedFiles: [], untrackedFiles: [] };
264
293
  }
265
294
  }
295
+ var exec3;
296
+ var init_drift = __esm({
297
+ "src/drift/drift.ts"() {
298
+ "use strict";
299
+ init_snapshot();
300
+ init_paths();
301
+ exec3 = promisify3(execFile3);
302
+ }
303
+ });
266
304
 
267
- // src/audit/docs.ts
268
- import fs4 from "fs/promises";
305
+ // src/audit/commands.ts
269
306
  import path7 from "path";
270
- import { execFile as execFile5 } from "child_process";
271
- import { promisify as promisify5 } from "util";
307
+ function splitCommands(line) {
308
+ const parts = [];
309
+ let quote = "", start = 0;
310
+ for (let i = 0; i < line.length; i++) {
311
+ const char = line[i];
312
+ if (char === "\\") {
313
+ i++;
314
+ continue;
315
+ }
316
+ if (quote) {
317
+ if (char === quote) quote = "";
318
+ continue;
319
+ }
320
+ if (char === "'" || char === '"') {
321
+ quote = char;
322
+ continue;
323
+ }
324
+ if (char === ";" || line.slice(i, i + 2) === "&&") {
325
+ parts.push(line.slice(start, i));
326
+ if (char === "&") i++;
327
+ start = i + 1;
328
+ }
329
+ }
330
+ parts.push(line.slice(start));
331
+ return parts;
332
+ }
333
+ function commandClaims(line, lineNo, cwd, shell = false) {
334
+ const claims = [];
335
+ const segments = splitCommands(line);
336
+ for (const segment of segments) {
337
+ const text2 = segment.trim().replace(/^\$\s+/, "");
338
+ if (text2.startsWith("#") || /^(?:echo|printf)\b/.test(text2)) continue;
339
+ const cd = text2.match(/^cd\s+(.+)$/);
340
+ if (cd) {
341
+ const directory = literal(cd[1]);
342
+ cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path7.posix.join(cwd ?? ".", directory);
343
+ continue;
344
+ }
345
+ for (const match of text2.matchAll(COMMAND)) {
346
+ const flags = match[2].trim();
347
+ const selector = flags.match(/^(?:--prefix|--dir|-C)(?:\s+|=)(.+)$/);
348
+ let directory = cwd;
349
+ let scopeUnknown;
350
+ if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
351
+ if (selector) {
352
+ const selected = literal(selector[1]);
353
+ directory = selected === null || cwd === null ? null : path7.posix.join(cwd ?? ".", selected);
354
+ } else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
355
+ const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
356
+ if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
357
+ scopeUnknown = "Package selection or conditional options after the script need review.";
358
+ }
359
+ if (/[|()]/.test(text2) || /\b(?:pushd|popd|if|for|while)\b/.test(text2) || directory === null) {
360
+ scopeUnknown = "The command's working directory or shell control flow could not be resolved.";
361
+ }
362
+ claims.push({
363
+ scriptName: match[3],
364
+ invocation: match[0],
365
+ excerpt: match[0],
366
+ line: lineNo,
367
+ ...directory !== void 0 && directory !== null ? { directory } : {},
368
+ ...scopeUnknown ? { scopeUnknown } : {}
369
+ });
370
+ }
371
+ if (/\b(?:cd|pushd|popd)\b/.test(text2)) cwd = null;
372
+ }
373
+ return { claims, cwd };
374
+ }
375
+ var TOKEN, COMMAND, literal;
376
+ var init_commands = __esm({
377
+ "src/audit/commands.ts"() {
378
+ "use strict";
379
+ TOKEN = `(?:"[^"\\n]*"|'[^'\\n]*'|[^\\s\`;&|]+)`;
380
+ COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
381
+ literal = (value) => {
382
+ const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
383
+ return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path7.posix.isAbsolute(unquoted) ? null : unquoted;
384
+ };
385
+ }
386
+ });
272
387
 
273
388
  // src/audit/tree.ts
274
- var MIN_GLYPH_LINES = 3;
275
- var GLYPHS = ["\u251C\u2500\u2500", "\u2514\u2500\u2500"];
276
389
  function glyphIndex(line) {
277
390
  for (const glyph of GLYPHS) {
278
391
  const idx = line.indexOf(glyph);
@@ -341,46 +454,17 @@ function extractTreeClaims(blockLines, blockStartLine) {
341
454
  }
342
455
  return claims;
343
456
  }
457
+ var MIN_GLYPH_LINES, GLYPHS;
458
+ var init_tree = __esm({
459
+ "src/audit/tree.ts"() {
460
+ "use strict";
461
+ MIN_GLYPH_LINES = 3;
462
+ GLYPHS = ["\u251C\u2500\u2500", "\u2514\u2500\u2500"];
463
+ }
464
+ });
344
465
 
345
466
  // src/audit/claims.ts
346
- var ROOT_FILE_NAMES = /* @__PURE__ */ new Set([
347
- "package.json",
348
- "package-lock.json",
349
- "pnpm-workspace.yaml",
350
- "tsconfig.json",
351
- "tsup.config.ts",
352
- "vitest.config.ts",
353
- "Makefile",
354
- "Dockerfile",
355
- "docker-compose.yml",
356
- "Cargo.toml",
357
- "go.mod",
358
- "go.sum",
359
- "pyproject.toml",
360
- "requirements.txt",
361
- "Gemfile",
362
- "composer.json",
363
- "settings.gradle.kts",
364
- "settings.gradle",
365
- "build.gradle.kts",
366
- "build.gradle",
367
- "manifest.json",
368
- "server.json",
369
- "README.md",
370
- "CHANGELOG.md",
371
- "LICENSE",
372
- "CLAUDE.md",
373
- "AGENTS.md",
374
- ".gitignore",
375
- ".env.example"
376
- ]);
377
- var SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
378
- var COMMAND_RE = /\b(npm|pnpm|yarn)\s+run\s+([A-Za-z0-9:_.-]+)/g;
379
- var COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
380
- var COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
381
- var IGNORE_LINE = "<!-- mason:ignore -->";
382
- var IGNORE_START = "<!-- mason:ignore-start -->";
383
- var IGNORE_END = "<!-- mason:ignore-end -->";
467
+ import MarkdownIt from "markdown-it";
384
468
  function normalizePathToken(token) {
385
469
  let t = token.trim();
386
470
  if (!t) return null;
@@ -444,11 +528,34 @@ function extractClaims(content) {
444
528
  const counts = [];
445
529
  const commands = /* @__PURE__ */ new Map();
446
530
  const addPath = (claim) => {
447
- if (!paths.has(claim.path)) paths.set(claim.path, claim);
531
+ const key = [claim.relativeTo ?? "scope", claim.path].join(":");
532
+ if (!paths.has(key)) paths.set(key, claim);
448
533
  };
449
534
  const addCommand = (claim) => {
450
- if (!commands.has(claim.scriptName)) commands.set(claim.scriptName, claim);
535
+ const key = JSON.stringify([claim.scriptName, claim.directory, claim.scopeUnknown]);
536
+ if (!commands.has(key)) commands.set(key, claim);
451
537
  };
538
+ const parsed = markdown.parse(lines.map((line, i) => ignored[i] ? "" : line).join("\n"), {});
539
+ for (const token of parsed) {
540
+ if (token.type !== "inline" || !token.map) continue;
541
+ let line = token.map[0] + 1;
542
+ for (const child of token.children ?? []) {
543
+ const target = child.type === "link_open" ? child.attrGet("href") : child.type === "image" ? child.attrGet("src") : null;
544
+ if (target && !/^(?:[a-z][a-z0-9+.-]*:|[\/#~])/i.test(target)) {
545
+ let pathname = null;
546
+ try {
547
+ pathname = decodeURIComponent(target.split(/[?#]/)[0]);
548
+ } catch {
549
+ }
550
+ if (pathname && !/[$*?{}<>\\\x00-\x1f]/.test(pathname)) {
551
+ addPath({ path: pathname, line, excerpt: target, relativeTo: "document" });
552
+ }
553
+ }
554
+ if (child.type === "softbreak" || child.type === "hardbreak") line++;
555
+ else line += child.content.split("\n").length - 1;
556
+ }
557
+ }
558
+ let shellCwd;
452
559
  let inFence = false;
453
560
  let fenceInfo = "";
454
561
  let fenceMarker = "";
@@ -479,6 +586,7 @@ function extractClaims(content) {
479
586
  fenceMarker = fenceMatch[1][0];
480
587
  fenceInfo = fenceMatch[2].trim().toLowerCase();
481
588
  blockLines = [];
589
+ shellCwd = void 0;
482
590
  blockStartLine = lineNo + 1;
483
591
  } else if (fenceMatch[1][0] === fenceMarker) {
484
592
  inFence = false;
@@ -489,14 +597,9 @@ function extractClaims(content) {
489
597
  if (inFence) {
490
598
  blockLines.push(ignored[i] ? "" : line);
491
599
  if (!ignored[i] && SHELL_FENCE_INFOS.has(fenceInfo)) {
492
- for (const m of line.matchAll(COMMAND_RE)) {
493
- addCommand({
494
- scriptName: m[2],
495
- invocation: m[0],
496
- line: lineNo,
497
- excerpt: m[0]
498
- });
499
- }
600
+ const scanned = commandClaims(line, lineNo, shellCwd, true);
601
+ shellCwd = scanned.cwd;
602
+ for (const claim of scanned.claims) addCommand(claim);
500
603
  }
501
604
  continue;
502
605
  }
@@ -523,14 +626,10 @@ function extractClaims(content) {
523
626
  excerpt: m[0]
524
627
  });
525
628
  }
526
- for (const m of line.matchAll(COMMAND_RE)) {
527
- addCommand({
528
- scriptName: m[2],
529
- invocation: m[0],
530
- line: lineNo,
531
- excerpt: m[0]
532
- });
629
+ for (const match of line.matchAll(/`([^`]+)`/g)) {
630
+ for (const claim of commandClaims(match[1], lineNo, void 0, true).claims) addCommand(claim);
533
631
  }
632
+ for (const claim of commandClaims(line.replace(/`[^`]+`/g, ""), lineNo).claims) addCommand(claim);
534
633
  }
535
634
  if (inFence) processBlock();
536
635
  return {
@@ -539,12 +638,56 @@ function extractClaims(content) {
539
638
  commands: [...commands.values()]
540
639
  };
541
640
  }
641
+ var ROOT_FILE_NAMES, SHELL_FENCE_INFOS, markdown, COUNT_RE, COUNT_DENYLIST_RE, IGNORE_LINE, IGNORE_START, IGNORE_END;
642
+ var init_claims = __esm({
643
+ "src/audit/claims.ts"() {
644
+ "use strict";
645
+ init_commands();
646
+ init_tree();
647
+ ROOT_FILE_NAMES = /* @__PURE__ */ new Set([
648
+ "package.json",
649
+ "package-lock.json",
650
+ "pnpm-workspace.yaml",
651
+ "tsconfig.json",
652
+ "tsup.config.ts",
653
+ "vitest.config.ts",
654
+ "Makefile",
655
+ "Dockerfile",
656
+ "docker-compose.yml",
657
+ "Cargo.toml",
658
+ "go.mod",
659
+ "go.sum",
660
+ "pyproject.toml",
661
+ "requirements.txt",
662
+ "Gemfile",
663
+ "composer.json",
664
+ "settings.gradle.kts",
665
+ "settings.gradle",
666
+ "build.gradle.kts",
667
+ "build.gradle",
668
+ "manifest.json",
669
+ "server.json",
670
+ "README.md",
671
+ "CHANGELOG.md",
672
+ "LICENSE",
673
+ "CLAUDE.md",
674
+ "AGENTS.md",
675
+ ".gitignore",
676
+ ".env.example"
677
+ ]);
678
+ SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
679
+ markdown = new MarkdownIt("commonmark", { html: false });
680
+ COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
681
+ COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
682
+ IGNORE_LINE = "<!-- mason:ignore -->";
683
+ IGNORE_START = "<!-- mason:ignore-start -->";
684
+ IGNORE_END = "<!-- mason:ignore-end -->";
685
+ }
686
+ });
542
687
 
543
688
  // src/audit/git.ts
544
689
  import { execFile as execFile4 } from "child_process";
545
690
  import { promisify as promisify4 } from "util";
546
- var exec4 = promisify4(execFile4);
547
- var COMMIT_FORMAT = "%H%x09%cI%x09%s";
548
691
  function parseCommitLine(line) {
549
692
  const parts = line.split(" ");
550
693
  if (parts.length < 3 || !parts[0]) return null;
@@ -554,7 +697,7 @@ async function lastCommitOf(resolvedRoot, relPath) {
554
697
  try {
555
698
  const { stdout } = await exec4(
556
699
  "git",
557
- ["log", "-1", `--format=${COMMIT_FORMAT}`, "--", relPath],
700
+ ["log", "-1", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
558
701
  { cwd: resolvedRoot }
559
702
  );
560
703
  const line = stdout.trim().split("\n")[0];
@@ -573,7 +716,7 @@ async function deletingCommitOf(resolvedRoot, relPath) {
573
716
  "--diff-filter=D",
574
717
  `--format=${COMMIT_FORMAT}`,
575
718
  "--",
576
- relPath
719
+ `:(literal)${relPath}`
577
720
  ],
578
721
  { cwd: resolvedRoot }
579
722
  );
@@ -587,7 +730,7 @@ async function firstCommitOf(resolvedRoot, relPath) {
587
730
  try {
588
731
  const { stdout } = await exec4(
589
732
  "git",
590
- ["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", relPath],
733
+ ["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
591
734
  { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
592
735
  );
593
736
  const line = stdout.trim().split("\n")[0];
@@ -624,164 +767,24 @@ async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
624
767
  return null;
625
768
  }
626
769
  }
627
-
628
- // src/audit/docs.ts
629
- var exec5 = promisify5(execFile5);
630
- var DOC_CANDIDATES = [
631
- "AGENTS.md",
632
- "CLAUDE.md",
633
- ".claude/CLAUDE.md"
634
- ];
635
- async function isDirty(resolvedRoot, relPath) {
636
- try {
637
- const { stdout } = await exec5(
638
- "git",
639
- ["status", "--porcelain", "--", relPath],
640
- { cwd: resolvedRoot }
641
- );
642
- return stdout.trim().length > 0;
643
- } catch {
644
- return false;
645
- }
646
- }
647
- async function discoverDocs(resolvedRoot) {
648
- const docs = await Promise.all(DOC_CANDIDATES.map(async (candidate) => {
649
- let content;
650
- try {
651
- content = await fs4.readFile(path7.join(resolvedRoot, candidate), "utf-8");
652
- } catch {
653
- return null;
654
- }
655
- const [lastCommit, dirty] = await Promise.all([lastCommitOf(resolvedRoot, candidate), isDirty(resolvedRoot, candidate)]);
656
- return {
657
- path: candidate,
658
- content,
659
- lineCount: content.split("\n").length,
660
- lastCommit,
661
- dirty,
662
- claims: extractClaims(content)
663
- };
664
- }));
665
- return docs.filter((doc) => doc !== null);
666
- }
667
-
668
- // src/audit/types.ts
669
- var ALL_CHECKS = [
670
- "deleted-reference",
671
- "new-module",
672
- "stale-count",
673
- "dead-command",
674
- "deps-changed",
675
- "decision-anchor-drift"
676
- ];
677
-
678
- // src/audit/checks/deleted-reference.ts
679
- import fs5 from "fs/promises";
680
- import path8 from "path";
681
- async function exists(absPath) {
682
- try {
683
- await fs5.access(absPath);
684
- return true;
685
- } catch {
686
- return false;
687
- }
688
- }
689
- async function checkDeletedReferences(ctx) {
690
- const result = emptyResult();
691
- for (const doc of ctx.docs) {
692
- const changes = ctx.changesSinceDoc.get(doc.path);
693
- const renames = /* @__PURE__ */ new Map();
694
- for (const change of changes ?? []) {
695
- if (change.status === "renamed" && change.previousPath) {
696
- renames.set(change.previousPath, change.path);
697
- }
698
- }
699
- for (const claim of doc.claims.paths) {
700
- if (claim.path === ".mason" || claim.path.startsWith(".mason/")) {
701
- continue;
702
- }
703
- if (await exists(path8.join(ctx.root, claim.path))) continue;
704
- const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
705
- const renamedTo = renames.get(claim.path) ?? null;
706
- if (renamedTo) {
707
- result.issues.push({
708
- type: "deleted-reference",
709
- message: `\`${claim.path}\` was renamed to \`${renamedTo}\``,
710
- anchor,
711
- confidence: "certain",
712
- evidence: {
713
- kind: "missing-path",
714
- claimed: claim.path,
715
- renamedTo,
716
- deletedInCommit: null,
717
- everTracked: true,
718
- parentDirExists: true
719
- }
720
- });
721
- continue;
722
- }
723
- const tracked = await lastCommitOf(ctx.root, claim.path);
724
- if (tracked) {
725
- const deleted = await deletingCommitOf(ctx.root, claim.path);
726
- const detail = deleted ? ` \u2013 deleted in ${deleted.hash.slice(0, 7)} "${deleted.subject}" (${deleted.date.slice(0, 10)})` : "";
727
- result.issues.push({
728
- type: "deleted-reference",
729
- message: `\`${claim.path}\` no longer exists${detail}`,
730
- anchor,
731
- confidence: "certain",
732
- evidence: {
733
- kind: "missing-path",
734
- claimed: claim.path,
735
- renamedTo: null,
736
- deletedInCommit: deleted,
737
- everTracked: true,
738
- parentDirExists: await exists(
739
- path8.join(ctx.root, path8.dirname(claim.path))
740
- )
741
- }
742
- });
743
- continue;
744
- }
745
- const parentDirExists = await exists(
746
- path8.join(ctx.root, path8.dirname(claim.path))
747
- );
748
- if (!parentDirExists) continue;
749
- const issue = {
750
- type: "deleted-reference",
751
- message: `\`${claim.path}\` does not exist (never tracked in git \u2013 possible typo or invented path)`,
752
- anchor,
753
- confidence: "likely",
754
- evidence: {
755
- kind: "missing-path",
756
- claimed: claim.path,
757
- renamedTo: null,
758
- deletedInCommit: null,
759
- everTracked: false,
760
- parentDirExists: true
761
- }
762
- };
763
- result.issues.push(issue);
764
- }
770
+ var exec4, COMMIT_FORMAT;
771
+ var init_git = __esm({
772
+ "src/audit/git.ts"() {
773
+ "use strict";
774
+ exec4 = promisify4(execFile4);
775
+ COMMIT_FORMAT = "%H%x09%cI%x09%s";
765
776
  }
766
- return result;
767
- }
768
-
769
- // src/audit/checks/new-module.ts
770
- import fg3 from "fast-glob";
771
- import fs7 from "fs/promises";
772
- import path10 from "path";
777
+ });
773
778
 
774
779
  // src/audit/inputs.ts
775
- import fs6 from "fs/promises";
780
+ import fs4 from "fs/promises";
776
781
  import * as nativeFs from "fs";
777
- import path9 from "path";
778
- import { execFile as execFile6 } from "child_process";
779
- import { promisify as promisify6 } from "util";
782
+ import path8 from "path";
783
+ import { execFile as execFile5 } from "child_process";
784
+ import { promisify as promisify5 } from "util";
780
785
  import fg2 from "fast-glob";
781
- var execute = promisify6(execFile6);
782
- var MAX_INPUTS = 1e5;
783
786
  async function auditInputPath(root, file) {
784
- if (file === ".") return fs6.realpath(root);
787
+ if (file === ".") return fs4.realpath(root);
785
788
  try {
786
789
  return await storePath(root, file);
787
790
  } catch (error) {
@@ -805,7 +808,7 @@ async function auditGlob(root, patterns, options) {
805
808
  if (Array.isArray(patterns) && !patterns.length) return [];
806
809
  const directories = /* @__PURE__ */ new Set();
807
810
  const readdir2 = ((directory, opts, callback) => {
808
- const relative = path9.relative(root, directory).split(path9.sep).join("/") || ".";
811
+ const relative = path8.relative(root, directory).split(path8.sep).join("/") || ".";
809
812
  directories.add(relative);
810
813
  if (directories.size > MAX_INPUTS) {
811
814
  callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
@@ -816,7 +819,7 @@ async function auditGlob(root, patterns, options) {
816
819
  }, callback);
817
820
  });
818
821
  const lstat2 = ((file, callback) => {
819
- const relative = path9.relative(root, file).split(path9.sep).join("/") || ".";
822
+ const relative = path8.relative(root, file).split(path8.sep).join("/") || ".";
820
823
  auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
821
824
  });
822
825
  const stream = fg2.stream(patterns, {
@@ -844,7 +847,7 @@ async function auditGlob(root, patterns, options) {
844
847
  counts.set(dir, (counts.get(dir) ?? 0) + 1);
845
848
  return counts;
846
849
  }, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
847
- throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count2]) => `${dir} (${count2})`).join(", ")}. Narrow this check's inputs.`);
850
+ throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count3]) => `${dir} (${count3})`).join(", ")}. Narrow this check's inputs.`);
848
851
  }
849
852
  }
850
853
  } finally {
@@ -885,100 +888,341 @@ async function gitIgnoredPaths(root, files) {
885
888
  throw error;
886
889
  }
887
890
  }
891
+ var execute, MAX_INPUTS;
892
+ var init_inputs = __esm({
893
+ "src/audit/inputs.ts"() {
894
+ "use strict";
895
+ init_files();
896
+ init_storage();
897
+ execute = promisify5(execFile5);
898
+ MAX_INPUTS = 1e5;
899
+ }
900
+ });
888
901
 
889
- // src/audit/checks/new-module.ts
890
- var DIR_DENYLIST = /* @__PURE__ */ new Set([
891
- "node_modules",
892
- "dist",
893
- "build",
894
- "out",
895
- "coverage",
896
- "target",
897
- "vendor",
898
- "__pycache__",
899
- "venv",
900
- ".venv",
901
- ".git",
902
- ".gradle",
903
- ".mason",
904
- ".claude",
905
- ".github",
906
- ".vscode",
907
- ".idea"
908
- ]);
909
- var SECOND_LEVEL_MIN_SOURCE_FILES = 2;
910
- var ENUMERATION_THRESHOLD = 2;
911
- function escapeRegExp(text2) {
912
- return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
913
- }
914
- function isMentioned(combinedDocs, name) {
915
- const re = new RegExp(
916
- `(^|[^A-Za-z0-9_-])${escapeRegExp(name)}(/|[^A-Za-z0-9_-]|$)`,
917
- "im"
918
- );
919
- return re.test(combinedDocs);
902
+ // src/audit/docs.ts
903
+ import fs5 from "fs/promises";
904
+ import path9 from "path";
905
+ import { execFile as execFile6 } from "child_process";
906
+ import { promisify as promisify6 } from "util";
907
+ import fg3 from "fast-glob";
908
+ async function isDirty(resolvedRoot, relPath) {
909
+ try {
910
+ const { stdout } = await exec5(
911
+ "git",
912
+ ["status", "--porcelain", "--", `:(literal)${relPath}`],
913
+ { cwd: resolvedRoot }
914
+ );
915
+ return stdout.trim().length > 0;
916
+ } catch {
917
+ return true;
918
+ }
920
919
  }
921
- async function checkNewModules(ctx) {
922
- const result = emptyResult();
923
- if (ctx.docs.length === 0) return result;
924
- const combinedDocs = ctx.docs.map((d) => d.content).join("\n");
925
- const primaryDoc = ctx.docs[0].path;
926
- const checkedDocs = ctx.docs.map((d) => d.path);
927
- const flag = async (dir, sourceFileCount) => {
928
- result.issues.push({
929
- type: "new-module",
930
- message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in any context file`,
931
- anchor: { doc: primaryDoc, line: null, excerpt: dir },
932
- confidence: "likely",
933
- evidence: {
934
- kind: "unmentioned-dir",
935
- dir,
936
- sourceFileCount,
937
- firstCommit: await firstCommitOf(ctx.root, dir),
938
- checkedDocs
939
- }
940
- });
920
+ async function discoverDocPaths(root) {
921
+ let candidates = [];
922
+ try {
923
+ const { stdout } = await exec5(
924
+ "git",
925
+ [
926
+ "ls-files",
927
+ "-z",
928
+ "--cached",
929
+ "--others",
930
+ "--exclude-standard",
931
+ "--",
932
+ ":(icase,glob)**/readme.md",
933
+ ":(icase,glob)**/agents.md",
934
+ ":(icase,glob)**/claude.md"
935
+ ],
936
+ { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 }
937
+ );
938
+ candidates = stdout.split("\0").filter(Boolean);
939
+ } catch (error) {
940
+ try {
941
+ await exec5("git", ["rev-parse", "--git-dir"], { cwd: root });
942
+ } catch {
943
+ return localDocPaths(root);
944
+ }
945
+ throw error;
946
+ }
947
+ candidates.push(...await localDocPaths(root));
948
+ const config = await loadProjectConfig(root);
949
+ const excluded = [
950
+ "**/node_modules/**",
951
+ "**/vendor/**",
952
+ "**/dist/**",
953
+ "**/build/**",
954
+ "**/target/**",
955
+ "**/.git/**",
956
+ "**/.venv/**",
957
+ "**/venv/**",
958
+ ".mason/**",
959
+ ...config.ignore ?? []
960
+ ];
961
+ const paths = [];
962
+ const unique = [...new Set(candidates)].sort();
963
+ for (let offset = 0; offset < unique.length; offset += 256) {
964
+ paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) => fg3.escapePath(p)), {
965
+ ignore: excluded,
966
+ followSymbolicLinks: false,
967
+ label: "Documentation discovery"
968
+ }));
969
+ if (paths.length > 1e4) throw new Error("Documentation discovery exceeds 10,000 relevant files; narrow .mason/config.json ignore patterns.");
970
+ }
971
+ const priority = (file) => {
972
+ const index = DOC_CANDIDATES.findIndex((p) => p.toLowerCase() === file.toLowerCase());
973
+ return index < 0 ? DOC_CANDIDATES.length : index;
941
974
  };
942
- for (const candidate of await moduleCandidates(ctx.root, combinedDocs)) {
943
- await flag(candidate.dir, candidate.sourceFileCount);
975
+ return paths.sort((a, b) => priority(a) - priority(b) || a.localeCompare(b));
976
+ }
977
+ async function localDocPaths(root) {
978
+ const paths = [];
979
+ for (const dir of [".", ".claude"]) {
980
+ let entries;
981
+ try {
982
+ entries = await fs5.readdir(await auditInputPath(root, dir), { withFileTypes: true });
983
+ } catch (error) {
984
+ if (error.code === "ENOENT") continue;
985
+ throw error;
986
+ }
987
+ for (const entry of entries) if (/^(?:agents|claude|readme)\.md$/i.test(entry.name)) {
988
+ paths.push(path9.posix.join(dir, entry.name));
989
+ }
944
990
  }
945
- return result;
991
+ return paths;
946
992
  }
947
- async function moduleCandidates(root, combinedDocs) {
948
- const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
949
- const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
950
- const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
951
- const listSubdirs = async (dir) => {
952
- const entries = await fs7.readdir(await auditInputPath(root, dir), { withFileTypes: true });
953
- const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path10.posix.join(dir, entry.name));
954
- const ignored = await gitIgnoredPaths(root, dirs);
955
- const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
956
- return (await auditGlob(root, dir === "." ? "*" : `${fg3.escapePath(dir)}/*`, {
957
- ignore,
958
- onlyDirectories: true,
959
- label: "Module directory discovery",
960
- select: (file) => visible.has(file)
961
- })).map((file) => path10.posix.basename(file));
993
+ function documentScope(file) {
994
+ const parent = path9.posix.dirname(file);
995
+ return path9.posix.basename(parent).toLowerCase() === ".claude" ? path9.posix.dirname(parent) : parent;
996
+ }
997
+ async function discoverDocs(resolvedRoot) {
998
+ const docs = [];
999
+ for (const candidate of await discoverDocPaths(resolvedRoot)) {
1000
+ const content = await readAuditInput(resolvedRoot, candidate);
1001
+ if (content === null) throw new Error("Document disappeared during discovery: " + candidate);
1002
+ const [lastCommit, dirty] = await Promise.all([lastCommitOf(resolvedRoot, candidate), isDirty(resolvedRoot, candidate)]);
1003
+ docs.push({
1004
+ path: candidate,
1005
+ content,
1006
+ scope: documentScope(candidate),
1007
+ kind: /^readme\.md$/i.test(path9.posix.basename(candidate)) ? "readme" : "instructions",
1008
+ lineCount: content.split("\n").length,
1009
+ lastCommit,
1010
+ dirty,
1011
+ claims: extractClaims(content)
1012
+ });
1013
+ }
1014
+ return docs;
1015
+ }
1016
+ var exec5, DOC_CANDIDATES;
1017
+ var init_docs = __esm({
1018
+ "src/audit/docs.ts"() {
1019
+ "use strict";
1020
+ init_claims();
1021
+ init_git();
1022
+ init_inputs();
1023
+ init_files();
1024
+ exec5 = promisify6(execFile6);
1025
+ DOC_CANDIDATES = [
1026
+ "AGENTS.md",
1027
+ "CLAUDE.md",
1028
+ ".claude/CLAUDE.md"
1029
+ ];
1030
+ }
1031
+ });
1032
+
1033
+ // src/audit/types.ts
1034
+ var ALL_CHECKS;
1035
+ var init_types = __esm({
1036
+ "src/audit/types.ts"() {
1037
+ "use strict";
1038
+ ALL_CHECKS = [
1039
+ "deleted-reference",
1040
+ "new-module",
1041
+ "stale-count",
1042
+ "dead-command",
1043
+ "deps-changed",
1044
+ "decision-anchor-drift"
1045
+ ];
1046
+ }
1047
+ });
1048
+
1049
+ // src/audit/scope.ts
1050
+ import fs6 from "fs/promises";
1051
+ import path10 from "path";
1052
+ function scopedPath(directory, value) {
1053
+ if (path10.posix.isAbsolute(value) || value.includes("\\") || /[\x00-\x1f]/.test(value)) return null;
1054
+ const resolved = path10.posix.normalize(path10.posix.join(directory, value));
1055
+ return resolved === ".." || resolved.startsWith("../") ? null : resolved;
1056
+ }
1057
+ function pathClaimScope(doc, claim) {
1058
+ const directory = claim.relativeTo === "document" ? path10.posix.dirname(doc) : documentScope(doc);
1059
+ const local = scopedPath(directory, claim.path);
1060
+ if (!local) return null;
1061
+ const candidates = claim.relativeTo === "document" || directory === "." ? [local] : [...new Set([local, scopedPath(".", claim.path)].filter((p) => p !== null))];
1062
+ return {
1063
+ basis: claim.relativeTo === "document" ? "document-link" : directory === "." ? "repository" : "document",
1064
+ directory,
1065
+ candidates
1066
+ };
1067
+ }
1068
+ async function pathExists(root, file) {
1069
+ const absolute = await auditInputPath(root, file);
1070
+ try {
1071
+ await fs6.access(absolute);
1072
+ return true;
1073
+ } catch (error) {
1074
+ if (error.code === "ENOENT") return false;
1075
+ throw error;
1076
+ }
1077
+ }
1078
+ function optionalMasonPath(file) {
1079
+ return file.split("/").includes(".mason");
1080
+ }
1081
+ var init_scope = __esm({
1082
+ "src/audit/scope.ts"() {
1083
+ "use strict";
1084
+ init_inputs();
1085
+ init_docs();
1086
+ }
1087
+ });
1088
+
1089
+ // src/audit/checks/deleted-reference.ts
1090
+ import path11 from "path";
1091
+ async function checkDeletedReferences(ctx) {
1092
+ const result = emptyResult();
1093
+ for (const doc of ctx.docs) {
1094
+ const changes = ctx.changesSinceDoc.get(doc.path);
1095
+ const renames = new Map((changes ?? []).filter((c) => c.status === "renamed" && c.previousPath).map((c) => [c.previousPath, c.path]));
1096
+ for (const claim of doc.claims.paths) {
1097
+ const scope = pathClaimScope(doc.path, claim);
1098
+ if (!scope) {
1099
+ result.skipped.push({
1100
+ check: "deleted-reference",
1101
+ doc: doc.path,
1102
+ reason: `Reference ${claim.excerpt} is outside the repository or has an unsupported path.`
1103
+ });
1104
+ continue;
1105
+ }
1106
+ if (scope.candidates.some(optionalMasonPath)) continue;
1107
+ if ((await Promise.all(scope.candidates.map((file) => pathExists(ctx.root, file)))).some(Boolean)) continue;
1108
+ const candidates = await Promise.all(scope.candidates.map(async (file) => ({
1109
+ file,
1110
+ tracked: await lastCommitOf(ctx.root, file),
1111
+ parent: await pathExists(ctx.root, path11.posix.dirname(file))
1112
+ })));
1113
+ const candidate = candidates.find((c) => c.tracked || renames.has(c.file)) ?? candidates.find((c) => c.parent);
1114
+ if (!candidate && claim.relativeTo !== "document") continue;
1115
+ const resolvedPath = candidate?.file ?? scope.candidates[0];
1116
+ const renamedTo = renames.get(resolvedPath) ?? null;
1117
+ const deletedInCommit = candidate?.tracked && !renamedTo ? await deletingCommitOf(ctx.root, resolvedPath) : null;
1118
+ const evidence = {
1119
+ kind: "missing-path",
1120
+ claimed: claim.path,
1121
+ resolvedPath,
1122
+ scope,
1123
+ renamedTo,
1124
+ deletedInCommit,
1125
+ everTracked: !!candidate?.tracked || !!renamedTo,
1126
+ parentDirExists: candidate?.parent ?? false
1127
+ };
1128
+ const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
1129
+ const message = renamedTo ? `\`${resolvedPath}\` was renamed to \`${renamedTo}\`` : `\`${resolvedPath}\` does not exist` + (deletedInCommit ? ` \u2013 deleted in ${deletedInCommit.hash.slice(0, 7)} "${deletedInCommit.subject}" (${deletedInCommit.date.slice(0, 10)})` : "");
1130
+ if (scope.basis !== "document" && (renamedTo || candidate?.tracked)) {
1131
+ result.issues.push({ type: "deleted-reference", message, anchor, confidence: "certain", evidence });
1132
+ } else {
1133
+ result.advisories.push({ resolution: "recheck", type: "deleted-reference", message: message + (scope.basis === "document" ? "; the bare path's document/repository scope needs review." : "; no tracked history establishes it as a required file. Review for a typo, generated output, or example."), anchor, evidence });
1134
+ }
1135
+ }
1136
+ }
1137
+ return result;
1138
+ }
1139
+ var init_deleted_reference = __esm({
1140
+ "src/audit/checks/deleted-reference.ts"() {
1141
+ "use strict";
1142
+ init_git();
1143
+ init_scope();
1144
+ init_checks();
1145
+ }
1146
+ });
1147
+
1148
+ // src/audit/checks/new-module.ts
1149
+ import fg4 from "fast-glob";
1150
+ import fs7 from "fs/promises";
1151
+ import path12 from "path";
1152
+ function escapeRegExp(text2) {
1153
+ return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1154
+ }
1155
+ function isMentioned(combinedDocs, name) {
1156
+ const re = new RegExp(
1157
+ `(^|[^A-Za-z0-9_-])${escapeRegExp(name)}(/|[^A-Za-z0-9_-]|$)`,
1158
+ "im"
1159
+ );
1160
+ return re.test(combinedDocs);
1161
+ }
1162
+ async function checkNewModules(ctx) {
1163
+ const result = emptyResult();
1164
+ if (ctx.docs.length === 0) return result;
1165
+ const combinedDocs = moduleDocumentation(ctx.docs);
1166
+ const primaryDoc = ctx.docs[0].path;
1167
+ const checkedDocs = ctx.docs.map((d) => d.path);
1168
+ const flag = async (dir, sourceFileCount) => {
1169
+ result.advisories.push({
1170
+ resolution: "recheck",
1171
+ type: "new-module",
1172
+ message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in the discovered documentation; review whether it needs documenting`,
1173
+ anchor: { doc: primaryDoc, line: null, excerpt: dir },
1174
+ evidence: {
1175
+ kind: "unmentioned-dir",
1176
+ dir,
1177
+ sourceFileCount,
1178
+ firstCommit: await firstCommitOf(ctx.root, dir),
1179
+ checkedDocs
1180
+ }
1181
+ });
1182
+ };
1183
+ for (const candidate of await moduleCandidates(ctx.root, combinedDocs)) {
1184
+ await flag(candidate.dir, candidate.sourceFileCount);
1185
+ }
1186
+ return result;
1187
+ }
1188
+ function moduleDocumentation(docs) {
1189
+ return docs.map((doc) => doc.content + "\n" + (path12.posix.dirname(doc.path) === "." ? "" : path12.posix.dirname(doc.path) + "/")).join("\n");
1190
+ }
1191
+ async function moduleCandidates(root, combinedDocs) {
1192
+ const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
1193
+ const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
1194
+ const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
1195
+ const listSubdirs = async (dir) => {
1196
+ const entries = await fs7.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1197
+ 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));
1198
+ const ignored = await gitIgnoredPaths(root, dirs);
1199
+ const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
1200
+ return (await auditGlob(root, dir === "." ? "*" : `${fg4.escapePath(dir)}/*`, {
1201
+ ignore,
1202
+ onlyDirectories: true,
1203
+ label: "Module directory discovery",
1204
+ select: (file) => visible.has(file)
1205
+ })).map((file) => path12.posix.basename(file));
962
1206
  };
963
1207
  const countSourceFiles = async (dir) => {
964
1208
  const files = sources.filter((file) => file.startsWith(dir + "/"));
965
- let count2 = 0;
1209
+ let count3 = 0;
966
1210
  for (let offset = 0; offset < files.length; offset += 256) {
967
- count2 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg3.escapePath(file)), {
1211
+ count3 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg4.escapePath(file)), {
968
1212
  ignore,
969
1213
  followSymbolicLinks: false,
970
1214
  label: `Source discovery in ${dir}`
971
1215
  })).length;
972
- if (count2 > 1e5) throw new Error(`Source discovery in ${dir} exceeds 100,000 relevant files. Exclude generated source with .mason/config.json ignore patterns or narrow this audit's checks.`);
1216
+ if (count3 > 1e5) throw new Error(`Source discovery in ${dir} exceeds 100,000 relevant files. Exclude generated source with .mason/config.json ignore patterns or narrow this audit's checks.`);
973
1217
  }
974
- return count2;
1218
+ return count3;
975
1219
  };
976
1220
  const candidates = [];
977
1221
  for (const topDir of await listSubdirs(".")) {
978
1222
  const topMentioned = isMentioned(combinedDocs, topDir);
979
1223
  if (!topMentioned) {
980
- const count2 = await countSourceFiles(topDir);
981
- if (count2 >= 1) candidates.push({ dir: topDir, sourceFileCount: count2 });
1224
+ const count3 = await countSourceFiles(topDir);
1225
+ if (count3 >= 1) candidates.push({ dir: topDir, sourceFileCount: count3 });
982
1226
  continue;
983
1227
  }
984
1228
  const subdirs = await listSubdirs(topDir);
@@ -986,18 +1230,48 @@ async function moduleCandidates(root, combinedDocs) {
986
1230
  if (mentioned.length < ENUMERATION_THRESHOLD) continue;
987
1231
  for (const sub of subdirs) {
988
1232
  if (isMentioned(combinedDocs, sub)) continue;
989
- const count2 = await countSourceFiles(`${topDir}/${sub}`);
990
- if (count2 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
991
- candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count2 });
1233
+ const count3 = await countSourceFiles(`${topDir}/${sub}`);
1234
+ if (count3 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
1235
+ candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count3 });
992
1236
  }
993
1237
  }
994
1238
  }
995
1239
  return candidates;
996
1240
  }
1241
+ var DIR_DENYLIST, SECOND_LEVEL_MIN_SOURCE_FILES, ENUMERATION_THRESHOLD;
1242
+ var init_new_module = __esm({
1243
+ "src/audit/checks/new-module.ts"() {
1244
+ "use strict";
1245
+ init_files();
1246
+ init_inputs();
1247
+ init_git();
1248
+ init_checks();
1249
+ DIR_DENYLIST = /* @__PURE__ */ new Set([
1250
+ "node_modules",
1251
+ "dist",
1252
+ "build",
1253
+ "out",
1254
+ "coverage",
1255
+ "target",
1256
+ "vendor",
1257
+ "__pycache__",
1258
+ "venv",
1259
+ ".venv",
1260
+ ".git",
1261
+ ".gradle",
1262
+ ".mason",
1263
+ ".claude",
1264
+ ".github",
1265
+ ".vscode",
1266
+ ".idea"
1267
+ ]);
1268
+ SECOND_LEVEL_MIN_SOURCE_FILES = 2;
1269
+ ENUMERATION_THRESHOLD = 2;
1270
+ }
1271
+ });
997
1272
 
998
1273
  // src/audit/checks/stale-count.ts
999
- import path11 from "path";
1000
- var MEMBERS_CAP = 50;
1274
+ import path13 from "path";
1001
1275
  async function countGradleModules(root) {
1002
1276
  for (const name of ["settings.gradle.kts", "settings.gradle"]) {
1003
1277
  const content = await readAuditInput(root, name);
@@ -1031,7 +1305,7 @@ async function countNpmWorkspaces(root) {
1031
1305
  return {
1032
1306
  actual: matched.length,
1033
1307
  countedFrom: "package.json workspaces",
1034
- members: matched.map((m) => path11.dirname(m)).sort()
1308
+ members: matched.map((m) => path13.dirname(m)).sort()
1035
1309
  };
1036
1310
  }
1037
1311
  }
@@ -1062,7 +1336,7 @@ async function countNpmWorkspaces(root) {
1062
1336
  return {
1063
1337
  actual: matched.length,
1064
1338
  countedFrom: "pnpm-workspace.yaml",
1065
- members: matched.map((m) => path11.dirname(m)).sort()
1339
+ members: matched.map((m) => path13.dirname(m)).sort()
1066
1340
  };
1067
1341
  }
1068
1342
  }
@@ -1084,8 +1358,8 @@ async function countCargoCrates(root) {
1084
1358
  ignore: ["**/target/**"],
1085
1359
  label: "Cargo workspace discovery"
1086
1360
  });
1087
- for (const m of matched) members.add(path11.dirname(m));
1088
- } else if (await readAuditInput(root, path11.posix.join(entry, "Cargo.toml")) !== null) {
1361
+ for (const m of matched) members.add(path13.dirname(m));
1362
+ } else if (await readAuditInput(root, path13.posix.join(entry, "Cargo.toml")) !== null) {
1089
1363
  members.add(entry);
1090
1364
  }
1091
1365
  }
@@ -1103,11 +1377,21 @@ async function resolveCountSource(root, claim) {
1103
1377
  if (unit === "crate") return countCargoCrates(root);
1104
1378
  return await countNpmWorkspaces(root) ?? await countCargoCrates(root) ?? await countGradleModules(root);
1105
1379
  }
1380
+ async function resolveDocCountSource(root, doc, claim) {
1381
+ const scope = documentScope(doc);
1382
+ const source = await resolveCountSource(path13.join(root, scope), claim);
1383
+ if (!source || scope === ".") return source;
1384
+ return {
1385
+ ...source,
1386
+ countedFrom: scope + "/" + source.countedFrom,
1387
+ members: source.members.map((member) => scope + "/" + member)
1388
+ };
1389
+ }
1106
1390
  async function checkStaleCounts(ctx) {
1107
1391
  const result = emptyResult();
1108
1392
  for (const doc of ctx.docs) {
1109
1393
  for (const claim of doc.claims.counts) {
1110
- const source = await resolveCountSource(ctx.root, claim);
1394
+ const source = await resolveDocCountSource(ctx.root, doc.path, claim);
1111
1395
  if (source === null) {
1112
1396
  result.skipped.push({
1113
1397
  check: "stale-count",
@@ -1135,64 +1419,106 @@ async function checkStaleCounts(ctx) {
1135
1419
  }
1136
1420
  return result;
1137
1421
  }
1422
+ var MEMBERS_CAP;
1423
+ var init_stale_count = __esm({
1424
+ "src/audit/checks/stale-count.ts"() {
1425
+ "use strict";
1426
+ init_docs();
1427
+ init_inputs();
1428
+ init_checks();
1429
+ MEMBERS_CAP = 50;
1430
+ }
1431
+ });
1138
1432
 
1139
1433
  // src/audit/checks/dead-command.ts
1140
- var AVAILABLE_SCRIPTS_CAP = 30;
1141
- async function scriptsOf(root, manifest) {
1142
- const raw = await readAuditInput(root, manifest);
1143
- if (raw === null) return null;
1434
+ import path14 from "path";
1435
+ function scriptsOf(raw) {
1144
1436
  try {
1145
1437
  const pkg = JSON.parse(raw);
1146
- return pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? Object.keys(pkg.scripts) : [];
1438
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return null;
1439
+ if (pkg.scripts === void 0) return [];
1440
+ return pkg.scripts && typeof pkg.scripts === "object" && !Array.isArray(pkg.scripts) && Object.values(pkg.scripts).every((value) => typeof value === "string") ? Object.keys(pkg.scripts) : null;
1147
1441
  } catch {
1148
1442
  return null;
1149
1443
  }
1150
1444
  }
1151
- async function checkDeadCommands(ctx) {
1152
- const result = emptyResult();
1153
- const commandClaims = ctx.docs.flatMap(
1154
- (doc) => doc.claims.commands.map((claim) => ({ doc, claim }))
1155
- );
1156
- if (commandClaims.length === 0) return result;
1157
- const rootScripts = await scriptsOf(ctx.root, "package.json");
1158
- if (rootScripts === null) {
1159
- result.skipped.push({
1160
- check: "dead-command",
1161
- reason: "no package.json at the repo root"
1162
- });
1163
- return result;
1164
- }
1165
- const rootSet = new Set(rootScripts);
1166
- let workspaceScripts = null;
1167
- let manifestsChecked = ["package.json"];
1168
- const loadWorkspaceScripts = async () => {
1169
- if (workspaceScripts !== null) return workspaceScripts;
1170
- workspaceScripts = /* @__PURE__ */ new Set();
1171
- const manifests = await commandManifests(ctx.root);
1172
- manifestsChecked = ["package.json", ...manifests.sort()];
1173
- for (const manifest of manifests) {
1174
- const scripts = await scriptsOf(ctx.root, manifest);
1175
- for (const name of scripts ?? []) workspaceScripts.add(name);
1176
- }
1177
- return workspaceScripts;
1445
+ async function commandInputs(root, docs) {
1446
+ const contents = /* @__PURE__ */ new Map();
1447
+ const read = async (file) => {
1448
+ if (!contents.has(file)) contents.set(file, await readAuditInput(root, file));
1449
+ return contents.get(file);
1178
1450
  };
1179
- for (const { doc, claim } of commandClaims) {
1180
- if (rootSet.has(claim.scriptName)) continue;
1181
- const elsewhere = await loadWorkspaceScripts();
1182
- if (elsewhere.has(claim.scriptName)) continue;
1183
- result.issues.push({
1184
- type: "dead-command",
1185
- message: `\`${claim.invocation}\` refers to script "${claim.scriptName}", which exists in no package.json`,
1186
- anchor: { doc: doc.path, line: claim.line, excerpt: claim.excerpt },
1187
- confidence: "certain",
1188
- evidence: {
1189
- kind: "missing-script",
1190
- scriptName: claim.scriptName,
1191
- invocation: claim.invocation,
1192
- manifestsChecked,
1193
- availableScripts: rootScripts.slice(0, AVAILABLE_SCRIPTS_CAP)
1451
+ await read("package.json");
1452
+ let inventory;
1453
+ const resolutions = [];
1454
+ for (const doc of docs) for (const claim of doc.claims.commands) {
1455
+ const base = documentScope(doc.path);
1456
+ const directory = scopedPath(base, claim.directory ?? ".");
1457
+ const scope = {
1458
+ basis: claim.directory === void 0 ? "document" : "explicit",
1459
+ directory: directory ?? base,
1460
+ candidates: []
1461
+ };
1462
+ const resolution = { doc: doc.path, claim, scope, manifest: null, scripts: [], elsewhere: [] };
1463
+ resolutions.push(resolution);
1464
+ if (claim.scopeUnknown || directory === null) {
1465
+ resolution.problem = claim.scopeUnknown ?? "The command's directory leaves the repository.";
1466
+ continue;
1467
+ }
1468
+ let cursor = directory;
1469
+ while (true) {
1470
+ const manifest = path14.posix.join(cursor, "package.json");
1471
+ scope.candidates.push(manifest);
1472
+ const raw = await read(manifest);
1473
+ if (raw !== null) {
1474
+ resolution.manifest = manifest;
1475
+ const scripts = scriptsOf(raw);
1476
+ if (scripts === null) resolution.problem = "Invalid package manifest: " + manifest;
1477
+ else resolution.scripts = scripts;
1478
+ break;
1194
1479
  }
1195
- });
1480
+ if (claim.directory !== void 0 || cursor === ".") break;
1481
+ cursor = path14.posix.dirname(cursor);
1482
+ }
1483
+ if (!resolution.manifest && !resolution.problem) {
1484
+ resolution.problem = `No package.json for the command's scope (${directory}).`;
1485
+ }
1486
+ if (resolution.problem || resolution.scripts.includes(claim.scriptName) || claim.directory !== void 0) continue;
1487
+ inventory ??= await commandManifests(root);
1488
+ for (const manifest of [.../* @__PURE__ */ new Set(["package.json", ...inventory])].sort()) {
1489
+ if (scope.candidates.includes(manifest)) continue;
1490
+ scope.candidates.push(manifest);
1491
+ const raw = await read(manifest);
1492
+ if (raw === null) continue;
1493
+ const scripts = scriptsOf(raw);
1494
+ if (scripts === null) resolution.problem = "Cannot establish script availability: invalid manifest " + manifest;
1495
+ else if (scripts.includes(claim.scriptName)) resolution.elsewhere.push(manifest);
1496
+ }
1497
+ }
1498
+ return { manifests: [...contents].sort(([a], [b]) => a.localeCompare(b)), resolutions };
1499
+ }
1500
+ async function checkDeadCommands(ctx) {
1501
+ const result = emptyResult();
1502
+ const { resolutions } = await commandInputs(ctx.root, ctx.docs);
1503
+ for (const { doc, claim, scope, scripts, elsewhere, problem, manifest } of resolutions) {
1504
+ if (problem) {
1505
+ result.skipped.push({ check: "dead-command", doc, reason: problem });
1506
+ continue;
1507
+ }
1508
+ if (scripts.includes(claim.scriptName)) continue;
1509
+ const evidence = {
1510
+ kind: "missing-script",
1511
+ scriptName: claim.scriptName,
1512
+ invocation: claim.invocation,
1513
+ manifestsChecked: scope.candidates,
1514
+ availableScripts: scripts.slice(0, AVAILABLE_SCRIPTS_CAP),
1515
+ scope
1516
+ };
1517
+ const anchor = { doc, line: claim.line, excerpt: claim.excerpt };
1518
+ const message = `\`${claim.invocation}\` names script "${claim.scriptName}", which is absent from ${manifest}`;
1519
+ const certain = documentScope(doc) === "." && (scope.basis === "explicit" || !elsewhere.length);
1520
+ if (certain) result.issues.push({ type: "dead-command", message, anchor, confidence: "certain", evidence });
1521
+ else result.advisories.push({ resolution: "recheck", type: "dead-command", message: message + (elsewhere.length ? `; it exists in ${elsewhere.join(", ")}. Clarify the intended working directory.` : "; the intended working directory needs review."), anchor, evidence });
1196
1522
  }
1197
1523
  return result;
1198
1524
  }
@@ -1202,23 +1528,33 @@ function commandManifests(root) {
1202
1528
  label: "Command manifest discovery"
1203
1529
  });
1204
1530
  }
1531
+ var AVAILABLE_SCRIPTS_CAP;
1532
+ var init_dead_command = __esm({
1533
+ "src/audit/checks/dead-command.ts"() {
1534
+ "use strict";
1535
+ init_inputs();
1536
+ init_docs();
1537
+ init_scope();
1538
+ init_checks();
1539
+ AVAILABLE_SCRIPTS_CAP = 30;
1540
+ }
1541
+ });
1205
1542
 
1206
1543
  // src/audit/release-metadata.ts
1207
1544
  import { execFile as execFile7 } from "child_process";
1208
1545
  import { promisify as promisify7 } from "util";
1209
- var exec6 = promisify7(execFile7);
1210
1546
  async function git(root, args) {
1211
1547
  return (await exec6("git", args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
1212
1548
  }
1213
1549
  function withoutAndroidReleaseValues(text2) {
1214
1550
  if (/\/\*|"""|'''/.test(text2) || !/id\s*\(?\s*["']com\.android\.(application|library)["']/.test(text2)) return null;
1215
- const scopes = [];
1551
+ const scopes2 = [];
1216
1552
  const normalized = [];
1217
1553
  let assignments = 0;
1218
1554
  for (const line of text2.split("\n")) {
1219
1555
  const code = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/.*$/g, (token) => token.startsWith("//") ? "" : '""');
1220
1556
  const assignment = line.match(/^(\s*)(versionName|versionCode)(\s*(?:=\s*|\s+))("[A-Za-z0-9._+-]+"|'[A-Za-z0-9._+-]+'|\d+)(\s*)$/);
1221
- if (assignment && scopes.join("/") === "android/defaultConfig" && (assignment[2] === "versionCode" ? /^\d+$/.test(assignment[4]) : /^["']/.test(assignment[4]))) {
1557
+ if (assignment && scopes2.join("/") === "android/defaultConfig" && (assignment[2] === "versionCode" ? /^\d+$/.test(assignment[4]) : /^["']/.test(assignment[4]))) {
1222
1558
  normalized.push(assignment[1] + assignment[2] + assignment[3] + "<release-value>" + assignment[5]);
1223
1559
  assignments++;
1224
1560
  } else {
@@ -1227,12 +1563,12 @@ function withoutAndroidReleaseValues(text2) {
1227
1563
  }
1228
1564
  const named = code.match(/^\s*(android|defaultConfig)\s*\{\s*$/)?.[1];
1229
1565
  for (const brace of code.matchAll(/[{}]/g)) {
1230
- if (brace[0] === "{") scopes.push(named ?? "unknown");
1231
- else if (!scopes.length) return null;
1232
- else scopes.pop();
1566
+ if (brace[0] === "{") scopes2.push(named ?? "unknown");
1567
+ else if (!scopes2.length) return null;
1568
+ else scopes2.pop();
1233
1569
  }
1234
1570
  }
1235
- return assignments && !scopes.length ? normalized.join("\n") : null;
1571
+ return assignments && !scopes2.length ? normalized.join("\n") : null;
1236
1572
  }
1237
1573
  async function releaseMetadataOnly(root, commit) {
1238
1574
  if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file))) return false;
@@ -1254,23 +1590,21 @@ async function releaseMetadataOnly(root, commit) {
1254
1590
  return false;
1255
1591
  }
1256
1592
  }
1593
+ var exec6;
1594
+ var init_release_metadata = __esm({
1595
+ "src/audit/release-metadata.ts"() {
1596
+ "use strict";
1597
+ exec6 = promisify7(execFile7);
1598
+ }
1599
+ });
1257
1600
 
1258
1601
  // src/audit/checks/deps-changed.ts
1259
- var MANIFEST_COMMITS_CAP = 10;
1260
- var MANIFEST_PATHSPECS = [
1261
- ":(glob)**/package.json",
1262
- ":(glob)**/build.gradle.kts",
1263
- ":(glob)**/build.gradle",
1264
- "settings.gradle.kts",
1265
- "settings.gradle",
1266
- "gradle/libs.versions.toml",
1267
- ":(glob)**/Cargo.toml",
1268
- "go.mod",
1269
- "pyproject.toml",
1270
- "requirements.txt",
1271
- "Gemfile",
1272
- "composer.json"
1273
- ];
1602
+ function manifestPathspecs(doc) {
1603
+ const scope = documentScope(doc);
1604
+ if (scope === ".") return MANIFEST_PATHSPECS;
1605
+ const escaped = scope.replace(/[\\*?\[\]]/g, "\\$&");
1606
+ return MANIFEST_PATHSPECS.map((spec) => spec.startsWith(":(glob)") ? ":(glob)" + escaped + "/" + spec.slice(":(glob)".length) : ":(literal)" + scope + "/" + spec);
1607
+ }
1274
1608
  async function checkDepsChanged(ctx) {
1275
1609
  const result = emptyResult();
1276
1610
  result.suppressedAdvisories = [];
@@ -1294,7 +1628,7 @@ async function checkDepsChanged(ctx) {
1294
1628
  const range = await commitsTouchingSince(
1295
1629
  ctx.root,
1296
1630
  doc.lastCommit.hash,
1297
- MANIFEST_PATHSPECS
1631
+ manifestPathspecs(doc.path)
1298
1632
  );
1299
1633
  if (range === null) {
1300
1634
  result.skipped.push({
@@ -1329,112 +1663,49 @@ async function checkDepsChanged(ctx) {
1329
1663
  }
1330
1664
  return result;
1331
1665
  }
1332
-
1333
- // src/decisions/drift.ts
1334
- import path14 from "path";
1335
-
1336
- // src/decisions/decisions.ts
1337
- import fs8 from "fs/promises";
1338
- import path13 from "path";
1339
- import { createHash } from "crypto";
1340
- import { z as z3 } from "zod";
1666
+ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
1667
+ var init_deps_changed = __esm({
1668
+ "src/audit/checks/deps-changed.ts"() {
1669
+ "use strict";
1670
+ init_docs();
1671
+ init_release_metadata();
1672
+ init_git();
1673
+ init_checks();
1674
+ MANIFEST_COMMITS_CAP = 10;
1675
+ MANIFEST_PATHSPECS = [
1676
+ ":(glob)**/package.json",
1677
+ ":(glob)**/build.gradle.kts",
1678
+ ":(glob)**/build.gradle",
1679
+ "settings.gradle.kts",
1680
+ "settings.gradle",
1681
+ "gradle/libs.versions.toml",
1682
+ ":(glob)**/Cargo.toml",
1683
+ "go.mod",
1684
+ "pyproject.toml",
1685
+ "requirements.txt",
1686
+ "Gemfile",
1687
+ "composer.json"
1688
+ ];
1689
+ }
1690
+ });
1341
1691
 
1342
1692
  // src/context/lexical.ts
1343
- import path12 from "path";
1693
+ import path15 from "path";
1694
+ var init_lexical = __esm({
1695
+ "src/context/lexical.ts"() {
1696
+ "use strict";
1697
+ }
1698
+ });
1699
+
1700
+ // src/context/trust.ts
1701
+ var init_trust = __esm({
1702
+ "src/context/trust.ts"() {
1703
+ "use strict";
1704
+ }
1705
+ });
1344
1706
 
1345
1707
  // src/decisions/provenance.ts
1346
1708
  import { z as z2 } from "zod";
1347
- var text = (max) => z2.string().trim().min(1).max(max);
1348
- var decisionSourceSchema = z2.object({
1349
- kind: z2.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
1350
- reference: text(1e3),
1351
- note: text(500).optional()
1352
- }).strict();
1353
- var attributionSchema = z2.object({
1354
- owner: text(200).nullable().optional(),
1355
- sources: z2.array(decisionSourceSchema).max(20).optional(),
1356
- actor: text(200).optional()
1357
- });
1358
- var contentSchema = z2.object({
1359
- title: z2.string().min(1),
1360
- body: z2.string().min(1),
1361
- category: z2.enum(["decision", "gotcha", "deprecation", "convention"]),
1362
- files: z2.array(z2.string().refine((f) => normalizeRepoPath(f) !== null)),
1363
- owner: text(200).optional(),
1364
- sources: z2.array(decisionSourceSchema).max(20)
1365
- });
1366
- var approvalSchema = z2.enum(["unreviewed", "proposed", "accepted"]);
1367
- var statusSchema = z2.enum(["active", "superseded", "retired"]);
1368
- var reviewEvidenceSchema = z2.object({
1369
- baseHash: z2.string(),
1370
- headHash: z2.string(),
1371
- historyAvailable: z2.boolean(),
1372
- changedFiles: z2.array(z2.string()),
1373
- localChanges: z2.array(z2.string())
1374
- });
1375
- var eventSchema = z2.object({
1376
- kind: z2.enum(["imported", "created", "revised", "accepted", "reaffirmed", "retired", "superseded"]),
1377
- at: z2.string().datetime(),
1378
- actor: text(200).optional(),
1379
- note: text(1500).optional(),
1380
- revision: z2.number().int().positive(),
1381
- content: contentSchema,
1382
- approval: approvalSchema,
1383
- status: statusSchema,
1384
- refreshedHash: z2.string(),
1385
- evidence: reviewEvidenceSchema.optional()
1386
- });
1387
- var legacySchema = z2.object({
1388
- version: z2.literal(1),
1389
- id: z2.string().regex(/^[a-zA-Z0-9_-]+$/),
1390
- title: z2.string().min(1),
1391
- body: z2.string().min(1),
1392
- category: contentSchema.shape.category,
1393
- files: contentSchema.shape.files,
1394
- createdAt: z2.string(),
1395
- updatedAt: z2.string(),
1396
- refreshedHash: z2.string(),
1397
- status: z2.enum(["active", "superseded"]),
1398
- supersededBy: z2.string().optional()
1399
- }).passthrough();
1400
- var currentSchema = legacySchema.extend({
1401
- version: z2.literal(2),
1402
- status: statusSchema,
1403
- approval: approvalSchema,
1404
- revision: z2.number().int().positive(),
1405
- owner: text(200).optional(),
1406
- sources: z2.array(decisionSourceSchema).max(20),
1407
- history: z2.array(eventSchema).min(1)
1408
- }).superRefine((record, ctx) => {
1409
- const invalid = (message) => ctx.addIssue({ code: "custom", message });
1410
- const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
1411
- let previous;
1412
- for (const event of record.history) {
1413
- if (!previous) {
1414
- if (!["created", "imported"].includes(event.kind) || event.revision !== 1) invalid("History must begin with creation or legacy import at revision 1");
1415
- if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed")) invalid("Initial records cannot claim acceptance");
1416
- } else {
1417
- if (["created", "imported"].includes(event.kind)) invalid("History cannot restart");
1418
- if (previous.status !== "active") invalid("Archived decisions cannot be changed");
1419
- if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0)) invalid("Invalid revision sequence");
1420
- if (event.kind !== "revised" && !same(event.content, previous.content)) invalid("A review cannot silently revise decision content");
1421
- if (event.kind === "reaffirmed" && previous.approval !== "accepted") invalid("Only accepted decisions can be reaffirmed");
1422
- if (event.kind === "accepted" && previous.approval === "accepted") invalid("Use reaffirmation for an accepted decision");
1423
- const approval = event.kind === "revised" ? "proposed" : ["accepted", "reaffirmed"].includes(event.kind) ? "accepted" : previous.approval;
1424
- if (event.approval !== approval) invalid("Approval disagrees with review history");
1425
- if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash) invalid("Only a review can refresh the evidence baseline");
1426
- }
1427
- if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active")) invalid("Lifecycle disagrees with history");
1428
- if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence)) invalid("Reviews require a named reviewer, reason, and code evidence");
1429
- if (["accepted", "reaffirmed"].includes(event.kind)) {
1430
- if (!event.content.owner || !event.content.sources.length) invalid("Accepted decisions require an owner and source");
1431
- if (!event.evidence || !/^[a-f0-9]{40,64}$/.test(event.evidence.headHash) || event.refreshedHash !== event.evidence.headHash || event.evidence.localChanges.length) invalid("Acceptance requires a committed evidence baseline");
1432
- }
1433
- previous = event;
1434
- }
1435
- if (!previous || !same(previous.content, decisionContent(record)) || previous.approval !== record.approval || previous.status !== record.status || previous.revision !== record.revision || previous.refreshedHash !== record.refreshedHash) invalid("Decision does not match the final history event");
1436
- });
1437
- var decisionSchema = z2.union([legacySchema, currentSchema]);
1438
1709
  function decisionContent(record) {
1439
1710
  return {
1440
1711
  title: record.title,
@@ -1478,20 +1749,111 @@ function decisionProvenance(record, freshness = "unknown") {
1478
1749
  lastReview: review ? { reviewer: review.actor, at: review.at, note: review.note, gitHash: review.refreshedHash } : null
1479
1750
  };
1480
1751
  }
1752
+ var text, decisionSourceSchema, attributionSchema, contentSchema, approvalSchema, statusSchema, reviewEvidenceSchema, eventSchema, legacySchema, currentSchema, decisionSchema;
1753
+ var init_provenance = __esm({
1754
+ "src/decisions/provenance.ts"() {
1755
+ "use strict";
1756
+ init_paths();
1757
+ init_trust();
1758
+ text = (max) => z2.string().trim().min(1).max(max);
1759
+ decisionSourceSchema = z2.object({
1760
+ kind: z2.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
1761
+ reference: text(1e3),
1762
+ note: text(500).optional()
1763
+ }).strict();
1764
+ attributionSchema = z2.object({
1765
+ owner: text(200).nullable().optional(),
1766
+ sources: z2.array(decisionSourceSchema).max(20).optional(),
1767
+ actor: text(200).optional()
1768
+ });
1769
+ contentSchema = z2.object({
1770
+ title: z2.string().min(1),
1771
+ body: z2.string().min(1),
1772
+ category: z2.enum(["decision", "gotcha", "deprecation", "convention"]),
1773
+ files: z2.array(z2.string().refine((f) => normalizeRepoPath(f) !== null)),
1774
+ owner: text(200).optional(),
1775
+ sources: z2.array(decisionSourceSchema).max(20)
1776
+ });
1777
+ approvalSchema = z2.enum(["unreviewed", "proposed", "accepted"]);
1778
+ statusSchema = z2.enum(["active", "superseded", "retired"]);
1779
+ reviewEvidenceSchema = z2.object({
1780
+ baseHash: z2.string(),
1781
+ headHash: z2.string(),
1782
+ historyAvailable: z2.boolean(),
1783
+ changedFiles: z2.array(z2.string()),
1784
+ localChanges: z2.array(z2.string())
1785
+ });
1786
+ eventSchema = z2.object({
1787
+ kind: z2.enum(["imported", "created", "revised", "accepted", "reaffirmed", "retired", "superseded"]),
1788
+ at: z2.string().datetime(),
1789
+ actor: text(200).optional(),
1790
+ note: text(1500).optional(),
1791
+ revision: z2.number().int().positive(),
1792
+ content: contentSchema,
1793
+ approval: approvalSchema,
1794
+ status: statusSchema,
1795
+ refreshedHash: z2.string(),
1796
+ evidence: reviewEvidenceSchema.optional()
1797
+ });
1798
+ legacySchema = z2.object({
1799
+ version: z2.literal(1),
1800
+ id: z2.string().regex(/^[a-zA-Z0-9_-]+$/),
1801
+ title: z2.string().min(1),
1802
+ body: z2.string().min(1),
1803
+ category: contentSchema.shape.category,
1804
+ files: contentSchema.shape.files,
1805
+ createdAt: z2.string(),
1806
+ updatedAt: z2.string(),
1807
+ refreshedHash: z2.string(),
1808
+ status: z2.enum(["active", "superseded"]),
1809
+ supersededBy: z2.string().optional()
1810
+ }).passthrough();
1811
+ currentSchema = legacySchema.extend({
1812
+ version: z2.literal(2),
1813
+ status: statusSchema,
1814
+ approval: approvalSchema,
1815
+ revision: z2.number().int().positive(),
1816
+ owner: text(200).optional(),
1817
+ sources: z2.array(decisionSourceSchema).max(20),
1818
+ history: z2.array(eventSchema).min(1)
1819
+ }).superRefine((record, ctx) => {
1820
+ const invalid = (message) => ctx.addIssue({ code: "custom", message });
1821
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
1822
+ let previous;
1823
+ for (const event of record.history) {
1824
+ if (!previous) {
1825
+ if (!["created", "imported"].includes(event.kind) || event.revision !== 1) invalid("History must begin with creation or legacy import at revision 1");
1826
+ if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed")) invalid("Initial records cannot claim acceptance");
1827
+ } else {
1828
+ if (["created", "imported"].includes(event.kind)) invalid("History cannot restart");
1829
+ if (previous.status !== "active") invalid("Archived decisions cannot be changed");
1830
+ if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0)) invalid("Invalid revision sequence");
1831
+ if (event.kind !== "revised" && !same(event.content, previous.content)) invalid("A review cannot silently revise decision content");
1832
+ if (event.kind === "reaffirmed" && previous.approval !== "accepted") invalid("Only accepted decisions can be reaffirmed");
1833
+ if (event.kind === "accepted" && previous.approval === "accepted") invalid("Use reaffirmation for an accepted decision");
1834
+ const approval = event.kind === "revised" ? "proposed" : ["accepted", "reaffirmed"].includes(event.kind) ? "accepted" : previous.approval;
1835
+ if (event.approval !== approval) invalid("Approval disagrees with review history");
1836
+ if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash) invalid("Only a review can refresh the evidence baseline");
1837
+ }
1838
+ if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active")) invalid("Lifecycle disagrees with history");
1839
+ if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence)) invalid("Reviews require a named reviewer, reason, and code evidence");
1840
+ if (["accepted", "reaffirmed"].includes(event.kind)) {
1841
+ if (!event.content.owner || !event.content.sources.length) invalid("Accepted decisions require an owner and source");
1842
+ if (!event.evidence || !/^[a-f0-9]{40,64}$/.test(event.evidence.headHash) || event.refreshedHash !== event.evidence.headHash || event.evidence.localChanges.length) invalid("Acceptance requires a committed evidence baseline");
1843
+ }
1844
+ previous = event;
1845
+ }
1846
+ if (!previous || !same(previous.content, decisionContent(record)) || previous.approval !== record.approval || previous.status !== record.status || previous.revision !== record.revision || previous.refreshedHash !== record.refreshedHash) invalid("Decision does not match the final history event");
1847
+ });
1848
+ decisionSchema = z2.union([legacySchema, currentSchema]);
1849
+ }
1850
+ });
1481
1851
 
1482
1852
  // src/decisions/decisions.ts
1483
- var TITLE_MAX_CHARS = 80;
1484
- var BODY_RECOMMENDED_CHARS = 1500;
1485
- var BODY_MAX_CHARS = 2500;
1486
- var boundedText = (field, max, hint) => z3.string({
1487
- errorMap: (issue, ctx) => ({ message: issue.code === "too_big" ? `${field} has ${ctx.data.length} characters; maximum is ${max} (${ctx.data.length - max} over). ${hint}` : ctx.defaultError })
1488
- }).trim().min(1, `${field} must be non-empty`).max(max);
1489
- var decisionTitleSchema = boundedText("title", TITLE_MAX_CHARS, "Tighten it to a specific headline.");
1490
- var decisionBodySchema = boundedText(
1491
- "body",
1492
- BODY_MAX_CHARS,
1493
- `Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer. Preserve the rule, reason, and exceptions; put supporting references in sources.`
1494
- );
1853
+ import fs8 from "fs/promises";
1854
+ import path16 from "path";
1855
+ import { createHash } from "crypto";
1856
+ import { z as z3 } from "zod";
1495
1857
  async function loadDecisionStore(rootDir) {
1496
1858
  const records = [];
1497
1859
  const diagnostics = [];
@@ -1515,10 +1877,34 @@ async function loadDecisionStore(rootDir) {
1515
1877
  }
1516
1878
  return { records, diagnostics };
1517
1879
  }
1880
+ var TITLE_MAX_CHARS, BODY_RECOMMENDED_CHARS, BODY_MAX_CHARS, boundedText, decisionTitleSchema, decisionBodySchema;
1881
+ var init_decisions = __esm({
1882
+ "src/decisions/decisions.ts"() {
1883
+ "use strict";
1884
+ init_storage();
1885
+ init_paths();
1886
+ init_snapshot();
1887
+ init_lexical();
1888
+ init_provenance();
1889
+ TITLE_MAX_CHARS = 80;
1890
+ BODY_RECOMMENDED_CHARS = 1500;
1891
+ BODY_MAX_CHARS = 2500;
1892
+ boundedText = (field, max, hint) => z3.string({
1893
+ errorMap: (issue, ctx) => ({ message: issue.code === "too_big" ? `${field} has ${ctx.data.length} characters; maximum is ${max} (${ctx.data.length - max} over). ${hint}` : ctx.defaultError })
1894
+ }).trim().min(1, `${field} must be non-empty`).max(max);
1895
+ decisionTitleSchema = boundedText("title", TITLE_MAX_CHARS, "Tighten it to a specific headline.");
1896
+ decisionBodySchema = boundedText(
1897
+ "body",
1898
+ BODY_MAX_CHARS,
1899
+ `Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer. Preserve the rule, reason, and exceptions; put supporting references in sources.`
1900
+ );
1901
+ }
1902
+ });
1518
1903
 
1519
1904
  // src/decisions/drift.ts
1905
+ import path17 from "path";
1520
1906
  async function computeDecisionDrift(rootDir, decisions) {
1521
- const resolvedRoot = path14.resolve(rootDir);
1907
+ const resolvedRoot = path17.resolve(rootDir);
1522
1908
  const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
1523
1909
  const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
1524
1910
  const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
@@ -1546,6 +1932,16 @@ async function computeDecisionDrift(rootDir, decisions) {
1546
1932
  }
1547
1933
  return report;
1548
1934
  }
1935
+ var init_drift2 = __esm({
1936
+ "src/decisions/drift.ts"() {
1937
+ "use strict";
1938
+ init_drift();
1939
+ init_paths();
1940
+ init_snapshot();
1941
+ init_decisions();
1942
+ init_provenance();
1943
+ }
1944
+ });
1549
1945
 
1550
1946
  // src/audit/checks/decision-anchor.ts
1551
1947
  async function checkDecisionAnchors(ctx) {
@@ -1589,187 +1985,50 @@ async function checkDecisionAnchors(ctx) {
1589
1985
  }
1590
1986
  return result;
1591
1987
  }
1988
+ var init_decision_anchor = __esm({
1989
+ "src/audit/checks/decision-anchor.ts"() {
1990
+ "use strict";
1991
+ init_drift2();
1992
+ init_decisions();
1993
+ init_provenance();
1994
+ init_checks();
1995
+ }
1996
+ });
1592
1997
 
1593
1998
  // src/audit/checks/index.ts
1594
- var CHECKS = {
1595
- "deleted-reference": checkDeletedReferences,
1596
- "new-module": checkNewModules,
1597
- "stale-count": checkStaleCounts,
1598
- "dead-command": checkDeadCommands,
1599
- "deps-changed": checkDepsChanged,
1600
- "decision-anchor-drift": checkDecisionAnchors
1601
- };
1602
1999
  function emptyResult() {
1603
2000
  return { issues: [], advisories: [], skipped: [] };
1604
2001
  }
1605
-
1606
- // src/audit/audit.ts
1607
- async function computeAudit(rootDir, options = {}) {
1608
- const resolvedRoot = path15.resolve(rootDir);
1609
- const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
1610
- if (docs.length === 0) return null;
1611
- const report = {
1612
- version: 1,
1613
- root: resolvedRoot,
1614
- gitAvailable: headHash !== "unknown",
1615
- headHash,
1616
- checksRun: [],
1617
- docs: docs.map((d) => ({
1618
- path: d.path,
1619
- lastCommit: d.lastCommit,
1620
- dirty: d.dirty,
1621
- lineCount: d.lineCount
1622
- })),
1623
- decisionsChecked: false,
1624
- issues: [],
1625
- advisories: [],
1626
- suppressedAdvisories: [],
1627
- skippedChecks: [],
1628
- clean: true
1629
- };
1630
- if (!report.gitAvailable) return report;
1631
- const changesSinceDoc = /* @__PURE__ */ new Map();
1632
- for (const doc of docs) {
1633
- changesSinceDoc.set(
1634
- doc.path,
1635
- doc.lastCommit ? await getChangesWithStatus(resolvedRoot, doc.lastCommit.hash) : null
1636
- );
1637
- }
1638
- let decisionsPresent = false;
1639
- try {
1640
- await fs9.access(path15.join(resolvedRoot, ".mason", "decisions"));
1641
- decisionsPresent = true;
1642
- } catch {
1643
- }
1644
- report.decisionsChecked = decisionsPresent;
1645
- const ctx = {
1646
- root: resolvedRoot,
1647
- docs,
1648
- headHash,
1649
- changesSinceDoc,
1650
- decisionsPresent
1651
- };
1652
- const selected = options.checks ?? ALL_CHECKS;
1653
- for (const name of ALL_CHECKS) {
1654
- if (!selected.includes(name)) continue;
1655
- const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, ctx) : CHECKS[name](ctx));
1656
- report.checksRun.push(name);
1657
- report.issues.push(...issues);
1658
- report.advisories.push(...advisories);
1659
- report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
1660
- report.skippedChecks.push(...skipped);
2002
+ var CHECKS;
2003
+ var init_checks = __esm({
2004
+ "src/audit/checks/index.ts"() {
2005
+ "use strict";
2006
+ init_deleted_reference();
2007
+ init_new_module();
2008
+ init_stale_count();
2009
+ init_dead_command();
2010
+ init_deps_changed();
2011
+ init_decision_anchor();
2012
+ CHECKS = {
2013
+ "deleted-reference": checkDeletedReferences,
2014
+ "new-module": checkNewModules,
2015
+ "stale-count": checkStaleCounts,
2016
+ "dead-command": checkDeadCommands,
2017
+ "deps-changed": checkDepsChanged,
2018
+ "decision-anchor-drift": checkDecisionAnchors
2019
+ };
1661
2020
  }
1662
- report.clean = report.issues.length === 0;
1663
- return report;
1664
- }
2021
+ });
1665
2022
 
1666
- // src/audit/repair.ts
1667
- import fs10 from "fs/promises";
1668
- import path16 from "path";
1669
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
2023
+ // src/audit/findings.ts
2024
+ import { createHash as createHash2 } from "crypto";
1670
2025
  import { z as z4 } from "zod";
1671
- var checkSchema = z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
1672
- var commitSchema = z4.object({ hash: z4.string().regex(/^[a-f0-9]{40,64}$/), date: z4.string(), subject: z4.string() });
1673
- var anchorSchema = z4.object({ doc: z4.string(), line: z4.number().int().positive().nullable(), excerpt: z4.string().nullable() });
1674
- var count = z4.number().int().nonnegative();
1675
- var evidenceSchema = z4.discriminatedUnion("kind", [
1676
- z4.object({
1677
- kind: z4.literal("missing-path"),
1678
- claimed: z4.string(),
1679
- renamedTo: z4.string().nullable(),
1680
- deletedInCommit: commitSchema.nullable(),
1681
- everTracked: z4.boolean(),
1682
- parentDirExists: z4.boolean()
1683
- }),
1684
- z4.object({
1685
- kind: z4.literal("unmentioned-dir"),
1686
- dir: z4.string(),
1687
- sourceFileCount: count,
1688
- firstCommit: commitSchema.nullable(),
1689
- checkedDocs: z4.array(z4.string())
1690
- }),
1691
- z4.object({
1692
- kind: z4.literal("count-mismatch"),
1693
- claimed: count,
1694
- actual: count,
1695
- unit: z4.string(),
1696
- countedFrom: z4.string(),
1697
- members: z4.array(z4.string())
1698
- }),
1699
- z4.object({
1700
- kind: z4.literal("missing-script"),
1701
- scriptName: z4.string(),
1702
- invocation: z4.string(),
1703
- manifestsChecked: z4.array(z4.string()),
1704
- availableScripts: z4.array(z4.string())
1705
- }),
1706
- z4.object({
1707
- kind: z4.literal("doc-behind-manifests"),
1708
- docLastCommit: commitSchema,
1709
- manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
1710
- totalCommits: count
1711
- }),
1712
- z4.object({
1713
- kind: z4.literal("decision-anchor"),
1714
- decisionId: z4.string(),
1715
- title: z4.string(),
1716
- changedFiles: z4.array(z4.string()),
1717
- refreshedHash: z4.string(),
1718
- provenance: z4.object({}).passthrough().optional()
1719
- })
1720
- ]);
1721
- var findingSchema = z4.object({ message: z4.string(), anchor: anchorSchema, evidence: evidenceSchema });
1722
- var issueSchema = findingSchema.extend({
1723
- type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
1724
- confidence: z4.enum(["certain", "likely"])
1725
- });
1726
- var advisorySchema = findingSchema.extend({ type: z4.enum(["deps-changed", "decision-anchor-drift"]) });
1727
- var checkResultSchema = z4.object({
1728
- issues: z4.array(issueSchema),
1729
- advisories: z4.array(advisorySchema),
1730
- suppressedAdvisories: z4.array(advisorySchema).optional(),
1731
- skipped: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
1732
- });
1733
- var reportSchema = z4.object({
1734
- version: z4.literal(1),
1735
- root: z4.string(),
1736
- gitAvailable: z4.literal(true),
1737
- headHash: commitSchema.shape.hash,
1738
- checksRun: z4.array(checkSchema).nonempty(),
1739
- docs: z4.array(z4.object({
1740
- path: z4.enum(DOC_CANDIDATES),
1741
- lastCommit: commitSchema.nullable(),
1742
- dirty: z4.boolean(),
1743
- lineCount: count
1744
- })).nonempty(),
1745
- decisionsChecked: z4.boolean(),
1746
- clean: z4.boolean(),
1747
- issues: z4.array(issueSchema),
1748
- advisories: z4.array(advisorySchema),
1749
- suppressedAdvisories: z4.array(advisorySchema).optional(),
1750
- skippedChecks: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
1751
- });
1752
- var baselineSchema = z4.object({
1753
- kind: z4.literal("mason-audit-repair"),
1754
- version: z4.literal(1),
1755
- createdAt: z4.string().datetime(),
1756
- report: reportSchema,
1757
- digest: z4.string().regex(/^[a-f0-9]{64}$/)
1758
- });
1759
- var digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
1760
- var repairFindingSchema = z4.object({
1761
- id: z4.string(),
1762
- original: z4.union([issueSchema, advisorySchema]),
1763
- status: z4.enum(["resolved", "unresolved", "review-required", "unverified"]),
1764
- reason: z4.string(),
1765
- current: z4.union([issueSchema, advisorySchema]).optional()
1766
- });
1767
2026
  function findingId(finding) {
1768
2027
  const e = finding.evidence;
1769
2028
  let key;
1770
2029
  switch (e.kind) {
1771
2030
  case "missing-path":
1772
- key = e.claimed;
2031
+ key = e.resolvedPath && e.resolvedPath !== e.claimed ? [e.claimed, e.resolvedPath] : e.claimed;
1773
2032
  break;
1774
2033
  case "unmentioned-dir":
1775
2034
  key = e.dir;
@@ -1778,7 +2037,7 @@ function findingId(finding) {
1778
2037
  key = [e.unit.replace(/s$/, ""), e.countedFrom];
1779
2038
  break;
1780
2039
  case "missing-script":
1781
- key = e.scriptName;
2040
+ key = e.scope && e.scope.directory !== "." ? [e.scriptName, e.scope.directory] : e.scriptName;
1782
2041
  break;
1783
2042
  case "doc-behind-manifests":
1784
2043
  key = null;
@@ -1789,12 +2048,199 @@ function findingId(finding) {
1789
2048
  }
1790
2049
  return digest([finding.type, finding.anchor.doc, key]);
1791
2050
  }
2051
+ var digest, checkSchema, commitSchema, anchorSchema, count, scopeSchema, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema;
2052
+ var init_findings = __esm({
2053
+ "src/audit/findings.ts"() {
2054
+ "use strict";
2055
+ digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
2056
+ checkSchema = z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
2057
+ commitSchema = z4.object({ hash: z4.string().regex(/^[a-f0-9]{40,64}$/), date: z4.string(), subject: z4.string() });
2058
+ anchorSchema = z4.object({ doc: z4.string(), line: z4.number().int().positive().nullable(), excerpt: z4.string().nullable() });
2059
+ count = z4.number().int().nonnegative();
2060
+ scopeSchema = z4.object({
2061
+ basis: z4.enum(["repository", "document", "document-link", "explicit"]),
2062
+ directory: z4.string(),
2063
+ candidates: z4.array(z4.string())
2064
+ });
2065
+ evidenceSchema = z4.discriminatedUnion("kind", [
2066
+ z4.object({
2067
+ kind: z4.literal("missing-path"),
2068
+ claimed: z4.string(),
2069
+ renamedTo: z4.string().nullable(),
2070
+ deletedInCommit: commitSchema.nullable(),
2071
+ everTracked: z4.boolean(),
2072
+ parentDirExists: z4.boolean(),
2073
+ resolvedPath: z4.string().optional(),
2074
+ scope: scopeSchema.optional()
2075
+ }),
2076
+ z4.object({
2077
+ kind: z4.literal("unmentioned-dir"),
2078
+ dir: z4.string(),
2079
+ sourceFileCount: count,
2080
+ firstCommit: commitSchema.nullable(),
2081
+ checkedDocs: z4.array(z4.string())
2082
+ }),
2083
+ z4.object({
2084
+ kind: z4.literal("count-mismatch"),
2085
+ claimed: count,
2086
+ actual: count,
2087
+ unit: z4.string(),
2088
+ countedFrom: z4.string(),
2089
+ members: z4.array(z4.string())
2090
+ }),
2091
+ z4.object({
2092
+ kind: z4.literal("missing-script"),
2093
+ scriptName: z4.string(),
2094
+ invocation: z4.string(),
2095
+ manifestsChecked: z4.array(z4.string()),
2096
+ availableScripts: z4.array(z4.string()),
2097
+ scope: scopeSchema.optional()
2098
+ }),
2099
+ z4.object({
2100
+ kind: z4.literal("doc-behind-manifests"),
2101
+ docLastCommit: commitSchema,
2102
+ manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
2103
+ totalCommits: count
2104
+ }),
2105
+ z4.object({
2106
+ kind: z4.literal("decision-anchor"),
2107
+ decisionId: z4.string(),
2108
+ title: z4.string(),
2109
+ changedFiles: z4.array(z4.string()),
2110
+ refreshedHash: z4.string(),
2111
+ provenance: z4.object({}).passthrough().optional()
2112
+ })
2113
+ ]);
2114
+ findingSchema = z4.object({ message: z4.string(), anchor: anchorSchema, evidence: evidenceSchema });
2115
+ issueSchema = findingSchema.extend({
2116
+ type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
2117
+ confidence: z4.enum(["certain", "likely"])
2118
+ });
2119
+ advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.literal("recheck").optional() });
2120
+ checkResultSchema = z4.object({
2121
+ issues: z4.array(issueSchema),
2122
+ advisories: z4.array(advisorySchema),
2123
+ suppressedAdvisories: z4.array(advisorySchema).optional(),
2124
+ skipped: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
2125
+ });
2126
+ }
2127
+ });
2128
+
2129
+ // src/automation/store.ts
2130
+ import fs9 from "fs/promises";
2131
+ import os from "os";
2132
+ import { z as z5 } from "zod";
2133
+ async function withLock(root, directory, run, waitMs = 5e3) {
2134
+ const file = await storePath(root, directory + "/lock", true);
2135
+ const deadline = Date.now() + waitMs;
2136
+ let handle;
2137
+ while (!handle) {
2138
+ try {
2139
+ handle = await fs9.open(file, "wx", 384);
2140
+ try {
2141
+ await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
2142
+ } catch (error) {
2143
+ await handle.close().catch(() => {
2144
+ });
2145
+ handle = void 0;
2146
+ await fs9.rm(file, { force: true }).catch(() => {
2147
+ });
2148
+ throw error;
2149
+ }
2150
+ } catch (error) {
2151
+ if (error.code !== "EEXIST") throw error;
2152
+ try {
2153
+ const owner = JSON.parse(await fs9.readFile(file, "utf8"));
2154
+ if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
2155
+ try {
2156
+ process.kill(owner.pid, 0);
2157
+ } catch (probe) {
2158
+ if (probe.code === "ESRCH") {
2159
+ const reclaim = file + ".reclaim";
2160
+ let guard;
2161
+ try {
2162
+ guard = await fs9.open(reclaim, "wx", 384);
2163
+ const current = JSON.parse(await fs9.readFile(file, "utf8"));
2164
+ if (current.pid === owner.pid && current.host === owner.host) await fs9.unlink(file);
2165
+ } finally {
2166
+ if (guard) {
2167
+ await guard.close();
2168
+ await fs9.rm(reclaim, { force: true });
2169
+ }
2170
+ }
2171
+ }
2172
+ }
2173
+ }
2174
+ } catch {
2175
+ }
2176
+ if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
2177
+ await new Promise((resolve) => setTimeout(resolve, 40));
2178
+ }
2179
+ }
2180
+ try {
2181
+ return await run();
2182
+ } finally {
2183
+ await handle.close();
2184
+ await fs9.unlink(file);
2185
+ }
2186
+ }
2187
+ var hostSchema, stateSchema;
2188
+ var init_store = __esm({
2189
+ "src/automation/store.ts"() {
2190
+ "use strict";
2191
+ init_storage();
2192
+ hostSchema = z5.enum(["claude", "codex"]);
2193
+ stateSchema = z5.object({
2194
+ version: z5.literal(1),
2195
+ root: z5.string(),
2196
+ gitDir: z5.string(),
2197
+ branch: z5.string(),
2198
+ baselines: z5.array(z5.object({ path: z5.string(), at: z5.string(), event: z5.string(), fingerprint: z5.string() })).max(128),
2199
+ sessions: z5.record(z5.object({
2200
+ host: hostSchema,
2201
+ seen: z5.string().nullable(),
2202
+ continued: z5.boolean(),
2203
+ initialIssues: z5.array(z5.string()),
2204
+ initialDocs: z5.record(z5.string().nullable()),
2205
+ lastUsed: z5.string(),
2206
+ mutationObserved: z5.boolean(),
2207
+ pending: z5.record(z5.string()),
2208
+ coverageGaps: z5.array(z5.string()),
2209
+ events: z5.record(z5.object({ at: z5.string(), count: z5.number().int().positive() }))
2210
+ })),
2211
+ updatedAt: z5.string(),
2212
+ fingerprint: z5.string().nullable(),
2213
+ latest: z5.string().nullable(),
2214
+ analysis: z5.object({ id: z5.string(), fingerprint: z5.string(), completedAt: z5.number(), path: z5.string() }).optional()
2215
+ });
2216
+ }
2217
+ });
2218
+
2219
+ // src/audit/repair.ts
2220
+ var repair_exports = {};
2221
+ __export(repair_exports, {
2222
+ checkResultSchema: () => checkResultSchema,
2223
+ findingId: () => findingId,
2224
+ formatRepairSummary: () => formatRepairSummary,
2225
+ loadRepairBaseline: () => loadRepairBaseline,
2226
+ prepareRepair: () => prepareRepair,
2227
+ repairExitCode: () => repairExitCode,
2228
+ repairFindingSchema: () => repairFindingSchema,
2229
+ verifyRepair: () => verifyRepair
2230
+ });
2231
+ import fs10 from "fs/promises";
2232
+ import path18 from "path";
2233
+ import { randomUUID as randomUUID2 } from "crypto";
2234
+ import { z as z6 } from "zod";
2235
+ function recheckable(finding) {
2236
+ return !("confidence" in finding) && finding.resolution === "recheck" && ["deleted-reference", "dead-command", "new-module"].includes(finding.type) && ["missing-path", "missing-script", "unmentioned-dir"].includes(finding.evidence.kind);
2237
+ }
1792
2238
  function allFindings(report) {
1793
2239
  return [...report.issues, ...report.advisories, ...report.suppressedAdvisories ?? []];
1794
2240
  }
1795
2241
  async function docState(root) {
1796
2242
  const docs = [];
1797
- for (const doc of DOC_CANDIDATES) {
2243
+ for (const doc of await discoverDocPaths(root)) {
1798
2244
  try {
1799
2245
  const content = await readBoundedFile(await storePath(root, doc), 10 * 1024 * 1024);
1800
2246
  if (content === null) throw new Error("Context file is not regular or exceeds 10 MiB: " + doc);
@@ -1817,7 +2263,7 @@ async function stableAudit(root, checks, options = {}) {
1817
2263
  }
1818
2264
  async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
1819
2265
  const root = await fs10.realpath(rootDir);
1820
- const selected = z4.array(checkSchema).nonempty().parse(checks);
2266
+ const selected = z6.array(checkSchema).nonempty().parse(checks);
1821
2267
  const report = await stableAudit(root, selected, options);
1822
2268
  if (!report) throw new Error("No context files found to prepare a repair.");
1823
2269
  if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
@@ -1832,23 +2278,29 @@ async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
1832
2278
  await writeStoreJson(root, baselinePath, { ...payload, digest: digest(payload) });
1833
2279
  return { version: 1, action: "prepare", baselinePath, report };
1834
2280
  }
1835
- async function verifyRepair(rootDir, baselinePath, options = {}) {
2281
+ async function loadRepairBaseline(rootDir, baselinePath) {
1836
2282
  const root = await fs10.realpath(rootDir);
1837
- const declaredRoot = path16.resolve(rootDir);
1838
- const relative = path16.isAbsolute(baselinePath) ? path16.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
2283
+ const declaredRoot = path18.resolve(rootDir);
2284
+ const relative = path18.isAbsolute(baselinePath) ? path18.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
1839
2285
  const stored = baselineSchema.parse(await readStoreJson(root, relative));
1840
2286
  const { digest: savedDigest, ...payload } = stored;
1841
2287
  if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
1842
2288
  if (stored.report.root !== root) throw new Error("Repair baseline belongs to a different repository.");
1843
2289
  const original = stored.report;
2290
+ return { root, relative, original };
2291
+ }
2292
+ async function verifyRepair(rootDir, baselinePath, options = {}) {
2293
+ const { root, relative, original } = await loadRepairBaseline(rootDir, baselinePath);
1844
2294
  const diagnostics = [];
2295
+ let reviewDigest;
1845
2296
  let current = null;
1846
2297
  try {
2298
+ reviewDigest = digest(await advisoryReviewInventory(root));
1847
2299
  current = await stableAudit(root, original.checksRun, options);
1848
2300
  if (!current) diagnostics.push("No context files remain available to audit.");
1849
2301
  else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
1850
2302
  for (const doc of original.docs) {
1851
- if (!original.issues.some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
2303
+ if (!allFindings(original).some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
1852
2304
  const content = await readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024);
1853
2305
  if (content === null || !content.trim()) {
1854
2306
  diagnostics.push("Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair.");
@@ -1865,21 +2317,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
1865
2317
  const originalIds = new Set(originalFindings.map(findingId));
1866
2318
  const missingDocs = original.docs.filter((doc) => !current?.docs.some((d) => d.path === doc.path));
1867
2319
  for (const doc of missingDocs) diagnostics.push("Original context file " + doc.path + " is unavailable; removing it does not verify a repair.");
1868
- const findings = originalFindings.map((finding) => {
2320
+ const findings = await Promise.all(originalFindings.map(async (finding) => {
1869
2321
  const id = findingId(finding);
1870
2322
  const now = currentById.get(id);
1871
2323
  const base = { id, original: finding, ...now ? { current: now } : {} };
1872
2324
  if (diagnostics.length || !current) {
1873
2325
  return { ...base, status: "unverified", reason: "The original audit scope could not be verified. See diagnostics." };
1874
2326
  }
1875
- if ("confidence" in finding && now) {
2327
+ if (now && ("confidence" in finding || "confidence" in now)) {
1876
2328
  return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
1877
2329
  }
1878
2330
  const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
1879
2331
  if (!current.checksRun?.includes(finding.type) || skipped.length) {
1880
2332
  return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
1881
2333
  }
1882
- if (!("confidence" in finding)) {
2334
+ const review = await assessAdvisory(root, now ?? finding, original.headHash);
2335
+ if (review) {
2336
+ if (review.status === "unverified") return { ...base, review, status: "unverified", reason: review.reason };
2337
+ if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
2338
+ return { ...base, review, status: "review-required", reason: review.reason };
2339
+ }
2340
+ if (!("confidence" in finding) && (!recheckable(finding) || now)) {
1883
2341
  return {
1884
2342
  ...base,
1885
2343
  status: "review-required",
@@ -1887,11 +2345,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
1887
2345
  };
1888
2346
  }
1889
2347
  return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
1890
- });
2348
+ }));
2349
+ try {
2350
+ if (reviewDigest !== digest(await advisoryReviewInventory(root)) || current?.headHash !== await getCurrentGitHash(root)) {
2351
+ diagnostics.push("HEAD or advisory review records changed during verification; repeat against stable evidence.");
2352
+ for (const finding of findings) {
2353
+ finding.status = "unverified";
2354
+ finding.reason = diagnostics.at(-1);
2355
+ }
2356
+ }
2357
+ } catch (error) {
2358
+ diagnostics.push(String(error));
2359
+ for (const finding of findings) {
2360
+ finding.status = "unverified";
2361
+ finding.reason = "Advisory review evidence became unavailable during verification.";
2362
+ }
2363
+ }
1891
2364
  const newFindings = [...currentById].filter(([id]) => !originalIds.has(id)).map(([, f]) => f);
2365
+ const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
1892
2366
  const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
1893
2367
  for (const f of findings) counts[f.status]++;
1894
- const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f));
2368
+ const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f) && !reviewedIds.has(findingId(f)));
1895
2369
  const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
1896
2370
  return {
1897
2371
  version: 1,
@@ -1905,7 +2379,7 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
1905
2379
  diagnostics,
1906
2380
  currentAudit: current,
1907
2381
  counts,
1908
- scope: "Original audit checks over current context files and repository evidence. Resolved means no longer detected by that check. Advisories require separate review; this is not a certification of documentation or application correctness."
2382
+ scope: "Original audit checks over current context files and repository evidence. Resolved means no longer detected by the check or covered by an explicit assessment of current evidence; inspect each finding's reason and review. This is not a certification of documentation or application correctness."
1909
2383
  };
1910
2384
  }
1911
2385
  function repairExitCode(report) {
@@ -1914,18 +2388,482 @@ function repairExitCode(report) {
1914
2388
  function formatRepairSummary(report) {
1915
2389
  return [
1916
2390
  "Repair verification: " + report.status + ". Baseline: " + report.baselinePath,
1917
- ...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + f.original.message + "\n " + f.reason),
2391
+ ...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + f.original.message + "\n " + f.reason + "\n findingId: " + f.id),
1918
2392
  ...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
1919
2393
  ...report.diagnostics.map((d) => " [unverified] " + d),
1920
2394
  ...(report.currentAudit?.skippedChecks ?? []).map((s) => " [skipped] " + s.check + ": " + s.reason),
1921
2395
  report.scope
1922
2396
  ].join("\n");
1923
2397
  }
2398
+ var count2, reportSchema, baselineSchema, repairFindingSchema;
2399
+ var init_repair = __esm({
2400
+ "src/audit/repair.ts"() {
2401
+ "use strict";
2402
+ init_audit();
2403
+ init_docs();
2404
+ init_snapshot();
2405
+ init_drift();
2406
+ init_storage();
2407
+ init_files();
2408
+ init_paths();
2409
+ init_types();
2410
+ init_findings();
2411
+ init_advisory_review();
2412
+ init_findings();
2413
+ count2 = z6.number().int().nonnegative();
2414
+ reportSchema = z6.object({
2415
+ version: z6.literal(1),
2416
+ root: z6.string(),
2417
+ gitAvailable: z6.literal(true),
2418
+ headHash: commitSchema.shape.hash,
2419
+ checksRun: z6.array(checkSchema).nonempty(),
2420
+ docs: z6.array(z6.object({
2421
+ path: z6.string().refine((value) => normalizeRepoPath(value) === value),
2422
+ scope: z6.string().optional(),
2423
+ kind: z6.enum(["instructions", "readme"]).optional(),
2424
+ lastCommit: commitSchema.nullable(),
2425
+ dirty: z6.boolean(),
2426
+ lineCount: count2
2427
+ })).nonempty(),
2428
+ decisionsChecked: z6.boolean(),
2429
+ clean: z6.boolean(),
2430
+ issues: z6.array(issueSchema),
2431
+ advisories: z6.array(advisorySchema),
2432
+ suppressedAdvisories: z6.array(advisorySchema).optional(),
2433
+ skippedChecks: z6.array(z6.object({ check: z6.string(), reason: z6.string(), doc: z6.string().optional() }))
2434
+ });
2435
+ baselineSchema = z6.object({
2436
+ kind: z6.literal("mason-audit-repair"),
2437
+ version: z6.literal(1),
2438
+ createdAt: z6.string().datetime(),
2439
+ report: reportSchema,
2440
+ digest: z6.string().regex(/^[a-f0-9]{64}$/)
2441
+ });
2442
+ repairFindingSchema = z6.object({
2443
+ id: z6.string(),
2444
+ original: z6.union([issueSchema, advisorySchema]),
2445
+ status: z6.enum(["resolved", "unresolved", "review-required", "unverified"]),
2446
+ reason: z6.string(),
2447
+ current: z6.union([issueSchema, advisorySchema]).optional(),
2448
+ review: reviewSummarySchema.optional()
2449
+ });
2450
+ }
2451
+ });
2452
+
2453
+ // src/audit/advisory-review.ts
2454
+ import fs11 from "fs/promises";
2455
+ import { execFile as execFile8 } from "child_process";
2456
+ import { promisify as promisify8 } from "util";
2457
+ import { z as z7 } from "zod";
2458
+ async function readRecord(root, id) {
2459
+ const raw = await readStoreJson(root, recordPath(id));
2460
+ if (raw === null) return null;
2461
+ const record = recordSchema.parse(raw);
2462
+ const { digest: stored, ...payload } = record;
2463
+ if (record.id !== id || digest(payload) !== stored || record.events.some((event) => findingId(event.finding) !== id || event.evidence.fingerprint !== digest([event.evidence.scope, event.evidence.tree, event.evidence.lastScopeCommit, event.evidence.paths]))) {
2464
+ throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
2465
+ }
2466
+ return record;
2467
+ }
2468
+ function scopes(finding) {
2469
+ const files = [finding.anchor.doc], e = finding.evidence;
2470
+ if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
2471
+ if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
2472
+ if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
2473
+ else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
2474
+ else if (e.kind === "unmentioned-dir") files.push(e.dir, ...e.checkedDocs);
2475
+ else throw new Error("This advisory has no supported review scope.");
2476
+ return [...new Set(files)].map((file) => {
2477
+ if (normalizeRepoPath(file) !== file) throw new Error("Advisory contains an invalid scoped path: " + file);
2478
+ return `:(literal)${file}`;
2479
+ }).sort();
2480
+ }
2481
+ async function reviewEvidence(root, finding) {
2482
+ if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
2483
+ const scope = scopes(finding);
2484
+ const head = (await git2(root, "rev-parse", "HEAD")).trim();
2485
+ const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
2486
+ if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
2487
+ const [tree, status, lastScopeCommit, document] = await Promise.all([
2488
+ tracked.length ? git2(root, "ls-tree", "-r", "-z", "HEAD", "--", ...tracked.map((file) => `:(literal)${file}`)) : "",
2489
+ git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...scope),
2490
+ git2(root, "log", "-1", "--format=%H", "HEAD", "--", ...scope).then((value) => value.trim()),
2491
+ readAuditInput(root, finding.anchor.doc)
2492
+ ]);
2493
+ if (!document?.trim()) throw new Error("Original advisory document is missing, empty or unreadable.");
2494
+ if (tree.length > 2 * 1024 * 1024 || tree.split("\0").length > 2e4) throw new Error("Advisory scope is too large for a complete review; narrow the documentation scope.");
2495
+ for (const entry of tree.split("\0").filter(Boolean)) {
2496
+ const tab = entry.indexOf(" "), file = entry.slice(tab + 1);
2497
+ if (entry.startsWith("120000 ") || entry.startsWith("160000 ")) throw new Error("Symlink or submodule in advisory review scope: " + file);
2498
+ await storePath(root, file);
2499
+ }
2500
+ const paths = finding.evidence.kind === "missing-path" ? await Promise.all(
2501
+ (finding.evidence.scope?.candidates ?? [finding.evidence.resolvedPath ?? finding.evidence.claimed]).map(async (file) => ({ path: file, exists: await pathExists(root, file) }))
2502
+ ) : [];
2503
+ let dirty = status.length > 0;
2504
+ if (finding.evidence.kind === "missing-script") {
2505
+ for (const file of [...finding.evidence.manifestsChecked, ...finding.evidence.scope?.candidates ?? []]) {
2506
+ if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
2507
+ }
2508
+ }
2509
+ const fingerprint = digest([scope, tree, lastScopeCommit, paths]);
2510
+ if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
2511
+ return {
2512
+ ...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
2513
+ dirty,
2514
+ documentDigest: digest(document)
2515
+ };
2516
+ }
2517
+ async function decisionAssessment(root, finding, baselineHead) {
2518
+ const evidence = finding.evidence;
2519
+ if (evidence.kind !== "decision-anchor") return null;
2520
+ const id = findingId(finding);
2521
+ const store = await loadDecisionStore(root);
2522
+ if (store.diagnostics.length) throw new Error("Decision records have unavailable or malformed evidence; review cannot be confirmed.");
2523
+ const record = store.records.find((record2) => record2.id === evidence.decisionId);
2524
+ if (!record || record.version !== 2) return null;
2525
+ const events = record.history.filter((event2) => ["accepted", "reaffirmed", "retired"].includes(event2.kind));
2526
+ const event = events.at(-1);
2527
+ if (!event?.evidence || !event.actor || !event.note || !event.evidence.historyAvailable || event.evidence.localChanges.length) return null;
2528
+ try {
2529
+ await git2(root, "merge-base", "--is-ancestor", baselineHead, event.evidence.headHash);
2530
+ } catch {
2531
+ return null;
2532
+ }
2533
+ const drift = await computeDecisionDrift(root, [record]);
2534
+ if (!drift.historyAvailable) throw new Error("Decision review history is unavailable.");
2535
+ const retired = record.status === "retired" && event.kind === "retired";
2536
+ const current = retired || record.status === "active" && record.approval === "accepted" && drift.freshness?.[record.id] === "current" && !drift.pendingProposals?.[record.id];
2537
+ return {
2538
+ id,
2539
+ status: current ? "current" : "reopened",
2540
+ outcome: event.kind,
2541
+ reviewer: event.actor,
2542
+ note: event.note,
2543
+ reviewedHead: event.evidence.headHash,
2544
+ recordPath: `.mason/decisions/${record.id}.json`,
2545
+ reason: current ? "The existing decision review records acceptance, reaffirmation or retirement covering the original finding." : "Decision content or anchor evidence changed after its review; review the decision again."
2546
+ };
2547
+ }
2548
+ async function assessAdvisory(root, finding, baselineHead) {
2549
+ if ("confidence" in finding) return null;
2550
+ const id = findingId(finding);
2551
+ try {
2552
+ if (finding.evidence.kind === "decision-anchor") return await decisionAssessment(root, finding, baselineHead);
2553
+ const record = await readRecord(root, id);
2554
+ if (!record) return null;
2555
+ const event = record.events.at(-1);
2556
+ const current = await reviewEvidence(root, event.finding);
2557
+ await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
2558
+ const base = {
2559
+ id,
2560
+ recordPath: recordPath(id),
2561
+ reviewer: event.reviewer,
2562
+ note: event.note,
2563
+ outcome: event.outcome,
2564
+ reviewedHead: event.evidence.head
2565
+ };
2566
+ if (current.dirty || current.fingerprint !== event.evidence.fingerprint || digest(scopes(finding)) !== digest(event.evidence.scope)) return { ...base, status: "reopened", reason: "Relevant document or repository evidence changed after the assessment; prepare a new review." };
2567
+ return { ...base, status: event.outcome === "deferred" ? "deferred" : "current", reason: event.outcome === "deferred" ? "The review was deferred and remains outstanding." : "An explicit assessment covers the current committed scope. This does not approve an engineering decision or certify correctness." };
2568
+ } catch (error) {
2569
+ return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
2570
+ }
2571
+ }
2572
+ async function advisoryReviewInventory(root) {
2573
+ let names;
2574
+ try {
2575
+ names = await fs11.readdir(await storePath(root, ".mason/reviews/advisories"));
2576
+ } catch (error) {
2577
+ if (error.code === "ENOENT") return [];
2578
+ throw error;
2579
+ }
2580
+ if (names.length > 5e3) throw new Error("Advisory review inventory exceeds 5,000 records.");
2581
+ return Promise.all(names.filter((name) => name.endsWith(".json")).sort().map(async (name) => [name, await readAuditInput(root, ".mason/reviews/advisories/" + name)]));
2582
+ }
2583
+ async function reviewAdvisory(rootDir, input) {
2584
+ const root = await fs11.realpath(rootDir), request = advisoryReviewRequest.parse(input);
2585
+ const { loadRepairBaseline: loadRepairBaseline2 } = await Promise.resolve().then(() => (init_repair(), repair_exports));
2586
+ const baseline = await loadRepairBaseline2(root, request.baselinePath);
2587
+ const finding = [...baseline.original.advisories, ...baseline.original.suppressedAdvisories ?? []].find((f) => findingId(f) === request.findingId);
2588
+ if (!finding) throw new Error("No advisory with this findingId in the original baseline. Provable issues must be repaired and rechecked.");
2589
+ if (finding.evidence.kind === "decision-anchor") return {
2590
+ status: "decision-review-required",
2591
+ decisionId: finding.evidence.decisionId,
2592
+ hint: "Use review_decision to prepare evidence and record authorized acceptance, reaffirmation or retirement, then verify this original repair baseline. An advisory assessment cannot approve a decision."
2593
+ };
2594
+ const prepare = async () => {
2595
+ const record = await readRecord(root, request.findingId);
2596
+ const evidence = await reviewEvidence(root, finding);
2597
+ await git2(root, "merge-base", "--is-ancestor", baseline.original.headHash, evidence.head);
2598
+ const token = digest({ baseline: baseline.original, finding, evidence, record });
2599
+ return { record, evidence, token };
2600
+ };
2601
+ if (request.action === "prepare") {
2602
+ const state = await prepare();
2603
+ let diff = null, diffNote;
2604
+ const from = finding.evidence.kind === "doc-behind-manifests" ? finding.evidence.docLastCommit.hash : baseline.original.headHash;
2605
+ try {
2606
+ const full = await git2(root, "diff", "--no-ext-diff", "--no-textconv", "--no-color", "--no-renames", "-U3", from, state.evidence.head, "--", ...state.evidence.scope);
2607
+ diff = full.slice(0, 16e3);
2608
+ if (full.length > diff.length) diffNote = "Diff preview truncated; inspect the full scoped diff before reviewing.";
2609
+ } catch {
2610
+ diffNote = "Diff preview unavailable; inspect the original cited evidence directly before reviewing.";
2611
+ }
2612
+ if ((await prepare()).token !== state.token) throw new Error("Review evidence changed during preparation; retry.");
2613
+ return {
2614
+ status: "prepared",
2615
+ findingId: request.findingId,
2616
+ finding,
2617
+ evidence: state.evidence,
2618
+ history: state.record?.events ?? [],
2619
+ reviewToken: state.token,
2620
+ diff,
2621
+ diffNote,
2622
+ hint: "Inspect the original finding, scoped document and relevant code changes. Record addressed, inapplicable or deferred only when the user or cited project review authorizes that assessment. Supply the actual reviewer, reason in note and reviewToken. Relevant edits must be committed first. Deferral remains outstanding. Review identities are recorded assertions, not authenticated approvals."
2623
+ };
2624
+ }
2625
+ if (!request.reviewer || !request.note || !request.reviewToken) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
2626
+ return withLock(root, ".mason/reports/advisory-review-locks/" + request.findingId, async () => {
2627
+ const state = await prepare();
2628
+ if (state.token !== request.reviewToken) throw new Error("Review conflict: evidence or review history changed since preparation. Prepare and inspect a new review.");
2629
+ if (state.evidence.dirty) throw new Error("Commit relevant document and repository edits before recording the assessment. Unrelated local work may remain.");
2630
+ if ((state.record?.events.length ?? 0) >= 200) throw new Error("Review history limit reached; existing events were retained.");
2631
+ const { dirty, documentDigest, ...evidence } = state.evidence;
2632
+ const event = eventSchema2.parse({
2633
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2634
+ reviewer: request.reviewer,
2635
+ note: request.note,
2636
+ outcome: request.action,
2637
+ finding,
2638
+ evidence
2639
+ });
2640
+ const payload = { version: 1, id: request.findingId, events: [...state.record?.events ?? [], event] };
2641
+ if ((await prepare()).token !== state.token) throw new Error("Review conflict: evidence changed while recording the assessment.");
2642
+ await writeStoreJson(root, recordPath(request.findingId), { ...payload, digest: digest(payload) });
2643
+ return {
2644
+ status: "recorded",
2645
+ recordPath: recordPath(request.findingId),
2646
+ event,
2647
+ hint: "Review and commit this assessment record through the normal project workflow, then verify the original repair baseline. Relevant changes reopen the assessment; deferral remains outstanding."
2648
+ };
2649
+ });
2650
+ }
2651
+ var execute2, git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
2652
+ var init_advisory_review = __esm({
2653
+ "src/audit/advisory-review.ts"() {
2654
+ "use strict";
2655
+ init_findings();
2656
+ init_deps_changed();
2657
+ init_inputs();
2658
+ init_scope();
2659
+ init_storage();
2660
+ init_paths();
2661
+ init_store();
2662
+ init_decisions();
2663
+ init_drift2();
2664
+ execute2 = promisify8(execFile8);
2665
+ git2 = async (root, ...args) => (await execute2("git", args, { cwd: root, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 })).stdout;
2666
+ hashSchema = z7.string().regex(/^[a-f0-9]{64}$/);
2667
+ commitSchema2 = z7.string().regex(/^[a-f0-9]{40,64}$/);
2668
+ evidenceSchema2 = z7.object({
2669
+ head: commitSchema2,
2670
+ fingerprint: hashSchema,
2671
+ scope: z7.array(z7.string()).min(1).max(1e3),
2672
+ tree: z7.string().max(2 * 1024 * 1024),
2673
+ lastScopeCommit: commitSchema2,
2674
+ paths: z7.array(z7.object({ path: z7.string(), exists: z7.boolean() })).max(1e3)
2675
+ });
2676
+ eventSchema2 = z7.object({
2677
+ at: z7.string().datetime(),
2678
+ reviewer: z7.string().trim().min(1).max(200),
2679
+ note: z7.string().trim().min(1).max(2500),
2680
+ outcome: z7.enum(["addressed", "inapplicable", "deferred"]),
2681
+ finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
2682
+ evidence: evidenceSchema2
2683
+ });
2684
+ recordSchema = z7.object({ version: z7.literal(1), id: hashSchema, events: z7.array(eventSchema2).min(1).max(200), digest: hashSchema });
2685
+ reviewSummarySchema = z7.object({
2686
+ id: hashSchema,
2687
+ status: z7.enum(["current", "reopened", "deferred", "unverified"]),
2688
+ reason: z7.string(),
2689
+ recordPath: z7.string().optional(),
2690
+ reviewer: z7.string().optional(),
2691
+ note: z7.string().optional(),
2692
+ outcome: z7.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
2693
+ reviewedHead: z7.string().optional()
2694
+ });
2695
+ advisoryReviewRequest = z7.object({
2696
+ baselinePath: z7.string().min(1),
2697
+ findingId: hashSchema,
2698
+ action: z7.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
2699
+ reviewer: eventSchema2.shape.reviewer.optional(),
2700
+ note: eventSchema2.shape.note.optional(),
2701
+ reviewToken: hashSchema.optional()
2702
+ });
2703
+ recordPath = (id) => `.mason/reviews/advisories/${hashSchema.parse(id)}.json`;
2704
+ }
2705
+ });
2706
+
2707
+ // src/audit/audit.ts
2708
+ import fs12 from "fs/promises";
2709
+ import path19 from "path";
2710
+ async function computeAudit(rootDir, options = {}) {
2711
+ const resolvedRoot = path19.resolve(rootDir);
2712
+ const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
2713
+ if (docs.length === 0) return null;
2714
+ const report = {
2715
+ version: 1,
2716
+ root: resolvedRoot,
2717
+ gitAvailable: headHash !== "unknown",
2718
+ headHash,
2719
+ checksRun: [],
2720
+ docs: docs.map((d) => ({
2721
+ path: d.path,
2722
+ scope: d.scope,
2723
+ kind: d.kind,
2724
+ lastCommit: d.lastCommit,
2725
+ dirty: d.dirty,
2726
+ lineCount: d.lineCount
2727
+ })),
2728
+ decisionsChecked: false,
2729
+ issues: [],
2730
+ advisories: [],
2731
+ suppressedAdvisories: [],
2732
+ skippedChecks: [],
2733
+ clean: true
2734
+ };
2735
+ if (!report.gitAvailable) return report;
2736
+ const changesSinceDoc = /* @__PURE__ */ new Map();
2737
+ for (const doc of docs) {
2738
+ changesSinceDoc.set(
2739
+ doc.path,
2740
+ doc.lastCommit ? await getChangesWithStatus(resolvedRoot, doc.lastCommit.hash) : null
2741
+ );
2742
+ }
2743
+ let decisionsPresent = false;
2744
+ try {
2745
+ await fs12.access(path19.join(resolvedRoot, ".mason", "decisions"));
2746
+ decisionsPresent = true;
2747
+ } catch {
2748
+ }
2749
+ report.decisionsChecked = decisionsPresent;
2750
+ const ctx = {
2751
+ root: resolvedRoot,
2752
+ docs,
2753
+ headHash,
2754
+ changesSinceDoc,
2755
+ decisionsPresent
2756
+ };
2757
+ const selected = options.checks ?? ALL_CHECKS;
2758
+ for (const name of ALL_CHECKS) {
2759
+ if (!selected.includes(name)) continue;
2760
+ const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, ctx) : CHECKS[name](ctx));
2761
+ report.checksRun.push(name);
2762
+ report.issues.push(...issues);
2763
+ report.advisories.push(...advisories);
2764
+ report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
2765
+ report.skippedChecks.push(...skipped);
2766
+ }
2767
+ report.clean = report.issues.length === 0;
2768
+ const reviews = (await Promise.all([...report.advisories, ...report.suppressedAdvisories].map((finding) => assessAdvisory(resolvedRoot, finding, headHash)))).filter((review) => review !== null);
2769
+ if (reviews.length) report.advisoryReviews = reviews;
2770
+ return report;
2771
+ }
2772
+ var init_audit = __esm({
2773
+ "src/audit/audit.ts"() {
2774
+ "use strict";
2775
+ init_drift();
2776
+ init_snapshot();
2777
+ init_docs();
2778
+ init_types();
2779
+ init_checks();
2780
+ init_advisory_review();
2781
+ }
2782
+ });
2783
+
2784
+ // src/audit/cli.ts
2785
+ init_audit();
2786
+ init_types();
2787
+ init_repair();
2788
+ init_findings();
2789
+ import path21 from "path";
2790
+
2791
+ // src/audit/advisory-cli.ts
2792
+ init_advisory_review();
2793
+ import path20 from "path";
2794
+ import { stripVTControlCharacters } from "util";
2795
+ var USAGE = `Usage: mason audit review --baseline <path> --finding <id> [options]
2796
+
2797
+ Prepare an advisory assessment against retained evidence (read-only by default).
2798
+ --dir <path> Repository root (default: current directory)
2799
+ --baseline <path> Original repair baseline
2800
+ --finding <id> findingId from repair verification or automation
2801
+ --outcome <value> Record addressed, inapplicable, or deferred
2802
+ --reviewer <name> Actual authorized reviewer
2803
+ --note <reason> Assessment reason
2804
+ --token <token> reviewToken returned by preparation
2805
+ --json Full evidence and review history
2806
+
2807
+ Recording requires prior preparation and committed relevant edits. Review records
2808
+ belong in Git under .mason/reviews/advisories/. Deferral remains outstanding.
2809
+ Decision findings use review_decision; this command never approves decisions.
2810
+ Exit 0: prepared/recorded; 2: unavailable, conflict, or decision review required.`;
2811
+ async function runAdvisoryCli(args, io = { out: (text2) => console.log(text2), err: (text2) => console.error(text2) }) {
2812
+ if (args.includes("--help") || args.includes("-h")) {
2813
+ io.out(USAGE);
2814
+ return 0;
2815
+ }
2816
+ try {
2817
+ const flags = {};
2818
+ let json = false;
2819
+ for (let i = 0; i < args.length; i++) {
2820
+ const flag = args[i];
2821
+ if (flag === "--json") {
2822
+ json = true;
2823
+ continue;
2824
+ }
2825
+ if (!["--dir", "--baseline", "--finding", "--outcome", "--reviewer", "--note", "--token"].includes(flag) || flags[flag]) throw new Error("Unexpected or repeated argument: " + flag);
2826
+ const value = args[++i];
2827
+ if (!value || value.startsWith("--")) throw new Error(flag + " requires a value.");
2828
+ flags[flag] = value;
2829
+ }
2830
+ if (!flags["--outcome"] && ["--reviewer", "--note", "--token"].some((key) => flags[key])) throw new Error("Use --outcome to record an assessment.");
2831
+ const input = {
2832
+ baselinePath: flags["--baseline"],
2833
+ findingId: flags["--finding"],
2834
+ action: flags["--outcome"] ?? "prepare",
2835
+ reviewer: flags["--reviewer"],
2836
+ note: flags["--note"],
2837
+ reviewToken: flags["--token"]
2838
+ };
2839
+ const result = await reviewAdvisory(path20.resolve(flags["--dir"] ?? process.cwd()), input);
2840
+ if (json) io.out(JSON.stringify(result, null, 2));
2841
+ else {
2842
+ const lines = [`Advisory review: ${result.status}.`];
2843
+ if ("finding" in result && result.finding && result.evidence) lines.push(
2844
+ `[${result.finding.type}] ${result.finding.anchor.doc}: ${result.finding.message}`,
2845
+ `Finding: ${result.findingId}`,
2846
+ `Prepared against commit ${result.evidence.head.slice(0, 12)}.`,
2847
+ result.evidence.dirty ? "Relevant files have uncommitted edits; commit them and prepare again before recording." : "Relevant files are committed.",
2848
+ `Review token: ${result.reviewToken}`,
2849
+ ...result.diff ? ["Scoped changes:", result.diff] : [],
2850
+ ...result.diffNote ? [result.diffNote] : []
2851
+ );
2852
+ if ("event" in result) lines.push(`${result.event.outcome} \u2014 ${result.event.reviewer}: ${result.event.note}`, `Record: ${result.recordPath}`);
2853
+ lines.push(result.hint);
2854
+ io.out(stripVTControlCharacters(lines.join("\n")));
2855
+ }
2856
+ return result.status === "decision-review-required" ? 2 : 0;
2857
+ } catch (error) {
2858
+ io.err(stripVTControlCharacters(error instanceof Error ? error.message : String(error)));
2859
+ return 2;
2860
+ }
2861
+ }
1924
2862
 
1925
2863
  // src/audit/cli.ts
1926
- var USAGE = `Usage: mason-audit [--dir <path>] [--json | --fix-prompt] [--checks <list>]
2864
+ var USAGE2 = `Usage: mason-audit [--dir <path>] [--json | --fix-prompt] [--checks <list>]
1927
2865
 
1928
- Audits the repo's AI context files (CLAUDE.md, .claude/CLAUDE.md, AGENTS.md)
2866
+ Audits README.md and agent instruction files at any depth, preserving filename case
1929
2867
  against repo reality: referenced paths that no longer exist, undocumented
1930
2868
  modules, stale counts, dead npm scripts, and manifests newer than the doc.
1931
2869
  Deterministic: no LLM call, no network \u2013 safe for CI. Works on any repo with
@@ -1944,6 +2882,8 @@ Options:
1944
2882
  ${ALL_CHECKS.join(", ")}
1945
2883
  --help Show this help
1946
2884
 
2885
+ Use mason audit review --help to prepare and record an authorized advisory assessment.
2886
+
1947
2887
  Exit codes:
1948
2888
  0 no issues (advisories may still be present)
1949
2889
  1 provable issues found
@@ -2007,7 +2947,10 @@ function issueLine(issue) {
2007
2947
  }
2008
2948
  function formatAuditSummary(report) {
2009
2949
  const lines = [];
2010
- const reviewCount = report.advisories.length + (report.suppressedAdvisories?.length ?? 0);
2950
+ const reviewed = new Set(report.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
2951
+ const open = report.advisories.filter((finding) => !reviewed.has(findingId(finding)));
2952
+ const suppressed = (report.suppressedAdvisories ?? []).filter((finding) => !reviewed.has(findingId(finding)));
2953
+ const reviewCount = open.length + suppressed.length;
2011
2954
  for (const doc of report.docs) {
2012
2955
  const docIssues = report.issues.filter((i) => i.anchor.doc === doc.path);
2013
2956
  const committed = doc.lastCommit ? `last committed ${doc.lastCommit.date.slice(0, 10)}, ${doc.lastCommit.hash.slice(0, 7)}` : "untracked";
@@ -2024,9 +2967,9 @@ function formatAuditSummary(report) {
2024
2967
  for (const issue of report.issues) {
2025
2968
  if (!docPaths.has(issue.anchor.doc)) lines.push(issueLine(issue));
2026
2969
  }
2027
- if (report.advisories.length > 0) {
2970
+ if (open.length > 0) {
2028
2971
  lines.push("Advisories (do not affect the exit code):");
2029
- for (const advisory of report.advisories) {
2972
+ for (const advisory of open) {
2030
2973
  lines.push(` [${advisory.type}] ${advisory.anchor.doc}: ${advisory.message}`);
2031
2974
  }
2032
2975
  }
@@ -2035,7 +2978,9 @@ function formatAuditSummary(report) {
2035
2978
  lines.push(` [skipped] ${skip.check}: ${skip.reason}`);
2036
2979
  }
2037
2980
  }
2038
- for (const advisory of report.suppressedAdvisories ?? []) {
2981
+ if (reviewed.size) lines.push(`${reviewed.size} advisories have recorded assessments covering current evidence (see --json).`);
2982
+ for (const review of report.advisoryReviews ?? []) if (review.status !== "current") lines.push(` [${review.status}] ${review.reason}`);
2983
+ for (const advisory of suppressed) {
2039
2984
  lines.push(` [suppressed; unresolved] ${advisory.type} ${advisory.anchor.doc}: ${advisory.message}`);
2040
2985
  }
2041
2986
  lines.push(
@@ -2072,10 +3017,10 @@ function formatFixPrompt(report, baselinePath) {
2072
3017
  "- dead-command: replace with the correct script from availableScripts if an obvious rename exists; otherwise remove the command mention."
2073
3018
  );
2074
3019
  lines.push(
2075
- "- new-module: add a one-line factual mention of the directory where sibling modules are described; read the directory's files first and describe only what you verified."
3020
+ "- new-module: review whether the directory needs documenting; an omission alone does not establish a defect. Preserve scope and describe only what you verified."
2076
3021
  );
2077
3022
  lines.push(
2078
- "- ADVISORIES require a separate assessment of the cited commits or decision evidence. Report any review you perform and what remains unknown. Their disappearance after edits or a commit does not establish review or approval."
3023
+ "- Historical ADVISORIES require a separate authorized assessment using review_advisory with the baselinePath and findingId from verification. Prepare and inspect evidence first; record addressed, inapplicable or deferred with the actual reviewer, note and reviewToken. Decision findings use review_decision. Candidates marked resolution: recheck can be rerun to see whether the condition remains; this does not establish semantic approval. Report what remains unknown."
2079
3024
  );
2080
3025
  lines.push("");
2081
3026
  lines.push("AUDIT REPORT (current context files and repository evidence, including local edits):");
@@ -2095,7 +3040,7 @@ function formatFixPrompt(report, baselinePath) {
2095
3040
  );
2096
3041
  lines.push("");
2097
3042
  lines.push(
2098
- "After edits, call mason_repair with action: verify and the original baselinePath, or mason-audit --verify-repair <baselinePath> --dir <project>. Repeat against the same baseline after any final documentation commit. Summarize resolved, unresolved, review-required, unverified, and new findings with their evidence. Do not report a suppressed or unavailable check as fixed. This audit covers the listed context files; independently discovered README or application issues need their own validation."
3043
+ "After edits, call mason_repair with action: verify and the original baselinePath, or mason-audit --verify-repair <baselinePath> --dir <project>. Repeat against the same baseline after any final documentation commit. Summarize resolved, unresolved, review-required, unverified, and new findings with their evidence. Do not report a suppressed or unavailable check as fixed. This audit covers the listed documents and supported claims; application behavior and code examples need their own validation."
2099
3044
  );
2100
3045
  return lines.join("\n");
2101
3046
  }
@@ -2105,6 +3050,7 @@ async function runAuditCli(argv, io = {
2105
3050
  err: (line) => process.stderr.write(`${line}
2106
3051
  `)
2107
3052
  }) {
3053
+ if (argv[0] === "review") return runAdvisoryCli(argv.slice(1), io);
2108
3054
  let args;
2109
3055
  try {
2110
3056
  args = parseArgs(argv);
@@ -2116,14 +3062,14 @@ async function runAuditCli(argv, io = {
2116
3062
  }
2117
3063
  } catch (error) {
2118
3064
  io.err(error instanceof Error ? error.message : String(error));
2119
- io.err(USAGE);
3065
+ io.err(USAGE2);
2120
3066
  return 2;
2121
3067
  }
2122
3068
  if (args.help) {
2123
- io.out(USAGE);
3069
+ io.out(USAGE2);
2124
3070
  return 0;
2125
3071
  }
2126
- const rootDir = path17.resolve(args.dir);
3072
+ const rootDir = path21.resolve(args.dir);
2127
3073
  if (args.baseline || args.prepareRepair) {
2128
3074
  try {
2129
3075
  if (args.baseline) {
@@ -2143,7 +3089,7 @@ ${formatAuditSummary(prepared.report)}`);
2143
3089
  const report = await computeAudit(rootDir, { checks: args.checks });
2144
3090
  if (!report) {
2145
3091
  io.err(
2146
- `No CLAUDE.md, .claude/CLAUDE.md, or AGENTS.md found in ${rootDir}.`
3092
+ `No README.md or agent instruction files found in ${rootDir}.`
2147
3093
  );
2148
3094
  return 2;
2149
3095
  }
@@ -2169,11 +3115,13 @@ ${formatAuditSummary(prepared.report)}`);
2169
3115
 
2170
3116
  // bin/mason-audit.ts
2171
3117
  runAuditCli(process.argv.slice(2)).then(
2172
- (code) => process.exit(code),
3118
+ (code) => {
3119
+ process.exitCode = code;
3120
+ },
2173
3121
  (err) => {
2174
3122
  process.stderr.write(`mason-audit error: ${err}
2175
3123
  `);
2176
- process.exit(2);
3124
+ process.exitCode = 2;
2177
3125
  }
2178
3126
  );
2179
3127
  //# sourceMappingURL=mason-audit.js.map