mason-context 0.16.3 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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) {
@@ -797,7 +800,7 @@ async function readAuditInput(root, file) {
797
800
  if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
798
801
  return value;
799
802
  } catch (error) {
800
- if (error.code === "ENOENT") return null;
803
+ if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return null;
801
804
  throw error;
802
805
  }
803
806
  }
@@ -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,351 @@ 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",
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
+ try {
1070
+ await fs6.access(await auditInputPath(root, file));
1071
+ return true;
1072
+ } catch (error) {
1073
+ if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return false;
1074
+ throw error;
1075
+ }
1076
+ }
1077
+ function gitMetadataPath(file) {
1078
+ return file.split("/").includes(".git");
1079
+ }
1080
+ function optionalMasonPath(file) {
1081
+ return file.split("/").includes(".mason");
1082
+ }
1083
+ var init_scope = __esm({
1084
+ "src/audit/scope.ts"() {
1085
+ "use strict";
1086
+ init_inputs();
1087
+ init_docs();
1088
+ }
1089
+ });
1090
+
1091
+ // src/audit/checks/deleted-reference.ts
1092
+ import path11 from "path";
1093
+ async function checkDeletedReferences(ctx) {
1094
+ const result = emptyResult();
1095
+ for (const doc of ctx.docs) {
1096
+ const changes = ctx.changesSinceDoc.get(doc.path);
1097
+ const renames = new Map((changes ?? []).filter((c) => c.status === "renamed" && c.previousPath).map((c) => [c.previousPath, c.path]));
1098
+ for (const claim of doc.claims.paths) {
1099
+ const scope = pathClaimScope(doc.path, claim);
1100
+ if (!scope) {
1101
+ result.skipped.push({
1102
+ check: "deleted-reference",
1103
+ doc: doc.path,
1104
+ reason: `Reference ${claim.excerpt} is outside the repository or has an unsupported path.`
1105
+ });
1106
+ continue;
1107
+ }
1108
+ if (scope.candidates.some(optionalMasonPath)) continue;
1109
+ if (scope.candidates.some(gitMetadataPath)) {
1110
+ result.skipped.push({
1111
+ check: "deleted-reference",
1112
+ doc: doc.path,
1113
+ reason: `Reference ${claim.path} names Git metadata, whose layout varies between checkouts; it was not verified as a repository file.`
1114
+ });
1115
+ continue;
1116
+ }
1117
+ if ((await Promise.all(scope.candidates.map((file) => pathExists(ctx.root, file)))).some(Boolean)) continue;
1118
+ const candidates = await Promise.all(scope.candidates.map(async (file) => ({
1119
+ file,
1120
+ tracked: await lastCommitOf(ctx.root, file),
1121
+ parent: await pathExists(ctx.root, path11.posix.dirname(file))
1122
+ })));
1123
+ const candidate = candidates.find((c) => c.tracked || renames.has(c.file)) ?? candidates.find((c) => c.parent);
1124
+ if (!candidate && claim.relativeTo !== "document") continue;
1125
+ const resolvedPath = candidate?.file ?? scope.candidates[0];
1126
+ const renamedTo = renames.get(resolvedPath) ?? null;
1127
+ const deletedInCommit = candidate?.tracked && !renamedTo ? await deletingCommitOf(ctx.root, resolvedPath) : null;
1128
+ const evidence = {
1129
+ kind: "missing-path",
1130
+ claimed: claim.path,
1131
+ resolvedPath,
1132
+ scope,
1133
+ renamedTo,
1134
+ deletedInCommit,
1135
+ everTracked: !!candidate?.tracked || !!renamedTo,
1136
+ parentDirExists: candidate?.parent ?? false
1137
+ };
1138
+ const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
1139
+ 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)})` : "");
1140
+ if (scope.basis !== "document" && (renamedTo || candidate?.tracked)) {
1141
+ result.issues.push({ type: "deleted-reference", message, anchor, confidence: "certain", evidence });
1142
+ } else {
1143
+ 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 });
1144
+ }
1145
+ }
1146
+ }
1147
+ return result;
1148
+ }
1149
+ var init_deleted_reference = __esm({
1150
+ "src/audit/checks/deleted-reference.ts"() {
1151
+ "use strict";
1152
+ init_git();
1153
+ init_scope();
1154
+ init_checks();
1155
+ }
1156
+ });
1157
+
1158
+ // src/audit/checks/new-module.ts
1159
+ import fg4 from "fast-glob";
1160
+ import fs7 from "fs/promises";
1161
+ import path12 from "path";
1162
+ function escapeRegExp(text2) {
1163
+ return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1164
+ }
1165
+ function isMentioned(combinedDocs, name) {
1166
+ const re = new RegExp(
1167
+ `(^|[^A-Za-z0-9_-])${escapeRegExp(name)}(/|[^A-Za-z0-9_-]|$)`,
1168
+ "im"
1169
+ );
1170
+ return re.test(combinedDocs);
1171
+ }
1172
+ async function checkNewModules(ctx) {
1173
+ const result = emptyResult();
1174
+ if (ctx.docs.length === 0) return result;
1175
+ const combinedDocs = moduleDocumentation(ctx.docs);
1176
+ const primaryDoc = ctx.docs[0].path;
1177
+ const checkedDocs = ctx.docs.map((d) => d.path);
1178
+ const flag = async (dir, sourceFileCount) => {
1179
+ result.advisories.push({
1180
+ resolution: "recheck",
1181
+ type: "new-module",
1182
+ message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in the discovered documentation; review whether it needs documenting`,
1183
+ anchor: { doc: primaryDoc, line: null, excerpt: dir },
1184
+ evidence: {
1185
+ kind: "unmentioned-dir",
1186
+ dir,
1187
+ sourceFileCount,
1188
+ firstCommit: await firstCommitOf(ctx.root, dir),
1189
+ checkedDocs
1190
+ }
1191
+ });
1192
+ };
1193
+ for (const candidate of await moduleCandidates(ctx.root, combinedDocs)) {
1194
+ await flag(candidate.dir, candidate.sourceFileCount);
1195
+ }
1196
+ return result;
1197
+ }
1198
+ function moduleDocumentation(docs) {
1199
+ return docs.map((doc) => doc.content + "\n" + (path12.posix.dirname(doc.path) === "." ? "" : path12.posix.dirname(doc.path) + "/")).join("\n");
1200
+ }
1201
+ async function moduleCandidates(root, combinedDocs) {
1202
+ const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
1203
+ const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
1204
+ const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
1205
+ const listSubdirs = async (dir) => {
1206
+ const entries = await fs7.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1207
+ 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));
1208
+ const ignored = await gitIgnoredPaths(root, dirs);
1209
+ const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
1210
+ return (await auditGlob(root, dir === "." ? "*" : `${fg4.escapePath(dir)}/*`, {
1211
+ ignore,
1212
+ onlyDirectories: true,
1213
+ label: "Module directory discovery",
960
1214
  select: (file) => visible.has(file)
961
- })).map((file) => path10.posix.basename(file));
1215
+ })).map((file) => path12.posix.basename(file));
962
1216
  };
963
1217
  const countSourceFiles = async (dir) => {
964
1218
  const files = sources.filter((file) => file.startsWith(dir + "/"));
965
- let count2 = 0;
1219
+ let count3 = 0;
966
1220
  for (let offset = 0; offset < files.length; offset += 256) {
967
- count2 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg3.escapePath(file)), {
1221
+ count3 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg4.escapePath(file)), {
968
1222
  ignore,
969
1223
  followSymbolicLinks: false,
970
1224
  label: `Source discovery in ${dir}`
971
1225
  })).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.`);
1226
+ 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
1227
  }
974
- return count2;
1228
+ return count3;
975
1229
  };
976
1230
  const candidates = [];
977
1231
  for (const topDir of await listSubdirs(".")) {
978
1232
  const topMentioned = isMentioned(combinedDocs, topDir);
979
1233
  if (!topMentioned) {
980
- const count2 = await countSourceFiles(topDir);
981
- if (count2 >= 1) candidates.push({ dir: topDir, sourceFileCount: count2 });
1234
+ const count3 = await countSourceFiles(topDir);
1235
+ if (count3 >= 1) candidates.push({ dir: topDir, sourceFileCount: count3 });
982
1236
  continue;
983
1237
  }
984
1238
  const subdirs = await listSubdirs(topDir);
@@ -986,18 +1240,48 @@ async function moduleCandidates(root, combinedDocs) {
986
1240
  if (mentioned.length < ENUMERATION_THRESHOLD) continue;
987
1241
  for (const sub of subdirs) {
988
1242
  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 });
1243
+ const count3 = await countSourceFiles(`${topDir}/${sub}`);
1244
+ if (count3 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
1245
+ candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count3 });
992
1246
  }
993
1247
  }
994
1248
  }
995
1249
  return candidates;
996
1250
  }
1251
+ var DIR_DENYLIST, SECOND_LEVEL_MIN_SOURCE_FILES, ENUMERATION_THRESHOLD;
1252
+ var init_new_module = __esm({
1253
+ "src/audit/checks/new-module.ts"() {
1254
+ "use strict";
1255
+ init_files();
1256
+ init_inputs();
1257
+ init_git();
1258
+ init_checks();
1259
+ DIR_DENYLIST = /* @__PURE__ */ new Set([
1260
+ "node_modules",
1261
+ "dist",
1262
+ "build",
1263
+ "out",
1264
+ "coverage",
1265
+ "target",
1266
+ "vendor",
1267
+ "__pycache__",
1268
+ "venv",
1269
+ ".venv",
1270
+ ".git",
1271
+ ".gradle",
1272
+ ".mason",
1273
+ ".claude",
1274
+ ".github",
1275
+ ".vscode",
1276
+ ".idea"
1277
+ ]);
1278
+ SECOND_LEVEL_MIN_SOURCE_FILES = 2;
1279
+ ENUMERATION_THRESHOLD = 2;
1280
+ }
1281
+ });
997
1282
 
998
1283
  // src/audit/checks/stale-count.ts
999
- import path11 from "path";
1000
- var MEMBERS_CAP = 50;
1284
+ import path13 from "path";
1001
1285
  async function countGradleModules(root) {
1002
1286
  for (const name of ["settings.gradle.kts", "settings.gradle"]) {
1003
1287
  const content = await readAuditInput(root, name);
@@ -1031,7 +1315,7 @@ async function countNpmWorkspaces(root) {
1031
1315
  return {
1032
1316
  actual: matched.length,
1033
1317
  countedFrom: "package.json workspaces",
1034
- members: matched.map((m) => path11.dirname(m)).sort()
1318
+ members: matched.map((m) => path13.dirname(m)).sort()
1035
1319
  };
1036
1320
  }
1037
1321
  }
@@ -1062,7 +1346,7 @@ async function countNpmWorkspaces(root) {
1062
1346
  return {
1063
1347
  actual: matched.length,
1064
1348
  countedFrom: "pnpm-workspace.yaml",
1065
- members: matched.map((m) => path11.dirname(m)).sort()
1349
+ members: matched.map((m) => path13.dirname(m)).sort()
1066
1350
  };
1067
1351
  }
1068
1352
  }
@@ -1084,8 +1368,8 @@ async function countCargoCrates(root) {
1084
1368
  ignore: ["**/target/**"],
1085
1369
  label: "Cargo workspace discovery"
1086
1370
  });
1087
- for (const m of matched) members.add(path11.dirname(m));
1088
- } else if (await readAuditInput(root, path11.posix.join(entry, "Cargo.toml")) !== null) {
1371
+ for (const m of matched) members.add(path13.dirname(m));
1372
+ } else if (await readAuditInput(root, path13.posix.join(entry, "Cargo.toml")) !== null) {
1089
1373
  members.add(entry);
1090
1374
  }
1091
1375
  }
@@ -1103,11 +1387,21 @@ async function resolveCountSource(root, claim) {
1103
1387
  if (unit === "crate") return countCargoCrates(root);
1104
1388
  return await countNpmWorkspaces(root) ?? await countCargoCrates(root) ?? await countGradleModules(root);
1105
1389
  }
1390
+ async function resolveDocCountSource(root, doc, claim) {
1391
+ const scope = documentScope(doc);
1392
+ const source = await resolveCountSource(path13.join(root, scope), claim);
1393
+ if (!source || scope === ".") return source;
1394
+ return {
1395
+ ...source,
1396
+ countedFrom: scope + "/" + source.countedFrom,
1397
+ members: source.members.map((member) => scope + "/" + member)
1398
+ };
1399
+ }
1106
1400
  async function checkStaleCounts(ctx) {
1107
1401
  const result = emptyResult();
1108
1402
  for (const doc of ctx.docs) {
1109
1403
  for (const claim of doc.claims.counts) {
1110
- const source = await resolveCountSource(ctx.root, claim);
1404
+ const source = await resolveDocCountSource(ctx.root, doc.path, claim);
1111
1405
  if (source === null) {
1112
1406
  result.skipped.push({
1113
1407
  check: "stale-count",
@@ -1135,64 +1429,106 @@ async function checkStaleCounts(ctx) {
1135
1429
  }
1136
1430
  return result;
1137
1431
  }
1432
+ var MEMBERS_CAP;
1433
+ var init_stale_count = __esm({
1434
+ "src/audit/checks/stale-count.ts"() {
1435
+ "use strict";
1436
+ init_docs();
1437
+ init_inputs();
1438
+ init_checks();
1439
+ MEMBERS_CAP = 50;
1440
+ }
1441
+ });
1138
1442
 
1139
1443
  // 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;
1444
+ import path14 from "path";
1445
+ function scriptsOf(raw) {
1144
1446
  try {
1145
1447
  const pkg = JSON.parse(raw);
1146
- return pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? Object.keys(pkg.scripts) : [];
1448
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return null;
1449
+ if (pkg.scripts === void 0) return [];
1450
+ 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
1451
  } catch {
1148
1452
  return null;
1149
1453
  }
1150
1454
  }
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;
1455
+ async function commandInputs(root, docs) {
1456
+ const contents = /* @__PURE__ */ new Map();
1457
+ const read = async (file) => {
1458
+ if (!contents.has(file)) contents.set(file, await readAuditInput(root, file));
1459
+ return contents.get(file);
1178
1460
  };
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)
1461
+ await read("package.json");
1462
+ let inventory;
1463
+ const resolutions = [];
1464
+ for (const doc of docs) for (const claim of doc.claims.commands) {
1465
+ const base = documentScope(doc.path);
1466
+ const directory = scopedPath(base, claim.directory ?? ".");
1467
+ const scope = {
1468
+ basis: claim.directory === void 0 ? "document" : "explicit",
1469
+ directory: directory ?? base,
1470
+ candidates: []
1471
+ };
1472
+ const resolution = { doc: doc.path, claim, scope, manifest: null, scripts: [], elsewhere: [] };
1473
+ resolutions.push(resolution);
1474
+ if (claim.scopeUnknown || directory === null) {
1475
+ resolution.problem = claim.scopeUnknown ?? "The command's directory leaves the repository.";
1476
+ continue;
1477
+ }
1478
+ let cursor = directory;
1479
+ while (true) {
1480
+ const manifest = path14.posix.join(cursor, "package.json");
1481
+ scope.candidates.push(manifest);
1482
+ const raw = await read(manifest);
1483
+ if (raw !== null) {
1484
+ resolution.manifest = manifest;
1485
+ const scripts = scriptsOf(raw);
1486
+ if (scripts === null) resolution.problem = "Invalid package manifest: " + manifest;
1487
+ else resolution.scripts = scripts;
1488
+ break;
1194
1489
  }
1195
- });
1490
+ if (claim.directory !== void 0 || cursor === ".") break;
1491
+ cursor = path14.posix.dirname(cursor);
1492
+ }
1493
+ if (!resolution.manifest && !resolution.problem) {
1494
+ resolution.problem = `No package.json for the command's scope (${directory}).`;
1495
+ }
1496
+ if (resolution.problem || resolution.scripts.includes(claim.scriptName) || claim.directory !== void 0) continue;
1497
+ inventory ??= await commandManifests(root);
1498
+ for (const manifest of [.../* @__PURE__ */ new Set(["package.json", ...inventory])].sort()) {
1499
+ if (scope.candidates.includes(manifest)) continue;
1500
+ scope.candidates.push(manifest);
1501
+ const raw = await read(manifest);
1502
+ if (raw === null) continue;
1503
+ const scripts = scriptsOf(raw);
1504
+ if (scripts === null) resolution.problem = "Cannot establish script availability: invalid manifest " + manifest;
1505
+ else if (scripts.includes(claim.scriptName)) resolution.elsewhere.push(manifest);
1506
+ }
1507
+ }
1508
+ return { manifests: [...contents].sort(([a], [b]) => a.localeCompare(b)), resolutions };
1509
+ }
1510
+ async function checkDeadCommands(ctx) {
1511
+ const result = emptyResult();
1512
+ const { resolutions } = await commandInputs(ctx.root, ctx.docs);
1513
+ for (const { doc, claim, scope, scripts, elsewhere, problem, manifest } of resolutions) {
1514
+ if (problem) {
1515
+ result.skipped.push({ check: "dead-command", doc, reason: problem });
1516
+ continue;
1517
+ }
1518
+ if (scripts.includes(claim.scriptName)) continue;
1519
+ const evidence = {
1520
+ kind: "missing-script",
1521
+ scriptName: claim.scriptName,
1522
+ invocation: claim.invocation,
1523
+ manifestsChecked: scope.candidates,
1524
+ availableScripts: scripts.slice(0, AVAILABLE_SCRIPTS_CAP),
1525
+ scope
1526
+ };
1527
+ const anchor = { doc, line: claim.line, excerpt: claim.excerpt };
1528
+ const message = `\`${claim.invocation}\` names script "${claim.scriptName}", which is absent from ${manifest}`;
1529
+ const certain = documentScope(doc) === "." && (scope.basis === "explicit" || !elsewhere.length);
1530
+ if (certain) result.issues.push({ type: "dead-command", message, anchor, confidence: "certain", evidence });
1531
+ 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
1532
  }
1197
1533
  return result;
1198
1534
  }
@@ -1202,23 +1538,33 @@ function commandManifests(root) {
1202
1538
  label: "Command manifest discovery"
1203
1539
  });
1204
1540
  }
1541
+ var AVAILABLE_SCRIPTS_CAP;
1542
+ var init_dead_command = __esm({
1543
+ "src/audit/checks/dead-command.ts"() {
1544
+ "use strict";
1545
+ init_inputs();
1546
+ init_docs();
1547
+ init_scope();
1548
+ init_checks();
1549
+ AVAILABLE_SCRIPTS_CAP = 30;
1550
+ }
1551
+ });
1205
1552
 
1206
1553
  // src/audit/release-metadata.ts
1207
1554
  import { execFile as execFile7 } from "child_process";
1208
1555
  import { promisify as promisify7 } from "util";
1209
- var exec6 = promisify7(execFile7);
1210
1556
  async function git(root, args) {
1211
1557
  return (await exec6("git", args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
1212
1558
  }
1213
1559
  function withoutAndroidReleaseValues(text2) {
1214
1560
  if (/\/\*|"""|'''/.test(text2) || !/id\s*\(?\s*["']com\.android\.(application|library)["']/.test(text2)) return null;
1215
- const scopes = [];
1561
+ const scopes2 = [];
1216
1562
  const normalized = [];
1217
1563
  let assignments = 0;
1218
1564
  for (const line of text2.split("\n")) {
1219
1565
  const code = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/.*$/g, (token) => token.startsWith("//") ? "" : '""');
1220
1566
  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]))) {
1567
+ if (assignment && scopes2.join("/") === "android/defaultConfig" && (assignment[2] === "versionCode" ? /^\d+$/.test(assignment[4]) : /^["']/.test(assignment[4]))) {
1222
1568
  normalized.push(assignment[1] + assignment[2] + assignment[3] + "<release-value>" + assignment[5]);
1223
1569
  assignments++;
1224
1570
  } else {
@@ -1227,12 +1573,12 @@ function withoutAndroidReleaseValues(text2) {
1227
1573
  }
1228
1574
  const named = code.match(/^\s*(android|defaultConfig)\s*\{\s*$/)?.[1];
1229
1575
  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();
1576
+ if (brace[0] === "{") scopes2.push(named ?? "unknown");
1577
+ else if (!scopes2.length) return null;
1578
+ else scopes2.pop();
1233
1579
  }
1234
1580
  }
1235
- return assignments && !scopes.length ? normalized.join("\n") : null;
1581
+ return assignments && !scopes2.length ? normalized.join("\n") : null;
1236
1582
  }
1237
1583
  async function releaseMetadataOnly(root, commit) {
1238
1584
  if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file))) return false;
@@ -1254,23 +1600,21 @@ async function releaseMetadataOnly(root, commit) {
1254
1600
  return false;
1255
1601
  }
1256
1602
  }
1603
+ var exec6;
1604
+ var init_release_metadata = __esm({
1605
+ "src/audit/release-metadata.ts"() {
1606
+ "use strict";
1607
+ exec6 = promisify7(execFile7);
1608
+ }
1609
+ });
1257
1610
 
1258
1611
  // 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
- ];
1612
+ function manifestPathspecs(doc) {
1613
+ const scope = documentScope(doc);
1614
+ if (scope === ".") return MANIFEST_PATHSPECS;
1615
+ const escaped = scope.replace(/[\\*?\[\]]/g, "\\$&");
1616
+ return MANIFEST_PATHSPECS.map((spec) => spec.startsWith(":(glob)") ? ":(glob)" + escaped + "/" + spec.slice(":(glob)".length) : ":(literal)" + scope + "/" + spec);
1617
+ }
1274
1618
  async function checkDepsChanged(ctx) {
1275
1619
  const result = emptyResult();
1276
1620
  result.suppressedAdvisories = [];
@@ -1294,7 +1638,7 @@ async function checkDepsChanged(ctx) {
1294
1638
  const range = await commitsTouchingSince(
1295
1639
  ctx.root,
1296
1640
  doc.lastCommit.hash,
1297
- MANIFEST_PATHSPECS
1641
+ manifestPathspecs(doc.path)
1298
1642
  );
1299
1643
  if (range === null) {
1300
1644
  result.skipped.push({
@@ -1329,112 +1673,49 @@ async function checkDepsChanged(ctx) {
1329
1673
  }
1330
1674
  return result;
1331
1675
  }
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";
1676
+ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
1677
+ var init_deps_changed = __esm({
1678
+ "src/audit/checks/deps-changed.ts"() {
1679
+ "use strict";
1680
+ init_docs();
1681
+ init_release_metadata();
1682
+ init_git();
1683
+ init_checks();
1684
+ MANIFEST_COMMITS_CAP = 10;
1685
+ MANIFEST_PATHSPECS = [
1686
+ ":(glob)**/package.json",
1687
+ ":(glob)**/build.gradle.kts",
1688
+ ":(glob)**/build.gradle",
1689
+ "settings.gradle.kts",
1690
+ "settings.gradle",
1691
+ "gradle/libs.versions.toml",
1692
+ ":(glob)**/Cargo.toml",
1693
+ "go.mod",
1694
+ "pyproject.toml",
1695
+ "requirements.txt",
1696
+ "Gemfile",
1697
+ "composer.json"
1698
+ ];
1699
+ }
1700
+ });
1341
1701
 
1342
1702
  // src/context/lexical.ts
1343
- import path12 from "path";
1703
+ import path15 from "path";
1704
+ var init_lexical = __esm({
1705
+ "src/context/lexical.ts"() {
1706
+ "use strict";
1707
+ }
1708
+ });
1709
+
1710
+ // src/context/trust.ts
1711
+ var init_trust = __esm({
1712
+ "src/context/trust.ts"() {
1713
+ "use strict";
1714
+ }
1715
+ });
1344
1716
 
1345
1717
  // src/decisions/provenance.ts
1346
1718
  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
1719
  function decisionContent(record) {
1439
1720
  return {
1440
1721
  title: record.title,
@@ -1478,20 +1759,111 @@ function decisionProvenance(record, freshness = "unknown") {
1478
1759
  lastReview: review ? { reviewer: review.actor, at: review.at, note: review.note, gitHash: review.refreshedHash } : null
1479
1760
  };
1480
1761
  }
1762
+ var text, decisionSourceSchema, attributionSchema, contentSchema, approvalSchema, statusSchema, reviewEvidenceSchema, eventSchema, legacySchema, currentSchema, decisionSchema;
1763
+ var init_provenance = __esm({
1764
+ "src/decisions/provenance.ts"() {
1765
+ "use strict";
1766
+ init_paths();
1767
+ init_trust();
1768
+ text = (max) => z2.string().trim().min(1).max(max);
1769
+ decisionSourceSchema = z2.object({
1770
+ kind: z2.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
1771
+ reference: text(1e3),
1772
+ note: text(500).optional()
1773
+ }).strict();
1774
+ attributionSchema = z2.object({
1775
+ owner: text(200).nullable().optional(),
1776
+ sources: z2.array(decisionSourceSchema).max(20).optional(),
1777
+ actor: text(200).optional()
1778
+ });
1779
+ contentSchema = z2.object({
1780
+ title: z2.string().min(1),
1781
+ body: z2.string().min(1),
1782
+ category: z2.enum(["decision", "gotcha", "deprecation", "convention"]),
1783
+ files: z2.array(z2.string().refine((f) => normalizeRepoPath(f) !== null)),
1784
+ owner: text(200).optional(),
1785
+ sources: z2.array(decisionSourceSchema).max(20)
1786
+ });
1787
+ approvalSchema = z2.enum(["unreviewed", "proposed", "accepted"]);
1788
+ statusSchema = z2.enum(["active", "superseded", "retired"]);
1789
+ reviewEvidenceSchema = z2.object({
1790
+ baseHash: z2.string(),
1791
+ headHash: z2.string(),
1792
+ historyAvailable: z2.boolean(),
1793
+ changedFiles: z2.array(z2.string()),
1794
+ localChanges: z2.array(z2.string())
1795
+ });
1796
+ eventSchema = z2.object({
1797
+ kind: z2.enum(["imported", "created", "revised", "accepted", "reaffirmed", "retired", "superseded"]),
1798
+ at: z2.string().datetime(),
1799
+ actor: text(200).optional(),
1800
+ note: text(1500).optional(),
1801
+ revision: z2.number().int().positive(),
1802
+ content: contentSchema,
1803
+ approval: approvalSchema,
1804
+ status: statusSchema,
1805
+ refreshedHash: z2.string(),
1806
+ evidence: reviewEvidenceSchema.optional()
1807
+ });
1808
+ legacySchema = z2.object({
1809
+ version: z2.literal(1),
1810
+ id: z2.string().regex(/^[a-zA-Z0-9_-]+$/),
1811
+ title: z2.string().min(1),
1812
+ body: z2.string().min(1),
1813
+ category: contentSchema.shape.category,
1814
+ files: contentSchema.shape.files,
1815
+ createdAt: z2.string(),
1816
+ updatedAt: z2.string(),
1817
+ refreshedHash: z2.string(),
1818
+ status: z2.enum(["active", "superseded"]),
1819
+ supersededBy: z2.string().optional()
1820
+ }).passthrough();
1821
+ currentSchema = legacySchema.extend({
1822
+ version: z2.literal(2),
1823
+ status: statusSchema,
1824
+ approval: approvalSchema,
1825
+ revision: z2.number().int().positive(),
1826
+ owner: text(200).optional(),
1827
+ sources: z2.array(decisionSourceSchema).max(20),
1828
+ history: z2.array(eventSchema).min(1)
1829
+ }).superRefine((record, ctx) => {
1830
+ const invalid = (message) => ctx.addIssue({ code: "custom", message });
1831
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
1832
+ let previous;
1833
+ for (const event of record.history) {
1834
+ if (!previous) {
1835
+ if (!["created", "imported"].includes(event.kind) || event.revision !== 1) invalid("History must begin with creation or legacy import at revision 1");
1836
+ if (event.approval !== (event.kind === "created" ? "proposed" : "unreviewed")) invalid("Initial records cannot claim acceptance");
1837
+ } else {
1838
+ if (["created", "imported"].includes(event.kind)) invalid("History cannot restart");
1839
+ if (previous.status !== "active") invalid("Archived decisions cannot be changed");
1840
+ if (event.revision !== previous.revision + (event.kind === "revised" ? 1 : 0)) invalid("Invalid revision sequence");
1841
+ if (event.kind !== "revised" && !same(event.content, previous.content)) invalid("A review cannot silently revise decision content");
1842
+ if (event.kind === "reaffirmed" && previous.approval !== "accepted") invalid("Only accepted decisions can be reaffirmed");
1843
+ if (event.kind === "accepted" && previous.approval === "accepted") invalid("Use reaffirmation for an accepted decision");
1844
+ const approval = event.kind === "revised" ? "proposed" : ["accepted", "reaffirmed"].includes(event.kind) ? "accepted" : previous.approval;
1845
+ if (event.approval !== approval) invalid("Approval disagrees with review history");
1846
+ if (!["accepted", "reaffirmed"].includes(event.kind) && event.refreshedHash !== previous.refreshedHash) invalid("Only a review can refresh the evidence baseline");
1847
+ }
1848
+ if (event.kind !== "imported" && event.status !== (event.kind === "retired" ? "retired" : event.kind === "superseded" ? "superseded" : "active")) invalid("Lifecycle disagrees with history");
1849
+ if (["accepted", "reaffirmed", "retired"].includes(event.kind) && (!event.actor || !event.note || !event.evidence)) invalid("Reviews require a named reviewer, reason, and code evidence");
1850
+ if (["accepted", "reaffirmed"].includes(event.kind)) {
1851
+ if (!event.content.owner || !event.content.sources.length) invalid("Accepted decisions require an owner and source");
1852
+ 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");
1853
+ }
1854
+ previous = event;
1855
+ }
1856
+ 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");
1857
+ });
1858
+ decisionSchema = z2.union([legacySchema, currentSchema]);
1859
+ }
1860
+ });
1481
1861
 
1482
1862
  // 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
- );
1863
+ import fs8 from "fs/promises";
1864
+ import path16 from "path";
1865
+ import { createHash } from "crypto";
1866
+ import { z as z3 } from "zod";
1495
1867
  async function loadDecisionStore(rootDir) {
1496
1868
  const records = [];
1497
1869
  const diagnostics = [];
@@ -1515,10 +1887,34 @@ async function loadDecisionStore(rootDir) {
1515
1887
  }
1516
1888
  return { records, diagnostics };
1517
1889
  }
1890
+ var TITLE_MAX_CHARS, BODY_RECOMMENDED_CHARS, BODY_MAX_CHARS, boundedText, decisionTitleSchema, decisionBodySchema;
1891
+ var init_decisions = __esm({
1892
+ "src/decisions/decisions.ts"() {
1893
+ "use strict";
1894
+ init_storage();
1895
+ init_paths();
1896
+ init_snapshot();
1897
+ init_lexical();
1898
+ init_provenance();
1899
+ TITLE_MAX_CHARS = 80;
1900
+ BODY_RECOMMENDED_CHARS = 1500;
1901
+ BODY_MAX_CHARS = 2500;
1902
+ boundedText = (field, max, hint) => z3.string({
1903
+ 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 })
1904
+ }).trim().min(1, `${field} must be non-empty`).max(max);
1905
+ decisionTitleSchema = boundedText("title", TITLE_MAX_CHARS, "Tighten it to a specific headline.");
1906
+ decisionBodySchema = boundedText(
1907
+ "body",
1908
+ BODY_MAX_CHARS,
1909
+ `Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer. Preserve the rule, reason, and exceptions; put supporting references in sources.`
1910
+ );
1911
+ }
1912
+ });
1518
1913
 
1519
1914
  // src/decisions/drift.ts
1915
+ import path17 from "path";
1520
1916
  async function computeDecisionDrift(rootDir, decisions) {
1521
- const resolvedRoot = path14.resolve(rootDir);
1917
+ const resolvedRoot = path17.resolve(rootDir);
1522
1918
  const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
1523
1919
  const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
1524
1920
  const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
@@ -1546,6 +1942,16 @@ async function computeDecisionDrift(rootDir, decisions) {
1546
1942
  }
1547
1943
  return report;
1548
1944
  }
1945
+ var init_drift2 = __esm({
1946
+ "src/decisions/drift.ts"() {
1947
+ "use strict";
1948
+ init_drift();
1949
+ init_paths();
1950
+ init_snapshot();
1951
+ init_decisions();
1952
+ init_provenance();
1953
+ }
1954
+ });
1549
1955
 
1550
1956
  // src/audit/checks/decision-anchor.ts
1551
1957
  async function checkDecisionAnchors(ctx) {
@@ -1589,187 +1995,50 @@ async function checkDecisionAnchors(ctx) {
1589
1995
  }
1590
1996
  return result;
1591
1997
  }
1998
+ var init_decision_anchor = __esm({
1999
+ "src/audit/checks/decision-anchor.ts"() {
2000
+ "use strict";
2001
+ init_drift2();
2002
+ init_decisions();
2003
+ init_provenance();
2004
+ init_checks();
2005
+ }
2006
+ });
1592
2007
 
1593
2008
  // 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
2009
  function emptyResult() {
1603
2010
  return { issues: [], advisories: [], skipped: [] };
1604
2011
  }
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);
2012
+ var CHECKS;
2013
+ var init_checks = __esm({
2014
+ "src/audit/checks/index.ts"() {
2015
+ "use strict";
2016
+ init_deleted_reference();
2017
+ init_new_module();
2018
+ init_stale_count();
2019
+ init_dead_command();
2020
+ init_deps_changed();
2021
+ init_decision_anchor();
2022
+ CHECKS = {
2023
+ "deleted-reference": checkDeletedReferences,
2024
+ "new-module": checkNewModules,
2025
+ "stale-count": checkStaleCounts,
2026
+ "dead-command": checkDeadCommands,
2027
+ "deps-changed": checkDepsChanged,
2028
+ "decision-anchor-drift": checkDecisionAnchors
2029
+ };
1661
2030
  }
1662
- report.clean = report.issues.length === 0;
1663
- return report;
1664
- }
2031
+ });
1665
2032
 
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";
2033
+ // src/audit/findings.ts
2034
+ import { createHash as createHash2 } from "crypto";
1670
2035
  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
2036
  function findingId(finding) {
1768
2037
  const e = finding.evidence;
1769
2038
  let key;
1770
2039
  switch (e.kind) {
1771
2040
  case "missing-path":
1772
- key = e.claimed;
2041
+ key = e.resolvedPath && e.resolvedPath !== e.claimed ? [e.claimed, e.resolvedPath] : e.claimed;
1773
2042
  break;
1774
2043
  case "unmentioned-dir":
1775
2044
  key = e.dir;
@@ -1778,7 +2047,7 @@ function findingId(finding) {
1778
2047
  key = [e.unit.replace(/s$/, ""), e.countedFrom];
1779
2048
  break;
1780
2049
  case "missing-script":
1781
- key = e.scriptName;
2050
+ key = e.scope && e.scope.directory !== "." ? [e.scriptName, e.scope.directory] : e.scriptName;
1782
2051
  break;
1783
2052
  case "doc-behind-manifests":
1784
2053
  key = null;
@@ -1789,12 +2058,199 @@ function findingId(finding) {
1789
2058
  }
1790
2059
  return digest([finding.type, finding.anchor.doc, key]);
1791
2060
  }
2061
+ var digest, checkSchema, commitSchema, anchorSchema, count, scopeSchema, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema;
2062
+ var init_findings = __esm({
2063
+ "src/audit/findings.ts"() {
2064
+ "use strict";
2065
+ digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
2066
+ checkSchema = z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
2067
+ commitSchema = z4.object({ hash: z4.string().regex(/^[a-f0-9]{40,64}$/), date: z4.string(), subject: z4.string() });
2068
+ anchorSchema = z4.object({ doc: z4.string(), line: z4.number().int().positive().nullable(), excerpt: z4.string().nullable() });
2069
+ count = z4.number().int().nonnegative();
2070
+ scopeSchema = z4.object({
2071
+ basis: z4.enum(["repository", "document", "document-link", "explicit"]),
2072
+ directory: z4.string(),
2073
+ candidates: z4.array(z4.string())
2074
+ });
2075
+ evidenceSchema = z4.discriminatedUnion("kind", [
2076
+ z4.object({
2077
+ kind: z4.literal("missing-path"),
2078
+ claimed: z4.string(),
2079
+ renamedTo: z4.string().nullable(),
2080
+ deletedInCommit: commitSchema.nullable(),
2081
+ everTracked: z4.boolean(),
2082
+ parentDirExists: z4.boolean(),
2083
+ resolvedPath: z4.string().optional(),
2084
+ scope: scopeSchema.optional()
2085
+ }),
2086
+ z4.object({
2087
+ kind: z4.literal("unmentioned-dir"),
2088
+ dir: z4.string(),
2089
+ sourceFileCount: count,
2090
+ firstCommit: commitSchema.nullable(),
2091
+ checkedDocs: z4.array(z4.string())
2092
+ }),
2093
+ z4.object({
2094
+ kind: z4.literal("count-mismatch"),
2095
+ claimed: count,
2096
+ actual: count,
2097
+ unit: z4.string(),
2098
+ countedFrom: z4.string(),
2099
+ members: z4.array(z4.string())
2100
+ }),
2101
+ z4.object({
2102
+ kind: z4.literal("missing-script"),
2103
+ scriptName: z4.string(),
2104
+ invocation: z4.string(),
2105
+ manifestsChecked: z4.array(z4.string()),
2106
+ availableScripts: z4.array(z4.string()),
2107
+ scope: scopeSchema.optional()
2108
+ }),
2109
+ z4.object({
2110
+ kind: z4.literal("doc-behind-manifests"),
2111
+ docLastCommit: commitSchema,
2112
+ manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
2113
+ totalCommits: count
2114
+ }),
2115
+ z4.object({
2116
+ kind: z4.literal("decision-anchor"),
2117
+ decisionId: z4.string(),
2118
+ title: z4.string(),
2119
+ changedFiles: z4.array(z4.string()),
2120
+ refreshedHash: z4.string(),
2121
+ provenance: z4.object({}).passthrough().optional()
2122
+ })
2123
+ ]);
2124
+ findingSchema = z4.object({ message: z4.string(), anchor: anchorSchema, evidence: evidenceSchema });
2125
+ issueSchema = findingSchema.extend({
2126
+ type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
2127
+ confidence: z4.enum(["certain", "likely"])
2128
+ });
2129
+ advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z4.literal("recheck").optional() });
2130
+ checkResultSchema = z4.object({
2131
+ issues: z4.array(issueSchema),
2132
+ advisories: z4.array(advisorySchema),
2133
+ suppressedAdvisories: z4.array(advisorySchema).optional(),
2134
+ skipped: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
2135
+ });
2136
+ }
2137
+ });
2138
+
2139
+ // src/automation/store.ts
2140
+ import fs9 from "fs/promises";
2141
+ import os from "os";
2142
+ import { z as z5 } from "zod";
2143
+ async function withLock(root, directory, run, waitMs = 5e3) {
2144
+ const file = await storePath(root, directory + "/lock", true);
2145
+ const deadline = Date.now() + waitMs;
2146
+ let handle;
2147
+ while (!handle) {
2148
+ try {
2149
+ handle = await fs9.open(file, "wx", 384);
2150
+ try {
2151
+ await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
2152
+ } catch (error) {
2153
+ await handle.close().catch(() => {
2154
+ });
2155
+ handle = void 0;
2156
+ await fs9.rm(file, { force: true }).catch(() => {
2157
+ });
2158
+ throw error;
2159
+ }
2160
+ } catch (error) {
2161
+ if (error.code !== "EEXIST") throw error;
2162
+ try {
2163
+ const owner = JSON.parse(await fs9.readFile(file, "utf8"));
2164
+ if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
2165
+ try {
2166
+ process.kill(owner.pid, 0);
2167
+ } catch (probe) {
2168
+ if (probe.code === "ESRCH") {
2169
+ const reclaim = file + ".reclaim";
2170
+ let guard;
2171
+ try {
2172
+ guard = await fs9.open(reclaim, "wx", 384);
2173
+ const current = JSON.parse(await fs9.readFile(file, "utf8"));
2174
+ if (current.pid === owner.pid && current.host === owner.host) await fs9.unlink(file);
2175
+ } finally {
2176
+ if (guard) {
2177
+ await guard.close();
2178
+ await fs9.rm(reclaim, { force: true });
2179
+ }
2180
+ }
2181
+ }
2182
+ }
2183
+ }
2184
+ } catch {
2185
+ }
2186
+ if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
2187
+ await new Promise((resolve) => setTimeout(resolve, 40));
2188
+ }
2189
+ }
2190
+ try {
2191
+ return await run();
2192
+ } finally {
2193
+ await handle.close();
2194
+ await fs9.unlink(file);
2195
+ }
2196
+ }
2197
+ var hostSchema, stateSchema;
2198
+ var init_store = __esm({
2199
+ "src/automation/store.ts"() {
2200
+ "use strict";
2201
+ init_storage();
2202
+ hostSchema = z5.enum(["claude", "codex"]);
2203
+ stateSchema = z5.object({
2204
+ version: z5.literal(1),
2205
+ root: z5.string(),
2206
+ gitDir: z5.string(),
2207
+ branch: z5.string(),
2208
+ baselines: z5.array(z5.object({ path: z5.string(), at: z5.string(), event: z5.string(), fingerprint: z5.string() })).max(128),
2209
+ sessions: z5.record(z5.object({
2210
+ host: hostSchema,
2211
+ seen: z5.string().nullable(),
2212
+ continued: z5.boolean(),
2213
+ initialIssues: z5.array(z5.string()),
2214
+ initialDocs: z5.record(z5.string().nullable()),
2215
+ lastUsed: z5.string(),
2216
+ mutationObserved: z5.boolean(),
2217
+ pending: z5.record(z5.string()),
2218
+ coverageGaps: z5.array(z5.string()),
2219
+ events: z5.record(z5.object({ at: z5.string(), count: z5.number().int().positive() }))
2220
+ })),
2221
+ updatedAt: z5.string(),
2222
+ fingerprint: z5.string().nullable(),
2223
+ latest: z5.string().nullable(),
2224
+ analysis: z5.object({ id: z5.string(), fingerprint: z5.string(), completedAt: z5.number(), path: z5.string() }).optional()
2225
+ });
2226
+ }
2227
+ });
2228
+
2229
+ // src/audit/repair.ts
2230
+ var repair_exports = {};
2231
+ __export(repair_exports, {
2232
+ checkResultSchema: () => checkResultSchema,
2233
+ findingId: () => findingId,
2234
+ formatRepairSummary: () => formatRepairSummary,
2235
+ loadRepairBaseline: () => loadRepairBaseline,
2236
+ prepareRepair: () => prepareRepair,
2237
+ repairExitCode: () => repairExitCode,
2238
+ repairFindingSchema: () => repairFindingSchema,
2239
+ verifyRepair: () => verifyRepair
2240
+ });
2241
+ import fs10 from "fs/promises";
2242
+ import path18 from "path";
2243
+ import { randomUUID as randomUUID2 } from "crypto";
2244
+ import { z as z6 } from "zod";
2245
+ function recheckable(finding) {
2246
+ 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);
2247
+ }
1792
2248
  function allFindings(report) {
1793
2249
  return [...report.issues, ...report.advisories, ...report.suppressedAdvisories ?? []];
1794
2250
  }
1795
2251
  async function docState(root) {
1796
2252
  const docs = [];
1797
- for (const doc of DOC_CANDIDATES) {
2253
+ for (const doc of await discoverDocPaths(root)) {
1798
2254
  try {
1799
2255
  const content = await readBoundedFile(await storePath(root, doc), 10 * 1024 * 1024);
1800
2256
  if (content === null) throw new Error("Context file is not regular or exceeds 10 MiB: " + doc);
@@ -1817,7 +2273,7 @@ async function stableAudit(root, checks, options = {}) {
1817
2273
  }
1818
2274
  async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
1819
2275
  const root = await fs10.realpath(rootDir);
1820
- const selected = z4.array(checkSchema).nonempty().parse(checks);
2276
+ const selected = z6.array(checkSchema).nonempty().parse(checks);
1821
2277
  const report = await stableAudit(root, selected, options);
1822
2278
  if (!report) throw new Error("No context files found to prepare a repair.");
1823
2279
  if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
@@ -1832,23 +2288,29 @@ async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
1832
2288
  await writeStoreJson(root, baselinePath, { ...payload, digest: digest(payload) });
1833
2289
  return { version: 1, action: "prepare", baselinePath, report };
1834
2290
  }
1835
- async function verifyRepair(rootDir, baselinePath, options = {}) {
2291
+ async function loadRepairBaseline(rootDir, baselinePath) {
1836
2292
  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;
2293
+ const declaredRoot = path18.resolve(rootDir);
2294
+ const relative = path18.isAbsolute(baselinePath) ? path18.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
1839
2295
  const stored = baselineSchema.parse(await readStoreJson(root, relative));
1840
2296
  const { digest: savedDigest, ...payload } = stored;
1841
2297
  if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
1842
2298
  if (stored.report.root !== root) throw new Error("Repair baseline belongs to a different repository.");
1843
2299
  const original = stored.report;
2300
+ return { root, relative, original };
2301
+ }
2302
+ async function verifyRepair(rootDir, baselinePath, options = {}) {
2303
+ const { root, relative, original } = await loadRepairBaseline(rootDir, baselinePath);
1844
2304
  const diagnostics = [];
2305
+ let reviewDigest;
1845
2306
  let current = null;
1846
2307
  try {
2308
+ reviewDigest = digest(await advisoryReviewInventory(root));
1847
2309
  current = await stableAudit(root, original.checksRun, options);
1848
2310
  if (!current) diagnostics.push("No context files remain available to audit.");
1849
2311
  else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
1850
2312
  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;
2313
+ if (!allFindings(original).some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
1852
2314
  const content = await readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024);
1853
2315
  if (content === null || !content.trim()) {
1854
2316
  diagnostics.push("Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair.");
@@ -1865,21 +2327,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
1865
2327
  const originalIds = new Set(originalFindings.map(findingId));
1866
2328
  const missingDocs = original.docs.filter((doc) => !current?.docs.some((d) => d.path === doc.path));
1867
2329
  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) => {
2330
+ const findings = await Promise.all(originalFindings.map(async (finding) => {
1869
2331
  const id = findingId(finding);
1870
2332
  const now = currentById.get(id);
1871
2333
  const base = { id, original: finding, ...now ? { current: now } : {} };
1872
2334
  if (diagnostics.length || !current) {
1873
2335
  return { ...base, status: "unverified", reason: "The original audit scope could not be verified. See diagnostics." };
1874
2336
  }
1875
- if ("confidence" in finding && now) {
2337
+ if (now && ("confidence" in finding || "confidence" in now)) {
1876
2338
  return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
1877
2339
  }
1878
2340
  const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
1879
2341
  if (!current.checksRun?.includes(finding.type) || skipped.length) {
1880
2342
  return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
1881
2343
  }
1882
- if (!("confidence" in finding)) {
2344
+ const review = await assessAdvisory(root, now ?? finding, original.headHash);
2345
+ if (review) {
2346
+ if (review.status === "unverified") return { ...base, review, status: "unverified", reason: review.reason };
2347
+ if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
2348
+ return { ...base, review, status: "review-required", reason: review.reason };
2349
+ }
2350
+ if (!("confidence" in finding) && (!recheckable(finding) || now)) {
1883
2351
  return {
1884
2352
  ...base,
1885
2353
  status: "review-required",
@@ -1887,11 +2355,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
1887
2355
  };
1888
2356
  }
1889
2357
  return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
1890
- });
2358
+ }));
2359
+ try {
2360
+ if (reviewDigest !== digest(await advisoryReviewInventory(root)) || current?.headHash !== await getCurrentGitHash(root)) {
2361
+ diagnostics.push("HEAD or advisory review records changed during verification; repeat against stable evidence.");
2362
+ for (const finding of findings) {
2363
+ finding.status = "unverified";
2364
+ finding.reason = diagnostics.at(-1);
2365
+ }
2366
+ }
2367
+ } catch (error) {
2368
+ diagnostics.push(String(error));
2369
+ for (const finding of findings) {
2370
+ finding.status = "unverified";
2371
+ finding.reason = "Advisory review evidence became unavailable during verification.";
2372
+ }
2373
+ }
1891
2374
  const newFindings = [...currentById].filter(([id]) => !originalIds.has(id)).map(([, f]) => f);
2375
+ const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
1892
2376
  const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
1893
2377
  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));
2378
+ 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
2379
  const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
1896
2380
  return {
1897
2381
  version: 1,
@@ -1905,7 +2389,7 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
1905
2389
  diagnostics,
1906
2390
  currentAudit: current,
1907
2391
  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."
2392
+ 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
2393
  };
1910
2394
  }
1911
2395
  function repairExitCode(report) {
@@ -1914,18 +2398,482 @@ function repairExitCode(report) {
1914
2398
  function formatRepairSummary(report) {
1915
2399
  return [
1916
2400
  "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),
2401
+ ...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + f.original.message + "\n " + f.reason + "\n findingId: " + f.id),
1918
2402
  ...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
1919
2403
  ...report.diagnostics.map((d) => " [unverified] " + d),
1920
2404
  ...(report.currentAudit?.skippedChecks ?? []).map((s) => " [skipped] " + s.check + ": " + s.reason),
1921
2405
  report.scope
1922
2406
  ].join("\n");
1923
2407
  }
2408
+ var count2, reportSchema, baselineSchema, repairFindingSchema;
2409
+ var init_repair = __esm({
2410
+ "src/audit/repair.ts"() {
2411
+ "use strict";
2412
+ init_audit();
2413
+ init_docs();
2414
+ init_snapshot();
2415
+ init_drift();
2416
+ init_storage();
2417
+ init_files();
2418
+ init_paths();
2419
+ init_types();
2420
+ init_findings();
2421
+ init_advisory_review();
2422
+ init_findings();
2423
+ count2 = z6.number().int().nonnegative();
2424
+ reportSchema = z6.object({
2425
+ version: z6.literal(1),
2426
+ root: z6.string(),
2427
+ gitAvailable: z6.literal(true),
2428
+ headHash: commitSchema.shape.hash,
2429
+ checksRun: z6.array(checkSchema).nonempty(),
2430
+ docs: z6.array(z6.object({
2431
+ path: z6.string().refine((value) => normalizeRepoPath(value) === value),
2432
+ scope: z6.string().optional(),
2433
+ kind: z6.enum(["instructions", "readme"]).optional(),
2434
+ lastCommit: commitSchema.nullable(),
2435
+ dirty: z6.boolean(),
2436
+ lineCount: count2
2437
+ })).nonempty(),
2438
+ decisionsChecked: z6.boolean(),
2439
+ clean: z6.boolean(),
2440
+ issues: z6.array(issueSchema),
2441
+ advisories: z6.array(advisorySchema),
2442
+ suppressedAdvisories: z6.array(advisorySchema).optional(),
2443
+ skippedChecks: z6.array(z6.object({ check: z6.string(), reason: z6.string(), doc: z6.string().optional() }))
2444
+ });
2445
+ baselineSchema = z6.object({
2446
+ kind: z6.literal("mason-audit-repair"),
2447
+ version: z6.literal(1),
2448
+ createdAt: z6.string().datetime(),
2449
+ report: reportSchema,
2450
+ digest: z6.string().regex(/^[a-f0-9]{64}$/)
2451
+ });
2452
+ repairFindingSchema = z6.object({
2453
+ id: z6.string(),
2454
+ original: z6.union([issueSchema, advisorySchema]),
2455
+ status: z6.enum(["resolved", "unresolved", "review-required", "unverified"]),
2456
+ reason: z6.string(),
2457
+ current: z6.union([issueSchema, advisorySchema]).optional(),
2458
+ review: reviewSummarySchema.optional()
2459
+ });
2460
+ }
2461
+ });
2462
+
2463
+ // src/audit/advisory-review.ts
2464
+ import fs11 from "fs/promises";
2465
+ import { execFile as execFile8 } from "child_process";
2466
+ import { promisify as promisify8 } from "util";
2467
+ import { z as z7 } from "zod";
2468
+ async function readRecord(root, id) {
2469
+ const raw = await readStoreJson(root, recordPath(id));
2470
+ if (raw === null) return null;
2471
+ const record = recordSchema.parse(raw);
2472
+ const { digest: stored, ...payload } = record;
2473
+ 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]))) {
2474
+ throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
2475
+ }
2476
+ return record;
2477
+ }
2478
+ function scopes(finding) {
2479
+ const files = [finding.anchor.doc], e = finding.evidence;
2480
+ if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
2481
+ if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
2482
+ if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
2483
+ else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
2484
+ else if (e.kind === "unmentioned-dir") files.push(e.dir, ...e.checkedDocs);
2485
+ else throw new Error("This advisory has no supported review scope.");
2486
+ return [...new Set(files)].map((file) => {
2487
+ if (normalizeRepoPath(file) !== file) throw new Error("Advisory contains an invalid scoped path: " + file);
2488
+ return `:(literal)${file}`;
2489
+ }).sort();
2490
+ }
2491
+ async function reviewEvidence(root, finding) {
2492
+ if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
2493
+ const scope = scopes(finding);
2494
+ const head = (await git2(root, "rev-parse", "HEAD")).trim();
2495
+ const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
2496
+ if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
2497
+ const [tree, status, lastScopeCommit, document] = await Promise.all([
2498
+ tracked.length ? git2(root, "ls-tree", "-r", "-z", "HEAD", "--", ...tracked.map((file) => `:(literal)${file}`)) : "",
2499
+ git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...scope),
2500
+ git2(root, "log", "-1", "--format=%H", "HEAD", "--", ...scope).then((value) => value.trim()),
2501
+ readAuditInput(root, finding.anchor.doc)
2502
+ ]);
2503
+ if (!document?.trim()) throw new Error("Original advisory document is missing, empty or unreadable.");
2504
+ 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.");
2505
+ for (const entry of tree.split("\0").filter(Boolean)) {
2506
+ const tab = entry.indexOf(" "), file = entry.slice(tab + 1);
2507
+ if (entry.startsWith("120000 ") || entry.startsWith("160000 ")) throw new Error("Symlink or submodule in advisory review scope: " + file);
2508
+ await storePath(root, file);
2509
+ }
2510
+ const paths = finding.evidence.kind === "missing-path" ? await Promise.all(
2511
+ (finding.evidence.scope?.candidates ?? [finding.evidence.resolvedPath ?? finding.evidence.claimed]).map(async (file) => ({ path: file, exists: await pathExists(root, file) }))
2512
+ ) : [];
2513
+ let dirty = status.length > 0;
2514
+ if (finding.evidence.kind === "missing-script") {
2515
+ for (const file of [...finding.evidence.manifestsChecked, ...finding.evidence.scope?.candidates ?? []]) {
2516
+ if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
2517
+ }
2518
+ }
2519
+ const fingerprint = digest([scope, tree, lastScopeCommit, paths]);
2520
+ if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
2521
+ return {
2522
+ ...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
2523
+ dirty,
2524
+ documentDigest: digest(document)
2525
+ };
2526
+ }
2527
+ async function decisionAssessment(root, finding, baselineHead) {
2528
+ const evidence = finding.evidence;
2529
+ if (evidence.kind !== "decision-anchor") return null;
2530
+ const id = findingId(finding);
2531
+ const store = await loadDecisionStore(root);
2532
+ if (store.diagnostics.length) throw new Error("Decision records have unavailable or malformed evidence; review cannot be confirmed.");
2533
+ const record = store.records.find((record2) => record2.id === evidence.decisionId);
2534
+ if (!record || record.version !== 2) return null;
2535
+ const events = record.history.filter((event2) => ["accepted", "reaffirmed", "retired"].includes(event2.kind));
2536
+ const event = events.at(-1);
2537
+ if (!event?.evidence || !event.actor || !event.note || !event.evidence.historyAvailable || event.evidence.localChanges.length) return null;
2538
+ try {
2539
+ await git2(root, "merge-base", "--is-ancestor", baselineHead, event.evidence.headHash);
2540
+ } catch {
2541
+ return null;
2542
+ }
2543
+ const drift = await computeDecisionDrift(root, [record]);
2544
+ if (!drift.historyAvailable) throw new Error("Decision review history is unavailable.");
2545
+ const retired = record.status === "retired" && event.kind === "retired";
2546
+ const current = retired || record.status === "active" && record.approval === "accepted" && drift.freshness?.[record.id] === "current" && !drift.pendingProposals?.[record.id];
2547
+ return {
2548
+ id,
2549
+ status: current ? "current" : "reopened",
2550
+ outcome: event.kind,
2551
+ reviewer: event.actor,
2552
+ note: event.note,
2553
+ reviewedHead: event.evidence.headHash,
2554
+ recordPath: `.mason/decisions/${record.id}.json`,
2555
+ 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."
2556
+ };
2557
+ }
2558
+ async function assessAdvisory(root, finding, baselineHead) {
2559
+ if ("confidence" in finding) return null;
2560
+ const id = findingId(finding);
2561
+ try {
2562
+ if (finding.evidence.kind === "decision-anchor") return await decisionAssessment(root, finding, baselineHead);
2563
+ const record = await readRecord(root, id);
2564
+ if (!record) return null;
2565
+ const event = record.events.at(-1);
2566
+ const current = await reviewEvidence(root, event.finding);
2567
+ await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
2568
+ const base = {
2569
+ id,
2570
+ recordPath: recordPath(id),
2571
+ reviewer: event.reviewer,
2572
+ note: event.note,
2573
+ outcome: event.outcome,
2574
+ reviewedHead: event.evidence.head
2575
+ };
2576
+ 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." };
2577
+ 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." };
2578
+ } catch (error) {
2579
+ return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
2580
+ }
2581
+ }
2582
+ async function advisoryReviewInventory(root) {
2583
+ let names;
2584
+ try {
2585
+ names = await fs11.readdir(await storePath(root, ".mason/reviews/advisories"));
2586
+ } catch (error) {
2587
+ if (error.code === "ENOENT") return [];
2588
+ throw error;
2589
+ }
2590
+ if (names.length > 5e3) throw new Error("Advisory review inventory exceeds 5,000 records.");
2591
+ return Promise.all(names.filter((name) => name.endsWith(".json")).sort().map(async (name) => [name, await readAuditInput(root, ".mason/reviews/advisories/" + name)]));
2592
+ }
2593
+ async function reviewAdvisory(rootDir, input) {
2594
+ const root = await fs11.realpath(rootDir), request = advisoryReviewRequest.parse(input);
2595
+ const { loadRepairBaseline: loadRepairBaseline2 } = await Promise.resolve().then(() => (init_repair(), repair_exports));
2596
+ const baseline = await loadRepairBaseline2(root, request.baselinePath);
2597
+ const finding = [...baseline.original.advisories, ...baseline.original.suppressedAdvisories ?? []].find((f) => findingId(f) === request.findingId);
2598
+ if (!finding) throw new Error("No advisory with this findingId in the original baseline. Provable issues must be repaired and rechecked.");
2599
+ if (finding.evidence.kind === "decision-anchor") return {
2600
+ status: "decision-review-required",
2601
+ decisionId: finding.evidence.decisionId,
2602
+ 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."
2603
+ };
2604
+ const prepare = async () => {
2605
+ const record = await readRecord(root, request.findingId);
2606
+ const evidence = await reviewEvidence(root, finding);
2607
+ await git2(root, "merge-base", "--is-ancestor", baseline.original.headHash, evidence.head);
2608
+ const token = digest({ baseline: baseline.original, finding, evidence, record });
2609
+ return { record, evidence, token };
2610
+ };
2611
+ if (request.action === "prepare") {
2612
+ const state = await prepare();
2613
+ let diff = null, diffNote;
2614
+ const from = finding.evidence.kind === "doc-behind-manifests" ? finding.evidence.docLastCommit.hash : baseline.original.headHash;
2615
+ try {
2616
+ const full = await git2(root, "diff", "--no-ext-diff", "--no-textconv", "--no-color", "--no-renames", "-U3", from, state.evidence.head, "--", ...state.evidence.scope);
2617
+ diff = full.slice(0, 16e3);
2618
+ if (full.length > diff.length) diffNote = "Diff preview truncated; inspect the full scoped diff before reviewing.";
2619
+ } catch {
2620
+ diffNote = "Diff preview unavailable; inspect the original cited evidence directly before reviewing.";
2621
+ }
2622
+ if ((await prepare()).token !== state.token) throw new Error("Review evidence changed during preparation; retry.");
2623
+ return {
2624
+ status: "prepared",
2625
+ findingId: request.findingId,
2626
+ finding,
2627
+ evidence: state.evidence,
2628
+ history: state.record?.events ?? [],
2629
+ reviewToken: state.token,
2630
+ diff,
2631
+ diffNote,
2632
+ 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."
2633
+ };
2634
+ }
2635
+ if (!request.reviewer || !request.note || !request.reviewToken) throw new Error("Prepare first; recording an assessment requires reviewToken, reviewer and note.");
2636
+ return withLock(root, ".mason/reports/advisory-review-locks/" + request.findingId, async () => {
2637
+ const state = await prepare();
2638
+ if (state.token !== request.reviewToken) throw new Error("Review conflict: evidence or review history changed since preparation. Prepare and inspect a new review.");
2639
+ if (state.evidence.dirty) throw new Error("Commit relevant document and repository edits before recording the assessment. Unrelated local work may remain.");
2640
+ if ((state.record?.events.length ?? 0) >= 200) throw new Error("Review history limit reached; existing events were retained.");
2641
+ const { dirty, documentDigest, ...evidence } = state.evidence;
2642
+ const event = eventSchema2.parse({
2643
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2644
+ reviewer: request.reviewer,
2645
+ note: request.note,
2646
+ outcome: request.action,
2647
+ finding,
2648
+ evidence
2649
+ });
2650
+ const payload = { version: 1, id: request.findingId, events: [...state.record?.events ?? [], event] };
2651
+ if ((await prepare()).token !== state.token) throw new Error("Review conflict: evidence changed while recording the assessment.");
2652
+ await writeStoreJson(root, recordPath(request.findingId), { ...payload, digest: digest(payload) });
2653
+ return {
2654
+ status: "recorded",
2655
+ recordPath: recordPath(request.findingId),
2656
+ event,
2657
+ 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."
2658
+ };
2659
+ });
2660
+ }
2661
+ var execute2, git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
2662
+ var init_advisory_review = __esm({
2663
+ "src/audit/advisory-review.ts"() {
2664
+ "use strict";
2665
+ init_findings();
2666
+ init_deps_changed();
2667
+ init_inputs();
2668
+ init_scope();
2669
+ init_storage();
2670
+ init_paths();
2671
+ init_store();
2672
+ init_decisions();
2673
+ init_drift2();
2674
+ execute2 = promisify8(execFile8);
2675
+ git2 = async (root, ...args) => (await execute2("git", args, { cwd: root, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 })).stdout;
2676
+ hashSchema = z7.string().regex(/^[a-f0-9]{64}$/);
2677
+ commitSchema2 = z7.string().regex(/^[a-f0-9]{40,64}$/);
2678
+ evidenceSchema2 = z7.object({
2679
+ head: commitSchema2,
2680
+ fingerprint: hashSchema,
2681
+ scope: z7.array(z7.string()).min(1).max(1e3),
2682
+ tree: z7.string().max(2 * 1024 * 1024),
2683
+ lastScopeCommit: commitSchema2,
2684
+ paths: z7.array(z7.object({ path: z7.string(), exists: z7.boolean() })).max(1e3)
2685
+ });
2686
+ eventSchema2 = z7.object({
2687
+ at: z7.string().datetime(),
2688
+ reviewer: z7.string().trim().min(1).max(200),
2689
+ note: z7.string().trim().min(1).max(2500),
2690
+ outcome: z7.enum(["addressed", "inapplicable", "deferred"]),
2691
+ finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
2692
+ evidence: evidenceSchema2
2693
+ });
2694
+ recordSchema = z7.object({ version: z7.literal(1), id: hashSchema, events: z7.array(eventSchema2).min(1).max(200), digest: hashSchema });
2695
+ reviewSummarySchema = z7.object({
2696
+ id: hashSchema,
2697
+ status: z7.enum(["current", "reopened", "deferred", "unverified"]),
2698
+ reason: z7.string(),
2699
+ recordPath: z7.string().optional(),
2700
+ reviewer: z7.string().optional(),
2701
+ note: z7.string().optional(),
2702
+ outcome: z7.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
2703
+ reviewedHead: z7.string().optional()
2704
+ });
2705
+ advisoryReviewRequest = z7.object({
2706
+ baselinePath: z7.string().min(1),
2707
+ findingId: hashSchema,
2708
+ action: z7.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
2709
+ reviewer: eventSchema2.shape.reviewer.optional(),
2710
+ note: eventSchema2.shape.note.optional(),
2711
+ reviewToken: hashSchema.optional()
2712
+ });
2713
+ recordPath = (id) => `.mason/reviews/advisories/${hashSchema.parse(id)}.json`;
2714
+ }
2715
+ });
2716
+
2717
+ // src/audit/audit.ts
2718
+ import fs12 from "fs/promises";
2719
+ import path19 from "path";
2720
+ async function computeAudit(rootDir, options = {}) {
2721
+ const resolvedRoot = path19.resolve(rootDir);
2722
+ const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
2723
+ if (docs.length === 0) return null;
2724
+ const report = {
2725
+ version: 1,
2726
+ root: resolvedRoot,
2727
+ gitAvailable: headHash !== "unknown",
2728
+ headHash,
2729
+ checksRun: [],
2730
+ docs: docs.map((d) => ({
2731
+ path: d.path,
2732
+ scope: d.scope,
2733
+ kind: d.kind,
2734
+ lastCommit: d.lastCommit,
2735
+ dirty: d.dirty,
2736
+ lineCount: d.lineCount
2737
+ })),
2738
+ decisionsChecked: false,
2739
+ issues: [],
2740
+ advisories: [],
2741
+ suppressedAdvisories: [],
2742
+ skippedChecks: [],
2743
+ clean: true
2744
+ };
2745
+ if (!report.gitAvailable) return report;
2746
+ const changesSinceDoc = /* @__PURE__ */ new Map();
2747
+ for (const doc of docs) {
2748
+ changesSinceDoc.set(
2749
+ doc.path,
2750
+ doc.lastCommit ? await getChangesWithStatus(resolvedRoot, doc.lastCommit.hash) : null
2751
+ );
2752
+ }
2753
+ let decisionsPresent = false;
2754
+ try {
2755
+ await fs12.access(path19.join(resolvedRoot, ".mason", "decisions"));
2756
+ decisionsPresent = true;
2757
+ } catch {
2758
+ }
2759
+ report.decisionsChecked = decisionsPresent;
2760
+ const ctx = {
2761
+ root: resolvedRoot,
2762
+ docs,
2763
+ headHash,
2764
+ changesSinceDoc,
2765
+ decisionsPresent
2766
+ };
2767
+ const selected = options.checks ?? ALL_CHECKS;
2768
+ for (const name of ALL_CHECKS) {
2769
+ if (!selected.includes(name)) continue;
2770
+ const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, ctx) : CHECKS[name](ctx));
2771
+ report.checksRun.push(name);
2772
+ report.issues.push(...issues);
2773
+ report.advisories.push(...advisories);
2774
+ report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
2775
+ report.skippedChecks.push(...skipped);
2776
+ }
2777
+ report.clean = report.issues.length === 0;
2778
+ const reviews = (await Promise.all([...report.advisories, ...report.suppressedAdvisories].map((finding) => assessAdvisory(resolvedRoot, finding, headHash)))).filter((review) => review !== null);
2779
+ if (reviews.length) report.advisoryReviews = reviews;
2780
+ return report;
2781
+ }
2782
+ var init_audit = __esm({
2783
+ "src/audit/audit.ts"() {
2784
+ "use strict";
2785
+ init_drift();
2786
+ init_snapshot();
2787
+ init_docs();
2788
+ init_types();
2789
+ init_checks();
2790
+ init_advisory_review();
2791
+ }
2792
+ });
1924
2793
 
1925
2794
  // src/audit/cli.ts
1926
- var USAGE = `Usage: mason-audit [--dir <path>] [--json | --fix-prompt] [--checks <list>]
2795
+ init_audit();
2796
+ init_types();
2797
+ init_repair();
2798
+ init_findings();
2799
+ import path21 from "path";
2800
+
2801
+ // src/audit/advisory-cli.ts
2802
+ init_advisory_review();
2803
+ import path20 from "path";
2804
+ import { stripVTControlCharacters } from "util";
2805
+ var USAGE = `Usage: mason audit review --baseline <path> --finding <id> [options]
1927
2806
 
1928
- Audits the repo's AI context files (CLAUDE.md, .claude/CLAUDE.md, AGENTS.md)
2807
+ Prepare an advisory assessment against retained evidence (read-only by default).
2808
+ --dir <path> Repository root (default: current directory)
2809
+ --baseline <path> Original repair baseline
2810
+ --finding <id> findingId from repair verification or automation
2811
+ --outcome <value> Record addressed, inapplicable, or deferred
2812
+ --reviewer <name> Actual authorized reviewer
2813
+ --note <reason> Assessment reason
2814
+ --token <token> reviewToken returned by preparation
2815
+ --json Full evidence and review history
2816
+
2817
+ Recording requires prior preparation and committed relevant edits. Review records
2818
+ belong in Git under .mason/reviews/advisories/. Deferral remains outstanding.
2819
+ Decision findings use review_decision; this command never approves decisions.
2820
+ Exit 0: prepared/recorded; 2: unavailable, conflict, or decision review required.`;
2821
+ async function runAdvisoryCli(args, io = { out: (text2) => console.log(text2), err: (text2) => console.error(text2) }) {
2822
+ if (args.includes("--help") || args.includes("-h")) {
2823
+ io.out(USAGE);
2824
+ return 0;
2825
+ }
2826
+ try {
2827
+ const flags = {};
2828
+ let json = false;
2829
+ for (let i = 0; i < args.length; i++) {
2830
+ const flag = args[i];
2831
+ if (flag === "--json") {
2832
+ json = true;
2833
+ continue;
2834
+ }
2835
+ if (!["--dir", "--baseline", "--finding", "--outcome", "--reviewer", "--note", "--token"].includes(flag) || flags[flag]) throw new Error("Unexpected or repeated argument: " + flag);
2836
+ const value = args[++i];
2837
+ if (!value || value.startsWith("--")) throw new Error(flag + " requires a value.");
2838
+ flags[flag] = value;
2839
+ }
2840
+ if (!flags["--outcome"] && ["--reviewer", "--note", "--token"].some((key) => flags[key])) throw new Error("Use --outcome to record an assessment.");
2841
+ const input = {
2842
+ baselinePath: flags["--baseline"],
2843
+ findingId: flags["--finding"],
2844
+ action: flags["--outcome"] ?? "prepare",
2845
+ reviewer: flags["--reviewer"],
2846
+ note: flags["--note"],
2847
+ reviewToken: flags["--token"]
2848
+ };
2849
+ const result = await reviewAdvisory(path20.resolve(flags["--dir"] ?? process.cwd()), input);
2850
+ if (json) io.out(JSON.stringify(result, null, 2));
2851
+ else {
2852
+ const lines = [`Advisory review: ${result.status}.`];
2853
+ if ("finding" in result && result.finding && result.evidence) lines.push(
2854
+ `[${result.finding.type}] ${result.finding.anchor.doc}: ${result.finding.message}`,
2855
+ `Finding: ${result.findingId}`,
2856
+ `Prepared against commit ${result.evidence.head.slice(0, 12)}.`,
2857
+ result.evidence.dirty ? "Relevant files have uncommitted edits; commit them and prepare again before recording." : "Relevant files are committed.",
2858
+ `Review token: ${result.reviewToken}`,
2859
+ ...result.diff ? ["Scoped changes:", result.diff] : [],
2860
+ ...result.diffNote ? [result.diffNote] : []
2861
+ );
2862
+ if ("event" in result) lines.push(`${result.event.outcome} \u2014 ${result.event.reviewer}: ${result.event.note}`, `Record: ${result.recordPath}`);
2863
+ lines.push(result.hint);
2864
+ io.out(stripVTControlCharacters(lines.join("\n")));
2865
+ }
2866
+ return result.status === "decision-review-required" ? 2 : 0;
2867
+ } catch (error) {
2868
+ io.err(stripVTControlCharacters(error instanceof Error ? error.message : String(error)));
2869
+ return 2;
2870
+ }
2871
+ }
2872
+
2873
+ // src/audit/cli.ts
2874
+ var USAGE2 = `Usage: mason-audit [--dir <path>] [--json | --fix-prompt] [--checks <list>]
2875
+
2876
+ Audits README.md and agent instruction files at any depth, preserving filename case
1929
2877
  against repo reality: referenced paths that no longer exist, undocumented
1930
2878
  modules, stale counts, dead npm scripts, and manifests newer than the doc.
1931
2879
  Deterministic: no LLM call, no network \u2013 safe for CI. Works on any repo with
@@ -1944,6 +2892,8 @@ Options:
1944
2892
  ${ALL_CHECKS.join(", ")}
1945
2893
  --help Show this help
1946
2894
 
2895
+ Use mason audit review --help to prepare and record an authorized advisory assessment.
2896
+
1947
2897
  Exit codes:
1948
2898
  0 no issues (advisories may still be present)
1949
2899
  1 provable issues found
@@ -2007,7 +2957,10 @@ function issueLine(issue) {
2007
2957
  }
2008
2958
  function formatAuditSummary(report) {
2009
2959
  const lines = [];
2010
- const reviewCount = report.advisories.length + (report.suppressedAdvisories?.length ?? 0);
2960
+ const reviewed = new Set(report.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
2961
+ const open = report.advisories.filter((finding) => !reviewed.has(findingId(finding)));
2962
+ const suppressed = (report.suppressedAdvisories ?? []).filter((finding) => !reviewed.has(findingId(finding)));
2963
+ const reviewCount = open.length + suppressed.length;
2011
2964
  for (const doc of report.docs) {
2012
2965
  const docIssues = report.issues.filter((i) => i.anchor.doc === doc.path);
2013
2966
  const committed = doc.lastCommit ? `last committed ${doc.lastCommit.date.slice(0, 10)}, ${doc.lastCommit.hash.slice(0, 7)}` : "untracked";
@@ -2024,9 +2977,9 @@ function formatAuditSummary(report) {
2024
2977
  for (const issue of report.issues) {
2025
2978
  if (!docPaths.has(issue.anchor.doc)) lines.push(issueLine(issue));
2026
2979
  }
2027
- if (report.advisories.length > 0) {
2980
+ if (open.length > 0) {
2028
2981
  lines.push("Advisories (do not affect the exit code):");
2029
- for (const advisory of report.advisories) {
2982
+ for (const advisory of open) {
2030
2983
  lines.push(` [${advisory.type}] ${advisory.anchor.doc}: ${advisory.message}`);
2031
2984
  }
2032
2985
  }
@@ -2035,7 +2988,9 @@ function formatAuditSummary(report) {
2035
2988
  lines.push(` [skipped] ${skip.check}: ${skip.reason}`);
2036
2989
  }
2037
2990
  }
2038
- for (const advisory of report.suppressedAdvisories ?? []) {
2991
+ if (reviewed.size) lines.push(`${reviewed.size} advisories have recorded assessments covering current evidence (see --json).`);
2992
+ for (const review of report.advisoryReviews ?? []) if (review.status !== "current") lines.push(` [${review.status}] ${review.reason}`);
2993
+ for (const advisory of suppressed) {
2039
2994
  lines.push(` [suppressed; unresolved] ${advisory.type} ${advisory.anchor.doc}: ${advisory.message}`);
2040
2995
  }
2041
2996
  lines.push(
@@ -2072,10 +3027,10 @@ function formatFixPrompt(report, baselinePath) {
2072
3027
  "- dead-command: replace with the correct script from availableScripts if an obvious rename exists; otherwise remove the command mention."
2073
3028
  );
2074
3029
  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."
3030
+ "- 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
3031
  );
2077
3032
  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."
3033
+ "- 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
3034
  );
2080
3035
  lines.push("");
2081
3036
  lines.push("AUDIT REPORT (current context files and repository evidence, including local edits):");
@@ -2095,7 +3050,7 @@ function formatFixPrompt(report, baselinePath) {
2095
3050
  );
2096
3051
  lines.push("");
2097
3052
  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."
3053
+ "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
3054
  );
2100
3055
  return lines.join("\n");
2101
3056
  }
@@ -2105,6 +3060,7 @@ async function runAuditCli(argv, io = {
2105
3060
  err: (line) => process.stderr.write(`${line}
2106
3061
  `)
2107
3062
  }) {
3063
+ if (argv[0] === "review") return runAdvisoryCli(argv.slice(1), io);
2108
3064
  let args;
2109
3065
  try {
2110
3066
  args = parseArgs(argv);
@@ -2116,14 +3072,14 @@ async function runAuditCli(argv, io = {
2116
3072
  }
2117
3073
  } catch (error) {
2118
3074
  io.err(error instanceof Error ? error.message : String(error));
2119
- io.err(USAGE);
3075
+ io.err(USAGE2);
2120
3076
  return 2;
2121
3077
  }
2122
3078
  if (args.help) {
2123
- io.out(USAGE);
3079
+ io.out(USAGE2);
2124
3080
  return 0;
2125
3081
  }
2126
- const rootDir = path17.resolve(args.dir);
3082
+ const rootDir = path21.resolve(args.dir);
2127
3083
  if (args.baseline || args.prepareRepair) {
2128
3084
  try {
2129
3085
  if (args.baseline) {
@@ -2143,7 +3099,7 @@ ${formatAuditSummary(prepared.report)}`);
2143
3099
  const report = await computeAudit(rootDir, { checks: args.checks });
2144
3100
  if (!report) {
2145
3101
  io.err(
2146
- `No CLAUDE.md, .claude/CLAUDE.md, or AGENTS.md found in ${rootDir}.`
3102
+ `No README.md or agent instruction files found in ${rootDir}.`
2147
3103
  );
2148
3104
  return 2;
2149
3105
  }
@@ -2169,11 +3125,13 @@ ${formatAuditSummary(prepared.report)}`);
2169
3125
 
2170
3126
  // bin/mason-audit.ts
2171
3127
  runAuditCli(process.argv.slice(2)).then(
2172
- (code) => process.exit(code),
3128
+ (code) => {
3129
+ process.exitCode = code;
3130
+ },
2173
3131
  (err) => {
2174
3132
  process.stderr.write(`mason-audit error: ${err}
2175
3133
  `);
2176
- process.exit(2);
3134
+ process.exitCode = 2;
2177
3135
  }
2178
3136
  );
2179
3137
  //# sourceMappingURL=mason-audit.js.map