mason-context 0.16.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -248,391 +248,255 @@ var init_storage = __esm({
248
248
  }
249
249
  });
250
250
 
251
- // src/automation/execution.ts
251
+ // src/automation/store.ts
252
+ import fs3 from "fs/promises";
252
253
  import os from "os";
253
- import { randomUUID as randomUUID2 } from "crypto";
254
254
  import { z } from "zod";
255
- function parseExecution(raw) {
256
- try {
257
- return executionSchema.parse(raw);
258
- } catch (error) {
259
- throw new Error("Invalid automation execution store; receipt history was retained.", { cause: error });
260
- }
261
- }
262
- function automationFailure(error) {
263
- const recorded = failureSchema.safeParse(error?.failure);
264
- if (recorded.success) return recorded.data;
265
- const message2 = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
266
- const codes = /* @__PURE__ */ new Set();
267
- let cause = error;
268
- for (let i = 0; cause && i < 8; i++) {
269
- codes.add(String(cause.code));
270
- cause = cause.cause;
271
- }
272
- const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
273
- return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
274
- }
275
- function failureMessage(error) {
276
- const failure = automationFailure(error);
277
- return `Mason automation unavailable [${failure.code}]; evidence capture/verification was not established. ${failure.message}` + (failure.receiptRecorded ? "" : " No durable failure receipt was recorded.");
278
- }
279
- async function recordExecution(root, directory, event, run) {
280
- const file = directory + "/execution.json";
281
- const raw = await readStoreJson(root, file);
282
- const log = raw === null ? { version: 1, attempts: [], discardedAttempts: 0 } : parseExecution(raw);
283
- for (const attempt2 of log.attempts) if (attempt2.status === "running") attempt2.status = "unknown";
284
- log.discardedAttempts += Math.max(0, log.attempts.length - 31);
285
- log.attempts = log.attempts.slice(-31);
286
- const started = performance.now();
287
- const attempt = {
288
- id: randomUUID2(),
289
- event,
290
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
291
- pid: process.pid,
292
- host: os.hostname(),
293
- status: "running"
294
- };
295
- log.attempts.push(attempt);
296
- await writeStoreJson(root, file, log);
255
+ function parseState(raw) {
297
256
  try {
298
- const result = await run();
299
- Object.assign(attempt, {
300
- status: "completed",
301
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
302
- durationMs: performance.now() - started,
303
- verificationStatus: result.report.status,
304
- reportPath: result.report.reportPath
305
- });
306
- await writeStoreJson(root, file, log);
307
- return result;
257
+ return stateSchema.parse(raw);
308
258
  } catch (error) {
309
- const failure = automationFailure(error);
310
- Object.assign(attempt, { status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: performance.now() - started, failure });
311
- delete attempt.verificationStatus;
312
- delete attempt.reportPath;
313
- try {
314
- await writeStoreJson(root, file, { ...log, attempts: log.attempts.map((a) => a === attempt ? { ...a, failure: { ...failure, receiptRecorded: true } } : a) });
315
- failure.receiptRecorded = true;
316
- } catch {
317
- }
318
- throw Object.assign(new Error(failure.message, { cause: error }), { failure });
259
+ throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
319
260
  }
320
261
  }
321
- async function executionStatus(root, directory) {
322
- const raw = await readStoreJson(root, directory + "/execution.json");
323
- if (raw === null) return { status: "not-observed", attempts: [] };
324
- const log = parseExecution(raw);
325
- let lock = null;
326
- if (log.attempts.some((attempt) => attempt.status === "running")) {
262
+ async function withLock(root, directory, run, waitMs = 5e3) {
263
+ const file = await storePath(root, directory + "/lock", true);
264
+ const deadline = Date.now() + waitMs;
265
+ let handle;
266
+ while (!handle) {
327
267
  try {
328
- lock = await readStoreJson(root, directory + "/lock");
329
- } catch {
330
- }
331
- }
332
- for (const attempt of log.attempts) {
333
- if (attempt.status !== "running") continue;
334
- let alive = false;
335
- if (attempt.host === os.hostname() && lock?.pid === attempt.pid && lock.host === attempt.host) {
268
+ handle = await fs3.open(file, "wx", 384);
336
269
  try {
337
- process.kill(attempt.pid, 0);
338
- alive = true;
270
+ await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
339
271
  } catch (error) {
340
- alive = error.code === "EPERM";
272
+ await handle.close().catch(() => {
273
+ });
274
+ handle = void 0;
275
+ await fs3.rm(file, { force: true }).catch(() => {
276
+ });
277
+ throw error;
278
+ }
279
+ } catch (error) {
280
+ if (error.code !== "EEXIST") throw error;
281
+ try {
282
+ const owner = JSON.parse(await fs3.readFile(file, "utf8"));
283
+ if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
284
+ try {
285
+ process.kill(owner.pid, 0);
286
+ } catch (probe) {
287
+ if (probe.code === "ESRCH") {
288
+ const reclaim = file + ".reclaim";
289
+ let guard;
290
+ try {
291
+ guard = await fs3.open(reclaim, "wx", 384);
292
+ const current = JSON.parse(await fs3.readFile(file, "utf8"));
293
+ if (current.pid === owner.pid && current.host === owner.host) await fs3.unlink(file);
294
+ } finally {
295
+ if (guard) {
296
+ await guard.close();
297
+ await fs3.rm(reclaim, { force: true });
298
+ }
299
+ }
300
+ }
301
+ }
302
+ }
303
+ } catch {
341
304
  }
305
+ if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
306
+ await new Promise((resolve) => setTimeout(resolve, 40));
342
307
  }
343
- if (!alive) attempt.status = "unknown";
344
308
  }
345
- return { status: log.attempts.at(-1)?.status ?? "not-observed", attempts: log.attempts, discardedAttempts: log.discardedAttempts };
309
+ try {
310
+ return await run();
311
+ } finally {
312
+ await handle.close();
313
+ await fs3.unlink(file);
314
+ }
346
315
  }
347
- var failureSchema, attemptSchema, executionSchema;
348
- var init_execution = __esm({
349
- "src/automation/execution.ts"() {
316
+ var hostSchema, events, stateSchema;
317
+ var init_store = __esm({
318
+ "src/automation/store.ts"() {
350
319
  "use strict";
351
320
  init_storage();
352
- failureSchema = z.object({
353
- code: z.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
354
- message: z.string(),
355
- retryable: z.boolean(),
356
- receiptRecorded: z.boolean()
357
- });
358
- attemptSchema = z.object({
359
- id: z.string(),
360
- event: z.string(),
361
- startedAt: z.string(),
362
- finishedAt: z.string().optional(),
363
- durationMs: z.number().nonnegative().optional(),
364
- pid: z.number().int().positive(),
365
- host: z.string(),
366
- status: z.enum(["running", "completed", "failed", "unknown"]),
367
- verificationStatus: z.string().optional(),
368
- reportPath: z.string().optional(),
369
- failure: failureSchema.optional()
321
+ hostSchema = z.enum(["claude", "codex"]);
322
+ events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
323
+ stateSchema = z.object({
324
+ version: z.literal(1),
325
+ root: z.string(),
326
+ gitDir: z.string(),
327
+ branch: z.string(),
328
+ baselines: z.array(z.object({ path: z.string(), at: z.string(), event: z.string(), fingerprint: z.string() })).max(128),
329
+ sessions: z.record(z.object({
330
+ host: hostSchema,
331
+ seen: z.string().nullable(),
332
+ continued: z.boolean(),
333
+ initialIssues: z.array(z.string()),
334
+ initialDocs: z.record(z.string().nullable()),
335
+ lastUsed: z.string(),
336
+ mutationObserved: z.boolean(),
337
+ pending: z.record(z.string()),
338
+ coverageGaps: z.array(z.string()),
339
+ events: z.record(z.object({ at: z.string(), count: z.number().int().positive() }))
340
+ })),
341
+ updatedAt: z.string(),
342
+ fingerprint: z.string().nullable(),
343
+ latest: z.string().nullable(),
344
+ analysis: z.object({ id: z.string(), fingerprint: z.string(), completedAt: z.number(), path: z.string() }).optional()
370
345
  });
371
- executionSchema = z.object({ version: z.literal(1), attempts: z.array(attemptSchema).max(32), discardedAttempts: z.number().int().nonnegative().default(0) });
372
346
  }
373
347
  });
374
348
 
375
- // src/test-map.ts
349
+ // src/audit/commands.ts
376
350
  import path4 from "path";
377
- async function buildTestMap(dir) {
378
- const rootDir = path4.resolve(dir);
379
- const access = await createFileAccess(rootDir);
380
- const testPatterns = [
381
- "**/*.test.*",
382
- "**/*.spec.*",
383
- "**/*Test.kt",
384
- "**/*Test.java",
385
- "**/*Tests.kt",
386
- "**/*Tests.java",
387
- "**/test_*.py",
388
- "**/*_test.py",
389
- "**/*_test.go",
390
- "**/*Tests.swift",
391
- "**/*Test.swift",
392
- "**/*_test.rs"
393
- ];
394
- const testFiles = await access.list(testPatterns);
395
- const sourceFiles = await access.list();
396
- const sourceByBaseName = /* @__PURE__ */ new Map();
397
- for (const file of sourceFiles) {
398
- if (testFiles.includes(file)) continue;
399
- const baseName = path4.basename(file).replace(/\.[^.]+$/, "");
400
- const existing = sourceByBaseName.get(baseName) ?? [];
401
- existing.push(file);
402
- sourceByBaseName.set(baseName, existing);
403
- }
404
- const paired = [];
405
- const unmatched = [];
406
- for (const testFile of testFiles) {
407
- const testBaseName = path4.basename(testFile).replace(/\.[^.]+$/, "");
408
- const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
409
- if (!sourceName) {
410
- unmatched.push(testFile);
351
+ function splitCommands(line) {
352
+ const parts = [];
353
+ let quote = "", start = 0;
354
+ for (let i = 0; i < line.length; i++) {
355
+ const char = line[i];
356
+ if (char === "\\") {
357
+ i++;
411
358
  continue;
412
359
  }
413
- const candidates = sourceByBaseName.get(sourceName);
414
- if (candidates && candidates.length > 0) {
415
- const testDir = path4.dirname(testFile);
416
- const bestMatch = candidates.reduce((best, candidate) => {
417
- const candidateDir = path4.dirname(candidate);
418
- const bestDir = path4.dirname(best);
419
- const candidateOverlap = commonSegments(testDir, candidateDir);
420
- const bestOverlap = commonSegments(testDir, bestDir);
421
- return candidateOverlap > bestOverlap ? candidate : best;
422
- });
423
- paired.push({
424
- test: testFile,
425
- source: bestMatch,
426
- confidence: candidates.length === 1 ? "exact" : "best-guess"
427
- });
428
- } else {
429
- unmatched.push(testFile);
360
+ if (quote) {
361
+ if (char === quote) quote = "";
362
+ continue;
363
+ }
364
+ if (char === "'" || char === '"') {
365
+ quote = char;
366
+ continue;
367
+ }
368
+ if (char === ";" || line.slice(i, i + 2) === "&&") {
369
+ parts.push(line.slice(start, i));
370
+ if (char === "&") i++;
371
+ start = i + 1;
430
372
  }
431
373
  }
432
- return { totalTestFiles: testFiles.length, paired, unmatched };
374
+ parts.push(line.slice(start));
375
+ return parts;
433
376
  }
434
- function commonSegments(pathA, pathB) {
435
- const segsA = pathA.split("/");
436
- const segsB = pathB.split("/");
437
- let count3 = 0;
438
- for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
439
- if (segsA[i] === segsB[i]) count3++;
440
- else break;
377
+ function commandClaims(line, lineNo, cwd, shell = false) {
378
+ const claims = [];
379
+ const segments = splitCommands(line);
380
+ for (const segment of segments) {
381
+ const text2 = segment.trim().replace(/^\$\s+/, "");
382
+ if (text2.startsWith("#") || /^(?:echo|printf)\b/.test(text2)) continue;
383
+ const cd = text2.match(/^cd\s+(.+)$/);
384
+ if (cd) {
385
+ const directory = literal(cd[1]);
386
+ cwd = directory === null || cwd === null || /\s/.test(directory) && !/^["']/.test(cd[1]) ? null : path4.posix.join(cwd ?? ".", directory);
387
+ continue;
388
+ }
389
+ for (const match of text2.matchAll(COMMAND)) {
390
+ const flags = match[2].trim();
391
+ const selector = flags.match(/^(?:--prefix|--dir|-C)(?:\s+|=)(.+)$/);
392
+ let directory = cwd;
393
+ let scopeUnknown;
394
+ if (shell && match.index !== 0 || match[3].startsWith("-")) scopeUnknown = "Unsupported shell wrapper or options before the script.";
395
+ if (selector) {
396
+ const selected = literal(selector[1]);
397
+ directory = selected === null || cwd === null ? null : path4.posix.join(cwd ?? ".", selected);
398
+ } else if (flags) scopeUnknown = "Unsupported package-manager options before run: " + flags;
399
+ const tail = text2.slice(match.index + match[0].length).split(/\s--(?:\s|$)/)[0];
400
+ if (/(?:^|\s)(?:--workspace(?:s)?|-w|--filter|-F|--prefix|--dir|-C|--if-present)(?:\s|=|$)/.test(tail)) {
401
+ scopeUnknown = "Package selection or conditional options after the script need review.";
402
+ }
403
+ if (/[|()]/.test(text2) || /\b(?:pushd|popd|if|for|while)\b/.test(text2) || directory === null) {
404
+ scopeUnknown = "The command's working directory or shell control flow could not be resolved.";
405
+ }
406
+ claims.push({
407
+ scriptName: match[3],
408
+ invocation: match[0],
409
+ excerpt: match[0],
410
+ line: lineNo,
411
+ ...directory !== void 0 && directory !== null ? { directory } : {},
412
+ ...scopeUnknown ? { scopeUnknown } : {}
413
+ });
414
+ }
415
+ if (/\b(?:cd|pushd|popd)\b/.test(text2)) cwd = null;
441
416
  }
442
- return count3;
417
+ return { claims, cwd };
443
418
  }
444
- var init_test_map = __esm({
445
- "src/test-map.ts"() {
419
+ var TOKEN, COMMAND, literal;
420
+ var init_commands = __esm({
421
+ "src/audit/commands.ts"() {
446
422
  "use strict";
447
- init_files();
423
+ TOKEN = `(?:"[^"\\n]*"|'[^'\\n]*'|[^\\s\`;&|]+)`;
424
+ COMMAND = new RegExp(`\\b(npm|pnpm|yarn)((?:[ \\t]+${TOKEN})*?)[ \\t]+run[ \\t]+([A-Za-z0-9:_.-]+)`, "g");
425
+ literal = (value) => {
426
+ const unquoted = value.replace(/^(["'])(.*)\1$/, "$2");
427
+ return /[$`*?\[\]{}~\\\x00-\x1f]/.test(unquoted) || path4.posix.isAbsolute(unquoted) ? null : unquoted;
428
+ };
448
429
  }
449
430
  });
450
431
 
451
- // src/snapshot/snapshot.ts
452
- import path5 from "path";
453
- import { execFile as execFile2 } from "child_process";
454
- import { promisify as promisify2 } from "util";
455
- import { z as z2 } from "zod";
456
- async function inspectSnapshot(rootDir) {
457
- try {
458
- const raw = await readStoreJson(rootDir, ".mason/snapshot.json");
459
- const snapshot = raw === null ? null : snapshotSchema.parse(raw);
460
- return { status: snapshot ? "available" : "missing", snapshot, diagnostics: [] };
461
- } catch (error) {
462
- return { status: "invalid", snapshot: null, diagnostics: [{
463
- path: ".mason/snapshot.json",
464
- message: error instanceof Error ? error.message : String(error)
465
- }] };
432
+ // src/audit/tree.ts
433
+ function glyphIndex(line) {
434
+ for (const glyph of GLYPHS) {
435
+ const idx = line.indexOf(glyph);
436
+ if (idx !== -1) return idx;
466
437
  }
438
+ return -1;
467
439
  }
468
- async function getCurrentGitHash(rootDir) {
469
- try {
470
- const { stdout } = await exec2("git", ["rev-parse", "HEAD"], {
471
- cwd: rootDir
440
+ function isSpacerLine(line) {
441
+ return /^[\s│|]*$/.test(line);
442
+ }
443
+ function entryName(afterGlyph) {
444
+ let name = afterGlyph.replace(/^\s+/, "");
445
+ const hash2 = name.search(/\s+#/);
446
+ if (hash2 !== -1) name = name.slice(0, hash2);
447
+ const columns = name.search(/\s{2,}/);
448
+ if (columns !== -1) name = name.slice(0, columns);
449
+ name = name.trim();
450
+ if (!name || /\s/.test(name)) return null;
451
+ return name;
452
+ }
453
+ function extractTreeClaims(blockLines, blockStartLine) {
454
+ const glyphLines = blockLines.filter((l) => glyphIndex(l) !== -1).length;
455
+ if (glyphLines < MIN_GLYPH_LINES) return [];
456
+ const claims = [];
457
+ const stack = [];
458
+ let rootPrefix = "";
459
+ let started = false;
460
+ for (let i = 0; i < blockLines.length; i++) {
461
+ const line = blockLines[i];
462
+ const col = glyphIndex(line);
463
+ if (col === -1) {
464
+ if (isSpacerLine(line)) continue;
465
+ if (!started) {
466
+ const candidate = line.trim();
467
+ if (candidate.endsWith("/") && !/\s/.test(candidate)) {
468
+ rootPrefix = candidate.replace(/\/+$/, "");
469
+ claims.push({
470
+ path: rootPrefix,
471
+ line: blockStartLine + i,
472
+ excerpt: candidate
473
+ });
474
+ }
475
+ continue;
476
+ }
477
+ return claims;
478
+ }
479
+ started = true;
480
+ const name = entryName(line.slice(col + GLYPHS[0].length));
481
+ if (name === null) return claims;
482
+ while (stack.length > 0 && stack[stack.length - 1].col >= col) {
483
+ stack.pop();
484
+ }
485
+ const isDir = name.endsWith("/");
486
+ const cleanName = name.replace(/\/+$/, "");
487
+ const segments = [
488
+ ...rootPrefix ? [rootPrefix] : [],
489
+ ...stack.map((s) => s.name),
490
+ cleanName
491
+ ];
492
+ claims.push({
493
+ path: segments.join("/"),
494
+ line: blockStartLine + i,
495
+ excerpt: name
472
496
  });
473
- return stdout.trim();
474
- } catch {
475
- return "unknown";
497
+ if (isDir) stack.push({ col, name: cleanName });
476
498
  }
477
- }
478
- var exec2, repoPath, verificationFields, featureSchema, flowSchema, snapshotSchema;
479
- var init_snapshot = __esm({
480
- "src/snapshot/snapshot.ts"() {
481
- "use strict";
482
- init_files();
483
- init_files();
484
- init_storage();
485
- init_paths();
486
- init_test_map();
487
- exec2 = promisify2(execFile2);
488
- repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
489
- verificationFields = {
490
- refreshedHash: z2.string().optional(),
491
- verifiedAt: z2.string().optional(),
492
- verifiedHash: z2.string().optional(),
493
- verificationFailed: z2.boolean().optional(),
494
- verificationNote: z2.string().optional()
495
- };
496
- featureSchema = z2.object({
497
- description: z2.string(),
498
- files: z2.array(repoPath),
499
- tests: z2.array(repoPath).optional(),
500
- type: z2.enum(["capability", "infrastructure"]).optional(),
501
- ...verificationFields
502
- }).passthrough();
503
- flowSchema = z2.object({ description: z2.string(), chain: z2.array(repoPath), ...verificationFields }).passthrough();
504
- snapshotSchema = z2.object({
505
- version: z2.literal(2),
506
- createdAt: z2.string(),
507
- updatedAt: z2.string(),
508
- gitHash: z2.string(),
509
- features: z2.record(featureSchema),
510
- flows: z2.record(flowSchema)
511
- }).passthrough();
512
- }
513
- });
514
-
515
- // src/drift/drift.ts
516
- import fs3 from "fs/promises";
517
- import path6 from "path";
518
- import { execFile as execFile3 } from "child_process";
519
- import { promisify as promisify3 } from "util";
520
- function parseChanges(output) {
521
- const fields = output.split("\0");
522
- const changes = [];
523
- for (let i = 0; i < fields.length && fields[i]; ) {
524
- const code = fields[i++];
525
- const first = fields[i++];
526
- if (!first) break;
527
- const second = /^[RC]/.test(code) ? fields[i++] : void 0;
528
- const change = second ? code.startsWith("R") ? { status: "renamed", path: second, previousPath: first } : { status: "added", path: second } : { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", path: first };
529
- if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/"))) continue;
530
- changes.push(change);
531
- }
532
- return changes;
533
- }
534
- function touchedPaths(changes) {
535
- return [...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))].sort();
536
- }
537
- async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
538
- if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-")) return null;
539
- try {
540
- const { stdout } = await exec3("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
541
- return parseChanges(stdout);
542
- } catch {
543
- return null;
544
- }
545
- }
546
- async function getWorkingTree(resolvedRoot) {
547
- try {
548
- const [diff, untracked] = await Promise.all([
549
- exec3("git", ["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
550
- exec3("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
551
- ]);
552
- const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
553
- return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
554
- } catch {
555
- return { available: false, changedFiles: [], untrackedFiles: [] };
556
- }
557
- }
558
- var exec3;
559
- var init_drift = __esm({
560
- "src/drift/drift.ts"() {
561
- "use strict";
562
- init_snapshot();
563
- init_paths();
564
- exec3 = promisify3(execFile3);
565
- }
566
- });
567
-
568
- // src/audit/tree.ts
569
- function glyphIndex(line) {
570
- for (const glyph of GLYPHS) {
571
- const idx = line.indexOf(glyph);
572
- if (idx !== -1) return idx;
573
- }
574
- return -1;
575
- }
576
- function isSpacerLine(line) {
577
- return /^[\s│|]*$/.test(line);
578
- }
579
- function entryName(afterGlyph) {
580
- let name = afterGlyph.replace(/^\s+/, "");
581
- const hash2 = name.search(/\s+#/);
582
- if (hash2 !== -1) name = name.slice(0, hash2);
583
- const columns = name.search(/\s{2,}/);
584
- if (columns !== -1) name = name.slice(0, columns);
585
- name = name.trim();
586
- if (!name || /\s/.test(name)) return null;
587
- return name;
588
- }
589
- function extractTreeClaims(blockLines, blockStartLine) {
590
- const glyphLines = blockLines.filter((l) => glyphIndex(l) !== -1).length;
591
- if (glyphLines < MIN_GLYPH_LINES) return [];
592
- const claims = [];
593
- const stack = [];
594
- let rootPrefix = "";
595
- let started = false;
596
- for (let i = 0; i < blockLines.length; i++) {
597
- const line = blockLines[i];
598
- const col = glyphIndex(line);
599
- if (col === -1) {
600
- if (isSpacerLine(line)) continue;
601
- if (!started) {
602
- const candidate = line.trim();
603
- if (candidate.endsWith("/") && !/\s/.test(candidate)) {
604
- rootPrefix = candidate.replace(/\/+$/, "");
605
- claims.push({
606
- path: rootPrefix,
607
- line: blockStartLine + i,
608
- excerpt: candidate
609
- });
610
- }
611
- continue;
612
- }
613
- return claims;
614
- }
615
- started = true;
616
- const name = entryName(line.slice(col + GLYPHS[0].length));
617
- if (name === null) return claims;
618
- while (stack.length > 0 && stack[stack.length - 1].col >= col) {
619
- stack.pop();
620
- }
621
- const isDir = name.endsWith("/");
622
- const cleanName = name.replace(/\/+$/, "");
623
- const segments = [
624
- ...rootPrefix ? [rootPrefix] : [],
625
- ...stack.map((s) => s.name),
626
- cleanName
627
- ];
628
- claims.push({
629
- path: segments.join("/"),
630
- line: blockStartLine + i,
631
- excerpt: name
632
- });
633
- if (isDir) stack.push({ col, name: cleanName });
634
- }
635
- return claims;
499
+ return claims;
636
500
  }
637
501
  var MIN_GLYPH_LINES, GLYPHS;
638
502
  var init_tree = __esm({
@@ -644,6 +508,7 @@ var init_tree = __esm({
644
508
  });
645
509
 
646
510
  // src/audit/claims.ts
511
+ import MarkdownIt from "markdown-it";
647
512
  function normalizePathToken(token) {
648
513
  let t = token.trim();
649
514
  if (!t) return null;
@@ -707,11 +572,34 @@ function extractClaims(content2) {
707
572
  const counts = [];
708
573
  const commands = /* @__PURE__ */ new Map();
709
574
  const addPath = (claim) => {
710
- if (!paths.has(claim.path)) paths.set(claim.path, claim);
575
+ const key = [claim.relativeTo ?? "scope", claim.path].join(":");
576
+ if (!paths.has(key)) paths.set(key, claim);
711
577
  };
712
578
  const addCommand = (claim) => {
713
- if (!commands.has(claim.scriptName)) commands.set(claim.scriptName, claim);
579
+ const key = JSON.stringify([claim.scriptName, claim.directory, claim.scopeUnknown]);
580
+ if (!commands.has(key)) commands.set(key, claim);
714
581
  };
582
+ const parsed = markdown.parse(lines.map((line, i) => ignored[i] ? "" : line).join("\n"), {});
583
+ for (const token of parsed) {
584
+ if (token.type !== "inline" || !token.map) continue;
585
+ let line = token.map[0] + 1;
586
+ for (const child of token.children ?? []) {
587
+ const target = child.type === "link_open" ? child.attrGet("href") : child.type === "image" ? child.attrGet("src") : null;
588
+ if (target && !/^(?:[a-z][a-z0-9+.-]*:|[\/#~])/i.test(target)) {
589
+ let pathname = null;
590
+ try {
591
+ pathname = decodeURIComponent(target.split(/[?#]/)[0]);
592
+ } catch {
593
+ }
594
+ if (pathname && !/[$*?{}<>\\\x00-\x1f]/.test(pathname)) {
595
+ addPath({ path: pathname, line, excerpt: target, relativeTo: "document" });
596
+ }
597
+ }
598
+ if (child.type === "softbreak" || child.type === "hardbreak") line++;
599
+ else line += child.content.split("\n").length - 1;
600
+ }
601
+ }
602
+ let shellCwd;
715
603
  let inFence = false;
716
604
  let fenceInfo = "";
717
605
  let fenceMarker = "";
@@ -742,6 +630,7 @@ function extractClaims(content2) {
742
630
  fenceMarker = fenceMatch[1][0];
743
631
  fenceInfo = fenceMatch[2].trim().toLowerCase();
744
632
  blockLines = [];
633
+ shellCwd = void 0;
745
634
  blockStartLine = lineNo + 1;
746
635
  } else if (fenceMatch[1][0] === fenceMarker) {
747
636
  inFence = false;
@@ -752,14 +641,9 @@ function extractClaims(content2) {
752
641
  if (inFence) {
753
642
  blockLines.push(ignored[i] ? "" : line);
754
643
  if (!ignored[i] && SHELL_FENCE_INFOS.has(fenceInfo)) {
755
- for (const m of line.matchAll(COMMAND_RE)) {
756
- addCommand({
757
- scriptName: m[2],
758
- invocation: m[0],
759
- line: lineNo,
760
- excerpt: m[0]
761
- });
762
- }
644
+ const scanned = commandClaims(line, lineNo, shellCwd, true);
645
+ shellCwd = scanned.cwd;
646
+ for (const claim of scanned.claims) addCommand(claim);
763
647
  }
764
648
  continue;
765
649
  }
@@ -786,14 +670,10 @@ function extractClaims(content2) {
786
670
  excerpt: m[0]
787
671
  });
788
672
  }
789
- for (const m of line.matchAll(COMMAND_RE)) {
790
- addCommand({
791
- scriptName: m[2],
792
- invocation: m[0],
793
- line: lineNo,
794
- excerpt: m[0]
795
- });
673
+ for (const match of line.matchAll(/`([^`]+)`/g)) {
674
+ for (const claim of commandClaims(match[1], lineNo, void 0, true).claims) addCommand(claim);
796
675
  }
676
+ for (const claim of commandClaims(line.replace(/`[^`]+`/g, ""), lineNo).claims) addCommand(claim);
797
677
  }
798
678
  if (inFence) processBlock();
799
679
  return {
@@ -802,10 +682,11 @@ function extractClaims(content2) {
802
682
  commands: [...commands.values()]
803
683
  };
804
684
  }
805
- var ROOT_FILE_NAMES, SHELL_FENCE_INFOS, COMMAND_RE, COUNT_RE, COUNT_DENYLIST_RE, IGNORE_LINE, IGNORE_START, IGNORE_END;
685
+ var ROOT_FILE_NAMES, SHELL_FENCE_INFOS, markdown, COUNT_RE, COUNT_DENYLIST_RE, IGNORE_LINE, IGNORE_START, IGNORE_END;
806
686
  var init_claims = __esm({
807
687
  "src/audit/claims.ts"() {
808
688
  "use strict";
689
+ init_commands();
809
690
  init_tree();
810
691
  ROOT_FILE_NAMES = /* @__PURE__ */ new Set([
811
692
  "package.json",
@@ -839,7 +720,7 @@ var init_claims = __esm({
839
720
  ".env.example"
840
721
  ]);
841
722
  SHELL_FENCE_INFOS = /* @__PURE__ */ new Set(["", "bash", "sh", "shell", "console", "zsh"]);
842
- COMMAND_RE = /\b(npm|pnpm|yarn)\s+run\s+([A-Za-z0-9:_.-]+)/g;
723
+ markdown = new MarkdownIt("commonmark", { html: false });
843
724
  COUNT_RE = /(\d+)\s+(modules?|packages?|workspaces?|crates?)\b/gi;
844
725
  COUNT_DENYLIST_RE = /^\s*(manager|registr|lock|json)/i;
845
726
  IGNORE_LINE = "<!-- mason:ignore -->";
@@ -849,8 +730,8 @@ var init_claims = __esm({
849
730
  });
850
731
 
851
732
  // src/audit/git.ts
852
- import { execFile as execFile4 } from "child_process";
853
- import { promisify as promisify4 } from "util";
733
+ import { execFile as execFile2 } from "child_process";
734
+ import { promisify as promisify2 } from "util";
854
735
  function parseCommitLine(line) {
855
736
  const parts = line.split(" ");
856
737
  if (parts.length < 3 || !parts[0]) return null;
@@ -858,9 +739,9 @@ function parseCommitLine(line) {
858
739
  }
859
740
  async function lastCommitOf(resolvedRoot, relPath) {
860
741
  try {
861
- const { stdout } = await exec4(
742
+ const { stdout } = await exec2(
862
743
  "git",
863
- ["log", "-1", `--format=${COMMIT_FORMAT}`, "--", relPath],
744
+ ["log", "-1", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
864
745
  { cwd: resolvedRoot }
865
746
  );
866
747
  const line = stdout.trim().split("\n")[0];
@@ -871,7 +752,7 @@ async function lastCommitOf(resolvedRoot, relPath) {
871
752
  }
872
753
  async function deletingCommitOf(resolvedRoot, relPath) {
873
754
  try {
874
- const { stdout } = await exec4(
755
+ const { stdout } = await exec2(
875
756
  "git",
876
757
  [
877
758
  "log",
@@ -879,7 +760,7 @@ async function deletingCommitOf(resolvedRoot, relPath) {
879
760
  "--diff-filter=D",
880
761
  `--format=${COMMIT_FORMAT}`,
881
762
  "--",
882
- relPath
763
+ `:(literal)${relPath}`
883
764
  ],
884
765
  { cwd: resolvedRoot }
885
766
  );
@@ -891,9 +772,9 @@ async function deletingCommitOf(resolvedRoot, relPath) {
891
772
  }
892
773
  async function firstCommitOf(resolvedRoot, relPath) {
893
774
  try {
894
- const { stdout } = await exec4(
775
+ const { stdout } = await exec2(
895
776
  "git",
896
- ["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", relPath],
777
+ ["log", "--reverse", `--format=${COMMIT_FORMAT}`, "--", `:(literal)${relPath}`],
897
778
  { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }
898
779
  );
899
780
  const line = stdout.trim().split("\n")[0];
@@ -905,7 +786,7 @@ async function firstCommitOf(resolvedRoot, relPath) {
905
786
  async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
906
787
  if (!fromHash || fromHash === "unknown") return null;
907
788
  try {
908
- const { stdout } = await exec4(
789
+ const { stdout } = await exec2(
909
790
  "git",
910
791
  [
911
792
  "log",
@@ -930,214 +811,48 @@ async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
930
811
  return null;
931
812
  }
932
813
  }
933
- var exec4, COMMIT_FORMAT;
814
+ var exec2, COMMIT_FORMAT;
934
815
  var init_git = __esm({
935
816
  "src/audit/git.ts"() {
936
817
  "use strict";
937
- exec4 = promisify4(execFile4);
818
+ exec2 = promisify2(execFile2);
938
819
  COMMIT_FORMAT = "%H%x09%cI%x09%s";
939
820
  }
940
821
  });
941
822
 
942
- // src/audit/docs.ts
823
+ // src/audit/inputs.ts
943
824
  import fs4 from "fs/promises";
944
- import path7 from "path";
945
- import { execFile as execFile5 } from "child_process";
946
- import { promisify as promisify5 } from "util";
947
- async function isDirty(resolvedRoot, relPath) {
825
+ import * as nativeFs from "fs";
826
+ import path5 from "path";
827
+ import { execFile as execFile3 } from "child_process";
828
+ import { promisify as promisify3 } from "util";
829
+ import fg2 from "fast-glob";
830
+ async function auditInputPath(root, file) {
831
+ if (file === ".") return fs4.realpath(root);
948
832
  try {
949
- const { stdout } = await exec5(
950
- "git",
951
- ["status", "--porcelain", "--", relPath],
952
- { cwd: resolvedRoot }
953
- );
954
- return stdout.trim().length > 0;
955
- } catch {
956
- return false;
957
- }
958
- }
959
- async function discoverDocs(resolvedRoot) {
960
- const docs = await Promise.all(DOC_CANDIDATES.map(async (candidate) => {
961
- let content2;
962
- try {
963
- content2 = await fs4.readFile(path7.join(resolvedRoot, candidate), "utf-8");
964
- } catch {
965
- return null;
833
+ return await storePath(root, file);
834
+ } catch (error) {
835
+ if (error instanceof Error && error.message.startsWith("Symlink")) {
836
+ throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
966
837
  }
967
- const [lastCommit, dirty] = await Promise.all([lastCommitOf(resolvedRoot, candidate), isDirty(resolvedRoot, candidate)]);
968
- return {
969
- path: candidate,
970
- content: content2,
971
- lineCount: content2.split("\n").length,
972
- lastCommit,
973
- dirty,
974
- claims: extractClaims(content2)
975
- };
976
- }));
977
- return docs.filter((doc) => doc !== null);
978
- }
979
- var exec5, DOC_CANDIDATES;
980
- var init_docs = __esm({
981
- "src/audit/docs.ts"() {
982
- "use strict";
983
- init_claims();
984
- init_git();
985
- exec5 = promisify5(execFile5);
986
- DOC_CANDIDATES = [
987
- "AGENTS.md",
988
- "CLAUDE.md",
989
- ".claude/CLAUDE.md"
990
- ];
991
- }
992
- });
993
-
994
- // src/audit/types.ts
995
- var ALL_CHECKS;
996
- var init_types = __esm({
997
- "src/audit/types.ts"() {
998
- "use strict";
999
- ALL_CHECKS = [
1000
- "deleted-reference",
1001
- "new-module",
1002
- "stale-count",
1003
- "dead-command",
1004
- "deps-changed",
1005
- "decision-anchor-drift"
1006
- ];
838
+ throw error;
1007
839
  }
1008
- });
1009
-
1010
- // src/audit/checks/deleted-reference.ts
1011
- import fs5 from "fs/promises";
1012
- import path8 from "path";
1013
- async function exists(absPath) {
840
+ }
841
+ async function readAuditInput(root, file) {
1014
842
  try {
1015
- await fs5.access(absPath);
1016
- return true;
1017
- } catch {
1018
- return false;
1019
- }
1020
- }
1021
- async function checkDeletedReferences(ctx) {
1022
- const result = emptyResult();
1023
- for (const doc of ctx.docs) {
1024
- const changes = ctx.changesSinceDoc.get(doc.path);
1025
- const renames = /* @__PURE__ */ new Map();
1026
- for (const change of changes ?? []) {
1027
- if (change.status === "renamed" && change.previousPath) {
1028
- renames.set(change.previousPath, change.path);
1029
- }
1030
- }
1031
- for (const claim of doc.claims.paths) {
1032
- if (claim.path === ".mason" || claim.path.startsWith(".mason/")) {
1033
- continue;
1034
- }
1035
- if (await exists(path8.join(ctx.root, claim.path))) continue;
1036
- const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
1037
- const renamedTo = renames.get(claim.path) ?? null;
1038
- if (renamedTo) {
1039
- result.issues.push({
1040
- type: "deleted-reference",
1041
- message: `\`${claim.path}\` was renamed to \`${renamedTo}\``,
1042
- anchor,
1043
- confidence: "certain",
1044
- evidence: {
1045
- kind: "missing-path",
1046
- claimed: claim.path,
1047
- renamedTo,
1048
- deletedInCommit: null,
1049
- everTracked: true,
1050
- parentDirExists: true
1051
- }
1052
- });
1053
- continue;
1054
- }
1055
- const tracked = await lastCommitOf(ctx.root, claim.path);
1056
- if (tracked) {
1057
- const deleted = await deletingCommitOf(ctx.root, claim.path);
1058
- const detail = deleted ? ` \u2013 deleted in ${deleted.hash.slice(0, 7)} "${deleted.subject}" (${deleted.date.slice(0, 10)})` : "";
1059
- result.issues.push({
1060
- type: "deleted-reference",
1061
- message: `\`${claim.path}\` no longer exists${detail}`,
1062
- anchor,
1063
- confidence: "certain",
1064
- evidence: {
1065
- kind: "missing-path",
1066
- claimed: claim.path,
1067
- renamedTo: null,
1068
- deletedInCommit: deleted,
1069
- everTracked: true,
1070
- parentDirExists: await exists(
1071
- path8.join(ctx.root, path8.dirname(claim.path))
1072
- )
1073
- }
1074
- });
1075
- continue;
1076
- }
1077
- const parentDirExists = await exists(
1078
- path8.join(ctx.root, path8.dirname(claim.path))
1079
- );
1080
- if (!parentDirExists) continue;
1081
- const issue = {
1082
- type: "deleted-reference",
1083
- message: `\`${claim.path}\` does not exist (never tracked in git \u2013 possible typo or invented path)`,
1084
- anchor,
1085
- confidence: "likely",
1086
- evidence: {
1087
- kind: "missing-path",
1088
- claimed: claim.path,
1089
- renamedTo: null,
1090
- deletedInCommit: null,
1091
- everTracked: false,
1092
- parentDirExists: true
1093
- }
1094
- };
1095
- result.issues.push(issue);
1096
- }
1097
- }
1098
- return result;
1099
- }
1100
- var init_deleted_reference = __esm({
1101
- "src/audit/checks/deleted-reference.ts"() {
1102
- "use strict";
1103
- init_git();
1104
- init_checks();
1105
- }
1106
- });
1107
-
1108
- // src/audit/inputs.ts
1109
- import fs6 from "fs/promises";
1110
- import * as nativeFs from "fs";
1111
- import path9 from "path";
1112
- import { execFile as execFile6 } from "child_process";
1113
- import { promisify as promisify6 } from "util";
1114
- import fg2 from "fast-glob";
1115
- async function auditInputPath(root, file) {
1116
- if (file === ".") return fs6.realpath(root);
1117
- try {
1118
- return await storePath(root, file);
1119
- } catch (error) {
1120
- if (error instanceof Error && error.message.startsWith("Symlink")) {
1121
- throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
1122
- }
1123
- throw error;
1124
- }
1125
- }
1126
- async function readAuditInput(root, file) {
1127
- try {
1128
- const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
1129
- if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
1130
- return value;
1131
- } catch (error) {
1132
- if (error.code === "ENOENT") return null;
1133
- throw error;
843
+ const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
844
+ if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
845
+ return value;
846
+ } catch (error) {
847
+ if (error.code === "ENOENT") return null;
848
+ throw error;
1134
849
  }
1135
850
  }
1136
851
  async function auditGlob(root, patterns, options) {
1137
852
  if (Array.isArray(patterns) && !patterns.length) return [];
1138
853
  const directories = /* @__PURE__ */ new Set();
1139
854
  const readdir2 = ((directory, opts, callback) => {
1140
- const relative = path9.relative(root, directory).split(path9.sep).join("/") || ".";
855
+ const relative = path5.relative(root, directory).split(path5.sep).join("/") || ".";
1141
856
  directories.add(relative);
1142
857
  if (directories.size > MAX_INPUTS) {
1143
858
  callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
@@ -1148,7 +863,7 @@ async function auditGlob(root, patterns, options) {
1148
863
  }, callback);
1149
864
  });
1150
865
  const lstat2 = ((file, callback) => {
1151
- const relative = path9.relative(root, file).split(path9.sep).join("/") || ".";
866
+ const relative = path5.relative(root, file).split(path5.sep).join("/") || ".";
1152
867
  auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
1153
868
  });
1154
869
  const stream = fg2.stream(patterns, {
@@ -1176,7 +891,7 @@ async function auditGlob(root, patterns, options) {
1176
891
  counts.set(dir, (counts.get(dir) ?? 0) + 1);
1177
892
  return counts;
1178
893
  }, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
1179
- 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.`);
894
+ throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count4]) => `${dir} (${count4})`).join(", ")}. Narrow this check's inputs.`);
1180
895
  }
1181
896
  }
1182
897
  } finally {
@@ -1217,21 +932,460 @@ async function gitIgnoredPaths(root, files) {
1217
932
  throw error;
1218
933
  }
1219
934
  }
1220
- var execute, MAX_INPUTS;
1221
- var init_inputs = __esm({
1222
- "src/audit/inputs.ts"() {
935
+ var execute, MAX_INPUTS;
936
+ var init_inputs = __esm({
937
+ "src/audit/inputs.ts"() {
938
+ "use strict";
939
+ init_files();
940
+ init_storage();
941
+ execute = promisify3(execFile3);
942
+ MAX_INPUTS = 1e5;
943
+ }
944
+ });
945
+
946
+ // src/audit/docs.ts
947
+ import fs5 from "fs/promises";
948
+ import path6 from "path";
949
+ import { execFile as execFile4 } from "child_process";
950
+ import { promisify as promisify4 } from "util";
951
+ import fg3 from "fast-glob";
952
+ async function isDirty(resolvedRoot, relPath) {
953
+ try {
954
+ const { stdout } = await exec3(
955
+ "git",
956
+ ["status", "--porcelain", "--", `:(literal)${relPath}`],
957
+ { cwd: resolvedRoot }
958
+ );
959
+ return stdout.trim().length > 0;
960
+ } catch {
961
+ return true;
962
+ }
963
+ }
964
+ async function discoverDocPaths(root) {
965
+ let candidates = [];
966
+ try {
967
+ const { stdout } = await exec3(
968
+ "git",
969
+ [
970
+ "ls-files",
971
+ "-z",
972
+ "--cached",
973
+ "--others",
974
+ "--exclude-standard",
975
+ "--",
976
+ ":(icase,glob)**/readme.md",
977
+ ":(icase,glob)**/agents.md",
978
+ ":(icase,glob)**/claude.md"
979
+ ],
980
+ { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 }
981
+ );
982
+ candidates = stdout.split("\0").filter(Boolean);
983
+ } catch (error) {
984
+ try {
985
+ await exec3("git", ["rev-parse", "--git-dir"], { cwd: root });
986
+ } catch {
987
+ return localDocPaths(root);
988
+ }
989
+ throw error;
990
+ }
991
+ candidates.push(...await localDocPaths(root));
992
+ const config = await loadProjectConfig(root);
993
+ const excluded = [
994
+ "**/node_modules/**",
995
+ "**/vendor/**",
996
+ "**/dist/**",
997
+ "**/build/**",
998
+ "**/target/**",
999
+ "**/.git/**",
1000
+ "**/.venv/**",
1001
+ "**/venv/**",
1002
+ ".mason/**",
1003
+ ...config.ignore ?? []
1004
+ ];
1005
+ const paths = [];
1006
+ const unique = [...new Set(candidates)].sort();
1007
+ for (let offset = 0; offset < unique.length; offset += 256) {
1008
+ paths.push(...await auditGlob(root, unique.slice(offset, offset + 256).map((p) => fg3.escapePath(p)), {
1009
+ ignore: excluded,
1010
+ followSymbolicLinks: false,
1011
+ label: "Documentation discovery"
1012
+ }));
1013
+ if (paths.length > 1e4) throw new Error("Documentation discovery exceeds 10,000 relevant files; narrow .mason/config.json ignore patterns.");
1014
+ }
1015
+ const priority2 = (file) => {
1016
+ const index2 = DOC_CANDIDATES.findIndex((p) => p.toLowerCase() === file.toLowerCase());
1017
+ return index2 < 0 ? DOC_CANDIDATES.length : index2;
1018
+ };
1019
+ return paths.sort((a, b) => priority2(a) - priority2(b) || a.localeCompare(b));
1020
+ }
1021
+ async function localDocPaths(root) {
1022
+ const paths = [];
1023
+ for (const dir of [".", ".claude"]) {
1024
+ let entries;
1025
+ try {
1026
+ entries = await fs5.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1027
+ } catch (error) {
1028
+ if (error.code === "ENOENT") continue;
1029
+ throw error;
1030
+ }
1031
+ for (const entry of entries) if (/^(?:agents|claude|readme)\.md$/i.test(entry.name)) {
1032
+ paths.push(path6.posix.join(dir, entry.name));
1033
+ }
1034
+ }
1035
+ return paths;
1036
+ }
1037
+ function documentScope(file) {
1038
+ const parent = path6.posix.dirname(file);
1039
+ return path6.posix.basename(parent).toLowerCase() === ".claude" ? path6.posix.dirname(parent) : parent;
1040
+ }
1041
+ async function discoverDocs(resolvedRoot) {
1042
+ const docs = [];
1043
+ for (const candidate of await discoverDocPaths(resolvedRoot)) {
1044
+ const content2 = await readAuditInput(resolvedRoot, candidate);
1045
+ if (content2 === null) throw new Error("Document disappeared during discovery: " + candidate);
1046
+ const [lastCommit, dirty] = await Promise.all([lastCommitOf(resolvedRoot, candidate), isDirty(resolvedRoot, candidate)]);
1047
+ docs.push({
1048
+ path: candidate,
1049
+ content: content2,
1050
+ scope: documentScope(candidate),
1051
+ kind: /^readme\.md$/i.test(path6.posix.basename(candidate)) ? "readme" : "instructions",
1052
+ lineCount: content2.split("\n").length,
1053
+ lastCommit,
1054
+ dirty,
1055
+ claims: extractClaims(content2)
1056
+ });
1057
+ }
1058
+ return docs;
1059
+ }
1060
+ var exec3, DOC_CANDIDATES;
1061
+ var init_docs = __esm({
1062
+ "src/audit/docs.ts"() {
1063
+ "use strict";
1064
+ init_claims();
1065
+ init_git();
1066
+ init_inputs();
1067
+ init_files();
1068
+ exec3 = promisify4(execFile4);
1069
+ DOC_CANDIDATES = [
1070
+ "AGENTS.md",
1071
+ "CLAUDE.md",
1072
+ ".claude/CLAUDE.md"
1073
+ ];
1074
+ }
1075
+ });
1076
+
1077
+ // src/audit/scope.ts
1078
+ import fs6 from "fs/promises";
1079
+ import path7 from "path";
1080
+ function scopedPath(directory, value) {
1081
+ if (path7.posix.isAbsolute(value) || value.includes("\\") || /[\x00-\x1f]/.test(value)) return null;
1082
+ const resolved = path7.posix.normalize(path7.posix.join(directory, value));
1083
+ return resolved === ".." || resolved.startsWith("../") ? null : resolved;
1084
+ }
1085
+ function pathClaimScope(doc, claim) {
1086
+ const directory = claim.relativeTo === "document" ? path7.posix.dirname(doc) : documentScope(doc);
1087
+ const local = scopedPath(directory, claim.path);
1088
+ if (!local) return null;
1089
+ const candidates = claim.relativeTo === "document" || directory === "." ? [local] : [...new Set([local, scopedPath(".", claim.path)].filter((p) => p !== null))];
1090
+ return {
1091
+ basis: claim.relativeTo === "document" ? "document-link" : directory === "." ? "repository" : "document",
1092
+ directory,
1093
+ candidates
1094
+ };
1095
+ }
1096
+ async function pathExists(root, file) {
1097
+ const absolute = await auditInputPath(root, file);
1098
+ try {
1099
+ await fs6.access(absolute);
1100
+ return true;
1101
+ } catch (error) {
1102
+ if (error.code === "ENOENT") return false;
1103
+ throw error;
1104
+ }
1105
+ }
1106
+ function optionalMasonPath(file) {
1107
+ return file.split("/").includes(".mason");
1108
+ }
1109
+ var init_scope = __esm({
1110
+ "src/audit/scope.ts"() {
1111
+ "use strict";
1112
+ init_inputs();
1113
+ init_docs();
1114
+ }
1115
+ });
1116
+
1117
+ // src/test-map.ts
1118
+ import path8 from "path";
1119
+ async function buildTestMap(dir) {
1120
+ const rootDir = path8.resolve(dir);
1121
+ const access = await createFileAccess(rootDir);
1122
+ const testPatterns = [
1123
+ "**/*.test.*",
1124
+ "**/*.spec.*",
1125
+ "**/*Test.kt",
1126
+ "**/*Test.java",
1127
+ "**/*Tests.kt",
1128
+ "**/*Tests.java",
1129
+ "**/test_*.py",
1130
+ "**/*_test.py",
1131
+ "**/*_test.go",
1132
+ "**/*Tests.swift",
1133
+ "**/*Test.swift",
1134
+ "**/*_test.rs"
1135
+ ];
1136
+ const testFiles = await access.list(testPatterns);
1137
+ const sourceFiles = await access.list();
1138
+ const sourceByBaseName = /* @__PURE__ */ new Map();
1139
+ for (const file of sourceFiles) {
1140
+ if (testFiles.includes(file)) continue;
1141
+ const baseName = path8.basename(file).replace(/\.[^.]+$/, "");
1142
+ const existing = sourceByBaseName.get(baseName) ?? [];
1143
+ existing.push(file);
1144
+ sourceByBaseName.set(baseName, existing);
1145
+ }
1146
+ const paired = [];
1147
+ const unmatched = [];
1148
+ for (const testFile of testFiles) {
1149
+ const testBaseName = path8.basename(testFile).replace(/\.[^.]+$/, "");
1150
+ const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
1151
+ if (!sourceName) {
1152
+ unmatched.push(testFile);
1153
+ continue;
1154
+ }
1155
+ const candidates = sourceByBaseName.get(sourceName);
1156
+ if (candidates && candidates.length > 0) {
1157
+ const testDir = path8.dirname(testFile);
1158
+ const bestMatch = candidates.reduce((best, candidate) => {
1159
+ const candidateDir = path8.dirname(candidate);
1160
+ const bestDir = path8.dirname(best);
1161
+ const candidateOverlap = commonSegments(testDir, candidateDir);
1162
+ const bestOverlap = commonSegments(testDir, bestDir);
1163
+ return candidateOverlap > bestOverlap ? candidate : best;
1164
+ });
1165
+ paired.push({
1166
+ test: testFile,
1167
+ source: bestMatch,
1168
+ confidence: candidates.length === 1 ? "exact" : "best-guess"
1169
+ });
1170
+ } else {
1171
+ unmatched.push(testFile);
1172
+ }
1173
+ }
1174
+ return { totalTestFiles: testFiles.length, paired, unmatched };
1175
+ }
1176
+ function commonSegments(pathA, pathB) {
1177
+ const segsA = pathA.split("/");
1178
+ const segsB = pathB.split("/");
1179
+ let count4 = 0;
1180
+ for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
1181
+ if (segsA[i] === segsB[i]) count4++;
1182
+ else break;
1183
+ }
1184
+ return count4;
1185
+ }
1186
+ var init_test_map = __esm({
1187
+ "src/test-map.ts"() {
1188
+ "use strict";
1189
+ init_files();
1190
+ }
1191
+ });
1192
+
1193
+ // src/snapshot/snapshot.ts
1194
+ import path9 from "path";
1195
+ import { execFile as execFile5 } from "child_process";
1196
+ import { promisify as promisify5 } from "util";
1197
+ import { z as z2 } from "zod";
1198
+ async function inspectSnapshot(rootDir) {
1199
+ try {
1200
+ const raw = await readStoreJson(rootDir, ".mason/snapshot.json");
1201
+ const snapshot = raw === null ? null : snapshotSchema.parse(raw);
1202
+ return { status: snapshot ? "available" : "missing", snapshot, diagnostics: [] };
1203
+ } catch (error) {
1204
+ return { status: "invalid", snapshot: null, diagnostics: [{
1205
+ path: ".mason/snapshot.json",
1206
+ message: error instanceof Error ? error.message : String(error)
1207
+ }] };
1208
+ }
1209
+ }
1210
+ async function getCurrentGitHash(rootDir) {
1211
+ try {
1212
+ const { stdout } = await exec4("git", ["rev-parse", "HEAD"], {
1213
+ cwd: rootDir
1214
+ });
1215
+ return stdout.trim();
1216
+ } catch {
1217
+ return "unknown";
1218
+ }
1219
+ }
1220
+ var exec4, repoPath, verificationFields, featureSchema, flowSchema, snapshotSchema;
1221
+ var init_snapshot = __esm({
1222
+ "src/snapshot/snapshot.ts"() {
1223
+ "use strict";
1224
+ init_files();
1225
+ init_files();
1226
+ init_storage();
1227
+ init_paths();
1228
+ init_test_map();
1229
+ exec4 = promisify5(execFile5);
1230
+ repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
1231
+ verificationFields = {
1232
+ refreshedHash: z2.string().optional(),
1233
+ verifiedAt: z2.string().optional(),
1234
+ verifiedHash: z2.string().optional(),
1235
+ verificationFailed: z2.boolean().optional(),
1236
+ verificationNote: z2.string().optional()
1237
+ };
1238
+ featureSchema = z2.object({
1239
+ description: z2.string(),
1240
+ files: z2.array(repoPath),
1241
+ tests: z2.array(repoPath).optional(),
1242
+ type: z2.enum(["capability", "infrastructure"]).optional(),
1243
+ ...verificationFields
1244
+ }).passthrough();
1245
+ flowSchema = z2.object({ description: z2.string(), chain: z2.array(repoPath), ...verificationFields }).passthrough();
1246
+ snapshotSchema = z2.object({
1247
+ version: z2.literal(2),
1248
+ createdAt: z2.string(),
1249
+ updatedAt: z2.string(),
1250
+ gitHash: z2.string(),
1251
+ features: z2.record(featureSchema),
1252
+ flows: z2.record(flowSchema)
1253
+ }).passthrough();
1254
+ }
1255
+ });
1256
+
1257
+ // src/drift/drift.ts
1258
+ import fs7 from "fs/promises";
1259
+ import path10 from "path";
1260
+ import { execFile as execFile6 } from "child_process";
1261
+ import { promisify as promisify6 } from "util";
1262
+ function parseChanges(output) {
1263
+ const fields = output.split("\0");
1264
+ const changes = [];
1265
+ for (let i = 0; i < fields.length && fields[i]; ) {
1266
+ const code = fields[i++];
1267
+ const first = fields[i++];
1268
+ if (!first) break;
1269
+ const second = /^[RC]/.test(code) ? fields[i++] : void 0;
1270
+ const change = second ? code.startsWith("R") ? { status: "renamed", path: second, previousPath: first } : { status: "added", path: second } : { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", path: first };
1271
+ if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/"))) continue;
1272
+ changes.push(change);
1273
+ }
1274
+ return changes;
1275
+ }
1276
+ function touchedPaths(changes) {
1277
+ return [...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))].sort();
1278
+ }
1279
+ async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
1280
+ if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-")) return null;
1281
+ try {
1282
+ const { stdout } = await exec5("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
1283
+ return parseChanges(stdout);
1284
+ } catch {
1285
+ return null;
1286
+ }
1287
+ }
1288
+ async function getWorkingTree(resolvedRoot) {
1289
+ try {
1290
+ const [diff, untracked] = await Promise.all([
1291
+ exec5("git", ["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
1292
+ exec5("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
1293
+ ]);
1294
+ const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
1295
+ return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
1296
+ } catch {
1297
+ return { available: false, changedFiles: [], untrackedFiles: [] };
1298
+ }
1299
+ }
1300
+ var exec5;
1301
+ var init_drift = __esm({
1302
+ "src/drift/drift.ts"() {
1303
+ "use strict";
1304
+ init_snapshot();
1305
+ init_paths();
1306
+ exec5 = promisify6(execFile6);
1307
+ }
1308
+ });
1309
+
1310
+ // src/audit/types.ts
1311
+ var ALL_CHECKS;
1312
+ var init_types = __esm({
1313
+ "src/audit/types.ts"() {
1314
+ "use strict";
1315
+ ALL_CHECKS = [
1316
+ "deleted-reference",
1317
+ "new-module",
1318
+ "stale-count",
1319
+ "dead-command",
1320
+ "deps-changed",
1321
+ "decision-anchor-drift"
1322
+ ];
1323
+ }
1324
+ });
1325
+
1326
+ // src/audit/checks/deleted-reference.ts
1327
+ import path11 from "path";
1328
+ async function checkDeletedReferences(ctx) {
1329
+ const result = emptyResult();
1330
+ for (const doc of ctx.docs) {
1331
+ const changes = ctx.changesSinceDoc.get(doc.path);
1332
+ const renames = new Map((changes ?? []).filter((c) => c.status === "renamed" && c.previousPath).map((c) => [c.previousPath, c.path]));
1333
+ for (const claim of doc.claims.paths) {
1334
+ const scope = pathClaimScope(doc.path, claim);
1335
+ if (!scope) {
1336
+ result.skipped.push({
1337
+ check: "deleted-reference",
1338
+ doc: doc.path,
1339
+ reason: `Reference ${claim.excerpt} is outside the repository or has an unsupported path.`
1340
+ });
1341
+ continue;
1342
+ }
1343
+ if (scope.candidates.some(optionalMasonPath)) continue;
1344
+ if ((await Promise.all(scope.candidates.map((file) => pathExists(ctx.root, file)))).some(Boolean)) continue;
1345
+ const candidates = await Promise.all(scope.candidates.map(async (file) => ({
1346
+ file,
1347
+ tracked: await lastCommitOf(ctx.root, file),
1348
+ parent: await pathExists(ctx.root, path11.posix.dirname(file))
1349
+ })));
1350
+ const candidate = candidates.find((c) => c.tracked || renames.has(c.file)) ?? candidates.find((c) => c.parent);
1351
+ if (!candidate && claim.relativeTo !== "document") continue;
1352
+ const resolvedPath = candidate?.file ?? scope.candidates[0];
1353
+ const renamedTo = renames.get(resolvedPath) ?? null;
1354
+ const deletedInCommit = candidate?.tracked && !renamedTo ? await deletingCommitOf(ctx.root, resolvedPath) : null;
1355
+ const evidence = {
1356
+ kind: "missing-path",
1357
+ claimed: claim.path,
1358
+ resolvedPath,
1359
+ scope,
1360
+ renamedTo,
1361
+ deletedInCommit,
1362
+ everTracked: !!candidate?.tracked || !!renamedTo,
1363
+ parentDirExists: candidate?.parent ?? false
1364
+ };
1365
+ const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
1366
+ const message2 = renamedTo ? `\`${resolvedPath}\` was renamed to \`${renamedTo}\`` : `\`${resolvedPath}\` does not exist` + (deletedInCommit ? ` \u2013 deleted in ${deletedInCommit.hash.slice(0, 7)} "${deletedInCommit.subject}" (${deletedInCommit.date.slice(0, 10)})` : "");
1367
+ if (scope.basis !== "document" && (renamedTo || candidate?.tracked)) {
1368
+ result.issues.push({ type: "deleted-reference", message: message2, anchor, confidence: "certain", evidence });
1369
+ } else {
1370
+ result.advisories.push({ resolution: "recheck", type: "deleted-reference", message: message2 + (scope.basis === "document" ? "; the bare path's document/repository scope needs review." : "; no tracked history establishes it as a required file. Review for a typo, generated output, or example."), anchor, evidence });
1371
+ }
1372
+ }
1373
+ }
1374
+ return result;
1375
+ }
1376
+ var init_deleted_reference = __esm({
1377
+ "src/audit/checks/deleted-reference.ts"() {
1223
1378
  "use strict";
1224
- init_files();
1225
- init_storage();
1226
- execute = promisify6(execFile6);
1227
- MAX_INPUTS = 1e5;
1379
+ init_git();
1380
+ init_scope();
1381
+ init_checks();
1228
1382
  }
1229
1383
  });
1230
1384
 
1231
1385
  // src/audit/checks/new-module.ts
1232
- import fg3 from "fast-glob";
1233
- import fs7 from "fs/promises";
1234
- import path10 from "path";
1386
+ import fg4 from "fast-glob";
1387
+ import fs8 from "fs/promises";
1388
+ import path12 from "path";
1235
1389
  function escapeRegExp(text2) {
1236
1390
  return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1237
1391
  }
@@ -1245,15 +1399,15 @@ function isMentioned(combinedDocs, name) {
1245
1399
  async function checkNewModules(ctx) {
1246
1400
  const result = emptyResult();
1247
1401
  if (ctx.docs.length === 0) return result;
1248
- const combinedDocs = ctx.docs.map((d) => d.content).join("\n");
1402
+ const combinedDocs = moduleDocumentation(ctx.docs);
1249
1403
  const primaryDoc = ctx.docs[0].path;
1250
1404
  const checkedDocs = ctx.docs.map((d) => d.path);
1251
1405
  const flag = async (dir, sourceFileCount) => {
1252
- result.issues.push({
1406
+ result.advisories.push({
1407
+ resolution: "recheck",
1253
1408
  type: "new-module",
1254
- message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in any context file`,
1409
+ message: `directory \`${dir}/\` contains ${sourceFileCount} source file${sourceFileCount === 1 ? "" : "s"} but is not mentioned in the discovered documentation; review whether it needs documenting`,
1255
1410
  anchor: { doc: primaryDoc, line: null, excerpt: dir },
1256
- confidence: "likely",
1257
1411
  evidence: {
1258
1412
  kind: "unmentioned-dir",
1259
1413
  dir,
@@ -1268,41 +1422,44 @@ async function checkNewModules(ctx) {
1268
1422
  }
1269
1423
  return result;
1270
1424
  }
1425
+ function moduleDocumentation(docs) {
1426
+ return docs.map((doc) => doc.content + "\n" + (path12.posix.dirname(doc.path) === "." ? "" : path12.posix.dirname(doc.path) + "/")).join("\n");
1427
+ }
1271
1428
  async function moduleCandidates(root, combinedDocs) {
1272
1429
  const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
1273
1430
  const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
1274
1431
  const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
1275
1432
  const listSubdirs = async (dir) => {
1276
- const entries = await fs7.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1277
- 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));
1433
+ const entries = await fs8.readdir(await auditInputPath(root, dir), { withFileTypes: true });
1434
+ 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));
1278
1435
  const ignored = await gitIgnoredPaths(root, dirs);
1279
1436
  const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
1280
- return (await auditGlob(root, dir === "." ? "*" : `${fg3.escapePath(dir)}/*`, {
1437
+ return (await auditGlob(root, dir === "." ? "*" : `${fg4.escapePath(dir)}/*`, {
1281
1438
  ignore,
1282
1439
  onlyDirectories: true,
1283
1440
  label: "Module directory discovery",
1284
1441
  select: (file) => visible.has(file)
1285
- })).map((file) => path10.posix.basename(file));
1442
+ })).map((file) => path12.posix.basename(file));
1286
1443
  };
1287
1444
  const countSourceFiles = async (dir) => {
1288
1445
  const files = sources.filter((file) => file.startsWith(dir + "/"));
1289
- let count3 = 0;
1446
+ let count4 = 0;
1290
1447
  for (let offset = 0; offset < files.length; offset += 256) {
1291
- count3 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg3.escapePath(file)), {
1448
+ count4 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg4.escapePath(file)), {
1292
1449
  ignore,
1293
1450
  followSymbolicLinks: false,
1294
1451
  label: `Source discovery in ${dir}`
1295
1452
  })).length;
1296
- 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.`);
1453
+ if (count4 > 1e5) throw new Error(`Source discovery in ${dir} exceeds 100,000 relevant files. Exclude generated source with .mason/config.json ignore patterns or narrow this audit's checks.`);
1297
1454
  }
1298
- return count3;
1455
+ return count4;
1299
1456
  };
1300
1457
  const candidates = [];
1301
1458
  for (const topDir of await listSubdirs(".")) {
1302
1459
  const topMentioned = isMentioned(combinedDocs, topDir);
1303
1460
  if (!topMentioned) {
1304
- const count3 = await countSourceFiles(topDir);
1305
- if (count3 >= 1) candidates.push({ dir: topDir, sourceFileCount: count3 });
1461
+ const count4 = await countSourceFiles(topDir);
1462
+ if (count4 >= 1) candidates.push({ dir: topDir, sourceFileCount: count4 });
1306
1463
  continue;
1307
1464
  }
1308
1465
  const subdirs = await listSubdirs(topDir);
@@ -1310,9 +1467,9 @@ async function moduleCandidates(root, combinedDocs) {
1310
1467
  if (mentioned.length < ENUMERATION_THRESHOLD) continue;
1311
1468
  for (const sub of subdirs) {
1312
1469
  if (isMentioned(combinedDocs, sub)) continue;
1313
- const count3 = await countSourceFiles(`${topDir}/${sub}`);
1314
- if (count3 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
1315
- candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count3 });
1470
+ const count4 = await countSourceFiles(`${topDir}/${sub}`);
1471
+ if (count4 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
1472
+ candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count4 });
1316
1473
  }
1317
1474
  }
1318
1475
  }
@@ -1351,7 +1508,7 @@ var init_new_module = __esm({
1351
1508
  });
1352
1509
 
1353
1510
  // src/audit/checks/stale-count.ts
1354
- import path11 from "path";
1511
+ import path13 from "path";
1355
1512
  async function countGradleModules(root) {
1356
1513
  for (const name of ["settings.gradle.kts", "settings.gradle"]) {
1357
1514
  const content2 = await readAuditInput(root, name);
@@ -1385,7 +1542,7 @@ async function countNpmWorkspaces(root) {
1385
1542
  return {
1386
1543
  actual: matched.length,
1387
1544
  countedFrom: "package.json workspaces",
1388
- members: matched.map((m) => path11.dirname(m)).sort()
1545
+ members: matched.map((m) => path13.dirname(m)).sort()
1389
1546
  };
1390
1547
  }
1391
1548
  }
@@ -1416,7 +1573,7 @@ async function countNpmWorkspaces(root) {
1416
1573
  return {
1417
1574
  actual: matched.length,
1418
1575
  countedFrom: "pnpm-workspace.yaml",
1419
- members: matched.map((m) => path11.dirname(m)).sort()
1576
+ members: matched.map((m) => path13.dirname(m)).sort()
1420
1577
  };
1421
1578
  }
1422
1579
  }
@@ -1438,8 +1595,8 @@ async function countCargoCrates(root) {
1438
1595
  ignore: ["**/target/**"],
1439
1596
  label: "Cargo workspace discovery"
1440
1597
  });
1441
- for (const m of matched) members.add(path11.dirname(m));
1442
- } else if (await readAuditInput(root, path11.posix.join(entry, "Cargo.toml")) !== null) {
1598
+ for (const m of matched) members.add(path13.dirname(m));
1599
+ } else if (await readAuditInput(root, path13.posix.join(entry, "Cargo.toml")) !== null) {
1443
1600
  members.add(entry);
1444
1601
  }
1445
1602
  }
@@ -1457,11 +1614,21 @@ async function resolveCountSource(root, claim) {
1457
1614
  if (unit === "crate") return countCargoCrates(root);
1458
1615
  return await countNpmWorkspaces(root) ?? await countCargoCrates(root) ?? await countGradleModules(root);
1459
1616
  }
1617
+ async function resolveDocCountSource(root, doc, claim) {
1618
+ const scope = documentScope(doc);
1619
+ const source = await resolveCountSource(path13.join(root, scope), claim);
1620
+ if (!source || scope === ".") return source;
1621
+ return {
1622
+ ...source,
1623
+ countedFrom: scope + "/" + source.countedFrom,
1624
+ members: source.members.map((member) => scope + "/" + member)
1625
+ };
1626
+ }
1460
1627
  async function checkStaleCounts(ctx) {
1461
1628
  const result = emptyResult();
1462
1629
  for (const doc of ctx.docs) {
1463
1630
  for (const claim of doc.claims.counts) {
1464
- const source = await resolveCountSource(ctx.root, claim);
1631
+ const source = await resolveDocCountSource(ctx.root, doc.path, claim);
1465
1632
  if (source === null) {
1466
1633
  result.skipped.push({
1467
1634
  check: "stale-count",
@@ -1493,6 +1660,7 @@ var MEMBERS_CAP;
1493
1660
  var init_stale_count = __esm({
1494
1661
  "src/audit/checks/stale-count.ts"() {
1495
1662
  "use strict";
1663
+ init_docs();
1496
1664
  init_inputs();
1497
1665
  init_checks();
1498
1666
  MEMBERS_CAP = 50;
@@ -1500,61 +1668,94 @@ var init_stale_count = __esm({
1500
1668
  });
1501
1669
 
1502
1670
  // src/audit/checks/dead-command.ts
1503
- async function scriptsOf(root, manifest) {
1504
- const raw = await readAuditInput(root, manifest);
1505
- if (raw === null) return null;
1671
+ import path14 from "path";
1672
+ function scriptsOf(raw) {
1506
1673
  try {
1507
1674
  const pkg = JSON.parse(raw);
1508
- return pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? Object.keys(pkg.scripts) : [];
1675
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return null;
1676
+ if (pkg.scripts === void 0) return [];
1677
+ 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;
1509
1678
  } catch {
1510
1679
  return null;
1511
1680
  }
1512
1681
  }
1682
+ async function commandInputs(root, docs) {
1683
+ const contents = /* @__PURE__ */ new Map();
1684
+ const read = async (file) => {
1685
+ if (!contents.has(file)) contents.set(file, await readAuditInput(root, file));
1686
+ return contents.get(file);
1687
+ };
1688
+ await read("package.json");
1689
+ let inventory;
1690
+ const resolutions = [];
1691
+ for (const doc of docs) for (const claim of doc.claims.commands) {
1692
+ const base = documentScope(doc.path);
1693
+ const directory = scopedPath(base, claim.directory ?? ".");
1694
+ const scope = {
1695
+ basis: claim.directory === void 0 ? "document" : "explicit",
1696
+ directory: directory ?? base,
1697
+ candidates: []
1698
+ };
1699
+ const resolution = { doc: doc.path, claim, scope, manifest: null, scripts: [], elsewhere: [] };
1700
+ resolutions.push(resolution);
1701
+ if (claim.scopeUnknown || directory === null) {
1702
+ resolution.problem = claim.scopeUnknown ?? "The command's directory leaves the repository.";
1703
+ continue;
1704
+ }
1705
+ let cursor = directory;
1706
+ while (true) {
1707
+ const manifest = path14.posix.join(cursor, "package.json");
1708
+ scope.candidates.push(manifest);
1709
+ const raw = await read(manifest);
1710
+ if (raw !== null) {
1711
+ resolution.manifest = manifest;
1712
+ const scripts = scriptsOf(raw);
1713
+ if (scripts === null) resolution.problem = "Invalid package manifest: " + manifest;
1714
+ else resolution.scripts = scripts;
1715
+ break;
1716
+ }
1717
+ if (claim.directory !== void 0 || cursor === ".") break;
1718
+ cursor = path14.posix.dirname(cursor);
1719
+ }
1720
+ if (!resolution.manifest && !resolution.problem) {
1721
+ resolution.problem = `No package.json for the command's scope (${directory}).`;
1722
+ }
1723
+ if (resolution.problem || resolution.scripts.includes(claim.scriptName) || claim.directory !== void 0) continue;
1724
+ inventory ??= await commandManifests(root);
1725
+ for (const manifest of [.../* @__PURE__ */ new Set(["package.json", ...inventory])].sort()) {
1726
+ if (scope.candidates.includes(manifest)) continue;
1727
+ scope.candidates.push(manifest);
1728
+ const raw = await read(manifest);
1729
+ if (raw === null) continue;
1730
+ const scripts = scriptsOf(raw);
1731
+ if (scripts === null) resolution.problem = "Cannot establish script availability: invalid manifest " + manifest;
1732
+ else if (scripts.includes(claim.scriptName)) resolution.elsewhere.push(manifest);
1733
+ }
1734
+ }
1735
+ return { manifests: [...contents].sort(([a], [b]) => a.localeCompare(b)), resolutions };
1736
+ }
1513
1737
  async function checkDeadCommands(ctx) {
1514
1738
  const result = emptyResult();
1515
- const commandClaims = ctx.docs.flatMap(
1516
- (doc) => doc.claims.commands.map((claim) => ({ doc, claim }))
1517
- );
1518
- if (commandClaims.length === 0) return result;
1519
- const rootScripts = await scriptsOf(ctx.root, "package.json");
1520
- if (rootScripts === null) {
1521
- result.skipped.push({
1522
- check: "dead-command",
1523
- reason: "no package.json at the repo root"
1524
- });
1525
- return result;
1526
- }
1527
- const rootSet = new Set(rootScripts);
1528
- let workspaceScripts = null;
1529
- let manifestsChecked = ["package.json"];
1530
- const loadWorkspaceScripts = async () => {
1531
- if (workspaceScripts !== null) return workspaceScripts;
1532
- workspaceScripts = /* @__PURE__ */ new Set();
1533
- const manifests = await commandManifests(ctx.root);
1534
- manifestsChecked = ["package.json", ...manifests.sort()];
1535
- for (const manifest of manifests) {
1536
- const scripts = await scriptsOf(ctx.root, manifest);
1537
- for (const name of scripts ?? []) workspaceScripts.add(name);
1739
+ const { resolutions } = await commandInputs(ctx.root, ctx.docs);
1740
+ for (const { doc, claim, scope, scripts, elsewhere, problem, manifest } of resolutions) {
1741
+ if (problem) {
1742
+ result.skipped.push({ check: "dead-command", doc, reason: problem });
1743
+ continue;
1538
1744
  }
1539
- return workspaceScripts;
1540
- };
1541
- for (const { doc, claim } of commandClaims) {
1542
- if (rootSet.has(claim.scriptName)) continue;
1543
- const elsewhere = await loadWorkspaceScripts();
1544
- if (elsewhere.has(claim.scriptName)) continue;
1545
- result.issues.push({
1546
- type: "dead-command",
1547
- message: `\`${claim.invocation}\` refers to script "${claim.scriptName}", which exists in no package.json`,
1548
- anchor: { doc: doc.path, line: claim.line, excerpt: claim.excerpt },
1549
- confidence: "certain",
1550
- evidence: {
1551
- kind: "missing-script",
1552
- scriptName: claim.scriptName,
1553
- invocation: claim.invocation,
1554
- manifestsChecked,
1555
- availableScripts: rootScripts.slice(0, AVAILABLE_SCRIPTS_CAP)
1556
- }
1557
- });
1745
+ if (scripts.includes(claim.scriptName)) continue;
1746
+ const evidence = {
1747
+ kind: "missing-script",
1748
+ scriptName: claim.scriptName,
1749
+ invocation: claim.invocation,
1750
+ manifestsChecked: scope.candidates,
1751
+ availableScripts: scripts.slice(0, AVAILABLE_SCRIPTS_CAP),
1752
+ scope
1753
+ };
1754
+ const anchor = { doc, line: claim.line, excerpt: claim.excerpt };
1755
+ const message2 = `\`${claim.invocation}\` names script "${claim.scriptName}", which is absent from ${manifest}`;
1756
+ const certain = documentScope(doc) === "." && (scope.basis === "explicit" || !elsewhere.length);
1757
+ if (certain) result.issues.push({ type: "dead-command", message: message2, anchor, confidence: "certain", evidence });
1758
+ else result.advisories.push({ resolution: "recheck", type: "dead-command", message: message2 + (elsewhere.length ? `; it exists in ${elsewhere.join(", ")}. Clarify the intended working directory.` : "; the intended working directory needs review."), anchor, evidence });
1558
1759
  }
1559
1760
  return result;
1560
1761
  }
@@ -1569,6 +1770,8 @@ var init_dead_command = __esm({
1569
1770
  "src/audit/checks/dead-command.ts"() {
1570
1771
  "use strict";
1571
1772
  init_inputs();
1773
+ init_docs();
1774
+ init_scope();
1572
1775
  init_checks();
1573
1776
  AVAILABLE_SCRIPTS_CAP = 30;
1574
1777
  }
@@ -1582,13 +1785,13 @@ async function git(root, args) {
1582
1785
  }
1583
1786
  function withoutAndroidReleaseValues(text2) {
1584
1787
  if (/\/\*|"""|'''/.test(text2) || !/id\s*\(?\s*["']com\.android\.(application|library)["']/.test(text2)) return null;
1585
- const scopes = [];
1788
+ const scopes2 = [];
1586
1789
  const normalized = [];
1587
1790
  let assignments = 0;
1588
1791
  for (const line of text2.split("\n")) {
1589
1792
  const code = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\/\/.*$/g, (token) => token.startsWith("//") ? "" : '""');
1590
1793
  const assignment = line.match(/^(\s*)(versionName|versionCode)(\s*(?:=\s*|\s+))("[A-Za-z0-9._+-]+"|'[A-Za-z0-9._+-]+'|\d+)(\s*)$/);
1591
- if (assignment && scopes.join("/") === "android/defaultConfig" && (assignment[2] === "versionCode" ? /^\d+$/.test(assignment[4]) : /^["']/.test(assignment[4]))) {
1794
+ if (assignment && scopes2.join("/") === "android/defaultConfig" && (assignment[2] === "versionCode" ? /^\d+$/.test(assignment[4]) : /^["']/.test(assignment[4]))) {
1592
1795
  normalized.push(assignment[1] + assignment[2] + assignment[3] + "<release-value>" + assignment[5]);
1593
1796
  assignments++;
1594
1797
  } else {
@@ -1597,12 +1800,12 @@ function withoutAndroidReleaseValues(text2) {
1597
1800
  }
1598
1801
  const named = code.match(/^\s*(android|defaultConfig)\s*\{\s*$/)?.[1];
1599
1802
  for (const brace of code.matchAll(/[{}]/g)) {
1600
- if (brace[0] === "{") scopes.push(named ?? "unknown");
1601
- else if (!scopes.length) return null;
1602
- else scopes.pop();
1803
+ if (brace[0] === "{") scopes2.push(named ?? "unknown");
1804
+ else if (!scopes2.length) return null;
1805
+ else scopes2.pop();
1603
1806
  }
1604
1807
  }
1605
- return assignments && !scopes.length ? normalized.join("\n") : null;
1808
+ return assignments && !scopes2.length ? normalized.join("\n") : null;
1606
1809
  }
1607
1810
  async function releaseMetadataOnly(root, commit) {
1608
1811
  if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file))) return false;
@@ -1633,6 +1836,12 @@ var init_release_metadata = __esm({
1633
1836
  });
1634
1837
 
1635
1838
  // src/audit/checks/deps-changed.ts
1839
+ function manifestPathspecs(doc) {
1840
+ const scope = documentScope(doc);
1841
+ if (scope === ".") return MANIFEST_PATHSPECS;
1842
+ const escaped = scope.replace(/[\\*?\[\]]/g, "\\$&");
1843
+ return MANIFEST_PATHSPECS.map((spec) => spec.startsWith(":(glob)") ? ":(glob)" + escaped + "/" + spec.slice(":(glob)".length) : ":(literal)" + scope + "/" + spec);
1844
+ }
1636
1845
  async function checkDepsChanged(ctx) {
1637
1846
  const result = emptyResult();
1638
1847
  result.suppressedAdvisories = [];
@@ -1656,7 +1865,7 @@ async function checkDepsChanged(ctx) {
1656
1865
  const range = await commitsTouchingSince(
1657
1866
  ctx.root,
1658
1867
  doc.lastCommit.hash,
1659
- MANIFEST_PATHSPECS
1868
+ manifestPathspecs(doc.path)
1660
1869
  );
1661
1870
  if (range === null) {
1662
1871
  result.skipped.push({
@@ -1695,6 +1904,7 @@ var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
1695
1904
  var init_deps_changed = __esm({
1696
1905
  "src/audit/checks/deps-changed.ts"() {
1697
1906
  "use strict";
1907
+ init_docs();
1698
1908
  init_release_metadata();
1699
1909
  init_git();
1700
1910
  init_checks();
@@ -1717,7 +1927,7 @@ var init_deps_changed = __esm({
1717
1927
  });
1718
1928
 
1719
1929
  // src/context/lexical.ts
1720
- import path12 from "path";
1930
+ import path15 from "path";
1721
1931
  var init_lexical = __esm({
1722
1932
  "src/context/lexical.ts"() {
1723
1933
  "use strict";
@@ -1900,15 +2110,16 @@ var init_provenance = __esm({
1900
2110
  });
1901
2111
 
1902
2112
  // src/decisions/decisions.ts
1903
- import fs8 from "fs/promises";
1904
- import path13 from "path";
2113
+ import fs9 from "fs/promises";
2114
+ import path16 from "path";
1905
2115
  import { createHash } from "crypto";
2116
+ import { z as z4 } from "zod";
1906
2117
  async function loadDecisionStore(rootDir) {
1907
2118
  const records = [];
1908
2119
  const diagnostics = [];
1909
2120
  let entries;
1910
2121
  try {
1911
- entries = await fs8.readdir(await storePath(rootDir, ".mason/decisions"));
2122
+ entries = await fs9.readdir(await storePath(rootDir, ".mason/decisions"));
1912
2123
  } catch (error) {
1913
2124
  if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
1914
2125
  return { records, diagnostics };
@@ -1926,6 +2137,7 @@ async function loadDecisionStore(rootDir) {
1926
2137
  }
1927
2138
  return { records, diagnostics };
1928
2139
  }
2140
+ var TITLE_MAX_CHARS, BODY_RECOMMENDED_CHARS, BODY_MAX_CHARS, boundedText, decisionTitleSchema, decisionBodySchema;
1929
2141
  var init_decisions = __esm({
1930
2142
  "src/decisions/decisions.ts"() {
1931
2143
  "use strict";
@@ -1934,13 +2146,25 @@ var init_decisions = __esm({
1934
2146
  init_snapshot();
1935
2147
  init_lexical();
1936
2148
  init_provenance();
2149
+ TITLE_MAX_CHARS = 80;
2150
+ BODY_RECOMMENDED_CHARS = 1500;
2151
+ BODY_MAX_CHARS = 2500;
2152
+ boundedText = (field, max, hint) => z4.string({
2153
+ 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 })
2154
+ }).trim().min(1, `${field} must be non-empty`).max(max);
2155
+ decisionTitleSchema = boundedText("title", TITLE_MAX_CHARS, "Tighten it to a specific headline.");
2156
+ decisionBodySchema = boundedText(
2157
+ "body",
2158
+ BODY_MAX_CHARS,
2159
+ `Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer. Preserve the rule, reason, and exceptions; put supporting references in sources.`
2160
+ );
1937
2161
  }
1938
2162
  });
1939
2163
 
1940
2164
  // src/decisions/drift.ts
1941
- import path14 from "path";
2165
+ import path17 from "path";
1942
2166
  async function computeDecisionDrift(rootDir, decisions) {
1943
- const resolvedRoot = path14.resolve(rootDir);
2167
+ const resolvedRoot = path17.resolve(rootDir);
1944
2168
  const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
1945
2169
  const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
1946
2170
  const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
@@ -2056,11 +2280,303 @@ var init_checks = __esm({
2056
2280
  }
2057
2281
  });
2058
2282
 
2283
+ // src/audit/findings.ts
2284
+ import { createHash as createHash2 } from "crypto";
2285
+ import { z as z5 } from "zod";
2286
+ function findingId(finding) {
2287
+ const e = finding.evidence;
2288
+ let key;
2289
+ switch (e.kind) {
2290
+ case "missing-path":
2291
+ key = e.resolvedPath && e.resolvedPath !== e.claimed ? [e.claimed, e.resolvedPath] : e.claimed;
2292
+ break;
2293
+ case "unmentioned-dir":
2294
+ key = e.dir;
2295
+ break;
2296
+ case "count-mismatch":
2297
+ key = [e.unit.replace(/s$/, ""), e.countedFrom];
2298
+ break;
2299
+ case "missing-script":
2300
+ key = e.scope && e.scope.directory !== "." ? [e.scriptName, e.scope.directory] : e.scriptName;
2301
+ break;
2302
+ case "doc-behind-manifests":
2303
+ key = null;
2304
+ break;
2305
+ case "decision-anchor":
2306
+ key = [e.decisionId, e.provenance?.revision, e.provenance?.approval];
2307
+ break;
2308
+ }
2309
+ return digest([finding.type, finding.anchor.doc, key]);
2310
+ }
2311
+ var digest, checkSchema, commitSchema, anchorSchema, count, scopeSchema, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema;
2312
+ var init_findings = __esm({
2313
+ "src/audit/findings.ts"() {
2314
+ "use strict";
2315
+ digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
2316
+ checkSchema = z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
2317
+ commitSchema = z5.object({ hash: z5.string().regex(/^[a-f0-9]{40,64}$/), date: z5.string(), subject: z5.string() });
2318
+ anchorSchema = z5.object({ doc: z5.string(), line: z5.number().int().positive().nullable(), excerpt: z5.string().nullable() });
2319
+ count = z5.number().int().nonnegative();
2320
+ scopeSchema = z5.object({
2321
+ basis: z5.enum(["repository", "document", "document-link", "explicit"]),
2322
+ directory: z5.string(),
2323
+ candidates: z5.array(z5.string())
2324
+ });
2325
+ evidenceSchema = z5.discriminatedUnion("kind", [
2326
+ z5.object({
2327
+ kind: z5.literal("missing-path"),
2328
+ claimed: z5.string(),
2329
+ renamedTo: z5.string().nullable(),
2330
+ deletedInCommit: commitSchema.nullable(),
2331
+ everTracked: z5.boolean(),
2332
+ parentDirExists: z5.boolean(),
2333
+ resolvedPath: z5.string().optional(),
2334
+ scope: scopeSchema.optional()
2335
+ }),
2336
+ z5.object({
2337
+ kind: z5.literal("unmentioned-dir"),
2338
+ dir: z5.string(),
2339
+ sourceFileCount: count,
2340
+ firstCommit: commitSchema.nullable(),
2341
+ checkedDocs: z5.array(z5.string())
2342
+ }),
2343
+ z5.object({
2344
+ kind: z5.literal("count-mismatch"),
2345
+ claimed: count,
2346
+ actual: count,
2347
+ unit: z5.string(),
2348
+ countedFrom: z5.string(),
2349
+ members: z5.array(z5.string())
2350
+ }),
2351
+ z5.object({
2352
+ kind: z5.literal("missing-script"),
2353
+ scriptName: z5.string(),
2354
+ invocation: z5.string(),
2355
+ manifestsChecked: z5.array(z5.string()),
2356
+ availableScripts: z5.array(z5.string()),
2357
+ scope: scopeSchema.optional()
2358
+ }),
2359
+ z5.object({
2360
+ kind: z5.literal("doc-behind-manifests"),
2361
+ docLastCommit: commitSchema,
2362
+ manifestCommits: z5.array(commitSchema.extend({ files: z5.array(z5.string()) })),
2363
+ totalCommits: count
2364
+ }),
2365
+ z5.object({
2366
+ kind: z5.literal("decision-anchor"),
2367
+ decisionId: z5.string(),
2368
+ title: z5.string(),
2369
+ changedFiles: z5.array(z5.string()),
2370
+ refreshedHash: z5.string(),
2371
+ provenance: z5.object({}).passthrough().optional()
2372
+ })
2373
+ ]);
2374
+ findingSchema = z5.object({ message: z5.string(), anchor: anchorSchema, evidence: evidenceSchema });
2375
+ issueSchema = findingSchema.extend({
2376
+ type: z5.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
2377
+ confidence: z5.enum(["certain", "likely"])
2378
+ });
2379
+ advisorySchema = findingSchema.extend({ type: checkSchema, resolution: z5.literal("recheck").optional() });
2380
+ checkResultSchema = z5.object({
2381
+ issues: z5.array(issueSchema),
2382
+ advisories: z5.array(advisorySchema),
2383
+ suppressedAdvisories: z5.array(advisorySchema).optional(),
2384
+ skipped: z5.array(z5.object({ check: z5.string(), reason: z5.string(), doc: z5.string().optional() }))
2385
+ });
2386
+ }
2387
+ });
2388
+
2389
+ // src/audit/advisory-review.ts
2390
+ import fs10 from "fs/promises";
2391
+ import { execFile as execFile8 } from "child_process";
2392
+ import { promisify as promisify8 } from "util";
2393
+ import { z as z6 } from "zod";
2394
+ async function readRecord(root, id) {
2395
+ const raw = await readStoreJson(root, recordPath(id));
2396
+ if (raw === null) return null;
2397
+ const record = recordSchema.parse(raw);
2398
+ const { digest: stored, ...payload } = record;
2399
+ 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]))) {
2400
+ throw new Error("Advisory review record is inconsistent or modified; inspect its history.");
2401
+ }
2402
+ return record;
2403
+ }
2404
+ function scopes(finding) {
2405
+ const files = [finding.anchor.doc], e = finding.evidence;
2406
+ if (e.kind === "decision-anchor") throw new Error("Use review_decision for decision acceptance, reaffirmation or retirement.");
2407
+ if (e.kind === "doc-behind-manifests") return [`:(literal)${finding.anchor.doc}`, ...manifestPathspecs(finding.anchor.doc)];
2408
+ if (e.kind === "missing-path") files.push(...e.scope?.candidates ?? [e.resolvedPath ?? e.claimed]);
2409
+ else if (e.kind === "missing-script") files.push(...e.manifestsChecked, ...e.scope?.candidates ?? []);
2410
+ else if (e.kind === "unmentioned-dir") files.push(e.dir, ...e.checkedDocs);
2411
+ else throw new Error("This advisory has no supported review scope.");
2412
+ return [...new Set(files)].map((file) => {
2413
+ if (normalizeRepoPath(file) !== file) throw new Error("Advisory contains an invalid scoped path: " + file);
2414
+ return `:(literal)${file}`;
2415
+ }).sort();
2416
+ }
2417
+ async function reviewEvidence(root, finding) {
2418
+ if (normalizeRepoPath(finding.anchor.doc) !== finding.anchor.doc) throw new Error("Invalid advisory document path.");
2419
+ const scope = scopes(finding);
2420
+ const head = (await git2(root, "rev-parse", "HEAD")).trim();
2421
+ const tracked = (await git2(root, "ls-files", "--cached", "-z", "--", ...scope)).split("\0").filter(Boolean);
2422
+ if (tracked.length > 2e4) throw new Error("Advisory scope exceeds 20,000 tracked files.");
2423
+ const [tree, status, lastScopeCommit, document] = await Promise.all([
2424
+ tracked.length ? git2(root, "ls-tree", "-r", "-z", "HEAD", "--", ...tracked.map((file) => `:(literal)${file}`)) : "",
2425
+ git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...scope),
2426
+ git2(root, "log", "-1", "--format=%H", "HEAD", "--", ...scope).then((value) => value.trim()),
2427
+ readAuditInput(root, finding.anchor.doc)
2428
+ ]);
2429
+ if (!document?.trim()) throw new Error("Original advisory document is missing, empty or unreadable.");
2430
+ 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.");
2431
+ for (const entry of tree.split("\0").filter(Boolean)) {
2432
+ const tab = entry.indexOf(" "), file = entry.slice(tab + 1);
2433
+ if (entry.startsWith("120000 ") || entry.startsWith("160000 ")) throw new Error("Symlink or submodule in advisory review scope: " + file);
2434
+ await storePath(root, file);
2435
+ }
2436
+ const paths = finding.evidence.kind === "missing-path" ? await Promise.all(
2437
+ (finding.evidence.scope?.candidates ?? [finding.evidence.resolvedPath ?? finding.evidence.claimed]).map(async (file) => ({ path: file, exists: await pathExists(root, file) }))
2438
+ ) : [];
2439
+ let dirty = status.length > 0;
2440
+ if (finding.evidence.kind === "missing-script") {
2441
+ for (const file of [...finding.evidence.manifestsChecked, ...finding.evidence.scope?.candidates ?? []]) {
2442
+ if (!tracked.includes(file) && await pathExists(root, file)) dirty = true;
2443
+ }
2444
+ }
2445
+ const fingerprint = digest([scope, tree, lastScopeCommit, paths]);
2446
+ if ((await git2(root, "rev-parse", "HEAD")).trim() !== head) throw new Error("HEAD changed while collecting review evidence.");
2447
+ return {
2448
+ ...evidenceSchema2.parse({ head, scope, tree, lastScopeCommit, paths, fingerprint }),
2449
+ dirty,
2450
+ documentDigest: digest(document)
2451
+ };
2452
+ }
2453
+ async function decisionAssessment(root, finding, baselineHead) {
2454
+ const evidence = finding.evidence;
2455
+ if (evidence.kind !== "decision-anchor") return null;
2456
+ const id = findingId(finding);
2457
+ const store = await loadDecisionStore(root);
2458
+ if (store.diagnostics.length) throw new Error("Decision records have unavailable or malformed evidence; review cannot be confirmed.");
2459
+ const record = store.records.find((record2) => record2.id === evidence.decisionId);
2460
+ if (!record || record.version !== 2) return null;
2461
+ const events2 = record.history.filter((event2) => ["accepted", "reaffirmed", "retired"].includes(event2.kind));
2462
+ const event = events2.at(-1);
2463
+ if (!event?.evidence || !event.actor || !event.note || !event.evidence.historyAvailable || event.evidence.localChanges.length) return null;
2464
+ try {
2465
+ await git2(root, "merge-base", "--is-ancestor", baselineHead, event.evidence.headHash);
2466
+ } catch {
2467
+ return null;
2468
+ }
2469
+ const drift = await computeDecisionDrift(root, [record]);
2470
+ if (!drift.historyAvailable) throw new Error("Decision review history is unavailable.");
2471
+ const retired = record.status === "retired" && event.kind === "retired";
2472
+ const current = retired || record.status === "active" && record.approval === "accepted" && drift.freshness?.[record.id] === "current" && !drift.pendingProposals?.[record.id];
2473
+ return {
2474
+ id,
2475
+ status: current ? "current" : "reopened",
2476
+ outcome: event.kind,
2477
+ reviewer: event.actor,
2478
+ note: event.note,
2479
+ reviewedHead: event.evidence.headHash,
2480
+ recordPath: `.mason/decisions/${record.id}.json`,
2481
+ 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."
2482
+ };
2483
+ }
2484
+ async function assessAdvisory(root, finding, baselineHead) {
2485
+ if ("confidence" in finding) return null;
2486
+ const id = findingId(finding);
2487
+ try {
2488
+ if (finding.evidence.kind === "decision-anchor") return await decisionAssessment(root, finding, baselineHead);
2489
+ const record = await readRecord(root, id);
2490
+ if (!record) return null;
2491
+ const event = record.events.at(-1);
2492
+ const current = await reviewEvidence(root, event.finding);
2493
+ await git2(root, "merge-base", "--is-ancestor", event.evidence.head, current.head);
2494
+ const base = {
2495
+ id,
2496
+ recordPath: recordPath(id),
2497
+ reviewer: event.reviewer,
2498
+ note: event.note,
2499
+ outcome: event.outcome,
2500
+ reviewedHead: event.evidence.head
2501
+ };
2502
+ 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." };
2503
+ 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." };
2504
+ } catch (error) {
2505
+ return { id, status: "unverified", reason: error instanceof Error ? error.message : String(error) };
2506
+ }
2507
+ }
2508
+ async function advisoryReviewInventory(root) {
2509
+ let names;
2510
+ try {
2511
+ names = await fs10.readdir(await storePath(root, ".mason/reviews/advisories"));
2512
+ } catch (error) {
2513
+ if (error.code === "ENOENT") return [];
2514
+ throw error;
2515
+ }
2516
+ if (names.length > 5e3) throw new Error("Advisory review inventory exceeds 5,000 records.");
2517
+ return Promise.all(names.filter((name) => name.endsWith(".json")).sort().map(async (name) => [name, await readAuditInput(root, ".mason/reviews/advisories/" + name)]));
2518
+ }
2519
+ var execute2, git2, hashSchema, commitSchema2, evidenceSchema2, eventSchema2, recordSchema, reviewSummarySchema, advisoryReviewRequest, recordPath;
2520
+ var init_advisory_review = __esm({
2521
+ "src/audit/advisory-review.ts"() {
2522
+ "use strict";
2523
+ init_findings();
2524
+ init_deps_changed();
2525
+ init_inputs();
2526
+ init_scope();
2527
+ init_storage();
2528
+ init_paths();
2529
+ init_store();
2530
+ init_decisions();
2531
+ init_drift2();
2532
+ execute2 = promisify8(execFile8);
2533
+ git2 = async (root, ...args) => (await execute2("git", args, { cwd: root, timeout: 1e4, maxBuffer: 8 * 1024 * 1024 })).stdout;
2534
+ hashSchema = z6.string().regex(/^[a-f0-9]{64}$/);
2535
+ commitSchema2 = z6.string().regex(/^[a-f0-9]{40,64}$/);
2536
+ evidenceSchema2 = z6.object({
2537
+ head: commitSchema2,
2538
+ fingerprint: hashSchema,
2539
+ scope: z6.array(z6.string()).min(1).max(1e3),
2540
+ tree: z6.string().max(2 * 1024 * 1024),
2541
+ lastScopeCommit: commitSchema2,
2542
+ paths: z6.array(z6.object({ path: z6.string(), exists: z6.boolean() })).max(1e3)
2543
+ });
2544
+ eventSchema2 = z6.object({
2545
+ at: z6.string().datetime(),
2546
+ reviewer: z6.string().trim().min(1).max(200),
2547
+ note: z6.string().trim().min(1).max(2500),
2548
+ outcome: z6.enum(["addressed", "inapplicable", "deferred"]),
2549
+ finding: advisorySchema.refine((finding) => finding.evidence.kind !== "decision-anchor", "Decision findings use decision reviews"),
2550
+ evidence: evidenceSchema2
2551
+ });
2552
+ recordSchema = z6.object({ version: z6.literal(1), id: hashSchema, events: z6.array(eventSchema2).min(1).max(200), digest: hashSchema });
2553
+ reviewSummarySchema = z6.object({
2554
+ id: hashSchema,
2555
+ status: z6.enum(["current", "reopened", "deferred", "unverified"]),
2556
+ reason: z6.string(),
2557
+ recordPath: z6.string().optional(),
2558
+ reviewer: z6.string().optional(),
2559
+ note: z6.string().optional(),
2560
+ outcome: z6.enum(["addressed", "inapplicable", "deferred", "accepted", "reaffirmed", "retired"]).optional(),
2561
+ reviewedHead: z6.string().optional()
2562
+ });
2563
+ advisoryReviewRequest = z6.object({
2564
+ baselinePath: z6.string().min(1),
2565
+ findingId: hashSchema,
2566
+ action: z6.enum(["prepare", "addressed", "inapplicable", "deferred"]).default("prepare"),
2567
+ reviewer: eventSchema2.shape.reviewer.optional(),
2568
+ note: eventSchema2.shape.note.optional(),
2569
+ reviewToken: hashSchema.optional()
2570
+ });
2571
+ recordPath = (id) => `.mason/reviews/advisories/${hashSchema.parse(id)}.json`;
2572
+ }
2573
+ });
2574
+
2059
2575
  // src/audit/audit.ts
2060
- import fs9 from "fs/promises";
2061
- import path15 from "path";
2576
+ import fs11 from "fs/promises";
2577
+ import path18 from "path";
2062
2578
  async function computeAudit(rootDir, options = {}) {
2063
- const resolvedRoot = path15.resolve(rootDir);
2579
+ const resolvedRoot = path18.resolve(rootDir);
2064
2580
  const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
2065
2581
  if (docs.length === 0) return null;
2066
2582
  const report = {
@@ -2071,6 +2587,8 @@ async function computeAudit(rootDir, options = {}) {
2071
2587
  checksRun: [],
2072
2588
  docs: docs.map((d) => ({
2073
2589
  path: d.path,
2590
+ scope: d.scope,
2591
+ kind: d.kind,
2074
2592
  lastCommit: d.lastCommit,
2075
2593
  dirty: d.dirty,
2076
2594
  lineCount: d.lineCount
@@ -2092,7 +2610,7 @@ async function computeAudit(rootDir, options = {}) {
2092
2610
  }
2093
2611
  let decisionsPresent = false;
2094
2612
  try {
2095
- await fs9.access(path15.join(resolvedRoot, ".mason", "decisions"));
2613
+ await fs11.access(path18.join(resolvedRoot, ".mason", "decisions"));
2096
2614
  decisionsPresent = true;
2097
2615
  } catch {
2098
2616
  }
@@ -2115,6 +2633,8 @@ async function computeAudit(rootDir, options = {}) {
2115
2633
  report.skippedChecks.push(...skipped);
2116
2634
  }
2117
2635
  report.clean = report.issues.length === 0;
2636
+ const reviews = (await Promise.all([...report.advisories, ...report.suppressedAdvisories].map((finding) => assessAdvisory(resolvedRoot, finding, headHash)))).filter((review) => review !== null);
2637
+ if (reviews.length) report.advisoryReviews = reviews;
2118
2638
  return report;
2119
2639
  }
2120
2640
  var init_audit = __esm({
@@ -2125,45 +2645,24 @@ var init_audit = __esm({
2125
2645
  init_docs();
2126
2646
  init_types();
2127
2647
  init_checks();
2648
+ init_advisory_review();
2128
2649
  }
2129
2650
  });
2130
2651
 
2131
2652
  // src/audit/repair.ts
2132
- import fs10 from "fs/promises";
2133
- import path16 from "path";
2134
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
2135
- import { z as z4 } from "zod";
2136
- function findingId(finding) {
2137
- const e = finding.evidence;
2138
- let key;
2139
- switch (e.kind) {
2140
- case "missing-path":
2141
- key = e.claimed;
2142
- break;
2143
- case "unmentioned-dir":
2144
- key = e.dir;
2145
- break;
2146
- case "count-mismatch":
2147
- key = [e.unit.replace(/s$/, ""), e.countedFrom];
2148
- break;
2149
- case "missing-script":
2150
- key = e.scriptName;
2151
- break;
2152
- case "doc-behind-manifests":
2153
- key = null;
2154
- break;
2155
- case "decision-anchor":
2156
- key = [e.decisionId, e.provenance?.revision, e.provenance?.approval];
2157
- break;
2158
- }
2159
- return digest([finding.type, finding.anchor.doc, key]);
2653
+ import fs12 from "fs/promises";
2654
+ import path19 from "path";
2655
+ import { randomUUID as randomUUID2 } from "crypto";
2656
+ import { z as z7 } from "zod";
2657
+ function recheckable(finding) {
2658
+ 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);
2160
2659
  }
2161
2660
  function allFindings(report) {
2162
2661
  return [...report.issues, ...report.advisories, ...report.suppressedAdvisories ?? []];
2163
2662
  }
2164
2663
  async function docState(root) {
2165
2664
  const docs = [];
2166
- for (const doc of DOC_CANDIDATES) {
2665
+ for (const doc of await discoverDocPaths(root)) {
2167
2666
  try {
2168
2667
  const content2 = await readBoundedFile(await storePath(root, doc), 10 * 1024 * 1024);
2169
2668
  if (content2 === null) throw new Error("Context file is not regular or exceeds 10 MiB: " + doc);
@@ -2185,8 +2684,8 @@ async function stableAudit(root, checks, options = {}) {
2185
2684
  return report;
2186
2685
  }
2187
2686
  async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
2188
- const root = await fs10.realpath(rootDir);
2189
- const selected = z4.array(checkSchema).nonempty().parse(checks);
2687
+ const root = await fs12.realpath(rootDir);
2688
+ const selected = z7.array(checkSchema).nonempty().parse(checks);
2190
2689
  const report = await stableAudit(root, selected, options);
2191
2690
  if (!report) throw new Error("No context files found to prepare a repair.");
2192
2691
  if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
@@ -2197,27 +2696,33 @@ async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
2197
2696
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2198
2697
  report: storedReport
2199
2698
  };
2200
- const baselinePath = ".mason/reports/repairs/" + randomUUID3() + ".json";
2699
+ const baselinePath = ".mason/reports/repairs/" + randomUUID2() + ".json";
2201
2700
  await writeStoreJson(root, baselinePath, { ...payload, digest: digest(payload) });
2202
2701
  return { version: 1, action: "prepare", baselinePath, report };
2203
2702
  }
2204
- async function verifyRepair(rootDir, baselinePath, options = {}) {
2205
- const root = await fs10.realpath(rootDir);
2206
- const declaredRoot = path16.resolve(rootDir);
2207
- const relative = path16.isAbsolute(baselinePath) ? path16.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
2703
+ async function loadRepairBaseline(rootDir, baselinePath) {
2704
+ const root = await fs12.realpath(rootDir);
2705
+ const declaredRoot = path19.resolve(rootDir);
2706
+ const relative = path19.isAbsolute(baselinePath) ? path19.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
2208
2707
  const stored = baselineSchema.parse(await readStoreJson(root, relative));
2209
2708
  const { digest: savedDigest, ...payload } = stored;
2210
2709
  if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
2211
2710
  if (stored.report.root !== root) throw new Error("Repair baseline belongs to a different repository.");
2212
2711
  const original = stored.report;
2712
+ return { root, relative, original };
2713
+ }
2714
+ async function verifyRepair(rootDir, baselinePath, options = {}) {
2715
+ const { root, relative, original } = await loadRepairBaseline(rootDir, baselinePath);
2213
2716
  const diagnostics = [];
2717
+ let reviewDigest;
2214
2718
  let current = null;
2215
2719
  try {
2720
+ reviewDigest = digest(await advisoryReviewInventory(root));
2216
2721
  current = await stableAudit(root, original.checksRun, options);
2217
2722
  if (!current) diagnostics.push("No context files remain available to audit.");
2218
2723
  else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
2219
2724
  for (const doc of original.docs) {
2220
- if (!original.issues.some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
2725
+ if (!allFindings(original).some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
2221
2726
  const content2 = await readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024);
2222
2727
  if (content2 === null || !content2.trim()) {
2223
2728
  diagnostics.push("Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair.");
@@ -2234,21 +2739,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
2234
2739
  const originalIds = new Set(originalFindings.map(findingId));
2235
2740
  const missingDocs = original.docs.filter((doc) => !current?.docs.some((d) => d.path === doc.path));
2236
2741
  for (const doc of missingDocs) diagnostics.push("Original context file " + doc.path + " is unavailable; removing it does not verify a repair.");
2237
- const findings = originalFindings.map((finding) => {
2742
+ const findings = await Promise.all(originalFindings.map(async (finding) => {
2238
2743
  const id = findingId(finding);
2239
2744
  const now = currentById.get(id);
2240
2745
  const base = { id, original: finding, ...now ? { current: now } : {} };
2241
2746
  if (diagnostics.length || !current) {
2242
2747
  return { ...base, status: "unverified", reason: "The original audit scope could not be verified. See diagnostics." };
2243
2748
  }
2244
- if ("confidence" in finding && now) {
2749
+ if (now && ("confidence" in finding || "confidence" in now)) {
2245
2750
  return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
2246
2751
  }
2247
2752
  const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
2248
2753
  if (!current.checksRun?.includes(finding.type) || skipped.length) {
2249
2754
  return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
2250
2755
  }
2251
- if (!("confidence" in finding)) {
2756
+ const review = await assessAdvisory(root, now ?? finding, original.headHash);
2757
+ if (review) {
2758
+ if (review.status === "unverified") return { ...base, review, status: "unverified", reason: review.reason };
2759
+ if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
2760
+ return { ...base, review, status: "review-required", reason: review.reason };
2761
+ }
2762
+ if (!("confidence" in finding) && (!recheckable(finding) || now)) {
2252
2763
  return {
2253
2764
  ...base,
2254
2765
  status: "review-required",
@@ -2256,11 +2767,27 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
2256
2767
  };
2257
2768
  }
2258
2769
  return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
2259
- });
2770
+ }));
2771
+ try {
2772
+ if (reviewDigest !== digest(await advisoryReviewInventory(root)) || current?.headHash !== await getCurrentGitHash(root)) {
2773
+ diagnostics.push("HEAD or advisory review records changed during verification; repeat against stable evidence.");
2774
+ for (const finding of findings) {
2775
+ finding.status = "unverified";
2776
+ finding.reason = diagnostics.at(-1);
2777
+ }
2778
+ }
2779
+ } catch (error) {
2780
+ diagnostics.push(String(error));
2781
+ for (const finding of findings) {
2782
+ finding.status = "unverified";
2783
+ finding.reason = "Advisory review evidence became unavailable during verification.";
2784
+ }
2785
+ }
2260
2786
  const newFindings = [...currentById].filter(([id]) => !originalIds.has(id)).map(([, f]) => f);
2787
+ const reviewedIds = new Set(current?.advisoryReviews?.filter((review) => review.status === "current").map((review) => review.id));
2261
2788
  const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
2262
2789
  for (const f of findings) counts[f.status]++;
2263
- const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f));
2790
+ 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)));
2264
2791
  const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
2265
2792
  return {
2266
2793
  version: 1,
@@ -2274,10 +2801,10 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
2274
2801
  diagnostics,
2275
2802
  currentAudit: current,
2276
2803
  counts,
2277
- 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."
2804
+ 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."
2278
2805
  };
2279
2806
  }
2280
- var checkSchema, commitSchema, anchorSchema, count, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema, reportSchema, baselineSchema, digest;
2807
+ var count2, reportSchema, baselineSchema, repairFindingSchema;
2281
2808
  var init_repair = __esm({
2282
2809
  "src/audit/repair.ts"() {
2283
2810
  "use strict";
@@ -2289,95 +2816,46 @@ var init_repair = __esm({
2289
2816
  init_files();
2290
2817
  init_paths();
2291
2818
  init_types();
2292
- checkSchema = z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
2293
- commitSchema = z4.object({ hash: z4.string().regex(/^[a-f0-9]{40,64}$/), date: z4.string(), subject: z4.string() });
2294
- anchorSchema = z4.object({ doc: z4.string(), line: z4.number().int().positive().nullable(), excerpt: z4.string().nullable() });
2295
- count = z4.number().int().nonnegative();
2296
- evidenceSchema = z4.discriminatedUnion("kind", [
2297
- z4.object({
2298
- kind: z4.literal("missing-path"),
2299
- claimed: z4.string(),
2300
- renamedTo: z4.string().nullable(),
2301
- deletedInCommit: commitSchema.nullable(),
2302
- everTracked: z4.boolean(),
2303
- parentDirExists: z4.boolean()
2304
- }),
2305
- z4.object({
2306
- kind: z4.literal("unmentioned-dir"),
2307
- dir: z4.string(),
2308
- sourceFileCount: count,
2309
- firstCommit: commitSchema.nullable(),
2310
- checkedDocs: z4.array(z4.string())
2311
- }),
2312
- z4.object({
2313
- kind: z4.literal("count-mismatch"),
2314
- claimed: count,
2315
- actual: count,
2316
- unit: z4.string(),
2317
- countedFrom: z4.string(),
2318
- members: z4.array(z4.string())
2319
- }),
2320
- z4.object({
2321
- kind: z4.literal("missing-script"),
2322
- scriptName: z4.string(),
2323
- invocation: z4.string(),
2324
- manifestsChecked: z4.array(z4.string()),
2325
- availableScripts: z4.array(z4.string())
2326
- }),
2327
- z4.object({
2328
- kind: z4.literal("doc-behind-manifests"),
2329
- docLastCommit: commitSchema,
2330
- manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
2331
- totalCommits: count
2332
- }),
2333
- z4.object({
2334
- kind: z4.literal("decision-anchor"),
2335
- decisionId: z4.string(),
2336
- title: z4.string(),
2337
- changedFiles: z4.array(z4.string()),
2338
- refreshedHash: z4.string(),
2339
- provenance: z4.object({}).passthrough().optional()
2340
- })
2341
- ]);
2342
- findingSchema = z4.object({ message: z4.string(), anchor: anchorSchema, evidence: evidenceSchema });
2343
- issueSchema = findingSchema.extend({
2344
- type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
2345
- confidence: z4.enum(["certain", "likely"])
2346
- });
2347
- advisorySchema = findingSchema.extend({ type: z4.enum(["deps-changed", "decision-anchor-drift"]) });
2348
- checkResultSchema = z4.object({
2349
- issues: z4.array(issueSchema),
2350
- advisories: z4.array(advisorySchema),
2351
- suppressedAdvisories: z4.array(advisorySchema).optional(),
2352
- skipped: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
2353
- });
2354
- reportSchema = z4.object({
2355
- version: z4.literal(1),
2356
- root: z4.string(),
2357
- gitAvailable: z4.literal(true),
2819
+ init_findings();
2820
+ init_advisory_review();
2821
+ init_findings();
2822
+ count2 = z7.number().int().nonnegative();
2823
+ reportSchema = z7.object({
2824
+ version: z7.literal(1),
2825
+ root: z7.string(),
2826
+ gitAvailable: z7.literal(true),
2358
2827
  headHash: commitSchema.shape.hash,
2359
- checksRun: z4.array(checkSchema).nonempty(),
2360
- docs: z4.array(z4.object({
2361
- path: z4.enum(DOC_CANDIDATES),
2828
+ checksRun: z7.array(checkSchema).nonempty(),
2829
+ docs: z7.array(z7.object({
2830
+ path: z7.string().refine((value) => normalizeRepoPath(value) === value),
2831
+ scope: z7.string().optional(),
2832
+ kind: z7.enum(["instructions", "readme"]).optional(),
2362
2833
  lastCommit: commitSchema.nullable(),
2363
- dirty: z4.boolean(),
2364
- lineCount: count
2834
+ dirty: z7.boolean(),
2835
+ lineCount: count2
2365
2836
  })).nonempty(),
2366
- decisionsChecked: z4.boolean(),
2367
- clean: z4.boolean(),
2368
- issues: z4.array(issueSchema),
2369
- advisories: z4.array(advisorySchema),
2370
- suppressedAdvisories: z4.array(advisorySchema).optional(),
2371
- skippedChecks: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
2837
+ decisionsChecked: z7.boolean(),
2838
+ clean: z7.boolean(),
2839
+ issues: z7.array(issueSchema),
2840
+ advisories: z7.array(advisorySchema),
2841
+ suppressedAdvisories: z7.array(advisorySchema).optional(),
2842
+ skippedChecks: z7.array(z7.object({ check: z7.string(), reason: z7.string(), doc: z7.string().optional() }))
2372
2843
  });
2373
- baselineSchema = z4.object({
2374
- kind: z4.literal("mason-audit-repair"),
2375
- version: z4.literal(1),
2376
- createdAt: z4.string().datetime(),
2844
+ baselineSchema = z7.object({
2845
+ kind: z7.literal("mason-audit-repair"),
2846
+ version: z7.literal(1),
2847
+ createdAt: z7.string().datetime(),
2377
2848
  report: reportSchema,
2378
- digest: z4.string().regex(/^[a-f0-9]{64}$/)
2849
+ digest: z7.string().regex(/^[a-f0-9]{64}$/)
2850
+ });
2851
+ repairFindingSchema = z7.object({
2852
+ id: z7.string(),
2853
+ original: z7.union([issueSchema, advisorySchema]),
2854
+ status: z7.enum(["resolved", "unresolved", "review-required", "unverified"]),
2855
+ reason: z7.string(),
2856
+ current: z7.union([issueSchema, advisorySchema]).optional(),
2857
+ review: reviewSummarySchema.optional()
2379
2858
  });
2380
- digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
2381
2859
  }
2382
2860
  });
2383
2861
 
@@ -2385,102 +2863,94 @@ var init_repair = __esm({
2385
2863
  var evidence_exports = {};
2386
2864
  __export(evidence_exports, {
2387
2865
  checkCache: () => checkCache,
2388
- git: () => git2,
2866
+ git: () => git3,
2389
2867
  hash: () => hash,
2390
2868
  readInputs: () => readInputs,
2391
2869
  workspace: () => workspace
2392
2870
  });
2393
- import fs11 from "fs/promises";
2394
- import path17 from "path";
2395
- import { createHash as createHash3 } from "crypto";
2396
- import { execFile as execFile8 } from "child_process";
2397
- import { promisify as promisify8 } from "util";
2398
- import fg4 from "fast-glob";
2399
- import { z as z5 } from "zod";
2400
- async function git2(root, ...args) {
2871
+ import fs13 from "fs/promises";
2872
+ import path20 from "path";
2873
+ import { createHash as createHash3 } from "crypto";
2874
+ import { execFile as execFile9 } from "child_process";
2875
+ import { promisify as promisify9 } from "util";
2876
+ import fg5 from "fast-glob";
2877
+ import { z as z8 } from "zod";
2878
+ async function git3(root, ...args) {
2401
2879
  return (await exec7("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout;
2402
2880
  }
2403
2881
  async function workspace(dir) {
2404
- const root = await fs11.realpath((await git2(dir, "rev-parse", "--show-toplevel")).trim());
2405
- const gitDir = await fs11.realpath((await git2(root, "rev-parse", "--absolute-git-dir")).trim());
2882
+ const root = await fs13.realpath((await git3(dir, "rev-parse", "--show-toplevel")).trim());
2883
+ const gitDir = await fs13.realpath((await git3(root, "rev-parse", "--absolute-git-dir")).trim());
2406
2884
  let branch;
2407
2885
  try {
2408
- branch = (await git2(root, "symbolic-ref", "--quiet", "HEAD")).trim();
2886
+ branch = (await git3(root, "symbolic-ref", "--quiet", "HEAD")).trim();
2409
2887
  } catch {
2410
2888
  branch = "detached";
2411
2889
  }
2412
2890
  return { root, gitDir, branch, directory: ".mason/reports/automation/" + hash([root, gitDir, branch]).slice(0, 24) };
2413
2891
  }
2414
2892
  async function readInputs(root) {
2893
+ const docPaths = await discoverDocPaths(root);
2415
2894
  const [headText, docStatus, shallowPath, replacements] = await Promise.all([
2416
- git2(root, "rev-parse", "HEAD"),
2417
- git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...DOC_CANDIDATES),
2418
- git2(root, "rev-parse", "--git-path", "shallow"),
2419
- git2(root, "for-each-ref", "--format=%(refname) %(objectname)", "refs/replace")
2895
+ git3(root, "rev-parse", "HEAD"),
2896
+ docPaths.length ? git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...docPaths.map((p) => `:(literal)${p}`)) : "",
2897
+ git3(root, "rev-parse", "--git-path", "shallow"),
2898
+ git3(root, "for-each-ref", "--format=%(refname) %(objectname)", "refs/replace")
2420
2899
  ]);
2421
2900
  const head = headText.trim();
2422
2901
  let shallow = null;
2423
2902
  try {
2424
- shallow = await fs11.readFile(path17.resolve(root, shallowPath.trim()), "utf8");
2903
+ shallow = await fs13.readFile(path20.resolve(root, shallowPath.trim()), "utf8");
2425
2904
  } catch (error) {
2426
2905
  if (error.code !== "ENOENT") throw error;
2427
2906
  }
2428
2907
  const docs = {};
2429
2908
  const docContents = [];
2430
2909
  const claims = [];
2431
- for (const file of DOC_CANDIDATES) {
2910
+ for (const file of docPaths) {
2432
2911
  const text2 = await content(root, file);
2433
2912
  docs[file] = text2 === null ? null : hash(text2);
2434
2913
  docContents.push([file, text2]);
2435
2914
  for (const claim of text2 ? extractClaims(text2).paths : []) {
2436
- if (internal(claim.path) || claim.path.startsWith(".mason/")) continue;
2437
- const exists2 = async (p) => fs11.access(path17.resolve(root, p)).then(() => true, () => false);
2438
- claims.push([claim.path, await exists2(claim.path), await exists2(path17.dirname(claim.path))]);
2439
- }
2440
- }
2441
- const combinedDocs = docContents.map(([, text2]) => text2 ?? "").join("\n");
2442
- const countClaims = docContents.flatMap(([, text2]) => text2 ? extractClaims(text2).counts : []);
2443
- const commandClaims = docContents.flatMap(([, text2]) => text2 ? extractClaims(text2).commands : []);
2444
- const rootPackage = await content(root, "package.json");
2445
- let rootScripts = null;
2446
- if (rootPackage !== null) {
2447
- try {
2448
- const pkg = JSON.parse(rootPackage);
2449
- rootScripts = pkg?.scripts && typeof pkg.scripts === "object" ? Object.keys(pkg.scripts) : [];
2450
- } catch {
2915
+ const scope = pathClaimScope(file, claim);
2916
+ for (const candidate of scope?.candidates ?? []) {
2917
+ if (optionalMasonPath(candidate)) continue;
2918
+ claims.push([candidate, await pathExists(root, candidate), await pathExists(root, path20.posix.dirname(candidate))]);
2919
+ }
2451
2920
  }
2452
2921
  }
2453
- const needsWorkspaceScripts = rootScripts !== null && commandClaims.some((claim) => !rootScripts.includes(claim.scriptName));
2922
+ const parsedDocs = docContents.map(([file, text2]) => ({ path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") }));
2923
+ const combinedDocs = moduleDocumentation(parsedDocs);
2924
+ const countClaims = parsedDocs.flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
2454
2925
  const decisionDirectory = await storePath(root, ".mason/decisions");
2455
- const decisionPresence = await fs11.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
2926
+ const decisionPresence = await fs13.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
2456
2927
  if (error.code === "ENOENT") return "absent";
2457
2928
  throw error;
2458
2929
  });
2459
- const [modules, counts, manifests, decisionFiles] = await Promise.all([
2930
+ const [modules, counts, commands, decisionFiles] = await Promise.all([
2460
2931
  combinedDocs ? moduleCandidates(root, combinedDocs) : [],
2461
- Promise.all(countClaims.map((claim) => resolveCountSource(root, claim))),
2462
- needsWorkspaceScripts ? commandManifests(root) : [],
2463
- fg4(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
2932
+ Promise.all(countClaims.map(({ doc, claim }) => resolveDocCountSource(root, doc, claim))),
2933
+ commandInputs(root, parsedDocs),
2934
+ fg5(".mason/decisions/*.json", { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false })
2464
2935
  ]);
2465
- const packages = [["package.json", rootPackage], ...await Promise.all(manifests.sort().map(async (file) => [file, await content(root, file)]))];
2466
2936
  const decisions = await Promise.all(decisionFiles.sort().map(async (file) => {
2467
2937
  await storePath(root, file);
2468
2938
  return [file, await content(root, file)];
2469
2939
  }));
2470
2940
  const [status, index2] = decisions.length ? await Promise.all([
2471
- git2(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
2472
- git2(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
2941
+ git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
2942
+ git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
2473
2943
  ]) : ["", ""];
2474
- const common = [3, engineVersion, head, shallow, replacements, docContents];
2944
+ const common = [4, engineVersion, head, shallow, replacements, docContents];
2475
2945
  const keys = {
2476
2946
  "deleted-reference": hash([common, claims, docStatus]),
2477
2947
  "new-module": hash([common, modules]),
2478
2948
  "stale-count": hash([common, counts]),
2479
- "dead-command": hash([common, packages]),
2949
+ "dead-command": hash([common, commands]),
2480
2950
  "deps-changed": hash([common, docStatus]),
2481
2951
  "decision-anchor-drift": hash([common, decisionPresence, decisions, status, index2])
2482
2952
  };
2483
- return { fingerprint: hash(keys), head, docs, keys };
2953
+ return { fingerprint: hash([keys, await advisoryReviewInventory(root)]), head, docs, keys };
2484
2954
  }
2485
2955
  function checkCache(raw, inputs) {
2486
2956
  let entries = {};
@@ -2507,11 +2977,12 @@ function checkCache(raw, inputs) {
2507
2977
  return { version: 1, entries: canonical, digest: hash(canonical) };
2508
2978
  } };
2509
2979
  }
2510
- var exec7, engineVersion, hash, content, internal, cacheSchema;
2980
+ var exec7, engineVersion, hash, content, cacheSchema;
2511
2981
  var init_evidence = __esm({
2512
2982
  "src/automation/evidence.ts"() {
2513
2983
  "use strict";
2514
2984
  init_docs();
2985
+ init_scope();
2515
2986
  init_claims();
2516
2987
  init_repair();
2517
2988
  init_checks();
@@ -2520,114 +2991,201 @@ var init_evidence = __esm({
2520
2991
  init_stale_count();
2521
2992
  init_dead_command();
2522
2993
  init_storage();
2523
- exec7 = promisify8(execFile8);
2524
- engineVersion = true ? "0.16.2" : "development";
2994
+ init_advisory_review();
2995
+ exec7 = promisify9(execFile9);
2996
+ engineVersion = true ? "0.17.0" : "development";
2525
2997
  hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
2526
2998
  content = readAuditInput;
2527
- internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
2528
- cacheSchema = z5.object({ version: z5.literal(1), entries: z5.record(z5.object({ key: z5.string(), result: checkResultSchema })), digest: z5.string() });
2999
+ cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
2529
3000
  }
2530
3001
  });
2531
3002
 
2532
- // src/automation/store.ts
2533
- import fs12 from "fs/promises";
3003
+ // src/automation/execution.ts
2534
3004
  import os2 from "os";
2535
- import { z as z6 } from "zod";
2536
- function parseState(raw) {
3005
+ import fs14 from "fs/promises";
3006
+ import { randomUUID as randomUUID3 } from "crypto";
3007
+ import { z as z9 } from "zod";
3008
+ function parseExecution(raw) {
2537
3009
  try {
2538
- return stateSchema.parse(raw);
3010
+ return executionSchema.parse(raw);
2539
3011
  } catch (error) {
2540
- throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
3012
+ throw new Error("Invalid automation execution store; receipt history was retained.", { cause: error });
2541
3013
  }
2542
3014
  }
2543
- async function withLock(root, directory, run) {
2544
- const file = await storePath(root, directory + "/lock", true);
2545
- const deadline = Date.now() + 5e3;
2546
- let handle;
2547
- while (!handle) {
3015
+ function automationFailure(error) {
3016
+ const recorded = failureSchema.safeParse(error?.failure);
3017
+ if (recorded.success) return recorded.data;
3018
+ const message2 = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
3019
+ const codes = /* @__PURE__ */ new Set();
3020
+ let cause = error;
3021
+ for (let i = 0; cause && i < 8; i++) {
3022
+ codes.add(String(cause.code));
3023
+ cause = cause.cause;
3024
+ }
3025
+ const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z9.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
3026
+ return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
3027
+ }
3028
+ function failureMessage(error) {
3029
+ const failure = automationFailure(error);
3030
+ return `Mason automation unavailable [${failure.code}]; evidence capture/verification was not established. ${failure.message}` + (failure.receiptRecorded ? "" : " No durable failure receipt was recorded.");
3031
+ }
3032
+ async function failureNotifications(root, directory) {
3033
+ const raw = await readStoreJson(root, directory + "/notifications.json");
3034
+ return raw === null ? { version: 1, sessions: {} } : notificationsSchema.parse(raw);
3035
+ }
3036
+ async function hookFailureMessage(dir, host, sessionId, error) {
3037
+ const failure = automationFailure(error);
3038
+ if (!failure.receiptRecorded) return failureMessage(error);
3039
+ try {
3040
+ const ws = await workspace(dir);
3041
+ return await withLock(ws.root, ws.directory + "/notifications", async () => {
3042
+ const notices = await failureNotifications(ws.root, ws.directory);
3043
+ const key = hash([host, sessionId]), signature = hash([failure.code, failure.message]);
3044
+ const previous = notices.sessions[key];
3045
+ const execution = await executionStatus(ws.root, ws.directory);
3046
+ const recovered = previous && execution.attempts.some((a) => a.status === "completed" && a.startedAt > previous.lastAt);
3047
+ const repeat = previous?.signature === signature && !recovered;
3048
+ notices.sessions[key] = {
3049
+ signature,
3050
+ code: failure.code,
3051
+ message: failure.message,
3052
+ count: repeat ? previous.count + 1 : 1,
3053
+ lastAt: (/* @__PURE__ */ new Date()).toISOString()
3054
+ };
3055
+ notices.sessions = Object.fromEntries(Object.entries(notices.sessions).sort(([, a], [, b]) => b.lastAt.localeCompare(a.lastAt)).slice(0, 32));
3056
+ await writeStoreJson(ws.root, ws.directory + "/notifications.json", notices);
3057
+ return repeat ? null : failureMessage(error) + " Repeated failures in this session are recorded in mason status --json.";
3058
+ });
3059
+ } catch {
3060
+ return failureMessage(error);
3061
+ }
3062
+ }
3063
+ async function refreshRunning(root, directory, attempts) {
3064
+ for (const attempt of attempts) {
3065
+ if (attempt.status !== "running") continue;
3066
+ let alive = false;
2548
3067
  try {
2549
- handle = await fs12.open(file, "wx", 384);
2550
- try {
2551
- await handle.writeFile(JSON.stringify({ pid: process.pid, host: os2.hostname() }));
2552
- } catch (error) {
2553
- await handle.close().catch(() => {
2554
- });
2555
- handle = void 0;
2556
- await fs12.rm(file, { force: true }).catch(() => {
2557
- });
2558
- throw error;
3068
+ const owner = await readStoreJson(root, attempt.lease ? leasePath(directory, attempt.id) : directory + "/lock");
3069
+ if (attempt.host === os2.hostname() && owner?.pid === attempt.pid && owner.host === attempt.host) {
3070
+ try {
3071
+ process.kill(attempt.pid, 0);
3072
+ alive = true;
3073
+ } catch (error) {
3074
+ alive = error.code === "EPERM";
3075
+ }
2559
3076
  }
3077
+ } catch {
3078
+ }
3079
+ if (!alive) attempt.status = "unknown";
3080
+ }
3081
+ }
3082
+ async function editExecution(root, directory, edit) {
3083
+ return withLock(root, directory + "/execution-lock", async () => {
3084
+ const file = directory + "/execution.json";
3085
+ const raw = await readStoreJson(root, file);
3086
+ const log = raw === null ? { version: 1, attempts: [], discardedAttempts: 0 } : parseExecution(raw);
3087
+ await refreshRunning(root, directory, log.attempts);
3088
+ edit(log);
3089
+ const finished = log.attempts.filter((a) => a.status !== "running");
3090
+ const discard = new Set(finished.slice(0, Math.max(0, finished.length - 32)).map((a) => a.id));
3091
+ log.discardedAttempts += discard.size;
3092
+ log.attempts = log.attempts.filter((a) => !discard.has(a.id));
3093
+ await writeStoreJson(root, file, log);
3094
+ });
3095
+ }
3096
+ async function recordExecution(root, directory, event, run) {
3097
+ const started = performance.now();
3098
+ const attempt = {
3099
+ id: randomUUID3(),
3100
+ event,
3101
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3102
+ pid: process.pid,
3103
+ host: os2.hostname(),
3104
+ status: "running",
3105
+ lease: true
3106
+ };
3107
+ const owner = leasePath(directory, attempt.id);
3108
+ const finish = async (changes) => editExecution(root, directory, (log) => {
3109
+ const current = log.attempts.find((a) => a.id === attempt.id);
3110
+ if (!current) throw new Error("Automation execution receipt disappeared; completion could not be recorded.");
3111
+ Object.assign(current, changes, { finishedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: performance.now() - started });
3112
+ });
3113
+ await writeStoreJson(root, owner, { pid: attempt.pid, host: attempt.host });
3114
+ try {
3115
+ await editExecution(root, directory, (log) => {
3116
+ if (log.attempts.filter((a) => a.status === "running").length >= 128) throw new Error("Too many active automation executions; existing receipts were retained.");
3117
+ log.attempts.push(attempt);
3118
+ });
3119
+ try {
3120
+ const result = await run();
3121
+ await finish({ status: "completed", verificationStatus: result.report.status, reportPath: result.report.reportPath });
3122
+ return result;
2560
3123
  } catch (error) {
2561
- if (error.code !== "EEXIST") throw error;
3124
+ const failure = automationFailure(error);
2562
3125
  try {
2563
- const owner = JSON.parse(await fs12.readFile(file, "utf8"));
2564
- if (owner.host === os2.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
2565
- try {
2566
- process.kill(owner.pid, 0);
2567
- } catch (probe) {
2568
- if (probe.code === "ESRCH") {
2569
- const reclaim = file + ".reclaim";
2570
- let guard;
2571
- try {
2572
- guard = await fs12.open(reclaim, "wx", 384);
2573
- const current = JSON.parse(await fs12.readFile(file, "utf8"));
2574
- if (current.pid === owner.pid && current.host === owner.host) await fs12.unlink(file);
2575
- } finally {
2576
- if (guard) {
2577
- await guard.close();
2578
- await fs12.rm(reclaim, { force: true });
2579
- }
2580
- }
2581
- }
2582
- }
2583
- }
3126
+ await finish({ status: "failed", failure: { ...failure, receiptRecorded: true } });
3127
+ failure.receiptRecorded = true;
2584
3128
  } catch {
2585
3129
  }
2586
- if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
2587
- await new Promise((resolve) => setTimeout(resolve, 40));
3130
+ throw Object.assign(new Error(failure.message, { cause: error }), { failure });
2588
3131
  }
2589
- }
2590
- try {
2591
- return await run();
2592
3132
  } finally {
2593
- await handle.close();
2594
- await fs12.unlink(file);
3133
+ await fs14.rm(await storePath(root, owner), { force: true }).catch(() => {
3134
+ });
2595
3135
  }
2596
3136
  }
2597
- var hostSchema, events, stateSchema;
2598
- var init_store = __esm({
2599
- "src/automation/store.ts"() {
3137
+ async function executionStatus(root, directory) {
3138
+ const raw = await readStoreJson(root, directory + "/execution.json");
3139
+ if (raw === null) return { status: "not-observed", attempts: [] };
3140
+ const log = parseExecution(raw);
3141
+ await refreshRunning(root, directory, log.attempts);
3142
+ const completed = [...log.attempts].reverse().find((a) => a.status === "completed");
3143
+ const outstanding = [...log.attempts].reverse().find((a) => ["failed", "unknown"].includes(a.status) && (!completed || (a.finishedAt ?? a.startedAt) >= completed.startedAt));
3144
+ const status = log.attempts.some((a) => a.status === "running") ? "running" : outstanding?.status ?? log.attempts.at(-1)?.status ?? "not-observed";
3145
+ return { status, attempts: log.attempts, discardedAttempts: log.discardedAttempts };
3146
+ }
3147
+ var failureSchema, attemptSchema, executionSchema, notificationsSchema, leasePath;
3148
+ var init_execution = __esm({
3149
+ "src/automation/execution.ts"() {
2600
3150
  "use strict";
3151
+ init_store();
3152
+ init_evidence();
2601
3153
  init_storage();
2602
- hostSchema = z6.enum(["claude", "codex"]);
2603
- events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
2604
- stateSchema = z6.object({
2605
- version: z6.literal(1),
2606
- root: z6.string(),
2607
- gitDir: z6.string(),
2608
- branch: z6.string(),
2609
- baselines: z6.array(z6.object({ path: z6.string(), at: z6.string(), event: z6.string(), fingerprint: z6.string() })).max(128),
2610
- sessions: z6.record(z6.object({
2611
- host: hostSchema,
2612
- seen: z6.string().nullable(),
2613
- continued: z6.boolean(),
2614
- initialIssues: z6.array(z6.string()),
2615
- initialDocs: z6.record(z6.string().nullable()),
2616
- lastUsed: z6.string(),
2617
- mutationObserved: z6.boolean(),
2618
- pending: z6.record(z6.string()),
2619
- coverageGaps: z6.array(z6.string()),
2620
- events: z6.record(z6.object({ at: z6.string(), count: z6.number().int().positive() }))
2621
- })),
2622
- updatedAt: z6.string(),
2623
- fingerprint: z6.string().nullable(),
2624
- latest: z6.string().nullable()
3154
+ failureSchema = z9.object({
3155
+ code: z9.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
3156
+ message: z9.string(),
3157
+ retryable: z9.boolean(),
3158
+ receiptRecorded: z9.boolean()
3159
+ });
3160
+ attemptSchema = z9.object({
3161
+ id: z9.string().regex(/^[a-zA-Z0-9_-]+$/),
3162
+ lease: z9.literal(true).optional(),
3163
+ event: z9.string(),
3164
+ startedAt: z9.string(),
3165
+ finishedAt: z9.string().optional(),
3166
+ durationMs: z9.number().nonnegative().optional(),
3167
+ pid: z9.number().int().positive(),
3168
+ host: z9.string(),
3169
+ status: z9.enum(["running", "completed", "failed", "unknown"]),
3170
+ verificationStatus: z9.string().optional(),
3171
+ reportPath: z9.string().optional(),
3172
+ failure: failureSchema.optional()
2625
3173
  });
3174
+ executionSchema = z9.object({ version: z9.literal(1), attempts: z9.array(attemptSchema).max(160), discardedAttempts: z9.number().int().nonnegative().default(0) });
3175
+ notificationsSchema = z9.object({ version: z9.literal(1), sessions: z9.record(z9.object({
3176
+ signature: z9.string(),
3177
+ code: failureSchema.shape.code,
3178
+ message: z9.string(),
3179
+ count: z9.number().int().positive(),
3180
+ lastAt: z9.string()
3181
+ })) });
3182
+ leasePath = (directory, id) => `${directory}/executions/${id}.json`;
2626
3183
  }
2627
3184
  });
2628
3185
 
2629
3186
  // src/automation/runtime.ts
2630
3187
  import { randomUUID as randomUUID4 } from "crypto";
3188
+ import { z as z10 } from "zod";
2631
3189
  async function observeReadOnlyTool(dir, event) {
2632
3190
  const ws = await workspace(dir);
2633
3191
  await withLock(ws.root, ws.directory, async () => {
@@ -2658,176 +3216,234 @@ function summarize(report) {
2658
3216
  "Keep original evidence. Address findings relevant to the authorized task; report unrelated findings and unresolved advisories without approving them."
2659
3217
  ].join("\n");
2660
3218
  }
3219
+ async function loadState(ws) {
3220
+ const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
3221
+ const state = raw === null ? {
3222
+ version: 1,
3223
+ root: ws.root,
3224
+ gitDir: ws.gitDir,
3225
+ branch: ws.branch,
3226
+ baselines: [],
3227
+ sessions: {},
3228
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3229
+ fingerprint: null,
3230
+ latest: null
3231
+ } : parseState(raw);
3232
+ if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
3233
+ throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
3234
+ }
3235
+ return state;
3236
+ }
3237
+ async function sharedAnalysis(ws, state, inputs, startedAt) {
3238
+ const pointer = state.analysis;
3239
+ if (!pointer || pointer.fingerprint !== inputs.fingerprint || pointer.completedAt < startedAt) return null;
3240
+ const stored = sharedSchema.parse(await readStoreJson(ws.root, pointer.path));
3241
+ const a = stored.analysis;
3242
+ if (stored.digest !== hash(a) || a.id !== pointer.id || a.fingerprint !== inputs.fingerprint || a.completedAt !== pointer.completedAt || a.report.root !== ws.root || a.report.branch !== ws.branch || a.report.head !== inputs.head || hash(a.report.baselinePaths) !== hash(state.baselines.map((b) => b.path))) {
3243
+ throw new Error("Invalid shared automation evidence; run a new check.");
3244
+ }
3245
+ if (a.baselineDigest !== await baselineDigest(ws, state)) throw new Error("Repair baseline was modified; use the original baseline.");
3246
+ return a;
3247
+ }
3248
+ async function verifyInputs(ws, inputs) {
3249
+ const [after, currentWs] = await Promise.all([readInputs(ws.root), workspace(ws.root)]);
3250
+ if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
3251
+ throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
3252
+ }
3253
+ }
3254
+ async function computeAnalysis(ws, state, inputs, event) {
3255
+ const baselines = [...state.baselines];
3256
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3257
+ const empty = {
3258
+ version: 1,
3259
+ status: "unavailable",
3260
+ root: ws.root,
3261
+ branch: ws.branch,
3262
+ head: inputs.head,
3263
+ baselinePaths: [],
3264
+ reportPath: "",
3265
+ findings: [],
3266
+ diagnostics: ["No README.md or agent instruction files were discovered. Documentation capture is unavailable; other Mason tools remain usable."],
3267
+ checks: { ran: [], reused: [], skipped: [] },
3268
+ counts: { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 },
3269
+ capture: "unknown",
3270
+ scope: SCOPE
3271
+ };
3272
+ if (!baselines.length && Object.values(inputs.docs).every((value) => value === null)) return { baselines, report: empty, cache: null };
3273
+ if (ws.branch === "detached" && state.latest) {
3274
+ const previous = await readStoreJson(ws.root, state.latest);
3275
+ if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
3276
+ try {
3277
+ await git3(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
3278
+ } catch {
3279
+ throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
3280
+ }
3281
+ }
3282
+ let cached = null;
3283
+ const diagnostics = [];
3284
+ try {
3285
+ cached = await readStoreJson(ws.root, ws.directory + "/cache.json");
3286
+ } catch {
3287
+ diagnostics.push("Unreadable automation cache; checks are being recomputed.");
3288
+ }
3289
+ const cache = checkCache(cached, inputs);
3290
+ if (cache.diagnostic) diagnostics.push(cache.diagnostic);
3291
+ const saveBaseline = async () => {
3292
+ if (baselines.length >= 128) throw new Error("128 retained baselines need review; automatic capture stopped without discarding original evidence.");
3293
+ const prepared = await prepareRepair(ws.root, ALL_CHECKS, cache.options);
3294
+ baselines.push({ path: prepared.baselinePath, at: now, event: event.event, fingerprint: inputs.fingerprint });
3295
+ };
3296
+ if (!baselines.length) await saveBaseline();
3297
+ const verifications = [];
3298
+ for (const baseline of baselines) verifications.push(await verifyRepair(ws.root, baseline.path, cache.options));
3299
+ const known = new Set(verifications.flatMap((v) => v.findings.map((f) => f.id)));
3300
+ if (verifications.some((v) => v.newFindings.some((f) => !known.has(findingId(f))))) {
3301
+ await saveBaseline();
3302
+ verifications.push(await verifyRepair(ws.root, baselines.at(-1).path, cache.options));
3303
+ }
3304
+ const merged = /* @__PURE__ */ new Map();
3305
+ for (const verification of verifications) {
3306
+ for (const finding of verification.findings) {
3307
+ const previous = merged.get(finding.id);
3308
+ if (!previous || priority[finding.status] > priority[previous.status]) merged.set(finding.id, finding);
3309
+ }
3310
+ diagnostics.push(...verification.diagnostics);
3311
+ }
3312
+ const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
3313
+ for (const finding of merged.values()) counts[finding.status]++;
3314
+ const report = {
3315
+ version: 1,
3316
+ status: diagnostics.length || verifications.some((v) => v.status === "incomplete") ? "incomplete" : counts.unresolved ? "issues-remain" : "verified",
3317
+ root: ws.root,
3318
+ branch: ws.branch,
3319
+ head: inputs.head,
3320
+ baselinePaths: baselines.map((b) => b.path),
3321
+ reportPath: "",
3322
+ findings: [...merged.values()],
3323
+ diagnostics: [...new Set(diagnostics)],
3324
+ checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: verifications.at(-1).currentAudit?.skippedChecks ?? [] },
3325
+ counts,
3326
+ capture: "unknown",
3327
+ scope: SCOPE
3328
+ };
3329
+ return { baselines, report, cache: cache.ran.size || cached === null || cache.diagnostic ? cache.serialize() : null };
3330
+ }
3331
+ async function analyze(ws, inputs, event, startedAt) {
3332
+ return withLock(ws.root, ws.directory + "/analysis/" + inputs.fingerprint, async () => {
3333
+ const before = await loadState(ws);
3334
+ const shared = await sharedAnalysis(ws, before, inputs, startedAt);
3335
+ if (shared) return { analysis: shared, shared: true };
3336
+ const originalDigest = await baselineDigest(ws, before);
3337
+ const computed = await computeAnalysis(ws, before, inputs, event);
3338
+ await verifyInputs(ws, inputs);
3339
+ if (originalDigest !== await baselineDigest(ws, before)) throw new Error("Repair baseline was modified during automation; original evidence must be inspected.");
3340
+ const id = randomUUID4();
3341
+ const analysis = analysisSchema.parse({
3342
+ id,
3343
+ fingerprint: inputs.fingerprint,
3344
+ completedAt: Date.now(),
3345
+ baselineDigest: await baselineDigest(ws, { ...before, baselines: computed.baselines }),
3346
+ report: computed.report
3347
+ });
3348
+ const analysisPath = ws.directory + "/checks/analysis-" + inputs.fingerprint + ".json";
3349
+ await withLock(ws.root, ws.directory, async () => {
3350
+ const state = await loadState(ws);
3351
+ if (hash(state.baselines) !== hash(before.baselines) || state.analysis?.id !== before.analysis?.id) {
3352
+ throw new Error("Automation evidence changed during analysis; retry against the retained baselines.");
3353
+ }
3354
+ await writeStoreJson(ws.root, analysisPath, { analysis, digest: hash(analysis) });
3355
+ if (computed.cache) await writeStoreJson(ws.root, ws.directory + "/cache.json", computed.cache);
3356
+ state.baselines = computed.baselines;
3357
+ state.analysis = { id, fingerprint: inputs.fingerprint, completedAt: analysis.completedAt, path: analysisPath };
3358
+ await writeStoreJson(ws.root, ws.directory + "/state.json", state);
3359
+ });
3360
+ return { analysis, shared: false };
3361
+ }, Math.max(0, 25e3 - (Date.now() - startedAt)));
3362
+ }
2661
3363
  async function automate(dir, event) {
3364
+ const startedAt = Date.now();
2662
3365
  const ws = await workspace(dir);
2663
- return withLock(ws.root, ws.directory, () => recordExecution(ws.root, ws.directory, event.event, async () => {
3366
+ return recordExecution(ws.root, ws.directory, event.event, async () => {
2664
3367
  const inputs = await readInputs(ws.root);
2665
- const statePath = ws.directory + "/state.json";
2666
- const raw = await readStoreJson(ws.root, statePath);
2667
- const now = (/* @__PURE__ */ new Date()).toISOString();
2668
- const state = raw === null ? {
2669
- version: 1,
2670
- root: ws.root,
2671
- gitDir: ws.gitDir,
2672
- branch: ws.branch,
2673
- baselines: [],
2674
- sessions: {},
2675
- updatedAt: now,
2676
- fingerprint: null,
2677
- latest: null
2678
- } : parseState(raw);
2679
- if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
2680
- throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
2681
- }
2682
- if (ws.branch === "detached" && state.latest) {
2683
- const previous = await readStoreJson(ws.root, state.latest);
2684
- if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
2685
- try {
2686
- await git2(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
2687
- } catch {
2688
- throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
3368
+ const result = await analyze(ws, inputs, event, startedAt);
3369
+ if (result.shared) {
3370
+ await verifyInputs(ws, inputs);
3371
+ if (result.analysis.baselineDigest !== await baselineDigest(ws, await loadState(ws))) {
3372
+ throw new Error("Repair baseline was modified during shared automation; original evidence must be inspected.");
2689
3373
  }
2690
3374
  }
2691
- const key = event.host && event.sessionId ? hash([event.host, event.sessionId]) : null;
2692
- const newSession = key !== null && !state.sessions[key];
2693
- if (key && !state.sessions[key]) {
2694
- const keys = Object.keys(state.sessions).sort((a, b) => state.sessions[a].lastUsed.localeCompare(state.sessions[b].lastUsed));
2695
- for (const expired of keys.slice(0, Math.max(0, keys.length - 31))) delete state.sessions[expired];
2696
- state.sessions[key] = {
2697
- host: event.host,
2698
- seen: null,
2699
- continued: false,
2700
- initialIssues: [],
2701
- initialDocs: inputs.docs,
2702
- lastUsed: now,
2703
- mutationObserved: false,
2704
- pending: {},
2705
- coverageGaps: [],
2706
- events: {}
2707
- };
2708
- }
2709
- const session = key ? state.sessions[key] : null;
2710
- if (session) {
2711
- session.lastUsed = now;
2712
- session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
2713
- if (event.event === "before_tool" && event.mutating && event.toolId) {
2714
- if (Object.keys(session.pending).length >= 128) throw new Error("Too many unfinished tool calls to track pre-edit evidence.");
2715
- session.pending[event.toolId] = inputs.fingerprint;
3375
+ return withLock(ws.root, ws.directory, async () => {
3376
+ const state = await loadState(ws);
3377
+ if (state.analysis?.id !== result.analysis.id) throw new Error("Automation evidence changed during publication; retry against the current inputs.");
3378
+ const report = structuredClone(result.analysis.report);
3379
+ if (result.shared) report.checks = { ...report.checks, ran: [], reused: [.../* @__PURE__ */ new Set([...report.checks.ran, ...report.checks.reused])], shared: true };
3380
+ report.reportPath = ws.directory + "/checks/" + randomUUID4() + ".json";
3381
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3382
+ const key = event.host && event.sessionId ? hash([event.host, event.sessionId]) : null;
3383
+ const newSession = key !== null && !state.sessions[key];
3384
+ if (key && !state.sessions[key]) {
3385
+ const keys = Object.keys(state.sessions).sort((a, b) => state.sessions[a].lastUsed.localeCompare(state.sessions[b].lastUsed));
3386
+ const removable = keys.filter((k) => !Object.keys(state.sessions[k].pending).length);
3387
+ for (const expired of removable.slice(0, Math.max(0, keys.length - 31))) delete state.sessions[expired];
3388
+ if (Object.keys(state.sessions).length >= 128) throw new Error("Too many sessions with unfinished tool calls; original evidence was retained.");
3389
+ state.sessions[key] = {
3390
+ host: event.host,
3391
+ seen: null,
3392
+ continued: false,
3393
+ initialIssues: report.findings.filter((f) => f.status === "unresolved").map((f) => f.id),
3394
+ initialDocs: inputs.docs,
3395
+ lastUsed: now,
3396
+ mutationObserved: false,
3397
+ pending: {},
3398
+ coverageGaps: [],
3399
+ events: {}
3400
+ };
2716
3401
  }
2717
- if (event.event === "after_tool" && event.mutating) {
2718
- session.mutationObserved = true;
2719
- if (!event.toolId || !session.pending[event.toolId]) {
2720
- const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
2721
- if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
3402
+ const session = key ? state.sessions[key] : null;
3403
+ if (session) {
3404
+ session.lastUsed = now;
3405
+ session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
3406
+ if (event.event === "before_tool" && event.mutating && event.toolId) {
3407
+ if (Object.keys(session.pending).length >= 128) throw new Error("Too many unfinished tool calls to track pre-edit evidence.");
3408
+ session.pending[event.toolId] = inputs.fingerprint;
3409
+ }
3410
+ if (event.event === "after_tool" && event.mutating) {
3411
+ session.mutationObserved = true;
3412
+ if (!event.toolId || !session.pending[event.toolId]) {
3413
+ const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
3414
+ if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
3415
+ }
3416
+ if (event.toolId) delete session.pending[event.toolId];
2722
3417
  }
2723
- if (event.toolId) delete session.pending[event.toolId];
3418
+ report.diagnostics.push(...session.coverageGaps);
3419
+ if (session.coverageGaps.length && report.status !== "unavailable") report.status = "incomplete";
3420
+ report.capture = report.status !== "unavailable" && !session.coverageGaps.length && (session.events.session_start || session.events.before_tool) ? "observed" : "unknown";
2724
3421
  }
2725
- }
2726
- if (!state.baselines.length && Object.values(inputs.docs).every((value) => value === null)) {
2727
- const report2 = {
2728
- version: 1,
2729
- status: "unavailable",
2730
- root: ws.root,
2731
- branch: ws.branch,
2732
- head: inputs.head,
2733
- baselinePaths: [],
2734
- reportPath: ws.directory + "/checks/" + randomUUID4() + ".json",
2735
- findings: [],
2736
- diagnostics: ["No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md exists. Documentation capture is unavailable; other Mason tools remain usable."],
2737
- checks: { ran: [], reused: [], skipped: [] },
2738
- counts: { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 },
2739
- capture: "unknown",
2740
- scope: SCOPE
2741
- };
2742
- const notify2 = !session || session.seen !== "no-docs";
2743
- if (session) session.seen = "no-docs";
2744
- state.fingerprint = inputs.fingerprint;
2745
- state.latest = report2.reportPath;
2746
- state.updatedAt = now;
2747
- await writeStoreJson(ws.root, report2.reportPath, report2);
2748
- await writeStoreJson(ws.root, statePath, state);
2749
- return { report: report2, message: notify2 ? summarize(report2) : null, continueOnce: false };
2750
- }
2751
- let cached = null;
2752
- const diagnostics = [];
2753
- try {
2754
- cached = await readStoreJson(ws.root, ws.directory + "/cache.json");
2755
- } catch {
2756
- diagnostics.push("Unreadable automation cache; checks are being recomputed.");
2757
- }
2758
- const cache = checkCache(cached, inputs);
2759
- if (cache.diagnostic) diagnostics.push(cache.diagnostic);
2760
- const saveBaseline = async () => {
2761
- if (state.baselines.length >= 128) throw new Error("128 retained baselines need review; automatic capture stopped without discarding original evidence.");
2762
- const prepared = await prepareRepair(ws.root, ALL_CHECKS, cache.options);
2763
- state.baselines.push({ path: prepared.baselinePath, at: now, event: event.event, fingerprint: inputs.fingerprint });
2764
- };
2765
- if (!state.baselines.length) await saveBaseline();
2766
- const verifications = [];
2767
- for (const baseline of state.baselines) verifications.push(await verifyRepair(ws.root, baseline.path, cache.options));
2768
- const known = new Set(verifications.flatMap((v) => v.findings.map((f) => f.id)));
2769
- if (verifications.some((v) => v.newFindings.some((f) => !known.has(findingId(f))))) {
2770
- await saveBaseline();
2771
- verifications.push(await verifyRepair(ws.root, state.baselines.at(-1).path, cache.options));
2772
- }
2773
- const merged = /* @__PURE__ */ new Map();
2774
- for (const verification of verifications) {
2775
- for (const finding of verification.findings) {
2776
- const previous = merged.get(finding.id);
2777
- if (!previous || priority[finding.status] > priority[previous.status]) merged.set(finding.id, finding);
3422
+ const signature = hash([report.status, report.findings, report.diagnostics, report.checks.skipped]);
3423
+ const relevant = report.findings.some((f) => f.status === "unresolved" && session && (!session.initialIssues.includes(f.id) || session.initialDocs[f.original.anchor.doc] !== inputs.docs[f.original.anchor.doc]));
3424
+ const continueOnce = event.event === "task_end" && !!session?.mutationObserved && relevant && !session.continued && !event.stopHookActive;
3425
+ const notify = !session || newSession || signature !== session.seen || continueOnce;
3426
+ if (session) {
3427
+ session.seen = signature;
3428
+ if (continueOnce) session.continued = true;
2778
3429
  }
2779
- diagnostics.push(...verification.diagnostics);
2780
- }
2781
- if (newSession && session) session.initialIssues = [...merged.values()].filter((f) => f.status === "unresolved").map((f) => f.id);
2782
- const current = verifications.at(-1).currentAudit;
2783
- const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
2784
- for (const finding of merged.values()) counts[finding.status]++;
2785
- if (session) diagnostics.push(...session.coverageGaps);
2786
- const capture = session && !session.coverageGaps.length && (session.events.session_start || session.events.before_tool) ? "observed" : "unknown";
2787
- const after = await readInputs(ws.root);
2788
- const currentWs = await workspace(ws.root);
2789
- if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
2790
- throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
2791
- }
2792
- const report = {
2793
- version: 1,
2794
- status: diagnostics.length || verifications.some((v) => v.status === "incomplete") ? "incomplete" : counts.unresolved ? "issues-remain" : "verified",
2795
- root: ws.root,
2796
- branch: ws.branch,
2797
- head: inputs.head,
2798
- baselinePaths: state.baselines.map((b) => b.path),
2799
- reportPath: ws.directory + "/checks/" + randomUUID4() + ".json",
2800
- findings: [...merged.values()],
2801
- diagnostics: [...new Set(diagnostics)],
2802
- checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: current?.skippedChecks ?? [] },
2803
- counts,
2804
- capture,
2805
- scope: SCOPE
2806
- };
2807
- const signature = hash([report.status, report.findings, report.diagnostics, report.checks.skipped]);
2808
- const relevant = report.findings.some((f) => f.status === "unresolved" && session && (!session.initialIssues.includes(f.id) || session.initialDocs[f.original.anchor.doc] !== inputs.docs[f.original.anchor.doc]));
2809
- const continueOnce = event.event === "task_end" && !!session?.mutationObserved && relevant && !session.continued && !event.stopHookActive;
2810
- const notify = !session || newSession || signature !== session.seen || continueOnce;
2811
- if (session) {
2812
- session.seen = signature;
2813
- if (continueOnce) session.continued = true;
2814
- }
2815
- const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || event.event === "task_end";
2816
- if (!persistReport) report.reportPath = state.latest;
2817
- state.updatedAt = now;
2818
- state.fingerprint = inputs.fingerprint;
2819
- state.latest = report.reportPath;
2820
- if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
2821
- if (cache.ran.size || cached === null || cache.diagnostic) await writeStoreJson(ws.root, ws.directory + "/cache.json", cache.serialize());
2822
- await writeStoreJson(ws.root, statePath, state);
2823
- return { report, message: notify ? summarize(report) : null, continueOnce };
2824
- }));
3430
+ const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || event.event === "task_end";
3431
+ if (!persistReport) report.reportPath = state.latest;
3432
+ state.updatedAt = now;
3433
+ state.fingerprint = inputs.fingerprint;
3434
+ state.latest = report.reportPath;
3435
+ if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
3436
+ await writeStoreJson(ws.root, ws.directory + "/state.json", state);
3437
+ return { report, message: notify ? summarize(report) : null, continueOnce };
3438
+ });
3439
+ });
2825
3440
  }
2826
3441
  async function automationStatus(dir) {
2827
3442
  const ws = await workspace(dir);
2828
3443
  const execution = await executionStatus(ws.root, ws.directory);
3444
+ const notifications = await failureNotifications(ws.root, ws.directory);
2829
3445
  const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
2830
- if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution };
3446
+ if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution, notifications };
2831
3447
  const state = parseState(raw);
2832
3448
  if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) throw new Error("Automation state belongs to another workspace.");
2833
3449
  const inputs = await readInputs(ws.root);
@@ -2849,10 +3465,11 @@ async function automationStatus(dir) {
2849
3465
  verificationStatus: unfinished ? "unavailable" : latest?.status ?? "unavailable",
2850
3466
  hosts: hosts2,
2851
3467
  execution,
3468
+ notifications,
2852
3469
  note: "Observed events do not prove all tool paths are intercepted. Run check to verify the retained evidence."
2853
3470
  };
2854
3471
  }
2855
- var SCOPE, priority, cleanText;
3472
+ var SCOPE, priority, cleanText, analysisSchema, sharedSchema, baselineDigest;
2856
3473
  var init_runtime = __esm({
2857
3474
  "src/automation/runtime.ts"() {
2858
3475
  "use strict";
@@ -2862,14 +3479,43 @@ var init_runtime = __esm({
2862
3479
  init_storage();
2863
3480
  init_evidence();
2864
3481
  init_store();
2865
- SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved claims no longer fail their checks; advisories need separate review. Repair only within the user's task authorization.";
3482
+ SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved findings no longer fail their checks or have a recorded assessment covering current evidence. Repair and review only within the user's task authorization.";
2866
3483
  priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
2867
3484
  cleanText = (text2) => text2.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
3485
+ analysisSchema = z10.object({
3486
+ id: z10.string(),
3487
+ fingerprint: z10.string(),
3488
+ completedAt: z10.number(),
3489
+ baselineDigest: z10.string(),
3490
+ report: z10.object({
3491
+ version: z10.literal(1),
3492
+ status: z10.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
3493
+ root: z10.string(),
3494
+ branch: z10.string(),
3495
+ head: z10.string(),
3496
+ baselinePaths: z10.array(z10.string()),
3497
+ reportPath: z10.string(),
3498
+ findings: z10.array(repairFindingSchema),
3499
+ diagnostics: z10.array(z10.string()),
3500
+ checks: z10.object({
3501
+ ran: z10.array(z10.string()),
3502
+ reused: z10.array(z10.string()),
3503
+ skipped: z10.array(z10.object({ check: z10.string(), reason: z10.string(), doc: z10.string().optional() }))
3504
+ }),
3505
+ counts: z10.object({ resolved: z10.number(), unresolved: z10.number(), "review-required": z10.number(), unverified: z10.number() }),
3506
+ capture: z10.enum(["observed", "unknown"]),
3507
+ scope: z10.string()
3508
+ })
3509
+ });
3510
+ sharedSchema = z10.object({ analysis: analysisSchema, digest: z10.string() });
3511
+ baselineDigest = async (ws, state) => hash(await Promise.all(
3512
+ state.baselines.map(async (b) => [b.path, await readStoreJson(ws.root, b.path)])
3513
+ ));
2868
3514
  }
2869
3515
  });
2870
3516
 
2871
3517
  // src/setup/model.ts
2872
- import { z as z7 } from "zod";
3518
+ import { z as z11 } from "zod";
2873
3519
  async function loadSetup(root) {
2874
3520
  const raw = await readStoreJson(root, SETUP_PATH);
2875
3521
  if (raw === null) return null;
@@ -2887,42 +3533,42 @@ var init_model = __esm({
2887
3533
  "src/setup/model.ts"() {
2888
3534
  "use strict";
2889
3535
  init_storage();
2890
- setupHostSchema = z7.object({
2891
- configuredVersion: z7.string(),
2892
- revision: z7.string().uuid(),
2893
- fingerprint: z7.string(),
2894
- mcpFingerprint: z7.string(),
2895
- instructions: z7.array(z7.string())
3536
+ setupHostSchema = z11.object({
3537
+ configuredVersion: z11.string(),
3538
+ revision: z11.string().uuid(),
3539
+ fingerprint: z11.string(),
3540
+ mcpFingerprint: z11.string(),
3541
+ instructions: z11.array(z11.string())
2896
3542
  });
2897
- setupSchema = z7.object({ version: z7.literal(2), hosts: z7.object({
3543
+ setupSchema = z11.object({ version: z11.literal(2), hosts: z11.object({
2898
3544
  codex: setupHostSchema.optional(),
2899
3545
  claude: setupHostSchema.optional()
2900
3546
  }) });
2901
3547
  SETUP_PATH = ".mason/local/setup.json";
2902
- receiptSchema = z7.object({
2903
- version: z7.literal(1),
2904
- host: z7.enum(["codex", "claude"]),
2905
- status: z7.enum(["installing", "configured"]),
2906
- initialReportPath: z7.string(),
2907
- initialBaselinePaths: z7.array(z7.string()),
2908
- root: z7.string(),
2909
- revision: z7.string().uuid(),
2910
- configuredAt: z7.string().optional()
3548
+ receiptSchema = z11.object({
3549
+ version: z11.literal(1),
3550
+ host: z11.enum(["codex", "claude"]),
3551
+ status: z11.enum(["installing", "configured"]),
3552
+ initialReportPath: z11.string(),
3553
+ initialBaselinePaths: z11.array(z11.string()),
3554
+ root: z11.string(),
3555
+ revision: z11.string().uuid(),
3556
+ configuredAt: z11.string().optional()
2911
3557
  });
2912
3558
  }
2913
3559
  });
2914
3560
 
2915
3561
  // src/setup/launcher.ts
2916
- import { execFile as execFile9 } from "child_process";
2917
- import { promisify as promisify9 } from "util";
2918
- import fs13 from "fs/promises";
3562
+ import { execFile as execFile10 } from "child_process";
3563
+ import { promisify as promisify10 } from "util";
3564
+ import fs15 from "fs/promises";
2919
3565
  function globalCommand(args, platform = process.platform) {
2920
3566
  return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
2921
3567
  }
2922
3568
  async function installedCommand() {
2923
3569
  const invocation = globalCommand(["internal-integration-version"]);
2924
3570
  try {
2925
- const { stdout } = await promisify9(execFile9)(
3571
+ const { stdout } = await promisify10(execFile10)(
2926
3572
  invocation.command,
2927
3573
  invocation.args,
2928
3574
  { timeout: 5e3, maxBuffer: 8192, windowsHide: true }
@@ -2940,7 +3586,7 @@ var init_launcher = __esm({
2940
3586
  "use strict";
2941
3587
  init_evidence();
2942
3588
  init_model();
2943
- executingVersion = () => true ? "0.16.2" : "development";
3589
+ executingVersion = () => true ? "0.17.0" : "development";
2944
3590
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
2945
3591
  hookCommand = (host) => `mason --setup-host ${host} auto`;
2946
3592
  }
@@ -2953,8 +3599,8 @@ __export(observations_exports, {
2953
3599
  observeActivation: () => observeActivation,
2954
3600
  readObservation: () => readObservation
2955
3601
  });
2956
- import fs14 from "fs/promises";
2957
- import { z as z8 } from "zod";
3602
+ import fs16 from "fs/promises";
3603
+ import { z as z12 } from "zod";
2958
3604
  function observationPath(directory, host, revision, version = executingVersion()) {
2959
3605
  return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
2960
3606
  }
@@ -2970,8 +3616,8 @@ async function observeActivation(dir, event, options = {}) {
2970
3616
  if (host !== "codex" && host !== "claude" || !revision || !process.env.MASON_SETUP_ROOT) return null;
2971
3617
  try {
2972
3618
  const capturedDirectory = options.directory?.match(/^\.mason\/reports\/automation\/[a-f0-9]{24}$/)?.[0] ?? options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
2973
- const ws = capturedDirectory ? { root: await fs14.realpath(dir), directory: capturedDirectory } : await workspace(dir);
2974
- if (ws.root !== await fs14.realpath(process.env.MASON_SETUP_ROOT)) return null;
3619
+ const ws = capturedDirectory ? { root: await fs16.realpath(dir), directory: capturedDirectory } : await workspace(dir);
3620
+ if (ws.root !== await fs16.realpath(process.env.MASON_SETUP_ROOT)) return null;
2975
3621
  const setup = await loadSetup(ws.root);
2976
3622
  if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
2977
3623
  const directory = ws.directory + "/activation";
@@ -3019,26 +3665,26 @@ var init_observations = __esm({
3019
3665
  init_storage();
3020
3666
  init_model();
3021
3667
  init_launcher();
3022
- observationSchema = z8.object({
3023
- version: z8.literal(1),
3024
- masonVersion: z8.string(),
3025
- root: z8.string(),
3026
- revision: z8.string(),
3027
- host: z8.enum(["codex", "claude"]),
3028
- contextCalls: z8.number().int().nonnegative(),
3029
- lastContextAt: z8.string().optional(),
3030
- sessions: z8.record(z8.object({
3031
- events: z8.array(z8.enum(events)),
3032
- at: z8.string(),
3033
- verificationStatus: z8.string().optional(),
3034
- reportPath: z8.string().optional()
3668
+ observationSchema = z12.object({
3669
+ version: z12.literal(1),
3670
+ masonVersion: z12.string(),
3671
+ root: z12.string(),
3672
+ revision: z12.string(),
3673
+ host: z12.enum(["codex", "claude"]),
3674
+ contextCalls: z12.number().int().nonnegative(),
3675
+ lastContextAt: z12.string().optional(),
3676
+ sessions: z12.record(z12.object({
3677
+ events: z12.array(z12.enum(events)),
3678
+ at: z12.string(),
3679
+ verificationStatus: z12.string().optional(),
3680
+ reportPath: z12.string().optional()
3035
3681
  }))
3036
3682
  });
3037
3683
  }
3038
3684
  });
3039
3685
 
3040
3686
  // src/automation/adapters.ts
3041
- import { z as z9 } from "zod";
3687
+ import { z as z13 } from "zod";
3042
3688
  function normalizeHook(host, raw) {
3043
3689
  hostSchema.parse(host);
3044
3690
  const input2 = inputSchema.parse(raw);
@@ -3054,6 +3700,7 @@ function normalizeHook(host, raw) {
3054
3700
  }
3055
3701
  async function runAutomationHook(host, stdin) {
3056
3702
  let name = "";
3703
+ let hook;
3057
3704
  try {
3058
3705
  if (Buffer.byteLength(stdin) > 1024 * 1024) throw new Error("Hook input exceeds 1 MiB.");
3059
3706
  let raw;
@@ -3063,6 +3710,7 @@ async function runAutomationHook(host, stdin) {
3063
3710
  throw new Error("Hook input is not valid JSON.");
3064
3711
  }
3065
3712
  const input2 = normalizeHook(host, raw);
3713
+ hook = input2;
3066
3714
  name = input2.name;
3067
3715
  if (["before_tool", "after_tool"].includes(input2.event.event) && input2.readOnly) {
3068
3716
  const observed = await observeReadOnlyTool(input2.cwd, input2.event);
@@ -3084,7 +3732,8 @@ async function runAutomationHook(host, stdin) {
3084
3732
  }
3085
3733
  return { hookSpecificOutput: { hookEventName: name, additionalContext: result.message } };
3086
3734
  } catch (error) {
3087
- const message2 = failureMessage(error);
3735
+ const message2 = hook ? await hookFailureMessage(hook.cwd, host, hook.event.sessionId, error) : failureMessage(error);
3736
+ if (!message2) return null;
3088
3737
  return { systemMessage: message2, ...["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse"].includes(name) ? { hookSpecificOutput: { hookEventName: name, additionalContext: message2 } } : {} };
3089
3738
  }
3090
3739
  }
@@ -3109,15 +3758,15 @@ var init_adapters = __esm({
3109
3758
  init_execution();
3110
3759
  init_runtime();
3111
3760
  init_store();
3112
- inputSchema = z9.object({
3113
- cwd: z9.string().min(1),
3114
- session_id: z9.string().min(1).max(500),
3115
- hook_event_name: z9.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
3116
- tool_name: z9.string().optional(),
3117
- tool_use_id: z9.string().max(500).optional(),
3118
- tool_input: z9.unknown().optional(),
3119
- stop_hook_active: z9.boolean().optional(),
3120
- permission_mode: z9.string().optional()
3761
+ inputSchema = z13.object({
3762
+ cwd: z13.string().min(1),
3763
+ session_id: z13.string().min(1).max(500),
3764
+ hook_event_name: z13.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
3765
+ tool_name: z13.string().optional(),
3766
+ tool_use_id: z13.string().max(500).optional(),
3767
+ tool_input: z13.unknown().optional(),
3768
+ stop_hook_active: z13.boolean().optional(),
3769
+ permission_mode: z13.string().optional()
3121
3770
  });
3122
3771
  lifecycle = {
3123
3772
  SessionStart: "session_start",
@@ -3137,8 +3786,8 @@ __export(files_exports, {
3137
3786
  managedBlock: () => managedBlock,
3138
3787
  readText: () => readText
3139
3788
  });
3140
- import fs15 from "fs/promises";
3141
- import path18 from "path";
3789
+ import fs17 from "fs/promises";
3790
+ import path21 from "path";
3142
3791
  import { randomUUID as randomUUID5 } from "crypto";
3143
3792
  async function readText(root, file) {
3144
3793
  try {
@@ -3167,14 +3816,14 @@ async function applyEdit(root, edit) {
3167
3816
  if (edit.after === null) {
3168
3817
  const file2 = await storePath(root, edit.path);
3169
3818
  if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
3170
- await fs15.unlink(file2);
3819
+ await fs17.unlink(file2);
3171
3820
  return true;
3172
3821
  }
3173
3822
  const file = await storePath(root, edit.path, true);
3174
- const temporary = path18.join(path18.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3823
+ const temporary = path21.join(path21.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3175
3824
  try {
3176
- const mode = await fs15.stat(file).then((s) => s.mode & 511, () => 384);
3177
- const handle = await fs15.open(temporary, "wx", mode);
3825
+ const mode = await fs17.stat(file).then((s) => s.mode & 511, () => 384);
3826
+ const handle = await fs17.open(temporary, "wx", mode);
3178
3827
  try {
3179
3828
  await handle.writeFile(edit.after, "utf8");
3180
3829
  await handle.sync();
@@ -3182,10 +3831,10 @@ async function applyEdit(root, edit) {
3182
3831
  await handle.close();
3183
3832
  }
3184
3833
  if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
3185
- await fs15.rename(temporary, file);
3834
+ await fs17.rename(temporary, file);
3186
3835
  return true;
3187
3836
  } finally {
3188
- await fs15.rm(temporary, { force: true });
3837
+ await fs17.rm(temporary, { force: true });
3189
3838
  }
3190
3839
  }
3191
3840
  var init_files2 = __esm({
@@ -3206,7 +3855,7 @@ __export(ownership_exports, {
3206
3855
  ownershipEdit: () => ownershipEdit,
3207
3856
  ownershipSchema: () => ownershipSchema
3208
3857
  });
3209
- import { z as z10 } from "zod";
3858
+ import { z as z14 } from "zod";
3210
3859
  import { parse } from "smol-toml";
3211
3860
  function blockSpan(text2, start, end) {
3212
3861
  const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
@@ -3257,30 +3906,30 @@ var init_ownership = __esm({
3257
3906
  ["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
3258
3907
  ["# mason:mcp:start", "# mason:mcp:end"]
3259
3908
  ];
3260
- fileSchema = z10.object({
3261
- created: z10.boolean(),
3262
- initialHash: z10.string(),
3263
- blocks: z10.record(z10.string()),
3264
- mcpHash: z10.string().optional(),
3265
- hookCommands: z10.array(z10.string()).optional(),
3266
- hooks: z10.record(z10.array(z10.string())).optional()
3909
+ fileSchema = z14.object({
3910
+ created: z14.boolean(),
3911
+ initialHash: z14.string(),
3912
+ blocks: z14.record(z14.string()),
3913
+ mcpHash: z14.string().optional(),
3914
+ hookCommands: z14.array(z14.string()).optional(),
3915
+ hooks: z14.record(z14.array(z14.string())).optional()
3267
3916
  });
3268
- ownershipSchema = z10.object({ version: z10.literal(1), files: z10.record(fileSchema) });
3917
+ ownershipSchema = z14.object({ version: z14.literal(1), files: z14.record(fileSchema) });
3269
3918
  }
3270
3919
  });
3271
3920
 
3272
3921
  // src/mcp/init.ts
3273
- import { z as z11 } from "zod";
3922
+ import { z as z15 } from "zod";
3274
3923
  var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
3275
3924
  var init_init = __esm({
3276
3925
  "src/mcp/init.ts"() {
3277
3926
  "use strict";
3278
3927
  init_storage();
3279
3928
  init_files2();
3280
- markerSchema = z11.object({
3281
- version: z11.literal(1),
3282
- initializedAt: z11.string(),
3283
- features: z11.object({ confluence: z11.boolean().optional() }).passthrough().optional()
3929
+ markerSchema = z15.object({
3930
+ version: z15.literal(1),
3931
+ initializedAt: z15.string(),
3932
+ features: z15.object({ confluence: z15.boolean().optional() }).passthrough().optional()
3284
3933
  }).passthrough();
3285
3934
  CLAUDE_MD_SECTION = `<!-- mason:start -->
3286
3935
  ## Mason project knowledge
@@ -3290,12 +3939,14 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
3290
3939
  - Task, bug, change request, or investigation of why the project works this way \u2192 \`get_context\` with the task text and known files: matching decisions, related tests, impact, and any available map entries.
3291
3940
  - Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
3292
3941
  - Investigation or correction reveals a reusable project lesson \u2192 \`save_decision\` before finishing. Capture only the lasting constraint and reason, with anchors and known sources, owner, and recorder. This creates a proposal; never infer approval. Preserve uncertain causes and unknown attribution. Route project lessons here; keep personal preferences in agent memory. Skip code summaries, current checkout status, temporary task permissions, session trivia, and secrets. No qualifying lesson means no record.
3942
+ - Before saving, compare the lesson with matching \`get_context\` records and any pending proposal. If new evidence changes the same lesson's assumptions, scope, or recommended action, revise it with \`save_decision(id: existingId)\`, including when it is accepted. Preserve supported rationale and sources, replace obsolete instructions, and add the new evidence. Create a separate record for a genuinely distinct lesson; skip unchanged restatements. Revision preserves history and leaves the accepted version operative until authorized review.
3293
3943
  - Consult trust metadata before relying on entries: unknown or changed freshness requires inspection, and failed verification means the description must be corrected. Check approval too: proposals are suggestions, legacy records are unreviewed, and accepted decisions are recorded constraints subject to freshness checks. An accepted revision remains operative while a pending proposal is reviewed; keep both versions and their freshness distinct.
3294
3944
  - Asked to review or re-verify a decision \u2192 \`review_decision\` first to inspect content, sources, history, and code changes. Record acceptance, reaffirmation, or retirement only when authorized by the user or a cited team review, with the actual reviewer and reason. Never infer approval from unchanged code. Review and commit the local record through the normal project workflow.
3295
3945
  - For an architectural overview, use \`get_snapshot\` if a map is available. If \`map.status\` is missing or invalid, use available decisions and source evidence; do not start building a map unless requested.
3296
3946
  - \`mason_init\` returns documentation audit and committed-diff review results, plus a short setup guide. Pass \`evidence\` with local CI manifest paths to include test and analysis results; the CLI equivalent is \`mason-review --evidence <manifest>\`. State skipped, unavailable, stale, or unknown checks explicitly. Related accepted decisions identify review context, not proven violations.
3297
3947
 
3298
3948
  - When documentation repair is authorized, use \`mason_repair(action: "prepare")\` before edits, keep its baselinePath, and use \`mason_repair(action: "verify", baselinePath)\` after edits and any final doc commit. Report every original finding's outcome and any new findings. Suppressed advisories remain unresolved; editing a doc does not approve it.
3949
+ - When advisory assessment is authorized, use \`review_advisory(action: "prepare", baselinePath, findingId)\` to inspect original evidence and history. Record \`addressed\`, \`inapplicable\`, or \`deferred\` with the actual reviewer, reason in note, and returned reviewToken only after relevant edits are committed. Commit the assessment and verify the same baseline again. Relevant evidence changes reopen it; deferral stays outstanding. Decision findings use \`review_decision\`; advisory closure never approves a decision.
3299
3950
 
3300
3951
  - When Mason automation is installed, use \`mason_automation(action: "status")\` to inspect configured hooks and observed events, and \`mason_automation(action: "check")\` to resume its retained repair evidence. CLI fallback: \`mason-auto status\` / \`mason-auto check\`. Preserve existing baselines across sessions. Automatic checks do not authorize unrelated repairs or approve advisories.
3301
3952
 
@@ -3311,7 +3962,7 @@ ${CLAUDE_MD_SECTION}`;
3311
3962
 
3312
3963
  1. Explain the actionable findings with their source evidence. Separate audit issues, advisories, and skipped checks. The review covers committed changes from the merge base to HEAD; workingTree paths are not included in that review. An unavailable or empty check is not proof that the project is correct. Use the CLI for full output if a summary is truncated.
3313
3964
  2. Address findings within the user's requested scope. Setup alone authorizes Mason host configuration, hooks, instructions, and ignore rules; rewriting existing project claims requires repair scope. If repair is authorized, call \`mason_repair(dir, action: "prepare")\` before editing; it saves the full original findings even when this summary is truncated. Inspect relevant source, make grounded edits, then call \`mason_repair(dir, action: "verify", baselinePath)\` with that same baseline, including after any final doc commit. Report resolved, unresolved, review-required, unverified, and new findings. Keep suppressed advisories visible even when setup has already dirtied a doc. Do not invent a decision just to populate the store.
3314
- 3. When the task reveals a real lesson or constraint, call \`save_decision\` with title, body, category, anchors, and known owner/source/actor information. Missing attribution can be added later. The tool writes a local proposal; editing it preserves revision history and requires a new acceptance, while any earlier accepted revision remains operative. An unchanged save never refreshes its evidence. When decision review is requested, \`review_decision\` prepares the record and code evidence before any authorized verdict. Review and commit records through the normal workflow. Retrieve it on the next relevant task with \`get_context(dir, task, files)\`.
3965
+ 3. When the task reveals a real lesson or constraint, compare matching \`get_context\` records first. If its assumptions, scope, or recommended action changed, revise that record using \`save_decision(id: existingId)\`; create a separate record for a genuinely distinct lesson and skip unchanged restatements. Include title, body, category, anchors, and known owner/source/actor information. Preserve supported rationale and sources, replace obsolete instructions, and add new evidence. Missing attribution can be added later. The tool writes a local proposal; editing it preserves revision history and requires a new acceptance, while any earlier accepted revision remains operative. An unchanged save never refreshes its evidence. When decision review is requested, \`review_decision\` prepares the record and code evidence before any authorized verdict. Review and commit records through the normal workflow. Retrieve it on the next relevant task with \`get_context(dir, task, files)\`.
3315
3966
 
3316
3967
  ${ASSISTANT_SETUP}
3317
3968
 
@@ -3357,8 +4008,8 @@ var git_exports = {};
3357
4008
  __export(git_exports, {
3358
4009
  isGitRepo: () => isGitRepo
3359
4010
  });
3360
- import { execFile as execFile10 } from "child_process";
3361
- import { promisify as promisify10 } from "util";
4011
+ import { execFile as execFile11 } from "child_process";
4012
+ import { promisify as promisify11 } from "util";
3362
4013
  async function isGitRepo(dir) {
3363
4014
  try {
3364
4015
  await exec8("git", ["rev-parse", "--git-dir"], { cwd: dir });
@@ -3371,7 +4022,7 @@ var exec8;
3371
4022
  var init_git2 = __esm({
3372
4023
  "src/utils/git.ts"() {
3373
4024
  "use strict";
3374
- exec8 = promisify10(execFile10);
4025
+ exec8 = promisify11(execFile11);
3375
4026
  }
3376
4027
  });
3377
4028
 
@@ -3387,7 +4038,7 @@ __export(config_exports, {
3387
4038
  });
3388
4039
  import { isDeepStrictEqual } from "util";
3389
4040
  import { parse as parse2, stringify } from "smol-toml";
3390
- import { z as z12 } from "zod";
4041
+ import { z as z16 } from "zod";
3391
4042
  async function instructionEdits(root, host) {
3392
4043
  const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
3393
4044
  const existing = found.find((f) => f.text !== null);
@@ -3456,11 +4107,11 @@ async function hookEdits(root, host) {
3456
4107
  }
3457
4108
  async function ancillaryEdits(root) {
3458
4109
  const ignore = await readText(root, ".gitignore");
3459
- const { git: git3 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
4110
+ const { git: git4 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
3460
4111
  const { isGitRepo: isGitRepo2 } = await Promise.resolve().then(() => (init_git2(), git_exports));
3461
4112
  let parentIgnored = false;
3462
4113
  try {
3463
- if (await isGitRepo2(root)) parentIgnored = !!(await git3(root, "check-ignore", "--no-index", ".mason")).trim();
4114
+ if (await isGitRepo2(root)) parentIgnored = !!(await git4(root, "check-ignore", "--no-index", ".mason")).trim();
3464
4115
  } catch (error) {
3465
4116
  if (error.code !== 1) throw error;
3466
4117
  }
@@ -3469,6 +4120,8 @@ async function ancillaryEdits(root) {
3469
4120
  ...retainParentRule ? ["!/.mason/", "/.mason/*"] : [],
3470
4121
  "!/.mason/decisions/",
3471
4122
  "!/.mason/decisions/**",
4123
+ "!/.mason/reviews/",
4124
+ "!/.mason/reviews/**",
3472
4125
  "!/.mason/config.json",
3473
4126
  "!/.mason/snapshot.json",
3474
4127
  "/.mason/local/",
@@ -3503,7 +4156,7 @@ var init_config = __esm({
3503
4156
  init_install();
3504
4157
  init_files2();
3505
4158
  init_launcher();
3506
- object = z12.record(z12.unknown());
4159
+ object = z16.record(z16.unknown());
3507
4160
  mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
3508
4161
  TOML_START = "# mason:mcp:start";
3509
4162
  TOML_END = "# mason:mcp:end";
@@ -3521,11 +4174,11 @@ __export(install_exports, {
3521
4174
  installAutomation: () => installAutomation,
3522
4175
  installedAutomation: () => installedAutomation,
3523
4176
  planAutomationInstall: () => planAutomationInstall,
3524
- recordSchema: () => recordSchema
4177
+ recordSchema: () => recordSchema2
3525
4178
  });
3526
- import { z as z13 } from "zod";
4179
+ import { z as z17 } from "zod";
3527
4180
  async function installationRecord(root) {
3528
- return recordSchema.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
4181
+ return recordSchema2.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
3529
4182
  }
3530
4183
  async function installAutomation(dir, host, command) {
3531
4184
  const ws = await workspace(dir);
@@ -3594,7 +4247,7 @@ async function installedAutomation(dir) {
3594
4247
  }
3595
4248
  return result;
3596
4249
  }
3597
- var groupSchema, automationConfigSchema, recordSchema, AUTOMATION_PATH, configPath;
4250
+ var groupSchema, automationConfigSchema, recordSchema2, AUTOMATION_PATH, configPath;
3598
4251
  var init_install = __esm({
3599
4252
  "src/automation/install.ts"() {
3600
4253
  "use strict";
@@ -3604,9 +4257,9 @@ var init_install = __esm({
3604
4257
  init_store();
3605
4258
  init_files2();
3606
4259
  init_ownership();
3607
- groupSchema = z13.object({ hooks: z13.array(z13.object({ type: z13.string(), command: z13.string().optional() }).passthrough()) }).passthrough();
3608
- automationConfigSchema = z13.object({ hooks: z13.record(z13.array(groupSchema)).optional() }).passthrough();
3609
- recordSchema = z13.object({ version: z13.literal(1), hosts: z13.record(z13.object({ command: z13.string() })) });
4260
+ groupSchema = z17.object({ hooks: z17.array(z17.object({ type: z17.string(), command: z17.string().optional() }).passthrough()) }).passthrough();
4261
+ automationConfigSchema = z17.object({ hooks: z17.record(z17.array(groupSchema)).optional() }).passthrough();
4262
+ recordSchema2 = z17.object({ version: z17.literal(1), hosts: z17.record(z17.object({ command: z17.string() })) });
3610
4263
  AUTOMATION_PATH = ".mason/local/automation.json";
3611
4264
  configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
3612
4265
  }
@@ -3620,8 +4273,8 @@ __export(teardown_exports, {
3620
4273
  });
3621
4274
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
3622
4275
  import { parse as parse3, stringify as stringify2 } from "smol-toml";
3623
- import fg5 from "fast-glob";
3624
- import { z as z14 } from "zod";
4276
+ import fg6 from "fast-glob";
4277
+ import { z as z18 } from "zod";
3625
4278
  async function planTeardown(root, selected) {
3626
4279
  const edits = [], diagnostics = [];
3627
4280
  const inputs = /* @__PURE__ */ new Map();
@@ -3641,7 +4294,7 @@ async function planTeardown(root, selected) {
3641
4294
  const setupBefore = await read(SETUP_PATH);
3642
4295
  const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
3643
4296
  const automationBefore = await read(AUTOMATION_PATH);
3644
- const automation = automationBefore === null ? null : recordSchema.parse(JSON.parse(automationBefore));
4297
+ const automation = automationBefore === null ? null : recordSchema2.parse(JSON.parse(automationBefore));
3645
4298
  const commands = (host) => new Set([
3646
4299
  automation?.hosts[host]?.command,
3647
4300
  ...ownership.files[configPath(host)]?.hookCommands ?? [],
@@ -3755,7 +4408,7 @@ async function planTeardown(root, selected) {
3755
4408
  if (diagnostics.length) edits.splice(instructionEditStart);
3756
4409
  if (!diagnostics.length) {
3757
4410
  const cleanup = [];
3758
- const receiptFiles = await fg5(
4411
+ const receiptFiles = await fg6(
3759
4412
  selected.map((host) => ".mason/reports/automation/*/setup-" + host + ".json"),
3760
4413
  { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false }
3761
4414
  );
@@ -3768,7 +4421,7 @@ async function planTeardown(root, selected) {
3768
4421
  if (!remainingHost) await attempt(".mason/local/project.json", async () => {
3769
4422
  const before = await read(".mason/local/project.json");
3770
4423
  if (before === null) return;
3771
- z14.object({ version: z14.literal(1), initializedAt: z14.string() }).strict().parse(JSON.parse(before));
4424
+ z18.object({ version: z18.literal(1), initializedAt: z18.string() }).strict().parse(JSON.parse(before));
3772
4425
  cleanup.push({ path: ".mason/local/project.json", before, after: null });
3773
4426
  });
3774
4427
  if (!diagnostics.length) {
@@ -3840,14 +4493,14 @@ var init_teardown = __esm({
3840
4493
  init_model();
3841
4494
  init_ownership();
3842
4495
  hosts = ["codex", "claude"];
3843
- object2 = z14.record(z14.unknown());
4496
+ object2 = z18.record(z18.unknown());
3844
4497
  json = (value) => JSON.stringify(value, null, 2) + "\n";
3845
4498
  }
3846
4499
  });
3847
4500
 
3848
4501
  // src/review/cochange.ts
3849
- import { execFile as execFile11 } from "child_process";
3850
- import { promisify as promisify11 } from "util";
4502
+ import { execFile as execFile12 } from "child_process";
4503
+ import { promisify as promisify12 } from "util";
3851
4504
  async function buildMatrix(resolvedRoot) {
3852
4505
  try {
3853
4506
  const { stdout: shallow } = await exec9("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
@@ -3912,7 +4565,7 @@ var exec9, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_R
3912
4565
  var init_cochange = __esm({
3913
4566
  "src/review/cochange.ts"() {
3914
4567
  "use strict";
3915
- exec9 = promisify11(execFile11);
4568
+ exec9 = promisify12(execFile12);
3916
4569
  HISTORY_COMMITS = 1500;
3917
4570
  MIN_FILE_COMMITS = 5;
3918
4571
  MIN_SHARED_COMMITS = 4;
@@ -3967,7 +4620,7 @@ var init_types2 = __esm({
3967
4620
  });
3968
4621
 
3969
4622
  // src/review/evidence/vitest.ts
3970
- import { z as z15 } from "zod";
4623
+ import { z as z19 } from "zod";
3971
4624
  function parseVitest(raw, sourceRoot) {
3972
4625
  const report = schema.parse(raw);
3973
4626
  const findings = [], diagnostics = [];
@@ -4010,30 +4663,30 @@ function parseVitest(raw, sourceRoot) {
4010
4663
  diagnostics
4011
4664
  };
4012
4665
  }
4013
- var count2, schema;
4666
+ var count3, schema;
4014
4667
  var init_vitest = __esm({
4015
4668
  "src/review/evidence/vitest.ts"() {
4016
4669
  "use strict";
4017
4670
  init_paths2();
4018
4671
  init_types2();
4019
- count2 = z15.number().int().nonnegative();
4020
- schema = z15.object({
4021
- success: z15.boolean(),
4022
- numTotalTests: count2,
4023
- numPassedTests: count2,
4024
- numFailedTests: count2,
4025
- numPendingTests: count2,
4026
- numTodoTests: count2,
4027
- numFailedTestSuites: count2,
4028
- testResults: z15.array(z15.object({
4029
- name: z15.string().min(1),
4030
- status: z15.enum(["passed", "failed"]),
4031
- message: z15.string().optional(),
4032
- assertionResults: z15.array(z15.object({
4033
- fullName: z15.string(),
4034
- status: z15.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
4035
- failureMessages: z15.array(z15.string()).nullable().optional(),
4036
- location: z15.object({ line: count2, column: count2 }).nullable().optional()
4672
+ count3 = z19.number().int().nonnegative();
4673
+ schema = z19.object({
4674
+ success: z19.boolean(),
4675
+ numTotalTests: count3,
4676
+ numPassedTests: count3,
4677
+ numFailedTests: count3,
4678
+ numPendingTests: count3,
4679
+ numTodoTests: count3,
4680
+ numFailedTestSuites: count3,
4681
+ testResults: z19.array(z19.object({
4682
+ name: z19.string().min(1),
4683
+ status: z19.enum(["passed", "failed"]),
4684
+ message: z19.string().optional(),
4685
+ assertionResults: z19.array(z19.object({
4686
+ fullName: z19.string(),
4687
+ status: z19.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
4688
+ failureMessages: z19.array(z19.string()).nullable().optional(),
4689
+ location: z19.object({ line: count3, column: count3 }).nullable().optional()
4037
4690
  }))
4038
4691
  }))
4039
4692
  });
@@ -4041,7 +4694,7 @@ var init_vitest = __esm({
4041
4694
  });
4042
4695
 
4043
4696
  // src/review/evidence/sarif.ts
4044
- import { z as z16 } from "zod";
4697
+ import { z as z20 } from "zod";
4045
4698
  function parseSarif(raw, sourceRoot) {
4046
4699
  const report = schema2.parse(raw), findings = [], diagnostics = [];
4047
4700
  let active = 0, suppressed = 0, absent = 0, unresolved = 0;
@@ -4132,40 +4785,98 @@ var init_sarif = __esm({
4132
4785
  "use strict";
4133
4786
  init_paths2();
4134
4787
  init_types2();
4135
- index = z16.number().int().nonnegative();
4136
- artifact = z16.object({ uri: z16.string().optional(), uriBaseId: z16.string().optional(), index: index.optional() });
4137
- message = z16.object({ text: z16.string().optional(), markdown: z16.string().optional(), id: z16.string().optional(), arguments: z16.array(z16.string()).optional() });
4138
- level = z16.enum(["error", "warning", "note", "none"]);
4139
- location = z16.object({ physicalLocation: z16.object({ artifactLocation: artifact.optional(), region: z16.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
4140
- schema2 = z16.object({
4141
- version: z16.literal("2.1.0"),
4142
- runs: z16.array(z16.object({
4143
- tool: z16.object({ driver: z16.object({ name: z16.string().min(1), rules: z16.array(z16.object({ id: z16.string(), defaultConfiguration: z16.object({ level: level.optional() }).optional(), messageStrings: z16.record(message).optional() })).optional(), globalMessageStrings: z16.record(message).optional() }) }),
4144
- invocations: z16.array(z16.object({ executionSuccessful: z16.boolean(), commandLine: z16.string().optional(), toolExecutionNotifications: z16.array(z16.object({ level: level.optional(), message })).optional() })).optional(),
4145
- versionControlProvenance: z16.array(z16.object({ revisionId: z16.string().optional() })).optional(),
4146
- originalUriBaseIds: z16.record(artifact).optional(),
4147
- artifacts: z16.array(z16.object({ location: artifact.optional() })).optional(),
4148
- results: z16.array(z16.object({
4149
- ruleId: z16.string().optional(),
4788
+ index = z20.number().int().nonnegative();
4789
+ artifact = z20.object({ uri: z20.string().optional(), uriBaseId: z20.string().optional(), index: index.optional() });
4790
+ message = z20.object({ text: z20.string().optional(), markdown: z20.string().optional(), id: z20.string().optional(), arguments: z20.array(z20.string()).optional() });
4791
+ level = z20.enum(["error", "warning", "note", "none"]);
4792
+ location = z20.object({ physicalLocation: z20.object({ artifactLocation: artifact.optional(), region: z20.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
4793
+ schema2 = z20.object({
4794
+ version: z20.literal("2.1.0"),
4795
+ runs: z20.array(z20.object({
4796
+ tool: z20.object({ driver: z20.object({ name: z20.string().min(1), rules: z20.array(z20.object({ id: z20.string(), defaultConfiguration: z20.object({ level: level.optional() }).optional(), messageStrings: z20.record(message).optional() })).optional(), globalMessageStrings: z20.record(message).optional() }) }),
4797
+ invocations: z20.array(z20.object({ executionSuccessful: z20.boolean(), commandLine: z20.string().optional(), toolExecutionNotifications: z20.array(z20.object({ level: level.optional(), message })).optional() })).optional(),
4798
+ versionControlProvenance: z20.array(z20.object({ revisionId: z20.string().optional() })).optional(),
4799
+ originalUriBaseIds: z20.record(artifact).optional(),
4800
+ artifacts: z20.array(z20.object({ location: artifact.optional() })).optional(),
4801
+ results: z20.array(z20.object({
4802
+ ruleId: z20.string().optional(),
4150
4803
  ruleIndex: index.optional(),
4151
4804
  message,
4152
4805
  level: level.optional(),
4153
- kind: z16.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
4154
- baselineState: z16.enum(["new", "unchanged", "updated", "absent"]).optional(),
4155
- suppressions: z16.array(z16.object({ kind: z16.enum(["inSource", "external"]), status: z16.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
4156
- locations: z16.array(location).optional(),
4157
- relatedLocations: z16.array(location).optional()
4806
+ kind: z20.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
4807
+ baselineState: z20.enum(["new", "unchanged", "updated", "absent"]).optional(),
4808
+ suppressions: z20.array(z20.object({ kind: z20.enum(["inSource", "external"]), status: z20.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
4809
+ locations: z20.array(location).optional(),
4810
+ relatedLocations: z20.array(location).optional()
4158
4811
  })).optional()
4159
4812
  }))
4160
4813
  });
4161
4814
  }
4162
4815
  });
4163
4816
 
4817
+ // src/review/evidence/check.ts
4818
+ import { z as z21 } from "zod";
4819
+ function parseCheck(raw, sourceRoot) {
4820
+ const report = schema3.parse(raw);
4821
+ const diagnostics = [], findings = [];
4822
+ const counts = { total: report.results.length, passed: 0, failed: 0, skipped: 0 };
4823
+ for (const [index2, result] of report.results.entries()) {
4824
+ counts[result.status]++;
4825
+ const locations = result.locations.flatMap((location2) => {
4826
+ const file = evidencePath(location2.file, sourceRoot);
4827
+ if (!file) {
4828
+ diagnostics.push(`Check location is outside the declared checkout or invalid: ${location2.file}`);
4829
+ return [];
4830
+ }
4831
+ return [{ ...location2, file }];
4832
+ });
4833
+ if (result.status === "failed") findings.push({
4834
+ id: String(index2),
4835
+ ...messagePreview([result.name, result.message].filter(Boolean).join("\n")),
4836
+ severity: "error",
4837
+ state: "active",
4838
+ locations
4839
+ });
4840
+ }
4841
+ if (!counts.total) diagnostics.push("No check results were recorded; an empty report is not passing evidence.");
4842
+ if (counts.skipped) diagnostics.push(`${counts.skipped} checks were skipped; verification is incomplete.`);
4843
+ return {
4844
+ outcome: counts.failed ? "failed" : !counts.total ? "unavailable" : !counts.passed ? "skipped" : "passed",
4845
+ counts,
4846
+ findings,
4847
+ incomplete: diagnostics.length > 0,
4848
+ diagnostics,
4849
+ ...report.commit ? { reportedCommits: [report.commit] } : {}
4850
+ };
4851
+ }
4852
+ var schema3;
4853
+ var init_check = __esm({
4854
+ "src/review/evidence/check.ts"() {
4855
+ "use strict";
4856
+ init_paths2();
4857
+ init_types2();
4858
+ schema3 = z21.object({
4859
+ version: z21.literal(1),
4860
+ commit: z21.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).optional(),
4861
+ results: z21.array(z21.object({
4862
+ name: z21.string().min(1).max(1e3),
4863
+ status: z21.enum(["passed", "failed", "skipped"]),
4864
+ message: z21.string().max(1e5).optional(),
4865
+ locations: z21.array(z21.object({
4866
+ file: z21.string().min(1).max(4e3),
4867
+ line: z21.number().int().positive().optional(),
4868
+ column: z21.number().int().positive().optional()
4869
+ })).max(100).default([])
4870
+ })).max(1e5)
4871
+ });
4872
+ }
4873
+ });
4874
+
4164
4875
  // src/review/evidence.ts
4165
- import path19 from "path";
4166
- import { z as z17 } from "zod";
4876
+ import path22 from "path";
4877
+ import { z as z22 } from "zod";
4167
4878
  function relativeArtifact(root, file) {
4168
- const relative = path19.isAbsolute(file) ? path19.relative(root, file) : file;
4879
+ const relative = path22.isAbsolute(file) ? path22.relative(root, file) : file;
4169
4880
  const normalized = normalizeRepoPath(relative);
4170
4881
  if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
4171
4882
  return normalized;
@@ -4204,7 +4915,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4204
4915
  let manifest, raw;
4205
4916
  try {
4206
4917
  manifest = relativeArtifact(root, supplied);
4207
- raw = z17.object({ version: z17.literal(1), checks: z17.array(z17.unknown()) }).parse(await readStoreJson(root, manifest));
4918
+ raw = z22.object({ version: z22.literal(1), checks: z22.array(z22.unknown()) }).parse(await readStoreJson(root, manifest));
4208
4919
  } catch (error) {
4209
4920
  output.diagnostics.push(`${supplied}: ${String(error)}`);
4210
4921
  continue;
@@ -4243,7 +4954,8 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4243
4954
  counts: {},
4244
4955
  incomplete: false,
4245
4956
  diagnostics: [],
4246
- truncated: false
4957
+ truncated: false,
4958
+ ...check.environment ? { environment: check.environment } : {}
4247
4959
  };
4248
4960
  output.checks.push(result);
4249
4961
  if (check.workingTreeClean !== true) result.diagnostics.push("The check's working tree was dirty or not recorded; its results cannot be attributed to the claimed commit alone.");
@@ -4256,13 +4968,13 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4256
4968
  }
4257
4969
  try {
4258
4970
  if (!check.report) throw new Error("Completed check has no report artifact.");
4259
- if (check.kind === "tests" !== (check.report.format === "vitest-json")) throw new Error("Test checks require vitest-json; analysis checks require sarif.");
4971
+ if (check.report.format !== "mason-check-json" && check.kind === "tests" !== (check.report.format === "vitest-json")) throw new Error("Test checks require vitest-json or mason-check-json; analysis checks require sarif or mason-check-json.");
4260
4972
  const file = relativeArtifact(root, check.report.path);
4261
4973
  const report = await readStoreJson(root, file);
4262
4974
  if (report === null) throw new Error(`Report artifact is missing: ${file}`);
4263
4975
  const sourceRoot = check.sourceRoot ?? root;
4264
- if (!path19.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
4265
- const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : parseSarif(report, sourceRoot);
4976
+ if (!path22.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
4977
+ const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
4266
4978
  result.outcome = parsed.outcome;
4267
4979
  result.counts = parsed.counts;
4268
4980
  result.incomplete = parsed.incomplete;
@@ -4282,7 +4994,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4282
4994
  result.outcome = "failed";
4283
4995
  result.diagnostics.push(`Check command exited ${check.exitCode} despite a report with no active failures.`);
4284
4996
  }
4285
- if (check.report.format === "vitest-json" && pairs === void 0) {
4997
+ if (check.kind === "tests" && pairs === void 0) {
4286
4998
  try {
4287
4999
  pairs = (await buildTestMap(root)).paired;
4288
5000
  } catch (error) {
@@ -4356,29 +5068,36 @@ var init_evidence2 = __esm({
4356
5068
  init_provenance();
4357
5069
  init_vitest();
4358
5070
  init_sarif();
5071
+ init_check();
4359
5072
  init_types2();
4360
- checkSchema2 = z17.object({
4361
- id: z17.string().min(1).max(100),
4362
- kind: z17.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
4363
- tool: z17.string().min(1).max(200),
4364
- command: z17.string().min(1).max(2e3),
4365
- commit: z17.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
4366
- workingTreeClean: z17.boolean().optional(),
4367
- source: z17.string().max(2e3).optional(),
4368
- sourceRoot: z17.string().min(1).max(4e3).optional(),
4369
- status: z17.enum(["completed", "skipped", "unavailable"]).default("completed"),
4370
- reason: z17.string().min(1).max(2e3).optional(),
4371
- exitCode: z17.number().int().nullable().optional(),
4372
- report: z17.object({ format: z17.enum(["vitest-json", "sarif"]), path: z17.string().min(1).max(4e3) }).optional()
5073
+ checkSchema2 = z22.object({
5074
+ id: z22.string().min(1).max(100),
5075
+ kind: z22.enum(["tests", "static-analysis", "security", "complexity", "duplication", "documentation"]),
5076
+ tool: z22.string().min(1).max(200),
5077
+ command: z22.string().min(1).max(2e3),
5078
+ commit: z22.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
5079
+ workingTreeClean: z22.boolean().optional(),
5080
+ source: z22.string().max(2e3).optional(),
5081
+ sourceRoot: z22.string().min(1).max(4e3).optional(),
5082
+ status: z22.enum(["completed", "skipped", "unavailable"]).default("completed"),
5083
+ reason: z22.string().min(1).max(2e3).optional(),
5084
+ exitCode: z22.number().int().nullable().optional(),
5085
+ environment: z22.object({
5086
+ toolVersion: z22.string().min(1).max(200),
5087
+ runtime: z22.string().min(1).max(200).optional(),
5088
+ platform: z22.string().min(1).max(200).optional(),
5089
+ configuration: z22.array(z22.string().min(1).max(4e3)).max(100).optional()
5090
+ }).optional(),
5091
+ report: z22.object({ format: z22.enum(["vitest-json", "sarif", "mason-check-json"]), path: z22.string().min(1).max(4e3) }).optional()
4373
5092
  });
4374
5093
  }
4375
5094
  });
4376
5095
 
4377
5096
  // src/review/review.ts
4378
- import fs16 from "fs/promises";
4379
- import path20 from "path";
4380
- import { execFile as execFile12 } from "child_process";
4381
- import { promisify as promisify12 } from "util";
5097
+ import fs18 from "fs/promises";
5098
+ import path23 from "path";
5099
+ import { execFile as execFile13 } from "child_process";
5100
+ import { promisify as promisify13 } from "util";
4382
5101
  async function resolveMergeBase(resolvedRoot, base, head) {
4383
5102
  try {
4384
5103
  const { stdout } = await exec10("git", ["merge-base", base, head], {
@@ -4405,7 +5124,7 @@ function anchorsTouched(record, changedFiles) {
4405
5124
  return matchingPaths(record.files, changedFiles);
4406
5125
  }
4407
5126
  async function computeReview(rootDir, base, options = {}) {
4408
- const resolvedRoot = path20.resolve(rootDir);
5127
+ const resolvedRoot = path23.resolve(rootDir);
4409
5128
  const head = await getCurrentGitHash(resolvedRoot);
4410
5129
  if (head === "unknown") return null;
4411
5130
  const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
@@ -4455,7 +5174,7 @@ async function computeReview(rootDir, base, options = {}) {
4455
5174
  analyzed,
4456
5175
  async (relPath) => {
4457
5176
  try {
4458
- await fs16.access(path20.join(resolvedRoot, relPath));
5177
+ await fs18.access(path23.join(resolvedRoot, relPath));
4459
5178
  return true;
4460
5179
  } catch {
4461
5180
  return false;
@@ -4500,7 +5219,7 @@ var init_review = __esm({
4500
5219
  init_cochange();
4501
5220
  init_evidence2();
4502
5221
  init_snapshot();
4503
- exec10 = promisify12(execFile12);
5222
+ exec10 = promisify13(execFile13);
4504
5223
  MAX_ANALYZED_FILES = 50;
4505
5224
  }
4506
5225
  });
@@ -4509,7 +5228,7 @@ var init_review = __esm({
4509
5228
  async function auditSummary(root) {
4510
5229
  try {
4511
5230
  const report = await computeAudit(root);
4512
- if (!report) return { status: "no-context-files", reason: "No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md found to audit." };
5231
+ if (!report) return { status: "no-context-files", reason: "No README.md or agent instruction files found to audit." };
4513
5232
  if (!report.gitAvailable) return { status: "unavailable", reason: "Audit needs readable Git history to verify documentation claims.", docs: report.docs };
4514
5233
  return {
4515
5234
  ...report,