mason-context 0.10.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/audit/cli.ts
4
- import path16 from "path";
4
+ import path17 from "path";
5
5
 
6
6
  // src/audit/audit.ts
7
7
  import fs9 from "fs/promises";
@@ -35,6 +35,10 @@ function normalizeRepoPath(value) {
35
35
  const normalized = path.posix.normalize(slash).replace(/\/$/, "");
36
36
  return normalized === "." ? null : normalized;
37
37
  }
38
+ function isWithinRoot(root, candidate) {
39
+ const relative = path.relative(root, candidate);
40
+ return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
41
+ }
38
42
  function anchorMatches(anchor, file) {
39
43
  const a = normalizeRepoPath(anchor);
40
44
  const f = normalizeRepoPath(file);
@@ -132,6 +136,26 @@ async function readStoreJson(root, relative) {
132
136
  throw new Error(`Invalid Mason store ${relative}: ${error instanceof Error ? error.message : String(error)}`);
133
137
  }
134
138
  }
139
+ async function writeStoreJson(root, relative, value) {
140
+ const payload = JSON.stringify(value, null, 2) + "\n";
141
+ if (Buffer.byteLength(payload) > 10 * 1024 * 1024) {
142
+ throw new Error(`Mason store ${relative} exceeds 10 MiB`);
143
+ }
144
+ const file = await storePath(root, relative, true);
145
+ const temporary = path3.join(path3.dirname(file), `.${path3.basename(file)}.${randomUUID()}.tmp`);
146
+ try {
147
+ const handle = await fs2.open(temporary, "wx", 384);
148
+ try {
149
+ await handle.writeFile(payload, "utf8");
150
+ await handle.sync();
151
+ } finally {
152
+ await handle.close();
153
+ }
154
+ await fs2.rename(temporary, file);
155
+ } finally {
156
+ await fs2.rm(temporary, { force: true });
157
+ }
158
+ }
135
159
 
136
160
  // src/snapshot/snapshot.ts
137
161
  import { z } from "zod";
@@ -794,8 +818,8 @@ async function checkNewModules(ctx) {
794
818
  const absTop = path9.join(ctx.root, topDir);
795
819
  const topMentioned = isMentioned(combinedDocs, topDir);
796
820
  if (!topMentioned) {
797
- const count = await countSourceFiles(absTop);
798
- if (count >= 1) await flag(topDir, count);
821
+ const count2 = await countSourceFiles(absTop);
822
+ if (count2 >= 1) await flag(topDir, count2);
799
823
  continue;
800
824
  }
801
825
  const subdirs = await listSubdirs(absTop);
@@ -803,9 +827,9 @@ async function checkNewModules(ctx) {
803
827
  if (mentioned.length < ENUMERATION_THRESHOLD) continue;
804
828
  for (const sub of subdirs) {
805
829
  if (isMentioned(combinedDocs, sub)) continue;
806
- const count = await countSourceFiles(path9.join(absTop, sub));
807
- if (count >= SECOND_LEVEL_MIN_SOURCE_FILES) {
808
- await flag(`${topDir}/${sub}`, count);
830
+ const count2 = await countSourceFiles(path9.join(absTop, sub));
831
+ if (count2 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
832
+ await flag(`${topDir}/${sub}`, count2);
809
833
  }
810
834
  }
811
835
  }
@@ -931,7 +955,15 @@ async function checkStaleCounts(ctx) {
931
955
  for (const doc of ctx.docs) {
932
956
  for (const claim of doc.claims.counts) {
933
957
  const source = await resolveCountSource(ctx.root, claim);
934
- if (source === null || source.actual === claim.count) continue;
958
+ if (source === null) {
959
+ result.skipped.push({
960
+ check: "stale-count",
961
+ doc: doc.path,
962
+ reason: `${doc.path}: cannot resolve a workspace manifest for "${claim.excerpt}"`
963
+ });
964
+ continue;
965
+ }
966
+ if (source.actual === claim.count) continue;
935
967
  result.issues.push({
936
968
  type: "stale-count",
937
969
  message: `says "${claim.excerpt}" but ${source.countedFrom} resolves to ${source.actual}`,
@@ -1039,10 +1071,12 @@ var MANIFEST_PATHSPECS = [
1039
1071
  ];
1040
1072
  async function checkDepsChanged(ctx) {
1041
1073
  const result = emptyResult();
1074
+ result.suppressedAdvisories = [];
1042
1075
  for (const doc of ctx.docs) {
1043
1076
  if (!doc.lastCommit) {
1044
1077
  result.skipped.push({
1045
1078
  check: "deps-changed",
1079
+ doc: doc.path,
1046
1080
  reason: `${doc.path} has no commit history`
1047
1081
  });
1048
1082
  continue;
@@ -1050,9 +1084,9 @@ async function checkDepsChanged(ctx) {
1050
1084
  if (doc.dirty) {
1051
1085
  result.skipped.push({
1052
1086
  check: "deps-changed",
1087
+ doc: doc.path,
1053
1088
  reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
1054
1089
  });
1055
- continue;
1056
1090
  }
1057
1091
  const range = await commitsTouchingSince(
1058
1092
  ctx.root,
@@ -1062,13 +1096,14 @@ async function checkDepsChanged(ctx) {
1062
1096
  if (range === null) {
1063
1097
  result.skipped.push({
1064
1098
  check: "deps-changed",
1099
+ doc: doc.path,
1065
1100
  reason: `${doc.path}: commit range unreachable (shallow clone?)`
1066
1101
  });
1067
1102
  continue;
1068
1103
  }
1069
1104
  if (range.total === 0) continue;
1070
1105
  const latest = range.commits[0];
1071
- result.advisories.push({
1106
+ (doc.dirty ? result.suppressedAdvisories : result.advisories).push({
1072
1107
  type: "deps-changed",
1073
1108
  message: `dependency manifests touched by ${range.total} commit${range.total === 1 ? "" : "s"} since ${doc.path} was last committed (latest: ${latest.hash.slice(0, 7)} "${latest.subject}")`,
1074
1109
  anchor: { doc: doc.path, line: null, excerpt: null },
@@ -1353,6 +1388,8 @@ async function computeAudit(rootDir, options = {}) {
1353
1388
  version: 1,
1354
1389
  root: resolvedRoot,
1355
1390
  gitAvailable: headHash !== "unknown",
1391
+ headHash,
1392
+ checksRun: [],
1356
1393
  docs: docs.map((d) => ({
1357
1394
  path: d.path,
1358
1395
  lastCommit: d.lastCommit,
@@ -1362,6 +1399,7 @@ async function computeAudit(rootDir, options = {}) {
1362
1399
  decisionsChecked: false,
1363
1400
  issues: [],
1364
1401
  advisories: [],
1402
+ suppressedAdvisories: [],
1365
1403
  skippedChecks: [],
1366
1404
  clean: true
1367
1405
  };
@@ -1390,15 +1428,269 @@ async function computeAudit(rootDir, options = {}) {
1390
1428
  const selected = options.checks ?? ALL_CHECKS;
1391
1429
  for (const name of ALL_CHECKS) {
1392
1430
  if (!selected.includes(name)) continue;
1393
- const { issues, advisories, skipped } = await CHECKS[name](ctx);
1431
+ const { issues, advisories, suppressedAdvisories, skipped } = await (options.runCheck ? options.runCheck(name, ctx) : CHECKS[name](ctx));
1432
+ report.checksRun.push(name);
1394
1433
  report.issues.push(...issues);
1395
1434
  report.advisories.push(...advisories);
1435
+ report.suppressedAdvisories.push(...suppressedAdvisories ?? []);
1396
1436
  report.skippedChecks.push(...skipped);
1397
1437
  }
1398
1438
  report.clean = report.issues.length === 0;
1399
1439
  return report;
1400
1440
  }
1401
1441
 
1442
+ // src/audit/repair.ts
1443
+ import fs10 from "fs/promises";
1444
+ import path16 from "path";
1445
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
1446
+ import { z as z3 } from "zod";
1447
+ var checkSchema = z3.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
1448
+ var commitSchema = z3.object({ hash: z3.string().regex(/^[a-f0-9]{40,64}$/), date: z3.string(), subject: z3.string() });
1449
+ var anchorSchema = z3.object({ doc: z3.string(), line: z3.number().int().positive().nullable(), excerpt: z3.string().nullable() });
1450
+ var count = z3.number().int().nonnegative();
1451
+ var evidenceSchema = z3.discriminatedUnion("kind", [
1452
+ z3.object({
1453
+ kind: z3.literal("missing-path"),
1454
+ claimed: z3.string(),
1455
+ renamedTo: z3.string().nullable(),
1456
+ deletedInCommit: commitSchema.nullable(),
1457
+ everTracked: z3.boolean(),
1458
+ parentDirExists: z3.boolean()
1459
+ }),
1460
+ z3.object({
1461
+ kind: z3.literal("unmentioned-dir"),
1462
+ dir: z3.string(),
1463
+ sourceFileCount: count,
1464
+ firstCommit: commitSchema.nullable(),
1465
+ checkedDocs: z3.array(z3.string())
1466
+ }),
1467
+ z3.object({
1468
+ kind: z3.literal("count-mismatch"),
1469
+ claimed: count,
1470
+ actual: count,
1471
+ unit: z3.string(),
1472
+ countedFrom: z3.string(),
1473
+ members: z3.array(z3.string())
1474
+ }),
1475
+ z3.object({
1476
+ kind: z3.literal("missing-script"),
1477
+ scriptName: z3.string(),
1478
+ invocation: z3.string(),
1479
+ manifestsChecked: z3.array(z3.string()),
1480
+ availableScripts: z3.array(z3.string())
1481
+ }),
1482
+ z3.object({
1483
+ kind: z3.literal("doc-behind-manifests"),
1484
+ docLastCommit: commitSchema,
1485
+ manifestCommits: z3.array(commitSchema.extend({ files: z3.array(z3.string()) })),
1486
+ totalCommits: count
1487
+ }),
1488
+ z3.object({
1489
+ kind: z3.literal("decision-anchor"),
1490
+ decisionId: z3.string(),
1491
+ title: z3.string(),
1492
+ changedFiles: z3.array(z3.string()),
1493
+ refreshedHash: z3.string(),
1494
+ provenance: z3.object({}).passthrough().optional()
1495
+ })
1496
+ ]);
1497
+ var findingSchema = z3.object({ message: z3.string(), anchor: anchorSchema, evidence: evidenceSchema });
1498
+ var issueSchema = findingSchema.extend({
1499
+ type: z3.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
1500
+ confidence: z3.enum(["certain", "likely"])
1501
+ });
1502
+ var advisorySchema = findingSchema.extend({ type: z3.enum(["deps-changed", "decision-anchor-drift"]) });
1503
+ var checkResultSchema = z3.object({
1504
+ issues: z3.array(issueSchema),
1505
+ advisories: z3.array(advisorySchema),
1506
+ suppressedAdvisories: z3.array(advisorySchema).optional(),
1507
+ skipped: z3.array(z3.object({ check: z3.string(), reason: z3.string(), doc: z3.string().optional() }))
1508
+ });
1509
+ var reportSchema = z3.object({
1510
+ version: z3.literal(1),
1511
+ root: z3.string(),
1512
+ gitAvailable: z3.literal(true),
1513
+ headHash: commitSchema.shape.hash,
1514
+ checksRun: z3.array(checkSchema).nonempty(),
1515
+ docs: z3.array(z3.object({
1516
+ path: z3.enum(DOC_CANDIDATES),
1517
+ lastCommit: commitSchema.nullable(),
1518
+ dirty: z3.boolean(),
1519
+ lineCount: count
1520
+ })).nonempty(),
1521
+ decisionsChecked: z3.boolean(),
1522
+ clean: z3.boolean(),
1523
+ issues: z3.array(issueSchema),
1524
+ advisories: z3.array(advisorySchema),
1525
+ suppressedAdvisories: z3.array(advisorySchema).optional(),
1526
+ skippedChecks: z3.array(z3.object({ check: z3.string(), reason: z3.string(), doc: z3.string().optional() }))
1527
+ });
1528
+ var baselineSchema = z3.object({
1529
+ kind: z3.literal("mason-audit-repair"),
1530
+ version: z3.literal(1),
1531
+ createdAt: z3.string().datetime(),
1532
+ report: reportSchema,
1533
+ digest: z3.string().regex(/^[a-f0-9]{64}$/)
1534
+ });
1535
+ var digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
1536
+ function findingId(finding) {
1537
+ const e = finding.evidence;
1538
+ let key;
1539
+ switch (e.kind) {
1540
+ case "missing-path":
1541
+ key = e.claimed;
1542
+ break;
1543
+ case "unmentioned-dir":
1544
+ key = e.dir;
1545
+ break;
1546
+ case "count-mismatch":
1547
+ key = [e.unit.replace(/s$/, ""), e.countedFrom];
1548
+ break;
1549
+ case "missing-script":
1550
+ key = e.scriptName;
1551
+ break;
1552
+ case "doc-behind-manifests":
1553
+ key = null;
1554
+ break;
1555
+ case "decision-anchor":
1556
+ key = [e.decisionId, e.provenance?.revision, e.provenance?.approval];
1557
+ break;
1558
+ }
1559
+ return digest([finding.type, finding.anchor.doc, key]);
1560
+ }
1561
+ function allFindings(report) {
1562
+ return [...report.issues, ...report.advisories, ...report.suppressedAdvisories ?? []];
1563
+ }
1564
+ async function docState(root) {
1565
+ const docs = [];
1566
+ for (const doc of DOC_CANDIDATES) {
1567
+ try {
1568
+ const content = await readBoundedFile(await storePath(root, doc), 10 * 1024 * 1024);
1569
+ if (content === null) throw new Error("Context file is not regular or exceeds 10 MiB: " + doc);
1570
+ docs.push([doc, digest(content)]);
1571
+ } catch (error) {
1572
+ if (error.code !== "ENOENT") throw error;
1573
+ docs.push([doc, null]);
1574
+ }
1575
+ }
1576
+ return digest(docs);
1577
+ }
1578
+ async function stableAudit(root, checks, options = {}) {
1579
+ const head = await getCurrentGitHash(root);
1580
+ const before = await docState(root);
1581
+ const report = await computeAudit(root, { ...options, checks });
1582
+ if (head !== await getCurrentGitHash(root) || before !== await docState(root) || report && report.headHash !== head) {
1583
+ throw new Error("HEAD or context files changed during the audit; retry against a stable checkout.");
1584
+ }
1585
+ return report;
1586
+ }
1587
+ async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
1588
+ const root = await fs10.realpath(rootDir);
1589
+ const selected = z3.array(checkSchema).nonempty().parse(checks);
1590
+ const report = await stableAudit(root, selected, options);
1591
+ if (!report) throw new Error("No context files found to prepare a repair.");
1592
+ if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
1593
+ const storedReport = reportSchema.parse(report);
1594
+ const payload = {
1595
+ kind: "mason-audit-repair",
1596
+ version: 1,
1597
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1598
+ report: storedReport
1599
+ };
1600
+ const baselinePath = ".mason/reports/repairs/" + randomUUID2() + ".json";
1601
+ await writeStoreJson(root, baselinePath, { ...payload, digest: digest(payload) });
1602
+ return { version: 1, action: "prepare", baselinePath, report };
1603
+ }
1604
+ async function verifyRepair(rootDir, baselinePath, options = {}) {
1605
+ const root = await fs10.realpath(rootDir);
1606
+ const declaredRoot = path16.resolve(rootDir);
1607
+ const relative = path16.isAbsolute(baselinePath) ? path16.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
1608
+ const stored = baselineSchema.parse(await readStoreJson(root, relative));
1609
+ const { digest: savedDigest, ...payload } = stored;
1610
+ if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
1611
+ if (stored.report.root !== root) throw new Error("Repair baseline belongs to a different repository.");
1612
+ const original = stored.report;
1613
+ const diagnostics = [];
1614
+ let current = null;
1615
+ try {
1616
+ current = await stableAudit(root, original.checksRun, options);
1617
+ if (!current) diagnostics.push("No context files remain available to audit.");
1618
+ else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
1619
+ for (const doc of original.docs) {
1620
+ if (!original.issues.some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
1621
+ const content = await readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024);
1622
+ if (content === null || !content.trim()) {
1623
+ diagnostics.push("Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair.");
1624
+ }
1625
+ }
1626
+ if (await getChangesWithStatus(root, original.headHash) === null) {
1627
+ diagnostics.push("The original audit commit is unavailable; repair history cannot be verified.");
1628
+ }
1629
+ } catch (error) {
1630
+ diagnostics.push(error instanceof Error ? error.message : String(error));
1631
+ }
1632
+ const currentById = new Map((current ? allFindings(current) : []).map((f) => [findingId(f), f]));
1633
+ const originalFindings = allFindings(original);
1634
+ const originalIds = new Set(originalFindings.map(findingId));
1635
+ const missingDocs = original.docs.filter((doc) => !current?.docs.some((d) => d.path === doc.path));
1636
+ for (const doc of missingDocs) diagnostics.push("Original context file " + doc.path + " is unavailable; removing it does not verify a repair.");
1637
+ const findings = originalFindings.map((finding) => {
1638
+ const id = findingId(finding);
1639
+ const now = currentById.get(id);
1640
+ const base = { id, original: finding, ...now ? { current: now } : {} };
1641
+ if (diagnostics.length || !current) {
1642
+ return { ...base, status: "unverified", reason: "The original audit scope could not be verified. See diagnostics." };
1643
+ }
1644
+ if ("confidence" in finding && now) {
1645
+ return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
1646
+ }
1647
+ const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
1648
+ if (!current.checksRun?.includes(finding.type) || skipped.length) {
1649
+ return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
1650
+ }
1651
+ if (!("confidence" in finding)) {
1652
+ return {
1653
+ ...base,
1654
+ status: "review-required",
1655
+ reason: "An audit cannot establish that this advisory was reviewed. Retain its original evidence and report a separate assessment; editing or committing the doc is not approval."
1656
+ };
1657
+ }
1658
+ return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
1659
+ });
1660
+ const newFindings = [...currentById].filter(([id]) => !originalIds.has(id)).map(([, f]) => f);
1661
+ const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
1662
+ for (const f of findings) counts[f.status]++;
1663
+ const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f));
1664
+ const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
1665
+ return {
1666
+ version: 1,
1667
+ action: "verify",
1668
+ baselinePath: relative,
1669
+ baselineHead: original.headHash,
1670
+ currentHead: current?.gitAvailable ? current.headHash : null,
1671
+ status: incomplete ? "incomplete" : issuesRemain ? "issues-remain" : "verified",
1672
+ findings,
1673
+ newFindings,
1674
+ diagnostics,
1675
+ currentAudit: current,
1676
+ counts,
1677
+ 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."
1678
+ };
1679
+ }
1680
+ function repairExitCode(report) {
1681
+ return report.status === "verified" ? 0 : report.status === "issues-remain" ? 1 : 2;
1682
+ }
1683
+ function formatRepairSummary(report) {
1684
+ return [
1685
+ "Repair verification: " + report.status + ". Baseline: " + report.baselinePath,
1686
+ ...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + f.original.message + "\n " + f.reason),
1687
+ ...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
1688
+ ...report.diagnostics.map((d) => " [unverified] " + d),
1689
+ ...(report.currentAudit?.skippedChecks ?? []).map((s) => " [skipped] " + s.check + ": " + s.reason),
1690
+ report.scope
1691
+ ].join("\n");
1692
+ }
1693
+
1402
1694
  // src/audit/cli.ts
1403
1695
  var USAGE = `Usage: mason-audit [--dir <path>] [--json | --fix-prompt] [--checks <list>]
1404
1696
 
@@ -1413,7 +1705,10 @@ Options:
1413
1705
  --json Print the full audit report as JSON (additive-only schema)
1414
1706
  --fix-prompt When issues exist, print a work order for ANY coding agent
1415
1707
  (Claude, Codex, Gemini, ...) \u2013 pipe it to your agent CLI to
1416
- close the loop. Prints the clean summary when there are none.
1708
+ repair the findings. Includes advisories that require review.
1709
+ --prepare-repair Save the original audit under .mason/reports/repairs/ before edits
1710
+ --verify-repair <path>
1711
+ Compare against that saved baseline, using its original checks
1417
1712
  --checks <list> Comma-separated subset of checks to run (default: all):
1418
1713
  ${ALL_CHECKS.join(", ")}
1419
1714
  --help Show this help
@@ -1421,14 +1716,19 @@ Options:
1421
1716
  Exit codes:
1422
1717
  0 no issues (advisories may still be present)
1423
1718
  1 provable issues found
1424
- 2 error (no context file, not a git repository, bad arguments)`;
1719
+ 2 error (no context file, not a git repository, bad arguments)
1720
+
1721
+ With --verify-repair: 0 verified by the original checks; 1 issues remain;
1722
+ 2 incomplete (unverified findings, skipped checks, or advisories needing review).
1723
+ Preparation writes only a baseline; verification and ordinary audits are read-only.`;
1425
1724
  function parseArgs(argv) {
1426
1725
  const parsed = {
1427
1726
  dir: process.cwd(),
1428
1727
  json: false,
1429
1728
  fixPrompt: false,
1430
1729
  help: false,
1431
- checks: void 0
1730
+ checks: void 0,
1731
+ prepareRepair: false
1432
1732
  };
1433
1733
  for (let i = 0; i < argv.length; i++) {
1434
1734
  const arg = argv[i];
@@ -1436,6 +1736,12 @@ function parseArgs(argv) {
1436
1736
  parsed.json = true;
1437
1737
  } else if (arg === "--fix-prompt") {
1438
1738
  parsed.fixPrompt = true;
1739
+ } else if (arg === "--prepare-repair") {
1740
+ parsed.prepareRepair = true;
1741
+ } else if (arg === "--verify-repair") {
1742
+ const value = argv[++i];
1743
+ if (!value || value.startsWith("--")) throw new Error("--verify-repair requires a baseline path");
1744
+ parsed.baseline = value;
1439
1745
  } else if (arg === "--help" || arg === "-h") {
1440
1746
  parsed.help = true;
1441
1747
  } else if (arg === "--dir") {
@@ -1446,6 +1752,7 @@ function parseArgs(argv) {
1446
1752
  const value = argv[++i];
1447
1753
  if (!value) throw new Error("--checks requires a comma-separated list");
1448
1754
  const names = value.split(",").map((n) => n.trim()).filter(Boolean);
1755
+ if (!names.length) throw new Error("--checks requires at least one check");
1449
1756
  for (const name of names) {
1450
1757
  if (!ALL_CHECKS.includes(name)) {
1451
1758
  throw new Error(
@@ -1469,6 +1776,7 @@ function issueLine(issue) {
1469
1776
  }
1470
1777
  function formatAuditSummary(report) {
1471
1778
  const lines = [];
1779
+ const reviewCount = report.advisories.length + (report.suppressedAdvisories?.length ?? 0);
1472
1780
  for (const doc of report.docs) {
1473
1781
  const docIssues = report.issues.filter((i) => i.anchor.doc === doc.path);
1474
1782
  const committed = doc.lastCommit ? `last committed ${doc.lastCommit.date.slice(0, 10)}, ${doc.lastCommit.hash.slice(0, 7)}` : "untracked";
@@ -1496,21 +1804,25 @@ function formatAuditSummary(report) {
1496
1804
  lines.push(` [skipped] ${skip.check}: ${skip.reason}`);
1497
1805
  }
1498
1806
  }
1807
+ for (const advisory of report.suppressedAdvisories ?? []) {
1808
+ lines.push(` [suppressed; unresolved] ${advisory.type} ${advisory.anchor.doc}: ${advisory.message}`);
1809
+ }
1499
1810
  lines.push(
1500
- report.clean ? `Context files are clean (${report.docs.length} doc${report.docs.length === 1 ? "" : "s"} audited).` : `${report.issues.length} issue${report.issues.length === 1 ? "" : "s"} across ${report.docs.length} doc${report.docs.length === 1 ? "" : "s"}.`
1811
+ report.clean ? reviewCount || report.skippedChecks.length ? `No audit issues detected (${report.docs.length} docs audited); ${reviewCount} advisories remain for review, ${report.skippedChecks.length} checks skipped.` : `Context files are clean (${report.docs.length} doc${report.docs.length === 1 ? "" : "s"} audited).` : `${report.issues.length} issue${report.issues.length === 1 ? "" : "s"} across ${report.docs.length} doc${report.docs.length === 1 ? "" : "s"}.`
1501
1812
  );
1502
1813
  return lines.join("\n");
1503
1814
  }
1504
- function formatFixPrompt(report) {
1815
+ function formatFixPrompt(report, baselinePath) {
1505
1816
  const flaggedDocs = [...new Set(report.issues.map((i) => i.anchor.doc))];
1506
1817
  const lines = [];
1507
1818
  lines.push(
1508
- "The AI context files in this repository contain claims that are provably out of date. Fix ONLY the flagged claims. Work autonomously; do not ask questions."
1819
+ "Review the flagged context claims using the evidence below. Make minimal repairs within the user's authorized scope. A setup-only or audit-only request does not authorize rewriting existing documentation."
1509
1820
  );
1510
1821
  lines.push("");
1511
1822
  lines.push("RULES:");
1823
+ lines.push(baselinePath ? `- Preserve the original repair baseline: ${JSON.stringify(baselinePath)}. Do not replace it after editing.` : "- Before the first edit, call mason_repair with action: prepare, or run mason-audit --prepare-repair --json with the same --dir and --checks. Keep the returned baselinePath through verification.");
1512
1824
  lines.push(
1513
- `- Edit ONLY these files: ${flaggedDocs.join(", ")}. Never modify source code, configs, or anything else \u2013 the docs must be brought to match the code, not the other way around.`
1825
+ `- Edit ONLY these files: ${flaggedDocs.join(", ") || "none (advisory review only)"}. Bring the docs into agreement with verified source evidence; do not change source code or configs to silence findings.`
1514
1826
  );
1515
1827
  lines.push(
1516
1828
  "- Keep diffs minimal: change the smallest span that makes each claim true."
@@ -1518,6 +1830,7 @@ function formatFixPrompt(report) {
1518
1830
  lines.push(
1519
1831
  "- Never invent content. Every replacement must be grounded in the evidence below or in files you read from this repository."
1520
1832
  );
1833
+ lines.push("- Inspect likely findings before editing: heuristic evidence may describe an intentional omission or an example.");
1521
1834
  lines.push(
1522
1835
  "- deleted-reference: if evidence shows renamedTo, update the path; otherwise remove the reference, or rephrase to past tense if the sentence is about history. Deleted paths inside directory trees: delete the tree line."
1523
1836
  );
@@ -1531,20 +1844,27 @@ function formatFixPrompt(report) {
1531
1844
  "- new-module: add a one-line factual mention of the directory where sibling modules are described; read the directory's files first and describe only what you verified."
1532
1845
  );
1533
1846
  lines.push(
1534
- "- Do NOT touch anything listed under ADVISORIES \u2013 list them in your summary for human review instead."
1847
+ "- ADVISORIES require a separate assessment of the cited commits or decision evidence. Report any review you perform and what remains unknown. Their disappearance after edits or a commit does not establish review or approval."
1535
1848
  );
1536
1849
  lines.push("");
1537
- lines.push("AUDIT REPORT (deterministic, computed against git HEAD):");
1850
+ lines.push("AUDIT REPORT (current context files and repository evidence, including local edits):");
1538
1851
  lines.push(
1539
1852
  JSON.stringify(
1540
- { issues: report.issues, advisories: report.advisories },
1853
+ {
1854
+ root: report.root,
1855
+ checks: report.checksRun,
1856
+ issues: report.issues,
1857
+ advisories: report.advisories,
1858
+ suppressedAdvisories: report.suppressedAdvisories,
1859
+ skippedChecks: report.skippedChecks
1860
+ },
1541
1861
  null,
1542
1862
  2
1543
1863
  )
1544
1864
  );
1545
1865
  lines.push("");
1546
1866
  lines.push(
1547
- "Finish by summarizing each edit and citing the evidence item it resolves."
1867
+ "After edits, call mason_repair with action: verify and the original baselinePath, or mason-audit --verify-repair <baselinePath> --dir <project>. Repeat against the same baseline after any final documentation commit. Summarize resolved, unresolved, review-required, unverified, and new findings with their evidence. Do not report a suppressed or unavailable check as fixed. This audit covers the listed context files; independently discovered README or application issues need their own validation."
1548
1868
  );
1549
1869
  return lines.join("\n");
1550
1870
  }
@@ -1560,6 +1880,9 @@ async function runAuditCli(argv, io = {
1560
1880
  if (args.json && args.fixPrompt) {
1561
1881
  throw new Error("--json and --fix-prompt are mutually exclusive");
1562
1882
  }
1883
+ if (args.baseline && (args.prepareRepair || args.checks || args.fixPrompt)) {
1884
+ throw new Error("--verify-repair cannot be combined with --prepare-repair, --checks, or --fix-prompt; verification uses the original scope");
1885
+ }
1563
1886
  } catch (error) {
1564
1887
  io.err(error instanceof Error ? error.message : String(error));
1565
1888
  io.err(USAGE);
@@ -1569,7 +1892,23 @@ async function runAuditCli(argv, io = {
1569
1892
  io.out(USAGE);
1570
1893
  return 0;
1571
1894
  }
1572
- const rootDir = path16.resolve(args.dir);
1895
+ const rootDir = path17.resolve(args.dir);
1896
+ if (args.baseline || args.prepareRepair) {
1897
+ try {
1898
+ if (args.baseline) {
1899
+ const verification = await verifyRepair(rootDir, args.baseline);
1900
+ io.out(args.json ? JSON.stringify(verification, null, 2) : formatRepairSummary(verification));
1901
+ return repairExitCode(verification);
1902
+ }
1903
+ const prepared = await prepareRepair(rootDir, args.checks);
1904
+ io.out(args.json ? JSON.stringify({ ...prepared, workOrder: formatFixPrompt(prepared.report, prepared.baselinePath) }, null, 2) : args.fixPrompt ? formatFixPrompt(prepared.report, prepared.baselinePath) : `Repair baseline: ${prepared.baselinePath}
1905
+ ${formatAuditSummary(prepared.report)}`);
1906
+ return prepared.report.clean ? 0 : 1;
1907
+ } catch (error) {
1908
+ io.err(error instanceof Error ? error.message : String(error));
1909
+ return 2;
1910
+ }
1911
+ }
1573
1912
  const report = await computeAudit(rootDir, { checks: args.checks });
1574
1913
  if (!report) {
1575
1914
  io.err(
@@ -1585,7 +1924,7 @@ async function runAuditCli(argv, io = {
1585
1924
  }
1586
1925
  if (args.fixPrompt) {
1587
1926
  io.out(
1588
- report.clean ? formatAuditSummary(report) : formatFixPrompt(report)
1927
+ report.clean && !report.advisories.length && !report.suppressedAdvisories?.length ? formatAuditSummary(report) : formatFixPrompt(report)
1589
1928
  );
1590
1929
  return report.clean ? 0 : 1;
1591
1930
  }