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.
package/dist/mason.js CHANGED
@@ -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 quote2 = "", 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 (quote2) {
361
+ if (char === quote2) quote2 = "";
362
+ continue;
363
+ }
364
+ if (char === "'" || char === '"') {
365
+ quote2 = 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",
@@ -927,67 +808,502 @@ async function commitsTouchingSince(resolvedRoot, fromHash, pathspecs) {
927
808
  }
928
809
  return { commits, total: commits.length };
929
810
  } catch {
930
- return null;
811
+ return null;
812
+ }
813
+ }
814
+ var exec2, COMMIT_FORMAT;
815
+ var init_git = __esm({
816
+ "src/audit/git.ts"() {
817
+ "use strict";
818
+ exec2 = promisify2(execFile2);
819
+ COMMIT_FORMAT = "%H%x09%cI%x09%s";
820
+ }
821
+ });
822
+
823
+ // src/audit/inputs.ts
824
+ import fs4 from "fs/promises";
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);
832
+ try {
833
+ return await storePath(root, file);
834
+ } catch (error) {
835
+ if (error instanceof Error && error.message.startsWith("Symlink")) {
836
+ throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
837
+ }
838
+ throw error;
839
+ }
840
+ }
841
+ async function readAuditInput(root, file) {
842
+ try {
843
+ const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
844
+ if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
845
+ return value;
846
+ } catch (error) {
847
+ if (error.code === "ENOENT") return null;
848
+ throw error;
849
+ }
850
+ }
851
+ async function auditGlob(root, patterns, options) {
852
+ if (Array.isArray(patterns) && !patterns.length) return [];
853
+ const directories = /* @__PURE__ */ new Set();
854
+ const readdir2 = ((directory, opts, callback) => {
855
+ const relative = path5.relative(root, directory).split(path5.sep).join("/") || ".";
856
+ directories.add(relative);
857
+ if (directories.size > MAX_INPUTS) {
858
+ callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
859
+ return;
860
+ }
861
+ auditInputPath(root, relative).then(() => {
862
+ nativeFs.readdir(directory, opts, callback);
863
+ }, callback);
864
+ });
865
+ const lstat2 = ((file, callback) => {
866
+ const relative = path5.relative(root, file).split(path5.sep).join("/") || ".";
867
+ auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
868
+ });
869
+ const stream = fg2.stream(patterns, {
870
+ cwd: root,
871
+ ignore: options.ignore,
872
+ onlyFiles: false,
873
+ onlyDirectories: options.onlyDirectories,
874
+ objectMode: true,
875
+ followSymbolicLinks: options.followSymbolicLinks ?? true,
876
+ fs: { readdir: readdir2, lstat: lstat2 },
877
+ concurrency: 16
878
+ });
879
+ const matches = [];
880
+ try {
881
+ for await (const value of stream) {
882
+ const entry = value;
883
+ const file = entry.path;
884
+ if (options.select && !options.select(file)) continue;
885
+ await auditInputPath(root, file);
886
+ if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
887
+ matches.push(file);
888
+ if (matches.length > MAX_INPUTS) {
889
+ const largest = [...matches.reduce((counts, item) => {
890
+ const dir = item.split("/")[0];
891
+ counts.set(dir, (counts.get(dir) ?? 0) + 1);
892
+ return counts;
893
+ }, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
894
+ throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count4]) => `${dir} (${count4})`).join(", ")}. Narrow this check's inputs.`);
895
+ }
896
+ }
897
+ } finally {
898
+ stream.destroy();
899
+ }
900
+ return matches.sort();
901
+ }
902
+ async function gitSourcePaths(root) {
903
+ const { stdout } = await execute(
904
+ "git",
905
+ [
906
+ "ls-files",
907
+ "-z",
908
+ "--cached",
909
+ "--others",
910
+ "--exclude-standard",
911
+ "--",
912
+ ...SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`)
913
+ ],
914
+ { cwd: root, maxBuffer: 32 * 1024 * 1024, timeout: 1e4 }
915
+ );
916
+ return [...new Set(stdout.split("\0").filter(Boolean))].sort();
917
+ }
918
+ async function gitIgnoredPaths(root, files) {
919
+ if (!files.length) return /* @__PURE__ */ new Set();
920
+ const operation = execute(
921
+ "git",
922
+ ["check-ignore", "-z", "--stdin"],
923
+ { cwd: root, maxBuffer: 4 * 1024 * 1024, timeout: 1e4 }
924
+ );
925
+ operation.child.stdin.on("error", () => {
926
+ });
927
+ operation.child.stdin.end(files.join("\0") + "\0");
928
+ try {
929
+ return new Set((await operation).stdout.split("\0").filter(Boolean));
930
+ } catch (error) {
931
+ if (error.code === 1) return /* @__PURE__ */ new Set();
932
+ throw error;
933
+ }
934
+ }
935
+ var execute, MAX_INPUTS;
936
+ var init_inputs = __esm({
937
+ "src/audit/inputs.ts"() {
938
+ "use strict";
939
+ init_files();
940
+ init_storage();
941
+ execute = promisify3(execFile3);
942
+ MAX_INPUTS = 1e5;
943
+ }
944
+ });
945
+
946
+ // src/audit/docs.ts
947
+ import fs5 from "fs/promises";
948
+ import path6 from "path";
949
+ import { execFile as execFile4 } from "child_process";
950
+ import { promisify as promisify4 } from "util";
951
+ import fg3 from "fast-glob";
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";
931
1218
  }
932
1219
  }
933
- var exec4, COMMIT_FORMAT;
934
- var init_git = __esm({
935
- "src/audit/git.ts"() {
1220
+ var exec4, repoPath, verificationFields, featureSchema, flowSchema, snapshotSchema;
1221
+ var init_snapshot = __esm({
1222
+ "src/snapshot/snapshot.ts"() {
936
1223
  "use strict";
937
- exec4 = promisify4(execFile4);
938
- COMMIT_FORMAT = "%H%x09%cI%x09%s";
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();
939
1254
  }
940
1255
  });
941
1256
 
942
- // src/audit/docs.ts
943
- 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) {
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;
948
1281
  try {
949
- const { stdout } = await exec5(
950
- "git",
951
- ["status", "--porcelain", "--", relPath],
952
- { cwd: resolvedRoot }
953
- );
954
- return stdout.trim().length > 0;
1282
+ const { stdout } = await exec5("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
1283
+ return parseChanges(stdout);
955
1284
  } catch {
956
- return false;
1285
+ return null;
957
1286
  }
958
1287
  }
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;
966
- }
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);
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
+ }
978
1299
  }
979
- var exec5, DOC_CANDIDATES;
980
- var init_docs = __esm({
981
- "src/audit/docs.ts"() {
1300
+ var exec5;
1301
+ var init_drift = __esm({
1302
+ "src/drift/drift.ts"() {
982
1303
  "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
- ];
1304
+ init_snapshot();
1305
+ init_paths();
1306
+ exec5 = promisify6(execFile6);
991
1307
  }
992
1308
  });
993
1309
 
@@ -1008,91 +1324,51 @@ var init_types = __esm({
1008
1324
  });
1009
1325
 
1010
1326
  // src/audit/checks/deleted-reference.ts
1011
- import fs5 from "fs/promises";
1012
- import path8 from "path";
1013
- async function exists(absPath) {
1014
- try {
1015
- await fs5.access(absPath);
1016
- return true;
1017
- } catch {
1018
- return false;
1019
- }
1020
- }
1327
+ import path11 from "path";
1021
1328
  async function checkDeletedReferences(ctx) {
1022
1329
  const result = emptyResult();
1023
1330
  for (const doc of ctx.docs) {
1024
1331
  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
- }
1332
+ const renames = new Map((changes ?? []).filter((c) => c.status === "renamed" && c.previousPath).map((c) => [c.previousPath, c.path]));
1031
1333
  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
- }
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.`
1074
1340
  });
1075
1341
  continue;
1076
1342
  }
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
- }
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
1094
1364
  };
1095
- result.issues.push(issue);
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
+ }
1096
1372
  }
1097
1373
  }
1098
1374
  return result;
@@ -1100,138 +1376,16 @@ async function checkDeletedReferences(ctx) {
1100
1376
  var init_deleted_reference = __esm({
1101
1377
  "src/audit/checks/deleted-reference.ts"() {
1102
1378
  "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;
1134
- }
1135
- }
1136
- async function auditGlob(root, patterns, options) {
1137
- if (Array.isArray(patterns) && !patterns.length) return [];
1138
- const directories = /* @__PURE__ */ new Set();
1139
- const readdir2 = ((directory, opts, callback) => {
1140
- const relative = path9.relative(root, directory).split(path9.sep).join("/") || ".";
1141
- directories.add(relative);
1142
- if (directories.size > MAX_INPUTS) {
1143
- callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
1144
- return;
1145
- }
1146
- auditInputPath(root, relative).then(() => {
1147
- nativeFs.readdir(directory, opts, callback);
1148
- }, callback);
1149
- });
1150
- const lstat2 = ((file, callback) => {
1151
- const relative = path9.relative(root, file).split(path9.sep).join("/") || ".";
1152
- auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
1153
- });
1154
- const stream = fg2.stream(patterns, {
1155
- cwd: root,
1156
- ignore: options.ignore,
1157
- onlyFiles: false,
1158
- onlyDirectories: options.onlyDirectories,
1159
- objectMode: true,
1160
- followSymbolicLinks: options.followSymbolicLinks ?? true,
1161
- fs: { readdir: readdir2, lstat: lstat2 },
1162
- concurrency: 16
1163
- });
1164
- const matches = [];
1165
- try {
1166
- for await (const value of stream) {
1167
- const entry = value;
1168
- const file = entry.path;
1169
- if (options.select && !options.select(file)) continue;
1170
- await auditInputPath(root, file);
1171
- if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
1172
- matches.push(file);
1173
- if (matches.length > MAX_INPUTS) {
1174
- const largest = [...matches.reduce((counts, item) => {
1175
- const dir = item.split("/")[0];
1176
- counts.set(dir, (counts.get(dir) ?? 0) + 1);
1177
- return counts;
1178
- }, /* @__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.`);
1180
- }
1181
- }
1182
- } finally {
1183
- stream.destroy();
1184
- }
1185
- return matches.sort();
1186
- }
1187
- async function gitSourcePaths(root) {
1188
- const { stdout } = await execute(
1189
- "git",
1190
- [
1191
- "ls-files",
1192
- "-z",
1193
- "--cached",
1194
- "--others",
1195
- "--exclude-standard",
1196
- "--",
1197
- ...SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`)
1198
- ],
1199
- { cwd: root, maxBuffer: 32 * 1024 * 1024, timeout: 1e4 }
1200
- );
1201
- return [...new Set(stdout.split("\0").filter(Boolean))].sort();
1202
- }
1203
- async function gitIgnoredPaths(root, files) {
1204
- if (!files.length) return /* @__PURE__ */ new Set();
1205
- const operation = execute(
1206
- "git",
1207
- ["check-ignore", "-z", "--stdin"],
1208
- { cwd: root, maxBuffer: 4 * 1024 * 1024, timeout: 1e4 }
1209
- );
1210
- operation.child.stdin.on("error", () => {
1211
- });
1212
- operation.child.stdin.end(files.join("\0") + "\0");
1213
- try {
1214
- return new Set((await operation).stdout.split("\0").filter(Boolean));
1215
- } catch (error) {
1216
- if (error.code === 1) return /* @__PURE__ */ new Set();
1217
- throw error;
1218
- }
1219
- }
1220
- var execute, MAX_INPUTS;
1221
- var init_inputs = __esm({
1222
- "src/audit/inputs.ts"() {
1223
- "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, args2) {
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, ...args2) => (await execute2("git", args2, { 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";
2871
+ import fs13 from "fs/promises";
2872
+ import path20 from "path";
2395
2873
  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, ...args2) {
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, ...args2) {
2401
2879
  return (await exec7("git", args2, { 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() });
3000
+ }
3001
+ });
3002
+
3003
+ // src/automation/execution.ts
3004
+ import os2 from "os";
3005
+ import fs14 from "fs/promises";
3006
+ import { randomUUID as randomUUID3 } from "crypto";
3007
+ import { z as z9 } from "zod";
3008
+ function parseExecution(raw) {
3009
+ try {
3010
+ return executionSchema.parse(raw);
3011
+ } catch (error) {
3012
+ throw new Error("Invalid automation execution store; receipt history was retained.", { cause: error });
3013
+ }
3014
+ }
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;
2529
3024
  }
2530
- });
2531
-
2532
- // src/automation/store.ts
2533
- import fs12 from "fs/promises";
2534
- import os2 from "os";
2535
- import { z as z6 } from "zod";
2536
- function parseState(raw) {
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);
2537
3039
  try {
2538
- return stateSchema.parse(raw);
2539
- } catch (error) {
2540
- throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
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);
2541
3061
  }
2542
3062
  }
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) {
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,27 +3533,27 @@ 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
  });
@@ -2922,16 +3568,16 @@ __export(launcher_exports, {
2922
3568
  mcpCommand: () => mcpCommand,
2923
3569
  prepareLaunch: () => prepareLaunch
2924
3570
  });
2925
- import { execFile as execFile9 } from "child_process";
2926
- import { promisify as promisify9 } from "util";
2927
- import fs13 from "fs/promises";
3571
+ import { execFile as execFile10 } from "child_process";
3572
+ import { promisify as promisify10 } from "util";
3573
+ import fs15 from "fs/promises";
2928
3574
  function globalCommand(args2, platform = process.platform) {
2929
3575
  return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args2] } : { command: "mason", args: args2 };
2930
3576
  }
2931
3577
  async function installedCommand() {
2932
3578
  const invocation = globalCommand(["internal-integration-version"]);
2933
3579
  try {
2934
- const { stdout } = await promisify9(execFile9)(
3580
+ const { stdout } = await promisify10(execFile10)(
2935
3581
  invocation.command,
2936
3582
  invocation.args,
2937
3583
  { timeout: 5e3, maxBuffer: 8192, windowsHide: true }
@@ -2944,7 +3590,7 @@ async function installedCommand() {
2944
3590
  }
2945
3591
  }
2946
3592
  async function prepareLaunch(host, options = {}) {
2947
- const root = await fs13.realpath((await git2(process.cwd(), "rev-parse", "--show-toplevel")).trim());
3593
+ const root = await fs15.realpath((await git3(process.cwd(), "rev-parse", "--show-toplevel")).trim());
2948
3594
  const entry = (await loadSetup(root))?.hosts[host];
2949
3595
  if (!entry) {
2950
3596
  if (options.allowInactive) return false;
@@ -2961,7 +3607,7 @@ var init_launcher = __esm({
2961
3607
  "use strict";
2962
3608
  init_evidence();
2963
3609
  init_model();
2964
- executingVersion = () => true ? "0.16.2" : "development";
3610
+ executingVersion = () => true ? "0.17.0" : "development";
2965
3611
  mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
2966
3612
  hookCommand = (host) => `mason --setup-host ${host} auto`;
2967
3613
  }
@@ -2974,8 +3620,8 @@ __export(observations_exports, {
2974
3620
  observeActivation: () => observeActivation,
2975
3621
  readObservation: () => readObservation
2976
3622
  });
2977
- import fs14 from "fs/promises";
2978
- import { z as z8 } from "zod";
3623
+ import fs16 from "fs/promises";
3624
+ import { z as z12 } from "zod";
2979
3625
  function observationPath(directory, host, revision, version = executingVersion()) {
2980
3626
  return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
2981
3627
  }
@@ -2991,8 +3637,8 @@ async function observeActivation(dir, event, options = {}) {
2991
3637
  if (host !== "codex" && host !== "claude" || !revision || !process.env.MASON_SETUP_ROOT) return null;
2992
3638
  try {
2993
3639
  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];
2994
- const ws = capturedDirectory ? { root: await fs14.realpath(dir), directory: capturedDirectory } : await workspace(dir);
2995
- if (ws.root !== await fs14.realpath(process.env.MASON_SETUP_ROOT)) return null;
3640
+ const ws = capturedDirectory ? { root: await fs16.realpath(dir), directory: capturedDirectory } : await workspace(dir);
3641
+ if (ws.root !== await fs16.realpath(process.env.MASON_SETUP_ROOT)) return null;
2996
3642
  const setup = await loadSetup(ws.root);
2997
3643
  if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
2998
3644
  const directory = ws.directory + "/activation";
@@ -3040,26 +3686,26 @@ var init_observations = __esm({
3040
3686
  init_storage();
3041
3687
  init_model();
3042
3688
  init_launcher();
3043
- observationSchema = z8.object({
3044
- version: z8.literal(1),
3045
- masonVersion: z8.string(),
3046
- root: z8.string(),
3047
- revision: z8.string(),
3048
- host: z8.enum(["codex", "claude"]),
3049
- contextCalls: z8.number().int().nonnegative(),
3050
- lastContextAt: z8.string().optional(),
3051
- sessions: z8.record(z8.object({
3052
- events: z8.array(z8.enum(events)),
3053
- at: z8.string(),
3054
- verificationStatus: z8.string().optional(),
3055
- reportPath: z8.string().optional()
3689
+ observationSchema = z12.object({
3690
+ version: z12.literal(1),
3691
+ masonVersion: z12.string(),
3692
+ root: z12.string(),
3693
+ revision: z12.string(),
3694
+ host: z12.enum(["codex", "claude"]),
3695
+ contextCalls: z12.number().int().nonnegative(),
3696
+ lastContextAt: z12.string().optional(),
3697
+ sessions: z12.record(z12.object({
3698
+ events: z12.array(z12.enum(events)),
3699
+ at: z12.string(),
3700
+ verificationStatus: z12.string().optional(),
3701
+ reportPath: z12.string().optional()
3056
3702
  }))
3057
3703
  });
3058
3704
  }
3059
3705
  });
3060
3706
 
3061
3707
  // src/automation/adapters.ts
3062
- import { z as z9 } from "zod";
3708
+ import { z as z13 } from "zod";
3063
3709
  function normalizeHook(host, raw) {
3064
3710
  hostSchema.parse(host);
3065
3711
  const input = inputSchema.parse(raw);
@@ -3075,6 +3721,7 @@ function normalizeHook(host, raw) {
3075
3721
  }
3076
3722
  async function runAutomationHook(host, stdin) {
3077
3723
  let name = "";
3724
+ let hook;
3078
3725
  try {
3079
3726
  if (Buffer.byteLength(stdin) > 1024 * 1024) throw new Error("Hook input exceeds 1 MiB.");
3080
3727
  let raw;
@@ -3084,6 +3731,7 @@ async function runAutomationHook(host, stdin) {
3084
3731
  throw new Error("Hook input is not valid JSON.");
3085
3732
  }
3086
3733
  const input = normalizeHook(host, raw);
3734
+ hook = input;
3087
3735
  name = input.name;
3088
3736
  if (["before_tool", "after_tool"].includes(input.event.event) && input.readOnly) {
3089
3737
  const observed = await observeReadOnlyTool(input.cwd, input.event);
@@ -3105,7 +3753,8 @@ async function runAutomationHook(host, stdin) {
3105
3753
  }
3106
3754
  return { hookSpecificOutput: { hookEventName: name, additionalContext: result.message } };
3107
3755
  } catch (error) {
3108
- const message2 = failureMessage(error);
3756
+ const message2 = hook ? await hookFailureMessage(hook.cwd, host, hook.event.sessionId, error) : failureMessage(error);
3757
+ if (!message2) return null;
3109
3758
  return { systemMessage: message2, ...["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse"].includes(name) ? { hookSpecificOutput: { hookEventName: name, additionalContext: message2 } } : {} };
3110
3759
  }
3111
3760
  }
@@ -3130,15 +3779,15 @@ var init_adapters = __esm({
3130
3779
  init_execution();
3131
3780
  init_runtime();
3132
3781
  init_store();
3133
- inputSchema = z9.object({
3134
- cwd: z9.string().min(1),
3135
- session_id: z9.string().min(1).max(500),
3136
- hook_event_name: z9.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
3137
- tool_name: z9.string().optional(),
3138
- tool_use_id: z9.string().max(500).optional(),
3139
- tool_input: z9.unknown().optional(),
3140
- stop_hook_active: z9.boolean().optional(),
3141
- permission_mode: z9.string().optional()
3782
+ inputSchema = z13.object({
3783
+ cwd: z13.string().min(1),
3784
+ session_id: z13.string().min(1).max(500),
3785
+ hook_event_name: z13.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
3786
+ tool_name: z13.string().optional(),
3787
+ tool_use_id: z13.string().max(500).optional(),
3788
+ tool_input: z13.unknown().optional(),
3789
+ stop_hook_active: z13.boolean().optional(),
3790
+ permission_mode: z13.string().optional()
3142
3791
  });
3143
3792
  lifecycle = {
3144
3793
  SessionStart: "session_start",
@@ -3158,8 +3807,8 @@ __export(files_exports, {
3158
3807
  managedBlock: () => managedBlock,
3159
3808
  readText: () => readText
3160
3809
  });
3161
- import fs15 from "fs/promises";
3162
- import path18 from "path";
3810
+ import fs17 from "fs/promises";
3811
+ import path21 from "path";
3163
3812
  import { randomUUID as randomUUID5 } from "crypto";
3164
3813
  async function readText(root, file) {
3165
3814
  try {
@@ -3188,14 +3837,14 @@ async function applyEdit(root, edit) {
3188
3837
  if (edit.after === null) {
3189
3838
  const file2 = await storePath(root, edit.path);
3190
3839
  if (await readText(root, edit.path) !== edit.before) throw new Error("Input changed during removal: " + edit.path);
3191
- await fs15.unlink(file2);
3840
+ await fs17.unlink(file2);
3192
3841
  return true;
3193
3842
  }
3194
3843
  const file = await storePath(root, edit.path, true);
3195
- const temporary = path18.join(path18.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3844
+ const temporary = path21.join(path21.dirname(file), ".mason-setup-" + randomUUID5() + ".tmp");
3196
3845
  try {
3197
- const mode = await fs15.stat(file).then((s) => s.mode & 511, () => 384);
3198
- const handle = await fs15.open(temporary, "wx", mode);
3846
+ const mode = await fs17.stat(file).then((s) => s.mode & 511, () => 384);
3847
+ const handle = await fs17.open(temporary, "wx", mode);
3199
3848
  try {
3200
3849
  await handle.writeFile(edit.after, "utf8");
3201
3850
  await handle.sync();
@@ -3203,10 +3852,10 @@ async function applyEdit(root, edit) {
3203
3852
  await handle.close();
3204
3853
  }
3205
3854
  if (await readText(root, edit.path) !== edit.before) throw new Error("Integration input changed during update: " + edit.path);
3206
- await fs15.rename(temporary, file);
3855
+ await fs17.rename(temporary, file);
3207
3856
  return true;
3208
3857
  } finally {
3209
- await fs15.rm(temporary, { force: true });
3858
+ await fs17.rm(temporary, { force: true });
3210
3859
  }
3211
3860
  }
3212
3861
  var init_files2 = __esm({
@@ -3227,7 +3876,7 @@ __export(ownership_exports, {
3227
3876
  ownershipEdit: () => ownershipEdit,
3228
3877
  ownershipSchema: () => ownershipSchema
3229
3878
  });
3230
- import { z as z10 } from "zod";
3879
+ import { z as z14 } from "zod";
3231
3880
  import { parse } from "smol-toml";
3232
3881
  function blockSpan(text2, start, end) {
3233
3882
  const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
@@ -3278,30 +3927,30 @@ var init_ownership = __esm({
3278
3927
  ["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
3279
3928
  ["# mason:mcp:start", "# mason:mcp:end"]
3280
3929
  ];
3281
- fileSchema = z10.object({
3282
- created: z10.boolean(),
3283
- initialHash: z10.string(),
3284
- blocks: z10.record(z10.string()),
3285
- mcpHash: z10.string().optional(),
3286
- hookCommands: z10.array(z10.string()).optional(),
3287
- hooks: z10.record(z10.array(z10.string())).optional()
3930
+ fileSchema = z14.object({
3931
+ created: z14.boolean(),
3932
+ initialHash: z14.string(),
3933
+ blocks: z14.record(z14.string()),
3934
+ mcpHash: z14.string().optional(),
3935
+ hookCommands: z14.array(z14.string()).optional(),
3936
+ hooks: z14.record(z14.array(z14.string())).optional()
3288
3937
  });
3289
- ownershipSchema = z10.object({ version: z10.literal(1), files: z10.record(fileSchema) });
3938
+ ownershipSchema = z14.object({ version: z14.literal(1), files: z14.record(fileSchema) });
3290
3939
  }
3291
3940
  });
3292
3941
 
3293
3942
  // src/mcp/init.ts
3294
- import { z as z11 } from "zod";
3943
+ import { z as z15 } from "zod";
3295
3944
  var markerSchema, CLAUDE_MD_SECTION, ASSISTANT_SETUP, QUICKSTART_PLAYBOOK, MAP_PLAYBOOK;
3296
3945
  var init_init = __esm({
3297
3946
  "src/mcp/init.ts"() {
3298
3947
  "use strict";
3299
3948
  init_storage();
3300
3949
  init_files2();
3301
- markerSchema = z11.object({
3302
- version: z11.literal(1),
3303
- initializedAt: z11.string(),
3304
- features: z11.object({ confluence: z11.boolean().optional() }).passthrough().optional()
3950
+ markerSchema = z15.object({
3951
+ version: z15.literal(1),
3952
+ initializedAt: z15.string(),
3953
+ features: z15.object({ confluence: z15.boolean().optional() }).passthrough().optional()
3305
3954
  }).passthrough();
3306
3955
  CLAUDE_MD_SECTION = `<!-- mason:start -->
3307
3956
  ## Mason project knowledge
@@ -3311,12 +3960,14 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
3311
3960
  - 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.
3312
3961
  - Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
3313
3962
  - 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.
3963
+ - 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.
3314
3964
  - 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.
3315
3965
  - 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.
3316
3966
  - 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.
3317
3967
  - \`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.
3318
3968
 
3319
3969
  - 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.
3970
+ - 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.
3320
3971
 
3321
3972
  - 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.
3322
3973
 
@@ -3332,7 +3983,7 @@ ${CLAUDE_MD_SECTION}`;
3332
3983
 
3333
3984
  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.
3334
3985
  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.
3335
- 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)\`.
3986
+ 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)\`.
3336
3987
 
3337
3988
  ${ASSISTANT_SETUP}
3338
3989
 
@@ -3378,8 +4029,8 @@ var git_exports = {};
3378
4029
  __export(git_exports, {
3379
4030
  isGitRepo: () => isGitRepo
3380
4031
  });
3381
- import { execFile as execFile10 } from "child_process";
3382
- import { promisify as promisify10 } from "util";
4032
+ import { execFile as execFile11 } from "child_process";
4033
+ import { promisify as promisify11 } from "util";
3383
4034
  async function isGitRepo(dir) {
3384
4035
  try {
3385
4036
  await exec8("git", ["rev-parse", "--git-dir"], { cwd: dir });
@@ -3392,7 +4043,7 @@ var exec8;
3392
4043
  var init_git2 = __esm({
3393
4044
  "src/utils/git.ts"() {
3394
4045
  "use strict";
3395
- exec8 = promisify10(execFile10);
4046
+ exec8 = promisify11(execFile11);
3396
4047
  }
3397
4048
  });
3398
4049
 
@@ -3408,7 +4059,7 @@ __export(config_exports, {
3408
4059
  });
3409
4060
  import { isDeepStrictEqual } from "util";
3410
4061
  import { parse as parse2, stringify } from "smol-toml";
3411
- import { z as z12 } from "zod";
4062
+ import { z as z16 } from "zod";
3412
4063
  async function instructionEdits(root, host) {
3413
4064
  const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
3414
4065
  const existing = found.find((f) => f.text !== null);
@@ -3477,11 +4128,11 @@ async function hookEdits(root, host) {
3477
4128
  }
3478
4129
  async function ancillaryEdits(root) {
3479
4130
  const ignore = await readText(root, ".gitignore");
3480
- const { git: git3 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
4131
+ const { git: git4 } = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
3481
4132
  const { isGitRepo: isGitRepo2 } = await Promise.resolve().then(() => (init_git2(), git_exports));
3482
4133
  let parentIgnored = false;
3483
4134
  try {
3484
- if (await isGitRepo2(root)) parentIgnored = !!(await git3(root, "check-ignore", "--no-index", ".mason")).trim();
4135
+ if (await isGitRepo2(root)) parentIgnored = !!(await git4(root, "check-ignore", "--no-index", ".mason")).trim();
3485
4136
  } catch (error) {
3486
4137
  if (error.code !== 1) throw error;
3487
4138
  }
@@ -3490,6 +4141,8 @@ async function ancillaryEdits(root) {
3490
4141
  ...retainParentRule ? ["!/.mason/", "/.mason/*"] : [],
3491
4142
  "!/.mason/decisions/",
3492
4143
  "!/.mason/decisions/**",
4144
+ "!/.mason/reviews/",
4145
+ "!/.mason/reviews/**",
3493
4146
  "!/.mason/config.json",
3494
4147
  "!/.mason/snapshot.json",
3495
4148
  "/.mason/local/",
@@ -3524,7 +4177,7 @@ var init_config = __esm({
3524
4177
  init_install();
3525
4178
  init_files2();
3526
4179
  init_launcher();
3527
- object = z12.record(z12.unknown());
4180
+ object = z16.record(z16.unknown());
3528
4181
  mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
3529
4182
  TOML_START = "# mason:mcp:start";
3530
4183
  TOML_END = "# mason:mcp:end";
@@ -3542,11 +4195,11 @@ __export(install_exports, {
3542
4195
  installAutomation: () => installAutomation,
3543
4196
  installedAutomation: () => installedAutomation,
3544
4197
  planAutomationInstall: () => planAutomationInstall,
3545
- recordSchema: () => recordSchema
4198
+ recordSchema: () => recordSchema2
3546
4199
  });
3547
- import { z as z13 } from "zod";
4200
+ import { z as z17 } from "zod";
3548
4201
  async function installationRecord(root) {
3549
- return recordSchema.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
4202
+ return recordSchema2.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
3550
4203
  }
3551
4204
  async function installAutomation(dir, host, command2) {
3552
4205
  const ws = await workspace(dir);
@@ -3615,7 +4268,7 @@ async function installedAutomation(dir) {
3615
4268
  }
3616
4269
  return result;
3617
4270
  }
3618
- var groupSchema, automationConfigSchema, recordSchema, AUTOMATION_PATH, configPath;
4271
+ var groupSchema, automationConfigSchema, recordSchema2, AUTOMATION_PATH, configPath;
3619
4272
  var init_install = __esm({
3620
4273
  "src/automation/install.ts"() {
3621
4274
  "use strict";
@@ -3625,9 +4278,9 @@ var init_install = __esm({
3625
4278
  init_store();
3626
4279
  init_files2();
3627
4280
  init_ownership();
3628
- groupSchema = z13.object({ hooks: z13.array(z13.object({ type: z13.string(), command: z13.string().optional() }).passthrough()) }).passthrough();
3629
- automationConfigSchema = z13.object({ hooks: z13.record(z13.array(groupSchema)).optional() }).passthrough();
3630
- recordSchema = z13.object({ version: z13.literal(1), hosts: z13.record(z13.object({ command: z13.string() })) });
4281
+ groupSchema = z17.object({ hooks: z17.array(z17.object({ type: z17.string(), command: z17.string().optional() }).passthrough()) }).passthrough();
4282
+ automationConfigSchema = z17.object({ hooks: z17.record(z17.array(groupSchema)).optional() }).passthrough();
4283
+ recordSchema2 = z17.object({ version: z17.literal(1), hosts: z17.record(z17.object({ command: z17.string() })) });
3631
4284
  AUTOMATION_PATH = ".mason/local/automation.json";
3632
4285
  configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
3633
4286
  }
@@ -3641,8 +4294,8 @@ __export(teardown_exports, {
3641
4294
  });
3642
4295
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
3643
4296
  import { parse as parse3, stringify as stringify2 } from "smol-toml";
3644
- import fg5 from "fast-glob";
3645
- import { z as z14 } from "zod";
4297
+ import fg6 from "fast-glob";
4298
+ import { z as z18 } from "zod";
3646
4299
  async function planTeardown(root, selected) {
3647
4300
  const edits = [], diagnostics = [];
3648
4301
  const inputs = /* @__PURE__ */ new Map();
@@ -3662,7 +4315,7 @@ async function planTeardown(root, selected) {
3662
4315
  const setupBefore = await read(SETUP_PATH);
3663
4316
  const setup = setupBefore === null ? null : setupSchema.parse(JSON.parse(setupBefore));
3664
4317
  const automationBefore = await read(AUTOMATION_PATH);
3665
- const automation = automationBefore === null ? null : recordSchema.parse(JSON.parse(automationBefore));
4318
+ const automation = automationBefore === null ? null : recordSchema2.parse(JSON.parse(automationBefore));
3666
4319
  const commands = (host) => new Set([
3667
4320
  automation?.hosts[host]?.command,
3668
4321
  ...ownership.files[configPath(host)]?.hookCommands ?? [],
@@ -3776,7 +4429,7 @@ async function planTeardown(root, selected) {
3776
4429
  if (diagnostics.length) edits.splice(instructionEditStart);
3777
4430
  if (!diagnostics.length) {
3778
4431
  const cleanup = [];
3779
- const receiptFiles = await fg5(
4432
+ const receiptFiles = await fg6(
3780
4433
  selected.map((host) => ".mason/reports/automation/*/setup-" + host + ".json"),
3781
4434
  { cwd: root, dot: true, onlyFiles: false, followSymbolicLinks: false }
3782
4435
  );
@@ -3789,7 +4442,7 @@ async function planTeardown(root, selected) {
3789
4442
  if (!remainingHost) await attempt(".mason/local/project.json", async () => {
3790
4443
  const before = await read(".mason/local/project.json");
3791
4444
  if (before === null) return;
3792
- z14.object({ version: z14.literal(1), initializedAt: z14.string() }).strict().parse(JSON.parse(before));
4445
+ z18.object({ version: z18.literal(1), initializedAt: z18.string() }).strict().parse(JSON.parse(before));
3793
4446
  cleanup.push({ path: ".mason/local/project.json", before, after: null });
3794
4447
  });
3795
4448
  if (!diagnostics.length) {
@@ -3861,14 +4514,14 @@ var init_teardown = __esm({
3861
4514
  init_model();
3862
4515
  init_ownership();
3863
4516
  hosts = ["codex", "claude"];
3864
- object2 = z14.record(z14.unknown());
4517
+ object2 = z18.record(z18.unknown());
3865
4518
  json = (value) => JSON.stringify(value, null, 2) + "\n";
3866
4519
  }
3867
4520
  });
3868
4521
 
3869
4522
  // src/review/cochange.ts
3870
- import { execFile as execFile11 } from "child_process";
3871
- import { promisify as promisify11 } from "util";
4523
+ import { execFile as execFile12 } from "child_process";
4524
+ import { promisify as promisify12 } from "util";
3872
4525
  async function buildMatrix(resolvedRoot) {
3873
4526
  try {
3874
4527
  const { stdout: shallow } = await exec9("git", ["rev-parse", "--is-shallow-repository"], { cwd: resolvedRoot });
@@ -3933,7 +4586,7 @@ var exec9, HISTORY_COMMITS, MIN_FILE_COMMITS, MIN_SHARED_COMMITS, MIN_COCHANGE_R
3933
4586
  var init_cochange = __esm({
3934
4587
  "src/review/cochange.ts"() {
3935
4588
  "use strict";
3936
- exec9 = promisify11(execFile11);
4589
+ exec9 = promisify12(execFile12);
3937
4590
  HISTORY_COMMITS = 1500;
3938
4591
  MIN_FILE_COMMITS = 5;
3939
4592
  MIN_SHARED_COMMITS = 4;
@@ -3988,7 +4641,7 @@ var init_types2 = __esm({
3988
4641
  });
3989
4642
 
3990
4643
  // src/review/evidence/vitest.ts
3991
- import { z as z15 } from "zod";
4644
+ import { z as z19 } from "zod";
3992
4645
  function parseVitest(raw, sourceRoot) {
3993
4646
  const report = schema.parse(raw);
3994
4647
  const findings = [], diagnostics = [];
@@ -4031,30 +4684,30 @@ function parseVitest(raw, sourceRoot) {
4031
4684
  diagnostics
4032
4685
  };
4033
4686
  }
4034
- var count2, schema;
4687
+ var count3, schema;
4035
4688
  var init_vitest = __esm({
4036
4689
  "src/review/evidence/vitest.ts"() {
4037
4690
  "use strict";
4038
4691
  init_paths2();
4039
4692
  init_types2();
4040
- count2 = z15.number().int().nonnegative();
4041
- schema = z15.object({
4042
- success: z15.boolean(),
4043
- numTotalTests: count2,
4044
- numPassedTests: count2,
4045
- numFailedTests: count2,
4046
- numPendingTests: count2,
4047
- numTodoTests: count2,
4048
- numFailedTestSuites: count2,
4049
- testResults: z15.array(z15.object({
4050
- name: z15.string().min(1),
4051
- status: z15.enum(["passed", "failed"]),
4052
- message: z15.string().optional(),
4053
- assertionResults: z15.array(z15.object({
4054
- fullName: z15.string(),
4055
- status: z15.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
4056
- failureMessages: z15.array(z15.string()).nullable().optional(),
4057
- location: z15.object({ line: count2, column: count2 }).nullable().optional()
4693
+ count3 = z19.number().int().nonnegative();
4694
+ schema = z19.object({
4695
+ success: z19.boolean(),
4696
+ numTotalTests: count3,
4697
+ numPassedTests: count3,
4698
+ numFailedTests: count3,
4699
+ numPendingTests: count3,
4700
+ numTodoTests: count3,
4701
+ numFailedTestSuites: count3,
4702
+ testResults: z19.array(z19.object({
4703
+ name: z19.string().min(1),
4704
+ status: z19.enum(["passed", "failed"]),
4705
+ message: z19.string().optional(),
4706
+ assertionResults: z19.array(z19.object({
4707
+ fullName: z19.string(),
4708
+ status: z19.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
4709
+ failureMessages: z19.array(z19.string()).nullable().optional(),
4710
+ location: z19.object({ line: count3, column: count3 }).nullable().optional()
4058
4711
  }))
4059
4712
  }))
4060
4713
  });
@@ -4062,7 +4715,7 @@ var init_vitest = __esm({
4062
4715
  });
4063
4716
 
4064
4717
  // src/review/evidence/sarif.ts
4065
- import { z as z16 } from "zod";
4718
+ import { z as z20 } from "zod";
4066
4719
  function parseSarif(raw, sourceRoot) {
4067
4720
  const report = schema2.parse(raw), findings = [], diagnostics = [];
4068
4721
  let active = 0, suppressed = 0, absent = 0, unresolved = 0;
@@ -4153,40 +4806,98 @@ var init_sarif = __esm({
4153
4806
  "use strict";
4154
4807
  init_paths2();
4155
4808
  init_types2();
4156
- index = z16.number().int().nonnegative();
4157
- artifact = z16.object({ uri: z16.string().optional(), uriBaseId: z16.string().optional(), index: index.optional() });
4158
- message = z16.object({ text: z16.string().optional(), markdown: z16.string().optional(), id: z16.string().optional(), arguments: z16.array(z16.string()).optional() });
4159
- level = z16.enum(["error", "warning", "note", "none"]);
4160
- location = z16.object({ physicalLocation: z16.object({ artifactLocation: artifact.optional(), region: z16.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
4161
- schema2 = z16.object({
4162
- version: z16.literal("2.1.0"),
4163
- runs: z16.array(z16.object({
4164
- 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() }) }),
4165
- invocations: z16.array(z16.object({ executionSuccessful: z16.boolean(), commandLine: z16.string().optional(), toolExecutionNotifications: z16.array(z16.object({ level: level.optional(), message })).optional() })).optional(),
4166
- versionControlProvenance: z16.array(z16.object({ revisionId: z16.string().optional() })).optional(),
4167
- originalUriBaseIds: z16.record(artifact).optional(),
4168
- artifacts: z16.array(z16.object({ location: artifact.optional() })).optional(),
4169
- results: z16.array(z16.object({
4170
- ruleId: z16.string().optional(),
4809
+ index = z20.number().int().nonnegative();
4810
+ artifact = z20.object({ uri: z20.string().optional(), uriBaseId: z20.string().optional(), index: index.optional() });
4811
+ message = z20.object({ text: z20.string().optional(), markdown: z20.string().optional(), id: z20.string().optional(), arguments: z20.array(z20.string()).optional() });
4812
+ level = z20.enum(["error", "warning", "note", "none"]);
4813
+ location = z20.object({ physicalLocation: z20.object({ artifactLocation: artifact.optional(), region: z20.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
4814
+ schema2 = z20.object({
4815
+ version: z20.literal("2.1.0"),
4816
+ runs: z20.array(z20.object({
4817
+ 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() }) }),
4818
+ invocations: z20.array(z20.object({ executionSuccessful: z20.boolean(), commandLine: z20.string().optional(), toolExecutionNotifications: z20.array(z20.object({ level: level.optional(), message })).optional() })).optional(),
4819
+ versionControlProvenance: z20.array(z20.object({ revisionId: z20.string().optional() })).optional(),
4820
+ originalUriBaseIds: z20.record(artifact).optional(),
4821
+ artifacts: z20.array(z20.object({ location: artifact.optional() })).optional(),
4822
+ results: z20.array(z20.object({
4823
+ ruleId: z20.string().optional(),
4171
4824
  ruleIndex: index.optional(),
4172
4825
  message,
4173
4826
  level: level.optional(),
4174
- kind: z16.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
4175
- baselineState: z16.enum(["new", "unchanged", "updated", "absent"]).optional(),
4176
- suppressions: z16.array(z16.object({ kind: z16.enum(["inSource", "external"]), status: z16.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
4177
- locations: z16.array(location).optional(),
4178
- relatedLocations: z16.array(location).optional()
4827
+ kind: z20.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
4828
+ baselineState: z20.enum(["new", "unchanged", "updated", "absent"]).optional(),
4829
+ suppressions: z20.array(z20.object({ kind: z20.enum(["inSource", "external"]), status: z20.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
4830
+ locations: z20.array(location).optional(),
4831
+ relatedLocations: z20.array(location).optional()
4179
4832
  })).optional()
4180
4833
  }))
4181
4834
  });
4182
4835
  }
4183
4836
  });
4184
4837
 
4838
+ // src/review/evidence/check.ts
4839
+ import { z as z21 } from "zod";
4840
+ function parseCheck(raw, sourceRoot) {
4841
+ const report = schema3.parse(raw);
4842
+ const diagnostics = [], findings = [];
4843
+ const counts = { total: report.results.length, passed: 0, failed: 0, skipped: 0 };
4844
+ for (const [index2, result] of report.results.entries()) {
4845
+ counts[result.status]++;
4846
+ const locations = result.locations.flatMap((location2) => {
4847
+ const file = evidencePath(location2.file, sourceRoot);
4848
+ if (!file) {
4849
+ diagnostics.push(`Check location is outside the declared checkout or invalid: ${location2.file}`);
4850
+ return [];
4851
+ }
4852
+ return [{ ...location2, file }];
4853
+ });
4854
+ if (result.status === "failed") findings.push({
4855
+ id: String(index2),
4856
+ ...messagePreview([result.name, result.message].filter(Boolean).join("\n")),
4857
+ severity: "error",
4858
+ state: "active",
4859
+ locations
4860
+ });
4861
+ }
4862
+ if (!counts.total) diagnostics.push("No check results were recorded; an empty report is not passing evidence.");
4863
+ if (counts.skipped) diagnostics.push(`${counts.skipped} checks were skipped; verification is incomplete.`);
4864
+ return {
4865
+ outcome: counts.failed ? "failed" : !counts.total ? "unavailable" : !counts.passed ? "skipped" : "passed",
4866
+ counts,
4867
+ findings,
4868
+ incomplete: diagnostics.length > 0,
4869
+ diagnostics,
4870
+ ...report.commit ? { reportedCommits: [report.commit] } : {}
4871
+ };
4872
+ }
4873
+ var schema3;
4874
+ var init_check = __esm({
4875
+ "src/review/evidence/check.ts"() {
4876
+ "use strict";
4877
+ init_paths2();
4878
+ init_types2();
4879
+ schema3 = z21.object({
4880
+ version: z21.literal(1),
4881
+ commit: z21.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).optional(),
4882
+ results: z21.array(z21.object({
4883
+ name: z21.string().min(1).max(1e3),
4884
+ status: z21.enum(["passed", "failed", "skipped"]),
4885
+ message: z21.string().max(1e5).optional(),
4886
+ locations: z21.array(z21.object({
4887
+ file: z21.string().min(1).max(4e3),
4888
+ line: z21.number().int().positive().optional(),
4889
+ column: z21.number().int().positive().optional()
4890
+ })).max(100).default([])
4891
+ })).max(1e5)
4892
+ });
4893
+ }
4894
+ });
4895
+
4185
4896
  // src/review/evidence.ts
4186
- import path19 from "path";
4187
- import { z as z17 } from "zod";
4897
+ import path22 from "path";
4898
+ import { z as z22 } from "zod";
4188
4899
  function relativeArtifact(root, file) {
4189
- const relative = path19.isAbsolute(file) ? path19.relative(root, file) : file;
4900
+ const relative = path22.isAbsolute(file) ? path22.relative(root, file) : file;
4190
4901
  const normalized = normalizeRepoPath(relative);
4191
4902
  if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
4192
4903
  return normalized;
@@ -4225,7 +4936,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4225
4936
  let manifest, raw;
4226
4937
  try {
4227
4938
  manifest = relativeArtifact(root, supplied);
4228
- raw = z17.object({ version: z17.literal(1), checks: z17.array(z17.unknown()) }).parse(await readStoreJson(root, manifest));
4939
+ raw = z22.object({ version: z22.literal(1), checks: z22.array(z22.unknown()) }).parse(await readStoreJson(root, manifest));
4229
4940
  } catch (error) {
4230
4941
  output.diagnostics.push(`${supplied}: ${String(error)}`);
4231
4942
  continue;
@@ -4264,7 +4975,8 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4264
4975
  counts: {},
4265
4976
  incomplete: false,
4266
4977
  diagnostics: [],
4267
- truncated: false
4978
+ truncated: false,
4979
+ ...check.environment ? { environment: check.environment } : {}
4268
4980
  };
4269
4981
  output.checks.push(result);
4270
4982
  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.");
@@ -4277,13 +4989,13 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4277
4989
  }
4278
4990
  try {
4279
4991
  if (!check.report) throw new Error("Completed check has no report artifact.");
4280
- if (check.kind === "tests" !== (check.report.format === "vitest-json")) throw new Error("Test checks require vitest-json; analysis checks require sarif.");
4992
+ 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.");
4281
4993
  const file = relativeArtifact(root, check.report.path);
4282
4994
  const report = await readStoreJson(root, file);
4283
4995
  if (report === null) throw new Error(`Report artifact is missing: ${file}`);
4284
4996
  const sourceRoot = check.sourceRoot ?? root;
4285
- if (!path19.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
4286
- const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : parseSarif(report, sourceRoot);
4997
+ if (!path22.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot)) throw new Error("sourceRoot must identify the absolute checkout root on the check runner.");
4998
+ const parsed = check.report.format === "vitest-json" ? parseVitest(report, sourceRoot) : check.report.format === "mason-check-json" ? parseCheck(report, sourceRoot) : parseSarif(report, sourceRoot);
4287
4999
  result.outcome = parsed.outcome;
4288
5000
  result.counts = parsed.counts;
4289
5001
  result.incomplete = parsed.incomplete;
@@ -4303,7 +5015,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
4303
5015
  result.outcome = "failed";
4304
5016
  result.diagnostics.push(`Check command exited ${check.exitCode} despite a report with no active failures.`);
4305
5017
  }
4306
- if (check.report.format === "vitest-json" && pairs === void 0) {
5018
+ if (check.kind === "tests" && pairs === void 0) {
4307
5019
  try {
4308
5020
  pairs = (await buildTestMap(root)).paired;
4309
5021
  } catch (error) {
@@ -4377,29 +5089,36 @@ var init_evidence2 = __esm({
4377
5089
  init_provenance();
4378
5090
  init_vitest();
4379
5091
  init_sarif();
5092
+ init_check();
4380
5093
  init_types2();
4381
- checkSchema2 = z17.object({
4382
- id: z17.string().min(1).max(100),
4383
- kind: z17.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
4384
- tool: z17.string().min(1).max(200),
4385
- command: z17.string().min(1).max(2e3),
4386
- commit: z17.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
4387
- workingTreeClean: z17.boolean().optional(),
4388
- source: z17.string().max(2e3).optional(),
4389
- sourceRoot: z17.string().min(1).max(4e3).optional(),
4390
- status: z17.enum(["completed", "skipped", "unavailable"]).default("completed"),
4391
- reason: z17.string().min(1).max(2e3).optional(),
4392
- exitCode: z17.number().int().nullable().optional(),
4393
- report: z17.object({ format: z17.enum(["vitest-json", "sarif"]), path: z17.string().min(1).max(4e3) }).optional()
5094
+ checkSchema2 = z22.object({
5095
+ id: z22.string().min(1).max(100),
5096
+ kind: z22.enum(["tests", "static-analysis", "security", "complexity", "duplication", "documentation"]),
5097
+ tool: z22.string().min(1).max(200),
5098
+ command: z22.string().min(1).max(2e3),
5099
+ commit: z22.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
5100
+ workingTreeClean: z22.boolean().optional(),
5101
+ source: z22.string().max(2e3).optional(),
5102
+ sourceRoot: z22.string().min(1).max(4e3).optional(),
5103
+ status: z22.enum(["completed", "skipped", "unavailable"]).default("completed"),
5104
+ reason: z22.string().min(1).max(2e3).optional(),
5105
+ exitCode: z22.number().int().nullable().optional(),
5106
+ environment: z22.object({
5107
+ toolVersion: z22.string().min(1).max(200),
5108
+ runtime: z22.string().min(1).max(200).optional(),
5109
+ platform: z22.string().min(1).max(200).optional(),
5110
+ configuration: z22.array(z22.string().min(1).max(4e3)).max(100).optional()
5111
+ }).optional(),
5112
+ report: z22.object({ format: z22.enum(["vitest-json", "sarif", "mason-check-json"]), path: z22.string().min(1).max(4e3) }).optional()
4394
5113
  });
4395
5114
  }
4396
5115
  });
4397
5116
 
4398
5117
  // src/review/review.ts
4399
- import fs16 from "fs/promises";
4400
- import path20 from "path";
4401
- import { execFile as execFile12 } from "child_process";
4402
- import { promisify as promisify12 } from "util";
5118
+ import fs18 from "fs/promises";
5119
+ import path23 from "path";
5120
+ import { execFile as execFile13 } from "child_process";
5121
+ import { promisify as promisify13 } from "util";
4403
5122
  async function resolveMergeBase(resolvedRoot, base, head) {
4404
5123
  try {
4405
5124
  const { stdout } = await exec10("git", ["merge-base", base, head], {
@@ -4426,7 +5145,7 @@ function anchorsTouched(record, changedFiles) {
4426
5145
  return matchingPaths(record.files, changedFiles);
4427
5146
  }
4428
5147
  async function computeReview(rootDir, base, options = {}) {
4429
- const resolvedRoot = path20.resolve(rootDir);
5148
+ const resolvedRoot = path23.resolve(rootDir);
4430
5149
  const head = await getCurrentGitHash(resolvedRoot);
4431
5150
  if (head === "unknown") return null;
4432
5151
  const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
@@ -4476,7 +5195,7 @@ async function computeReview(rootDir, base, options = {}) {
4476
5195
  analyzed,
4477
5196
  async (relPath) => {
4478
5197
  try {
4479
- await fs16.access(path20.join(resolvedRoot, relPath));
5198
+ await fs18.access(path23.join(resolvedRoot, relPath));
4480
5199
  return true;
4481
5200
  } catch {
4482
5201
  return false;
@@ -4521,7 +5240,7 @@ var init_review = __esm({
4521
5240
  init_cochange();
4522
5241
  init_evidence2();
4523
5242
  init_snapshot();
4524
- exec10 = promisify12(execFile12);
5243
+ exec10 = promisify13(execFile13);
4525
5244
  MAX_ANALYZED_FILES = 50;
4526
5245
  }
4527
5246
  });
@@ -4530,7 +5249,7 @@ var init_review = __esm({
4530
5249
  async function auditSummary(root) {
4531
5250
  try {
4532
5251
  const report = await computeAudit(root);
4533
- if (!report) return { status: "no-context-files", reason: "No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md found to audit." };
5252
+ if (!report) return { status: "no-context-files", reason: "No README.md or agent instruction files found to audit." };
4534
5253
  if (!report.gitAvailable) return { status: "unavailable", reason: "Audit needs readable Git history to verify documentation claims.", docs: report.docs };
4535
5254
  return {
4536
5255
  ...report,
@@ -4839,16 +5558,16 @@ var init_setup = __esm({
4839
5558
  });
4840
5559
 
4841
5560
  // src/distribution/bundle.ts
4842
- import fs17 from "fs/promises";
4843
- import path21 from "path";
5561
+ import fs19 from "fs/promises";
5562
+ import path24 from "path";
4844
5563
  import { createHash as createHash4 } from "crypto";
4845
- import { z as z18 } from "zod";
5564
+ import { z as z23 } from "zod";
4846
5565
  async function readBundle(root) {
4847
- const bytes = await fs17.readFile(await storePath(root, "bundle.json"));
5566
+ const bytes = await fs19.readFile(await storePath(root, "bundle.json"));
4848
5567
  const manifest = bundleSchema.parse(JSON.parse(bytes.toString("utf8")));
4849
5568
  const windows = manifest.target.startsWith("win32-");
4850
5569
  for (const file of Object.keys(manifest.files)) {
4851
- if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path21.isAbsolute(file)) {
5570
+ if (file.includes("\\") || file.split("/").some((p) => !p || p === "." || p === "..") || path24.isAbsolute(file)) {
4852
5571
  throw new Error("Unsafe path in Mason bundle: " + file);
4853
5572
  }
4854
5573
  }
@@ -4863,7 +5582,7 @@ async function verifyBundle(root, expectedHash) {
4863
5582
  if (bundle.manifest.target !== `${process.platform}-${process.arch}`) throw new Error("Mason bundle is for another platform. Install this platform's bundle and rerun setup.");
4864
5583
  const files = Object.keys(bundle.manifest.files);
4865
5584
  for (let i = 0; i < files.length; i += 8) await Promise.all(files.slice(i, i + 8).map(async (file) => {
4866
- if (sha256(await fs17.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
5585
+ if (sha256(await fs19.readFile(await storePath(root, file))) !== bundle.manifest.files[file]) throw new Error("Mason bundle checksum mismatch: " + file);
4867
5586
  }));
4868
5587
  return bundle;
4869
5588
  }
@@ -4873,31 +5592,31 @@ var init_bundle = __esm({
4873
5592
  "use strict";
4874
5593
  init_storage();
4875
5594
  targets = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "win32-arm64", "win32-x64"];
4876
- digest2 = z18.string().regex(/^[a-f0-9]{64}$/);
4877
- bundleSchema = z18.object({
4878
- format: z18.literal(1),
4879
- version: z18.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
4880
- target: z18.enum(targets),
4881
- nodeVersion: z18.string(),
4882
- files: z18.record(digest2)
5595
+ digest2 = z23.string().regex(/^[a-f0-9]{64}$/);
5596
+ bundleSchema = z23.object({
5597
+ format: z23.literal(1),
5598
+ version: z23.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
5599
+ target: z23.enum(targets),
5600
+ nodeVersion: z23.string(),
5601
+ files: z23.record(digest2)
4883
5602
  }).strict();
4884
5603
  sha256 = (bytes) => createHash4("sha256").update(bytes).digest("hex");
4885
5604
  }
4886
5605
  });
4887
5606
 
4888
5607
  // src/distribution/path.ts
4889
- import fs18 from "fs/promises";
5608
+ import fs20 from "fs/promises";
4890
5609
  import os3 from "os";
4891
- import path22 from "path";
4892
- import { execFile as execFile13 } from "child_process";
4893
- import { promisify as promisify13 } from "util";
5610
+ import path25 from "path";
5611
+ import { execFile as execFile14 } from "child_process";
5612
+ import { promisify as promisify14 } from "util";
4894
5613
  import { createHash as createHash5, randomUUID as randomUUID7 } from "crypto";
4895
- import { z as z19 } from "zod";
5614
+ import { z as z24 } from "zod";
4896
5615
  async function configurePath(bin, previous, save, options = {}) {
4897
5616
  const env = options.env ?? process.env, platform = options.platform ?? process.platform;
4898
5617
  const changes = [...previous];
4899
5618
  const currentPath = platform === "win32" ? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] : env.PATH;
4900
- const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path22.posix.isAbsolute(entry) && path22.posix.resolve(entry) === path22.posix.resolve(bin));
5619
+ const alreadyOnPath = platform === "win32" ? hasWindowsEntry(currentPath ?? null, bin, env) : (currentPath ?? "").split(":").some((entry) => path25.posix.isAbsolute(entry) && path25.posix.resolve(entry) === path25.posix.resolve(bin));
4901
5620
  const message2 = alreadyOnPath ? "Run: mason setup --host codex" : "Open a new terminal, then run: mason setup --host codex";
4902
5621
  const manual = (reason2) => ({ status: "manual", changes, message: `${reason2}
4903
5622
  ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --host codex`}` });
@@ -4915,33 +5634,33 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
4915
5634
  }
4916
5635
  return { status: "configured", changes, message: message2 };
4917
5636
  }
4918
- const shell = path22.basename(env.SHELL ?? os3.userInfo().shell ?? "");
5637
+ const shell = path25.basename(env.SHELL ?? os3.userInfo().shell ?? "");
4919
5638
  const userHome = options.userHome ?? os3.homedir();
4920
5639
  let profiles;
4921
5640
  if (!["bash", "zsh", "sh"].includes(shell)) return manual(`Automatic PATH setup is unavailable for ${shell || "an unknown shell"}.`);
4922
- if (env.MASON_PROFILE) profiles = [path22.resolve(env.MASON_PROFILE)];
4923
- else if (shell === "zsh") profiles = [path22.join(env.ZDOTDIR || userHome, ".zshrc")];
4924
- else if (shell === "sh") profiles = [path22.join(userHome, ".profile")];
5641
+ if (env.MASON_PROFILE) profiles = [path25.resolve(env.MASON_PROFILE)];
5642
+ else if (shell === "zsh") profiles = [path25.join(env.ZDOTDIR || userHome, ".zshrc")];
5643
+ else if (shell === "sh") profiles = [path25.join(userHome, ".profile")];
4925
5644
  else {
4926
- const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path22.join(userHome, name));
5645
+ const candidates = [".bash_profile", ".bash_login", ".profile"].map((name) => path25.join(userHome, name));
4927
5646
  let login = candidates[2];
4928
- for (const file of candidates) if (await fs18.lstat(file).then(() => true, (error) => {
5647
+ for (const file of candidates) if (await fs20.lstat(file).then(() => true, (error) => {
4929
5648
  if (error.code === "ENOENT") return false;
4930
5649
  throw error;
4931
5650
  })) {
4932
5651
  login = file;
4933
5652
  break;
4934
5653
  }
4935
- profiles = [path22.join(userHome, ".bashrc"), login];
5654
+ profiles = [path25.join(userHome, ".bashrc"), login];
4936
5655
  }
4937
5656
  const marker = `# >>> mason PATH ${createHash5("sha256").update(bin).digest("hex").slice(0, 12)} >>>`;
4938
5657
  for (const profile of profiles) {
4939
- const stat = await fs18.lstat(profile).catch((error) => {
5658
+ const stat = await fs20.lstat(profile).catch((error) => {
4940
5659
  if (error.code === "ENOENT") return null;
4941
5660
  throw error;
4942
5661
  });
4943
- const file = stat?.isSymbolicLink() ? await fs18.realpath(profile) : path22.resolve(profile);
4944
- const original = await fs18.readFile(file).catch((error) => {
5662
+ const file = stat?.isSymbolicLink() ? await fs20.realpath(profile) : path25.resolve(profile);
5663
+ const original = await fs20.readFile(file).catch((error) => {
4945
5664
  if (error.code === "ENOENT" && !stat) return null;
4946
5665
  throw error;
4947
5666
  });
@@ -4956,8 +5675,8 @@ ${alreadyOnPath ? message2 : `Add ${bin} to your PATH, then run: mason setup --h
4956
5675
  changes.push({ kind: "profile", file, block, created: original === null });
4957
5676
  await save(changes);
4958
5677
  }
4959
- await fs18.mkdir(path22.dirname(file), { recursive: true });
4960
- const handle = await fs18.open(file, original === null ? "ax+" : "a+", 384);
5678
+ await fs20.mkdir(path25.dirname(file), { recursive: true });
5679
+ const handle = await fs20.open(file, original === null ? "ax+" : "a+", 384);
4961
5680
  try {
4962
5681
  if (!(await handle.readFile()).equals(original ?? Buffer.alloc(0))) throw new Error(`Shell profile changed during installation: ${profile}`);
4963
5682
  await handle.writeFile(block);
@@ -4981,13 +5700,13 @@ async function removePath(changes) {
4981
5700
  await windowsPath("remove", change.entry, current, change.created);
4982
5701
  continue;
4983
5702
  }
4984
- const stat = await fs18.lstat(change.file).catch((error) => {
5703
+ const stat = await fs20.lstat(change.file).catch((error) => {
4985
5704
  if (error.code === "ENOENT") return null;
4986
5705
  throw error;
4987
5706
  });
4988
5707
  if (!stat) continue;
4989
5708
  if (!stat.isFile()) throw new Error("profile type changed");
4990
- const bytes = await fs18.readFile(change.file), text2 = bytes.toString("utf8");
5709
+ const bytes = await fs20.readFile(change.file), text2 = bytes.toString("utf8");
4991
5710
  if (!Buffer.from(text2).equals(bytes)) throw new Error("profile encoding changed");
4992
5711
  const index2 = text2.indexOf(change.block);
4993
5712
  if (index2 < 0 || text2.indexOf(change.block, index2 + change.block.length) >= 0) throw new Error("PATH block was edited or removed");
@@ -4997,7 +5716,7 @@ async function removePath(changes) {
4997
5716
  const temporary = change.file + ".mason-" + randomUUID7();
4998
5717
  try {
4999
5718
  if (stat.nlink > 1) throw new Error("profile has multiple hard links");
5000
- const handle = await fs18.open(temporary, "wx", stat.mode & 511);
5719
+ const handle = await fs20.open(temporary, "wx", stat.mode & 511);
5001
5720
  try {
5002
5721
  await handle.writeFile(next);
5003
5722
  await handle.chmod(stat.mode & 511);
@@ -5006,11 +5725,11 @@ async function removePath(changes) {
5006
5725
  } finally {
5007
5726
  await handle.close();
5008
5727
  }
5009
- const current = await fs18.lstat(change.file);
5010
- if (!current.isFile() || current.ino !== stat.ino || !(await fs18.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
5011
- await fs18.rename(temporary, change.file);
5728
+ const current = await fs20.lstat(change.file);
5729
+ if (!current.isFile() || current.ino !== stat.ino || !(await fs20.readFile(change.file)).equals(bytes)) throw new Error("profile changed during uninstall");
5730
+ await fs20.rename(temporary, change.file);
5012
5731
  } finally {
5013
- await fs18.rm(temporary, { force: true });
5732
+ await fs20.rm(temporary, { force: true });
5014
5733
  }
5015
5734
  } catch (error) {
5016
5735
  warnings.push(`Retained PATH setting ${change.kind === "profile" ? change.file : change.entry}: ${error instanceof Error ? error.message : String(error)}`);
@@ -5044,20 +5763,20 @@ try {
5044
5763
  @{ value = $next; kind = $kind } | ConvertTo-Json -Compress
5045
5764
  } finally { $key.Dispose() }
5046
5765
  `;
5047
- const { stdout } = await execute2("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, timeout: 15e3, maxBuffer: 1024 * 1024 });
5766
+ const { stdout } = await execute3("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, timeout: 15e3, maxBuffer: 1024 * 1024 });
5048
5767
  return JSON.parse(stdout.replace(/^\uFEFF/, "").trim());
5049
5768
  }
5050
- var execute2, pathChangeSchema, quote, normalizeWindowsEntry, hasWindowsEntry;
5769
+ var execute3, pathChangeSchema, quote, normalizeWindowsEntry, hasWindowsEntry;
5051
5770
  var init_path = __esm({
5052
5771
  "src/distribution/path.ts"() {
5053
5772
  "use strict";
5054
- execute2 = promisify13(execFile13);
5055
- pathChangeSchema = z19.discriminatedUnion("kind", [
5056
- z19.object({ kind: z19.literal("profile"), file: z19.string(), block: z19.string(), created: z19.boolean() }),
5057
- z19.object({ kind: z19.literal("windows"), entry: z19.string(), created: z19.boolean() })
5773
+ execute3 = promisify14(execFile14);
5774
+ pathChangeSchema = z24.discriminatedUnion("kind", [
5775
+ z24.object({ kind: z24.literal("profile"), file: z24.string(), block: z24.string(), created: z24.boolean() }),
5776
+ z24.object({ kind: z24.literal("windows"), entry: z24.string(), created: z24.boolean() })
5058
5777
  ]);
5059
5778
  quote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
5060
- normalizeWindowsEntry = (value) => path22.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
5779
+ normalizeWindowsEntry = (value) => path25.win32.normalize(value.trim().replace(/^"(.*)"$/, "$1")).replace(/[\\/]+$/, "").toLowerCase();
5061
5780
  hasWindowsEntry = (value, entry, env = process.env) => {
5062
5781
  const variables = new Map(Object.entries(env).map(([key, value2]) => [key.toLowerCase(), value2]));
5063
5782
  const expand = (part) => part.replace(/%([^%]+)%/g, (match, key) => variables.get(key.toLowerCase()) ?? match);
@@ -5075,41 +5794,41 @@ __export(install_exports2, {
5075
5794
  uninstallStandalone: () => uninstallStandalone,
5076
5795
  upgradeStandalone: () => upgradeStandalone
5077
5796
  });
5078
- import fs19 from "fs/promises";
5797
+ import fs21 from "fs/promises";
5079
5798
  import os4 from "os";
5080
- import path23 from "path";
5799
+ import path26 from "path";
5081
5800
  import { fileURLToPath } from "url";
5082
5801
  import { spawn } from "child_process";
5083
5802
  import { randomUUID as randomUUID8 } from "crypto";
5084
- import { z as z20 } from "zod";
5803
+ import { z as z25 } from "zod";
5085
5804
  async function replace(file, content2, mode = 384) {
5086
5805
  const temp = file + ".tmp-" + randomUUID8();
5087
5806
  try {
5088
- await fs19.writeFile(temp, content2, { mode, flag: "wx" });
5089
- await fs19.rename(temp, file);
5807
+ await fs21.writeFile(temp, content2, { mode, flag: "wx" });
5808
+ await fs21.rename(temp, file);
5090
5809
  } finally {
5091
- await fs19.rm(temp, { force: true });
5810
+ await fs21.rm(temp, { force: true });
5092
5811
  }
5093
5812
  }
5094
5813
  async function installStandalone(source) {
5095
5814
  const bundle = await verifyBundle(source);
5096
- const selectedHome = path23.resolve(defaultHome());
5097
- await fs19.mkdir(selectedHome, { recursive: true });
5098
- const home = await fs19.realpath(selectedHome);
5099
- const selectedBin = path23.resolve(process.env.MASON_BIN_DIR ?? (process.platform === "win32" ? path23.join(home, "bin") : path23.join(os4.homedir(), ".local/bin")));
5815
+ const selectedHome = path26.resolve(defaultHome());
5816
+ await fs21.mkdir(selectedHome, { recursive: true });
5817
+ const home = await fs21.realpath(selectedHome);
5818
+ const selectedBin = path26.resolve(process.env.MASON_BIN_DIR ?? (process.platform === "win32" ? path26.join(home, "bin") : path26.join(os4.homedir(), ".local/bin")));
5100
5819
  return withLock(home, ".install-lock", async () => {
5101
5820
  const raw = await readStoreJson(home, "install.json");
5102
- const previous = raw === null ? null : recordSchema2.parse(raw);
5103
- if (!previous) for (const file of await fs19.readdir(home)) {
5821
+ const previous = raw === null ? null : recordSchema3.parse(raw);
5822
+ if (!previous) for (const file of await fs21.readdir(home)) {
5104
5823
  if (file === ".install-lock") continue;
5105
- const directory = path23.join(home, file);
5106
- const stat = await fs19.lstat(directory);
5107
- if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs19.readdir(directory)).length) {
5824
+ const directory = path26.join(home, file);
5825
+ const stat = await fs21.lstat(directory);
5826
+ if (!["versions", "bin"].includes(file) || !stat.isDirectory() || (await fs21.readdir(directory)).length) {
5108
5827
  throw new Error("Mason installation directory is not empty and has no ownership receipt.");
5109
5828
  }
5110
5829
  }
5111
- await fs19.mkdir(selectedBin, { recursive: true });
5112
- const bin = await fs19.realpath(selectedBin);
5830
+ await fs21.mkdir(selectedBin, { recursive: true });
5831
+ const bin = await fs21.realpath(selectedBin);
5113
5832
  if (previous && (previous.home !== home || previous.bin !== bin)) throw new Error("Installation paths differ from the existing Mason receipt.");
5114
5833
  const id = bundle.manifestHash.slice(0, 24);
5115
5834
  const destination = await storePath(home, "versions/" + id);
@@ -5120,10 +5839,10 @@ $previousToken = $env:MASON_UNINSTALL_TOKEN
5120
5839
  $token = [Guid]::NewGuid().ToString()
5121
5840
  $env:MASON_UNINSTALL_TOKEN = $token
5122
5841
  try {
5123
- & ${psQuote(path23.join(destination, "node.exe"))} ${psQuote(path23.join(destination, "app/dist/mason.js"))} @args
5842
+ & ${psQuote(path26.join(destination, "node.exe"))} ${psQuote(path26.join(destination, "app/dist/mason.js"))} @args
5124
5843
  $code = $LASTEXITCODE
5125
5844
  if ($code -eq 0 -and $args.Count -eq 1 -and $args[0] -eq 'uninstall') {
5126
- if ([IO.File]::ReadAllText(${psQuote(path23.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
5845
+ if ([IO.File]::ReadAllText(${psQuote(path26.join(home, ".uninstall-request.txt"))}) -cne $token) { throw 'Mason did not authorize installation cleanup.' }
5127
5846
  Remove-Item -LiteralPath ${psQuote(home)} -Recurse -Force
5128
5847
  Write-Output ${psQuote("Removed standalone installation: " + home)}
5129
5848
  }
@@ -5132,11 +5851,11 @@ try {
5132
5851
  finally { $env:MASON_UNINSTALL_TOKEN = $previousToken }
5133
5852
  `
5134
5853
  } : { mason: `#!/bin/sh
5135
- exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destination, "app/dist/mason.js"))} "$@"
5854
+ exec ${shQuote(path26.join(destination, "node"))} ${shQuote(path26.join(destination, "app/dist/mason.js"))} "$@"
5136
5855
  ` };
5137
5856
  for (const [name, content2] of Object.entries(launchers)) {
5138
5857
  const file = await storePath(bin, name);
5139
- const old = await fs19.readFile(file, "utf8").catch((error) => {
5858
+ const old = await fs21.readFile(file, "utf8").catch((error) => {
5140
5859
  if (error.code === "ENOENT") return null;
5141
5860
  throw error;
5142
5861
  });
@@ -5145,7 +5864,7 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
5145
5864
  const retired = Object.entries({ ...previous?.previousLaunchers, ...previous?.launchers }).filter(([name]) => !(name in launchers));
5146
5865
  const checkRetired = async (name, expected) => {
5147
5866
  if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
5148
- const actual = await fs19.readFile(await storePath(bin, name), "utf8").catch((error) => {
5867
+ const actual = await fs21.readFile(await storePath(bin, name), "utf8").catch((error) => {
5149
5868
  if (error.code === "ENOENT") return null;
5150
5869
  throw error;
5151
5870
  });
@@ -5155,28 +5874,28 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
5155
5874
  const receipt = { format: 1, home, bin, current: id, version: bundle.manifest.version, launchers, pathChanges: previous?.pathChanges ?? [] };
5156
5875
  await writeStoreJson(home, "install.json", { ...receipt, previousLaunchers: previous?.previousLaunchers ?? previous?.launchers });
5157
5876
  const stage = await storePath(home, "versions/.install-" + randomUUID8(), true);
5158
- await fs19.mkdir(stage);
5877
+ await fs21.mkdir(stage);
5159
5878
  try {
5160
5879
  for (const file of ["bundle.json", ...Object.keys(bundle.manifest.files)]) {
5161
5880
  const from = await storePath(source, file), to = await storePath(stage, file, true);
5162
- await fs19.copyFile(from, to);
5163
- await fs19.chmod(to, (await fs19.stat(from)).mode & 511);
5881
+ await fs21.copyFile(from, to);
5882
+ await fs21.chmod(to, (await fs21.stat(from)).mode & 511);
5164
5883
  }
5165
5884
  await verifyBundle(stage, bundle.manifestHash);
5166
- const exists2 = await fs19.lstat(destination).then(() => true, (error) => {
5885
+ const exists = await fs21.lstat(destination).then(() => true, (error) => {
5167
5886
  if (error.code === "ENOENT") return false;
5168
5887
  throw error;
5169
5888
  });
5170
- if (exists2) await verifyBundle(destination, bundle.manifestHash);
5171
- else await fs19.rename(stage, destination);
5889
+ if (exists) await verifyBundle(destination, bundle.manifestHash);
5890
+ else await fs21.rename(stage, destination);
5172
5891
  for (const [name, content2] of Object.entries(launchers)) await replace(await storePath(bin, name, true), content2, 493);
5173
5892
  for (const [name, expected] of retired) {
5174
5893
  await checkRetired(name, expected);
5175
- await fs19.rm(await storePath(bin, name), { force: true });
5894
+ await fs21.rm(await storePath(bin, name), { force: true });
5176
5895
  }
5177
5896
  await writeStoreJson(home, "install.json", receipt);
5178
5897
  } finally {
5179
- await fs19.rm(stage, { recursive: true, force: true });
5898
+ await fs21.rm(stage, { recursive: true, force: true });
5180
5899
  }
5181
5900
  const pathResult = await configurePath(bin, receipt.pathChanges, async (changes) => {
5182
5901
  receipt.pathChanges = changes;
@@ -5186,12 +5905,12 @@ exec ${shQuote(path23.join(destination, "node"))} ${shQuote(path23.join(destinat
5186
5905
  });
5187
5906
  }
5188
5907
  async function currentInstallation() {
5189
- const app = path23.dirname(path23.dirname(fileURLToPath(import.meta.url)));
5190
- const bundle = path23.dirname(app), home = path23.dirname(path23.dirname(bundle));
5908
+ const app = path26.dirname(path26.dirname(fileURLToPath(import.meta.url)));
5909
+ const bundle = path26.dirname(app), home = path26.dirname(path26.dirname(bundle));
5191
5910
  const raw = await readStoreJson(home, "install.json");
5192
5911
  if (!raw) throw new Error("Run this command from a standalone installation. npm users can upgrade with npm.");
5193
- const record = recordSchema2.parse(raw);
5194
- if (record.home !== await fs19.realpath(home) || path23.dirname(bundle) !== path23.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
5912
+ const record = recordSchema3.parse(raw);
5913
+ if (record.home !== await fs21.realpath(home) || path26.dirname(bundle) !== path26.join(home, "versions")) throw new Error("Invalid standalone installation receipt.");
5195
5914
  return { home, bundle, record };
5196
5915
  }
5197
5916
  async function upgradeStandalone(version) {
@@ -5199,7 +5918,7 @@ async function upgradeStandalone(version) {
5199
5918
  const { home, bundle, record } = await currentInstallation();
5200
5919
  await verifyBundle(bundle);
5201
5920
  const windows = process.platform === "win32";
5202
- const script = path23.join(bundle, windows ? "install.ps1" : "install.sh");
5921
+ const script = path26.join(bundle, windows ? "install.ps1" : "install.sh");
5203
5922
  return new Promise((resolve, reject) => {
5204
5923
  const child = spawn(
5205
5924
  windows ? "powershell.exe" : "sh",
@@ -5220,7 +5939,7 @@ async function uninstallStandalone() {
5220
5939
  for (const [name, expected] of Object.entries(record.launchers)) {
5221
5940
  if (!launcherNames.includes(name)) throw new Error("Invalid owned launcher name.");
5222
5941
  const file = await storePath(record.bin, name);
5223
- const actual = await fs19.readFile(file, "utf8").catch((error) => {
5942
+ const actual = await fs21.readFile(file, "utf8").catch((error) => {
5224
5943
  if (error.code === "ENOENT") return null;
5225
5944
  throw error;
5226
5945
  });
@@ -5228,12 +5947,12 @@ async function uninstallStandalone() {
5228
5947
  }
5229
5948
  for (const warning of await removePath(record.pathChanges ?? [])) console.error(warning);
5230
5949
  if (process.platform === "win32") await replace(await storePath(home, ".uninstall-request.txt", true), token);
5231
- for (const name of Object.keys(record.launchers)) await fs19.rm(await storePath(record.bin, name), { force: true });
5950
+ for (const name of Object.keys(record.launchers)) await fs21.rm(await storePath(record.bin, name), { force: true });
5232
5951
  });
5233
- if (process.platform !== "win32") await fs19.rm(home, { recursive: true, force: true });
5952
+ if (process.platform !== "win32") await fs21.rm(home, { recursive: true, force: true });
5234
5953
  return home;
5235
5954
  }
5236
- var recordSchema2, shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames, defaultHome;
5955
+ var recordSchema3, shQuote, psQuote, WINDOWS_BATCH_LAUNCHER, launcherNames, defaultHome;
5237
5956
  var init_install2 = __esm({
5238
5957
  "src/distribution/install.ts"() {
5239
5958
  "use strict";
@@ -5241,21 +5960,21 @@ var init_install2 = __esm({
5241
5960
  init_storage();
5242
5961
  init_store();
5243
5962
  init_path();
5244
- recordSchema2 = z20.object({
5245
- format: z20.literal(1),
5246
- home: z20.string(),
5247
- bin: z20.string(),
5248
- current: z20.string().regex(/^[a-f0-9]{24}$/),
5249
- version: z20.string(),
5250
- launchers: z20.record(z20.string()),
5251
- previousLaunchers: z20.record(z20.string()).optional(),
5252
- pathChanges: z20.array(pathChangeSchema).optional()
5963
+ recordSchema3 = z25.object({
5964
+ format: z25.literal(1),
5965
+ home: z25.string(),
5966
+ bin: z25.string(),
5967
+ current: z25.string().regex(/^[a-f0-9]{24}$/),
5968
+ version: z25.string(),
5969
+ launchers: z25.record(z25.string()),
5970
+ previousLaunchers: z25.record(z25.string()).optional(),
5971
+ pathChanges: z25.array(pathChangeSchema).optional()
5253
5972
  });
5254
5973
  shQuote = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
5255
5974
  psQuote = (s) => "'" + s.replace(/'/g, "''") + "'";
5256
5975
  WINDOWS_BATCH_LAUNCHER = '@echo off\r\ngoto #_mason_handoff_# 2>NUL || title %COMSPEC% & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0mason-launcher.ps1" %*\r\n';
5257
5976
  launcherNames = ["mason", "mason.cmd", "mason.ps1", "mason-launcher.ps1"];
5258
- defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path23.join(process.env.LOCALAPPDATA ?? os4.homedir(), "Mason") : path23.join(os4.homedir(), ".local/share/mason"));
5977
+ defaultHome = () => process.env.MASON_HOME ?? (process.platform === "win32" ? path26.join(process.env.LOCALAPPDATA ?? os4.homedir(), "Mason") : path26.join(os4.homedir(), ".local/share/mason"));
5259
5978
  }
5260
5979
  });
5261
5980
 
@@ -5385,7 +6104,7 @@ var inputArgs = process.argv.slice(2);
5385
6104
  var managedLaunch = inputArgs[0] === "--setup-host";
5386
6105
  var setupHost = managedLaunch ? inputArgs.splice(0, 2)[1] : void 0;
5387
6106
  var [command, ...args] = inputArgs;
5388
- var usage = `Mason ${"0.16.2"}
6107
+ var usage = `Mason ${"0.17.0"}
5389
6108
 
5390
6109
  Usage: mason <command> [options]
5391
6110
 
@@ -5409,9 +6128,9 @@ try {
5409
6128
  const active = await prepareLaunch2(setupHost, { allowInactive: command === "auto" && isHookCommand(args) });
5410
6129
  if (!active) process.exit(0);
5411
6130
  }
5412
- if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.16.2" }));
6131
+ if (command === "internal-integration-version") console.log(JSON.stringify({ protocol: 1, version: "0.17.0" }));
5413
6132
  else if (!command || ["--help", "-h", "help"].includes(command)) console.log(usage);
5414
- else if (["--version", "-v"].includes(command)) console.log("0.16.2");
6133
+ else if (["--version", "-v"].includes(command)) console.log("0.17.0");
5415
6134
  else if (command === "internal-install") {
5416
6135
  if (args.length) throw new Error("internal-install takes no arguments.");
5417
6136
  const { installStandalone: installStandalone2 } = await Promise.resolve().then(() => (init_install2(), install_exports2));