mason-context 0.10.1 → 0.11.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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0 — 2026-09-05
4
+
5
+ Mason now keeps the original audit evidence visible while an assistant repairs documentation. A dependency warning suppressed by local edits stays unresolved, and a later documentation commit does not silently clear its review requirement.
6
+
7
+ - Add `mason_repair` and `mason-audit --prepare-repair` / `--verify-repair` to retain original audit evidence through edits and the final documentation commit. Verification distinguishes resolved, unresolved, review-required, unverified, and new findings; it preserves unavailable history and missing-document diagnostics.
8
+ - Retain suppressed dependency advisories when setup or repairs dirty context files. An advisory disappearing after a documentation commit no longer loses its evidence in a prepared repair. Ordinary audit exit codes stay unchanged; explicit repair verification reports incomplete scope separately.
9
+ - Route authorized repairs through preparation and verification in assistant instructions and work orders. Setup alone does not authorize rewriting existing claims; advisories require a separate assessment.
10
+
11
+ Upgrade to `mason-context@0.11.0`, restart the assistant, and refresh its marker-delimited Mason instructions through `mason_init` to enable the repair workflow. No decision-store migration is required. Explicit repair verification exits 2 for incomplete checks or outstanding advisory review; ordinary audit exit codes are unchanged.
12
+
3
13
  ## 0.10.1 — 2026-09-05
4
14
 
5
15
  Editing an accepted decision previously hid its accepted content from ordinary retrieval until the draft was reviewed. Mason now keeps the accepted constraint visible alongside the proposed replacement.
package/README.md CHANGED
@@ -130,6 +130,7 @@ Mason records assertions of review; it does not authenticate reviewer identity,
130
130
  | Tool | Purpose |
131
131
  |---|---|
132
132
  | `mason_init` | Read-only audit/review findings and quickstart guide; optional `base` for review, `evidence` for local CI manifests, `mode: "map"` for an architecture build. |
133
+ | `mason_repair` | Prepare an audit repair baseline; verify the same original findings after edits. Reports unresolved advisories and unavailable checks. |
133
134
  | `mason_complete_init` | Records assistant instruction setup; preserves prior settings on repeated calls. |
134
135
  | `generate_snapshot_batch` | Map step — returns one batch of files for the assistant to summarize. |
135
136
  | `save_partial_snapshot` | Persists the partial map for one batch. |
@@ -272,7 +273,29 @@ Issues drive the exit code; **advisories never do** — they're facts an agent c
272
273
 
273
274
  ### The context files maintain themselves
274
275
 
275
- Same split as the concept map: detection is deterministic and free, the fix is any agent you already run. `--fix-prompt` emits a work order scoped to exactly the flagged claims — fix only these, minimal diffs, never invent content, never touch source code. The reusable workflow runs the audit, hands the work order to your agent, verifies the audit is clean afterwards (and that the agent touched nothing but the context files), then opens a PR citing the evidence — it never commits to the audited branch, and it skips cleanly when an audit PR is already open:
276
+ ### Track a repair through verification
277
+
278
+ Ask your assistant: *"Use Mason to prepare a repair, fix the documented issues within scope, and verify against the original findings."* The assistant calls `mason_repair` with `action: "prepare"`, makes grounded edits, and then calls it with `action: "verify"` and the returned `baselinePath`. Setup alone only installs assistant instructions; repairing existing claims needs to be part of your request.
279
+
280
+ The CLI provides the same workflow:
281
+
282
+ ```bash
283
+ mason-audit --dir . --prepare-repair --fix-prompt
284
+ # After applying the work order, use the exact baseline path it returned:
285
+ mason-audit --dir . --verify-repair .mason/reports/repairs/<id>.json
286
+ ```
287
+
288
+ Preparation saves the full original audit under `.mason/reports/repairs/`; it does not edit documentation. Ordinary audits and verification remain read-only. Add `.mason/reports/` to your ignore rules if you want these local artifacts excluded from commits. Keep the same baseline through any final documentation commit, then verify again. Do not regenerate it to clear unresolved findings. `--json` is supported for preparation and verification; use `--checks` only during preparation to select a scope.
289
+
290
+ Each original finding is **resolved** (its check no longer reports it), **unresolved**, **review-required**, or **unverified**. New findings are separate. A shifted line number does not erase the original claim, and a missing document, unavailable history, or skipped check cannot count as a fix. Inspect the edit for meaning: these deterministic checks do not establish complete documentation correctness. README files and arbitrary build commands are outside this audit's current scope.
291
+
292
+ Dependency evidence suppressed by local edits is retained in `suppressedAdvisories`, including when setup has already dirtied the document. Committing that document does not prove the dependency change was reviewed: the original advisory stays in the repair report. Record your assessment separately; this workflow does not approve advisories or decisions. Baselines are validated local evidence with a checksum to detect accidental edits, not authenticated attestations.
293
+
294
+ Ordinary audit exit codes remain **0** for no issues (advisories may exist), **1** for issues, and **2** for errors. Explicit `--verify-repair` uses **0** for verified scope, **1** for remaining/new issues, and **2** for incomplete verification, including advisories needing review or skipped checks. Incomplete verification takes precedence when both issues and unavailable evidence remain.
295
+
296
+ ### Repair pull requests in CI
297
+
298
+ `--fix-prompt` emits a work order scoped to the flagged claims and the user's authorization. The reusable workflow below prepares a baseline, checks that the agent touched only context files, and verifies the original issues both before and after the documentation commit. It opens a PR only when those issues are resolved by their checks and no new issues appear. Advisories and skipped checks remain visible in the PR; their review is not a condition for proposing documentation repairs. The workflow never commits to the audited branch and skips when an audit PR is already open:
276
299
 
277
300
  ```yaml
278
301
  name: Context audit
@@ -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,263 @@ 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 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 reportSchema = z3.object({
1504
+ version: z3.literal(1),
1505
+ root: z3.string(),
1506
+ gitAvailable: z3.literal(true),
1507
+ headHash: commitSchema.shape.hash,
1508
+ checksRun: z3.array(checkSchema).nonempty(),
1509
+ docs: z3.array(z3.object({
1510
+ path: z3.enum(DOC_CANDIDATES),
1511
+ lastCommit: commitSchema.nullable(),
1512
+ dirty: z3.boolean(),
1513
+ lineCount: count
1514
+ })).nonempty(),
1515
+ decisionsChecked: z3.boolean(),
1516
+ clean: z3.boolean(),
1517
+ issues: z3.array(issueSchema),
1518
+ advisories: z3.array(advisorySchema),
1519
+ suppressedAdvisories: z3.array(advisorySchema).optional(),
1520
+ skippedChecks: z3.array(z3.object({ check: z3.string(), reason: z3.string(), doc: z3.string().optional() }))
1521
+ });
1522
+ var baselineSchema = z3.object({
1523
+ kind: z3.literal("mason-audit-repair"),
1524
+ version: z3.literal(1),
1525
+ createdAt: z3.string().datetime(),
1526
+ report: reportSchema,
1527
+ digest: z3.string().regex(/^[a-f0-9]{64}$/)
1528
+ });
1529
+ var digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
1530
+ function findingId(finding) {
1531
+ const e = finding.evidence;
1532
+ let key;
1533
+ switch (e.kind) {
1534
+ case "missing-path":
1535
+ key = e.claimed;
1536
+ break;
1537
+ case "unmentioned-dir":
1538
+ key = e.dir;
1539
+ break;
1540
+ case "count-mismatch":
1541
+ key = [e.unit.replace(/s$/, ""), e.countedFrom];
1542
+ break;
1543
+ case "missing-script":
1544
+ key = e.scriptName;
1545
+ break;
1546
+ case "doc-behind-manifests":
1547
+ key = null;
1548
+ break;
1549
+ case "decision-anchor":
1550
+ key = [e.decisionId, e.provenance?.revision, e.provenance?.approval];
1551
+ break;
1552
+ }
1553
+ return digest([finding.type, finding.anchor.doc, key]);
1554
+ }
1555
+ function allFindings(report) {
1556
+ return [...report.issues, ...report.advisories, ...report.suppressedAdvisories ?? []];
1557
+ }
1558
+ async function docState(root) {
1559
+ const docs = [];
1560
+ for (const doc of DOC_CANDIDATES) {
1561
+ try {
1562
+ const content = await readBoundedFile(await storePath(root, doc), 10 * 1024 * 1024);
1563
+ if (content === null) throw new Error("Context file is not regular or exceeds 10 MiB: " + doc);
1564
+ docs.push([doc, digest(content)]);
1565
+ } catch (error) {
1566
+ if (error.code !== "ENOENT") throw error;
1567
+ docs.push([doc, null]);
1568
+ }
1569
+ }
1570
+ return digest(docs);
1571
+ }
1572
+ async function stableAudit(root, checks) {
1573
+ const head = await getCurrentGitHash(root);
1574
+ const before = await docState(root);
1575
+ const report = await computeAudit(root, { checks });
1576
+ if (head !== await getCurrentGitHash(root) || before !== await docState(root) || report && report.headHash !== head) {
1577
+ throw new Error("HEAD or context files changed during the audit; retry against a stable checkout.");
1578
+ }
1579
+ return report;
1580
+ }
1581
+ async function prepareRepair(rootDir, checks = ALL_CHECKS) {
1582
+ const root = await fs10.realpath(rootDir);
1583
+ const selected = z3.array(checkSchema).nonempty().parse(checks);
1584
+ const report = await stableAudit(root, selected);
1585
+ if (!report) throw new Error("No context files found to prepare a repair.");
1586
+ if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
1587
+ const storedReport = reportSchema.parse(report);
1588
+ const payload = {
1589
+ kind: "mason-audit-repair",
1590
+ version: 1,
1591
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1592
+ report: storedReport
1593
+ };
1594
+ const baselinePath = ".mason/reports/repairs/" + randomUUID2() + ".json";
1595
+ await writeStoreJson(root, baselinePath, { ...payload, digest: digest(payload) });
1596
+ return { version: 1, action: "prepare", baselinePath, report };
1597
+ }
1598
+ async function verifyRepair(rootDir, baselinePath) {
1599
+ const root = await fs10.realpath(rootDir);
1600
+ const declaredRoot = path16.resolve(rootDir);
1601
+ const relative = path16.isAbsolute(baselinePath) ? path16.relative(isWithinRoot(declaredRoot, baselinePath) ? declaredRoot : root, baselinePath) : baselinePath;
1602
+ const stored = baselineSchema.parse(await readStoreJson(root, relative));
1603
+ const { digest: savedDigest, ...payload } = stored;
1604
+ if (digest(payload) !== savedDigest) throw new Error("Repair baseline was modified; use the original baseline.");
1605
+ if (stored.report.root !== root) throw new Error("Repair baseline belongs to a different repository.");
1606
+ const original = stored.report;
1607
+ const diagnostics = [];
1608
+ let current = null;
1609
+ try {
1610
+ current = await stableAudit(root, original.checksRun);
1611
+ if (!current) diagnostics.push("No context files remain available to audit.");
1612
+ else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
1613
+ for (const doc of original.docs) {
1614
+ if (!original.issues.some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
1615
+ const content = await readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024);
1616
+ if (content === null || !content.trim()) {
1617
+ diagnostics.push("Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair.");
1618
+ }
1619
+ }
1620
+ if (await getChangesWithStatus(root, original.headHash) === null) {
1621
+ diagnostics.push("The original audit commit is unavailable; repair history cannot be verified.");
1622
+ }
1623
+ } catch (error) {
1624
+ diagnostics.push(error instanceof Error ? error.message : String(error));
1625
+ }
1626
+ const currentById = new Map((current ? allFindings(current) : []).map((f) => [findingId(f), f]));
1627
+ const originalFindings = allFindings(original);
1628
+ const originalIds = new Set(originalFindings.map(findingId));
1629
+ const missingDocs = original.docs.filter((doc) => !current?.docs.some((d) => d.path === doc.path));
1630
+ for (const doc of missingDocs) diagnostics.push("Original context file " + doc.path + " is unavailable; removing it does not verify a repair.");
1631
+ const findings = originalFindings.map((finding) => {
1632
+ const id = findingId(finding);
1633
+ const now = currentById.get(id);
1634
+ const base = { id, original: finding, ...now ? { current: now } : {} };
1635
+ if (diagnostics.length || !current) {
1636
+ return { ...base, status: "unverified", reason: "The original audit scope could not be verified. See diagnostics." };
1637
+ }
1638
+ if ("confidence" in finding && now) {
1639
+ return { ...base, status: "unresolved", reason: "The original check still reports this claim." };
1640
+ }
1641
+ const skipped = current.skippedChecks.filter((s) => s.check === finding.type && (!s.doc || s.doc === finding.anchor.doc));
1642
+ if (!current.checksRun?.includes(finding.type) || skipped.length) {
1643
+ return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
1644
+ }
1645
+ if (!("confidence" in finding)) {
1646
+ return {
1647
+ ...base,
1648
+ status: "review-required",
1649
+ 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."
1650
+ };
1651
+ }
1652
+ return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
1653
+ });
1654
+ const newFindings = [...currentById].filter(([id]) => !originalIds.has(id)).map(([, f]) => f);
1655
+ const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
1656
+ for (const f of findings) counts[f.status]++;
1657
+ const incomplete = diagnostics.length > 0 || counts.unverified > 0 || counts["review-required"] > 0 || (current?.skippedChecks.length ?? 0) > 0 || newFindings.some((f) => !("confidence" in f));
1658
+ const issuesRemain = counts.unresolved > 0 || newFindings.some((f) => "confidence" in f);
1659
+ return {
1660
+ version: 1,
1661
+ action: "verify",
1662
+ baselinePath: relative,
1663
+ baselineHead: original.headHash,
1664
+ currentHead: current?.gitAvailable ? current.headHash : null,
1665
+ status: incomplete ? "incomplete" : issuesRemain ? "issues-remain" : "verified",
1666
+ findings,
1667
+ newFindings,
1668
+ diagnostics,
1669
+ currentAudit: current,
1670
+ counts,
1671
+ 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."
1672
+ };
1673
+ }
1674
+ function repairExitCode(report) {
1675
+ return report.status === "verified" ? 0 : report.status === "issues-remain" ? 1 : 2;
1676
+ }
1677
+ function formatRepairSummary(report) {
1678
+ return [
1679
+ "Repair verification: " + report.status + ". Baseline: " + report.baselinePath,
1680
+ ...report.findings.map((f) => " [" + f.status + "] " + f.original.type + " " + f.original.anchor.doc + ": " + f.original.message + "\n " + f.reason),
1681
+ ...report.newFindings.map((f) => " [new] " + f.type + " " + f.anchor.doc + ": " + f.message),
1682
+ ...report.diagnostics.map((d) => " [unverified] " + d),
1683
+ ...(report.currentAudit?.skippedChecks ?? []).map((s) => " [skipped] " + s.check + ": " + s.reason),
1684
+ report.scope
1685
+ ].join("\n");
1686
+ }
1687
+
1402
1688
  // src/audit/cli.ts
1403
1689
  var USAGE = `Usage: mason-audit [--dir <path>] [--json | --fix-prompt] [--checks <list>]
1404
1690
 
@@ -1413,7 +1699,10 @@ Options:
1413
1699
  --json Print the full audit report as JSON (additive-only schema)
1414
1700
  --fix-prompt When issues exist, print a work order for ANY coding agent
1415
1701
  (Claude, Codex, Gemini, ...) \u2013 pipe it to your agent CLI to
1416
- close the loop. Prints the clean summary when there are none.
1702
+ repair the findings. Includes advisories that require review.
1703
+ --prepare-repair Save the original audit under .mason/reports/repairs/ before edits
1704
+ --verify-repair <path>
1705
+ Compare against that saved baseline, using its original checks
1417
1706
  --checks <list> Comma-separated subset of checks to run (default: all):
1418
1707
  ${ALL_CHECKS.join(", ")}
1419
1708
  --help Show this help
@@ -1421,14 +1710,19 @@ Options:
1421
1710
  Exit codes:
1422
1711
  0 no issues (advisories may still be present)
1423
1712
  1 provable issues found
1424
- 2 error (no context file, not a git repository, bad arguments)`;
1713
+ 2 error (no context file, not a git repository, bad arguments)
1714
+
1715
+ With --verify-repair: 0 verified by the original checks; 1 issues remain;
1716
+ 2 incomplete (unverified findings, skipped checks, or advisories needing review).
1717
+ Preparation writes only a baseline; verification and ordinary audits are read-only.`;
1425
1718
  function parseArgs(argv) {
1426
1719
  const parsed = {
1427
1720
  dir: process.cwd(),
1428
1721
  json: false,
1429
1722
  fixPrompt: false,
1430
1723
  help: false,
1431
- checks: void 0
1724
+ checks: void 0,
1725
+ prepareRepair: false
1432
1726
  };
1433
1727
  for (let i = 0; i < argv.length; i++) {
1434
1728
  const arg = argv[i];
@@ -1436,6 +1730,12 @@ function parseArgs(argv) {
1436
1730
  parsed.json = true;
1437
1731
  } else if (arg === "--fix-prompt") {
1438
1732
  parsed.fixPrompt = true;
1733
+ } else if (arg === "--prepare-repair") {
1734
+ parsed.prepareRepair = true;
1735
+ } else if (arg === "--verify-repair") {
1736
+ const value = argv[++i];
1737
+ if (!value || value.startsWith("--")) throw new Error("--verify-repair requires a baseline path");
1738
+ parsed.baseline = value;
1439
1739
  } else if (arg === "--help" || arg === "-h") {
1440
1740
  parsed.help = true;
1441
1741
  } else if (arg === "--dir") {
@@ -1446,6 +1746,7 @@ function parseArgs(argv) {
1446
1746
  const value = argv[++i];
1447
1747
  if (!value) throw new Error("--checks requires a comma-separated list");
1448
1748
  const names = value.split(",").map((n) => n.trim()).filter(Boolean);
1749
+ if (!names.length) throw new Error("--checks requires at least one check");
1449
1750
  for (const name of names) {
1450
1751
  if (!ALL_CHECKS.includes(name)) {
1451
1752
  throw new Error(
@@ -1469,6 +1770,7 @@ function issueLine(issue) {
1469
1770
  }
1470
1771
  function formatAuditSummary(report) {
1471
1772
  const lines = [];
1773
+ const reviewCount = report.advisories.length + (report.suppressedAdvisories?.length ?? 0);
1472
1774
  for (const doc of report.docs) {
1473
1775
  const docIssues = report.issues.filter((i) => i.anchor.doc === doc.path);
1474
1776
  const committed = doc.lastCommit ? `last committed ${doc.lastCommit.date.slice(0, 10)}, ${doc.lastCommit.hash.slice(0, 7)}` : "untracked";
@@ -1496,21 +1798,25 @@ function formatAuditSummary(report) {
1496
1798
  lines.push(` [skipped] ${skip.check}: ${skip.reason}`);
1497
1799
  }
1498
1800
  }
1801
+ for (const advisory of report.suppressedAdvisories ?? []) {
1802
+ lines.push(` [suppressed; unresolved] ${advisory.type} ${advisory.anchor.doc}: ${advisory.message}`);
1803
+ }
1499
1804
  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"}.`
1805
+ 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
1806
  );
1502
1807
  return lines.join("\n");
1503
1808
  }
1504
- function formatFixPrompt(report) {
1809
+ function formatFixPrompt(report, baselinePath) {
1505
1810
  const flaggedDocs = [...new Set(report.issues.map((i) => i.anchor.doc))];
1506
1811
  const lines = [];
1507
1812
  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."
1813
+ "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
1814
  );
1510
1815
  lines.push("");
1511
1816
  lines.push("RULES:");
1817
+ 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
1818
  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.`
1819
+ `- 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
1820
  );
1515
1821
  lines.push(
1516
1822
  "- Keep diffs minimal: change the smallest span that makes each claim true."
@@ -1518,6 +1824,7 @@ function formatFixPrompt(report) {
1518
1824
  lines.push(
1519
1825
  "- Never invent content. Every replacement must be grounded in the evidence below or in files you read from this repository."
1520
1826
  );
1827
+ lines.push("- Inspect likely findings before editing: heuristic evidence may describe an intentional omission or an example.");
1521
1828
  lines.push(
1522
1829
  "- 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
1830
  );
@@ -1531,20 +1838,27 @@ function formatFixPrompt(report) {
1531
1838
  "- 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
1839
  );
1533
1840
  lines.push(
1534
- "- Do NOT touch anything listed under ADVISORIES \u2013 list them in your summary for human review instead."
1841
+ "- 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
1842
  );
1536
1843
  lines.push("");
1537
- lines.push("AUDIT REPORT (deterministic, computed against git HEAD):");
1844
+ lines.push("AUDIT REPORT (current context files and repository evidence, including local edits):");
1538
1845
  lines.push(
1539
1846
  JSON.stringify(
1540
- { issues: report.issues, advisories: report.advisories },
1847
+ {
1848
+ root: report.root,
1849
+ checks: report.checksRun,
1850
+ issues: report.issues,
1851
+ advisories: report.advisories,
1852
+ suppressedAdvisories: report.suppressedAdvisories,
1853
+ skippedChecks: report.skippedChecks
1854
+ },
1541
1855
  null,
1542
1856
  2
1543
1857
  )
1544
1858
  );
1545
1859
  lines.push("");
1546
1860
  lines.push(
1547
- "Finish by summarizing each edit and citing the evidence item it resolves."
1861
+ "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
1862
  );
1549
1863
  return lines.join("\n");
1550
1864
  }
@@ -1560,6 +1874,9 @@ async function runAuditCli(argv, io = {
1560
1874
  if (args.json && args.fixPrompt) {
1561
1875
  throw new Error("--json and --fix-prompt are mutually exclusive");
1562
1876
  }
1877
+ if (args.baseline && (args.prepareRepair || args.checks || args.fixPrompt)) {
1878
+ throw new Error("--verify-repair cannot be combined with --prepare-repair, --checks, or --fix-prompt; verification uses the original scope");
1879
+ }
1563
1880
  } catch (error) {
1564
1881
  io.err(error instanceof Error ? error.message : String(error));
1565
1882
  io.err(USAGE);
@@ -1569,7 +1886,23 @@ async function runAuditCli(argv, io = {
1569
1886
  io.out(USAGE);
1570
1887
  return 0;
1571
1888
  }
1572
- const rootDir = path16.resolve(args.dir);
1889
+ const rootDir = path17.resolve(args.dir);
1890
+ if (args.baseline || args.prepareRepair) {
1891
+ try {
1892
+ if (args.baseline) {
1893
+ const verification = await verifyRepair(rootDir, args.baseline);
1894
+ io.out(args.json ? JSON.stringify(verification, null, 2) : formatRepairSummary(verification));
1895
+ return repairExitCode(verification);
1896
+ }
1897
+ const prepared = await prepareRepair(rootDir, args.checks);
1898
+ 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}
1899
+ ${formatAuditSummary(prepared.report)}`);
1900
+ return prepared.report.clean ? 0 : 1;
1901
+ } catch (error) {
1902
+ io.err(error instanceof Error ? error.message : String(error));
1903
+ return 2;
1904
+ }
1905
+ }
1573
1906
  const report = await computeAudit(rootDir, { checks: args.checks });
1574
1907
  if (!report) {
1575
1908
  io.err(
@@ -1585,7 +1918,7 @@ async function runAuditCli(argv, io = {
1585
1918
  }
1586
1919
  if (args.fixPrompt) {
1587
1920
  io.out(
1588
- report.clean ? formatAuditSummary(report) : formatFixPrompt(report)
1921
+ report.clean && !report.advisories.length && !report.suppressedAdvisories?.length ? formatAuditSummary(report) : formatFixPrompt(report)
1589
1922
  );
1590
1923
  return report.clean ? 0 : 1;
1591
1924
  }