mason-context 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mason-mcp.js CHANGED
@@ -79,6 +79,26 @@ function decisionContent(record) {
79
79
  function decisionApproval(record) {
80
80
  return record.version === 1 ? "unreviewed" : record.approval;
81
81
  }
82
+ function effectiveDecision(record) {
83
+ if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed") return record;
84
+ let index2 = record.history.length - 1;
85
+ while (index2 >= 0 && !["accepted", "reaffirmed"].includes(record.history[index2].kind)) index2--;
86
+ if (index2 < 0) return record;
87
+ const event = record.history[index2];
88
+ return {
89
+ ...record,
90
+ ...event.content,
91
+ owner: event.content.owner,
92
+ approval: "accepted",
93
+ revision: event.revision,
94
+ refreshedHash: event.refreshedHash,
95
+ updatedAt: event.at,
96
+ history: record.history.slice(0, index2 + 1)
97
+ };
98
+ }
99
+ function decisionAnchors(record) {
100
+ return [.../* @__PURE__ */ new Set([...effectiveDecision(record).files, ...record.files])];
101
+ }
82
102
  function importLegacy(record, now) {
83
103
  if (record.version === 2) return record;
84
104
  const content = decisionContent({ title: record.title, body: record.body, category: record.category, files: record.files });
@@ -122,6 +142,22 @@ function decisionTrust(record, freshness) {
122
142
  const review = decisionProvenance(record, freshness).lastReview;
123
143
  return assessTrust(review ? { verifiedAt: review.at, verifiedHash: review.gitHash } : {}, freshness);
124
144
  }
145
+ function revisionKnowledge(record, freshness) {
146
+ return { ...decisionContent(record), ...decisionProvenance(record, freshness), trust: decisionTrust(record, freshness) };
147
+ }
148
+ function decisionKnowledge(record, freshness = "unknown", proposalFreshness = "unknown") {
149
+ const effective = effectiveDecision(record);
150
+ return {
151
+ ...revisionKnowledge(effective, freshness),
152
+ ...effective !== record ? { pendingProposal: revisionKnowledge(record, proposalFreshness) } : {}
153
+ };
154
+ }
155
+ function compactDecisionKnowledge(...args) {
156
+ const { body, pendingProposal, ...summary } = decisionKnowledge(...args);
157
+ if (!pendingProposal) return summary;
158
+ const { body: proposalBody, ...proposal } = pendingProposal;
159
+ return { ...summary, pendingProposal: proposal };
160
+ }
125
161
  var text, decisionSourceSchema, attributionSchema, contentSchema, approvalSchema, statusSchema, reviewEvidenceSchema, eventSchema, legacySchema, currentSchema, decisionSchema, DECISION_GUIDANCE;
126
162
  var init_provenance = __esm({
127
163
  "src/decisions/provenance.ts"() {
@@ -219,7 +255,7 @@ var init_provenance = __esm({
219
255
  if (!previous || !same(previous.content, decisionContent(record)) || previous.approval !== record.approval || previous.status !== record.status || previous.revision !== record.revision || previous.refreshedHash !== record.refreshedHash) invalid("Decision does not match the final history event");
220
256
  });
221
257
  decisionSchema = z.union([legacySchema, currentSchema]);
222
- DECISION_GUIDANCE = "Accepted decisions are recorded team constraints, subject to freshness checks. Proposals are suggestions; legacy unreviewed records need confirmation. Use review_decision to inspect provenance and record an authorized review; identities and sources are recorded assertions, not authenticated proof.";
258
+ DECISION_GUIDANCE = "Accepted decisions are recorded team constraints, subject to freshness checks. A pendingProposal is an unaccepted replacement; the accepted revision remains operative until explicit acceptance or retirement. Proposals are suggestions; legacy unreviewed records need confirmation. Use review_decision to inspect provenance and record an authorized review; identities and sources are recorded assertions, not authenticated proof.";
223
259
  }
224
260
  });
225
261
 
@@ -867,7 +903,7 @@ async function upsertDecision(rootDir, input) {
867
903
  warnings.push(`anchor file does not exist on disk: ${file}`);
868
904
  }
869
905
  }
870
- const hint = "Saved locally for review and commit. Proposals are not accepted constraints. Use review_decision to inspect evidence and record an authorized acceptance or reaffirmation.";
906
+ const hint = "Saved locally for review and commit. Proposals are not accepted constraints; an existing accepted revision remains operative while its replacement is proposed. Use review_decision to inspect evidence and record an authorized acceptance or reaffirmation.";
871
907
  if (input.id) {
872
908
  const original = byId.get(input.id);
873
909
  if (!original) return { status: "error", error: `no decision with id "${input.id}"` };
@@ -907,7 +943,7 @@ async function upsertDecision(rootDir, input) {
907
943
  }
908
944
  const old = input.supersedes ? byId.get(input.supersedes) : void 0;
909
945
  if (input.supersedes && !old) return { status: "error", error: `no decision with id "${input.supersedes}" to supersede` };
910
- if (old && (old.status !== "active" || decisionApproval(old) === "accepted")) {
946
+ if (old && (old.status !== "active" || decisionApproval(effectiveDecision(old)) === "accepted")) {
911
947
  return { status: "error", error: "A proposal cannot supersede an accepted or archived record. Create and review the replacement separately, then explicitly retire the old decision with review_decision." };
912
948
  }
913
949
  if (!input.force) {
@@ -1153,12 +1189,8 @@ async function computeDecisionDrift(rootDir, decisions) {
1153
1189
  const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
1154
1190
  const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
1155
1191
  const changesByHash = /* @__PURE__ */ new Map();
1156
- for (const record of store.records) {
1157
- if (record.status !== "active") continue;
1158
- if (record.files.length === 0) {
1159
- report.freshness[record.id] = "unknown";
1160
- continue;
1161
- }
1192
+ const inspect = async (record) => {
1193
+ if (record.files.length === 0) return { freshness: "unknown", changedFiles: [] };
1162
1194
  let touched = changesByHash.get(record.refreshedHash);
1163
1195
  if (touched === void 0) {
1164
1196
  const changes = record.refreshedHash === head && head !== "unknown" ? [] : await getChangesWithStatus(resolvedRoot, record.refreshedHash);
@@ -1167,9 +1199,16 @@ async function computeDecisionDrift(rootDir, decisions) {
1167
1199
  }
1168
1200
  if (touched === null) report.historyAvailable = false;
1169
1201
  const hits = touched ? matchingPaths(record.files, touched) : [];
1170
- if (hits.length) report.staleDecisions[record.id] = hits;
1171
1202
  const localHits = matchingPaths(record.files, workingTree.changedFiles);
1172
- report.freshness[record.id] = touched === null || !workingTree.available ? "unknown" : hits.length || localHits.length ? "changed" : "current";
1203
+ return { freshness: touched === null || !workingTree.available ? "unknown" : hits.length || localHits.length ? "changed" : "current", changedFiles: hits };
1204
+ };
1205
+ for (const record of store.records) {
1206
+ if (record.status !== "active") continue;
1207
+ const effective = effectiveDecision(record);
1208
+ const state = await inspect(effective);
1209
+ report.freshness[record.id] = state.freshness;
1210
+ if (state.changedFiles.length) report.staleDecisions[record.id] = state.changedFiles;
1211
+ if (effective !== record) (report.pendingProposals ??= {})[record.id] = await inspect(record);
1173
1212
  }
1174
1213
  return report;
1175
1214
  }
@@ -1180,6 +1219,7 @@ var init_drift2 = __esm({
1180
1219
  init_paths();
1181
1220
  init_snapshot();
1182
1221
  init_decisions();
1222
+ init_provenance();
1183
1223
  }
1184
1224
  });
1185
1225
 
@@ -1407,7 +1447,7 @@ async function assembleContext(rootDir, task, files) {
1407
1447
  };
1408
1448
  if (!snapshot) {
1409
1449
  const decisions2 = matchDecisions(allDecisions, taskTokens, anchorBoost, /* @__PURE__ */ new Set(), decisionDrift);
1410
- const { impact: impact2, relatedTests: relatedTests2 } = await collectImpact(resolvedRoot, [...anchorFiles, ...Object.values(decisions2).flatMap((d) => d.files)]);
1450
+ const { impact: impact2, relatedTests: relatedTests2 } = await collectImpact(resolvedRoot, [...anchorFiles, ...Object.values(decisions2).flatMap((d) => [...d.files, ...d.pendingProposal?.files ?? []])]);
1411
1451
  const invalid = mapState.status === "invalid";
1412
1452
  return {
1413
1453
  exists: false,
@@ -1420,7 +1460,7 @@ async function assembleContext(rootDir, task, files) {
1420
1460
  relatedTests: relatedTests2,
1421
1461
  diagnostics: [...mapState.diagnostics, ...store.diagnostics],
1422
1462
  freshness: { stale: null, recommendation: invalid ? "repair-map" : "no-map", staleMatches: [] },
1423
- hint: (invalid ? "The concept map is invalid; consult diagnostics and repair it before relying on map entries. " : "No concept map is present. Maps are optional; decisions and file impact work now. ") + (Object.keys(decisions2).length ? DECISION_GUIDANCE + " " + trustHint(Object.values(decisions2).map((d) => d.trust)) : "No saved decision matched. Inspect the source and use save_decision for a learned constraint or incident rationale. ") + (store.diagnostics.length ? " Some decision records are invalid; consult diagnostics before assuming all constraints were retrieved." : "")
1463
+ hint: (invalid ? "The concept map is invalid; consult diagnostics and repair it before relying on map entries. " : "No concept map is present. Maps are optional; decisions and file impact work now. ") + (Object.keys(decisions2).length ? DECISION_GUIDANCE + " " + trustHint(Object.values(decisions2).flatMap((d) => [d.trust, ...d.pendingProposal ? [d.pendingProposal.trust] : []])) : "No saved decision matched. Inspect the source and use save_decision for a learned constraint or incident rationale. ") + (store.diagnostics.length ? " Some decision records are invalid; consult diagnostics before assuming all constraints were retrieved." : "")
1424
1464
  };
1425
1465
  }
1426
1466
  const drift = await computeDrift(resolvedRoot);
@@ -1447,7 +1487,7 @@ async function assembleContext(rootDir, task, files) {
1447
1487
  );
1448
1488
  if (featureScores.length === 0 && flowScores.length === 0) {
1449
1489
  const bundle = noMatchBundle(snapshot, task, decisions);
1450
- Object.assign(bundle, await collectImpact(resolvedRoot, [...anchorFiles, ...Object.values(decisions).flatMap((d) => d.files)]));
1490
+ Object.assign(bundle, await collectImpact(resolvedRoot, [...anchorFiles, ...Object.values(decisions).flatMap((d) => [...d.files, ...d.pendingProposal?.files ?? []])]));
1451
1491
  bundle.diagnostics = store.diagnostics;
1452
1492
  bundle.freshness = drift;
1453
1493
  bundle.trust = {
@@ -1461,7 +1501,7 @@ async function assembleContext(rootDir, task, files) {
1461
1501
  bundle.hint += " " + trustHint([
1462
1502
  ...Object.values(bundle.trust.features),
1463
1503
  ...Object.values(bundle.trust.flows),
1464
- ...Object.values(decisions).map((d) => d.trust)
1504
+ ...Object.values(decisions).flatMap((d) => [d.trust, ...d.pendingProposal ? [d.pendingProposal.trust] : []])
1465
1505
  ]);
1466
1506
  if (Object.keys(decisions).length) bundle.hint += " " + DECISION_GUIDANCE;
1467
1507
  if (store.diagnostics.length) bundle.hint += " Some decision records are invalid; consult diagnostics.";
@@ -1500,7 +1540,7 @@ async function assembleContext(rootDir, task, files) {
1500
1540
  ...anchorFiles,
1501
1541
  ...featureScores.flatMap((e) => e.feat.files),
1502
1542
  ...flowScores.flatMap((e) => e.flow.chain),
1503
- ...Object.values(decisions).flatMap((d) => d.files)
1543
+ ...Object.values(decisions).flatMap((d) => [...d.files, ...d.pendingProposal?.files ?? []])
1504
1544
  ]);
1505
1545
  const relatedTests = [
1506
1546
  .../* @__PURE__ */ new Set([
@@ -1524,32 +1564,21 @@ async function assembleContext(rootDir, task, files) {
1524
1564
  recommendation: drift?.recommendation ?? "up-to-date",
1525
1565
  staleMatches
1526
1566
  },
1527
- hint: (Object.keys(decisions).length ? DECISION_GUIDANCE + " " : "") + trustHint([...Object.values(features), ...Object.values(flows), ...Object.values(decisions)].map((e) => e.trust)) + (store.diagnostics.length ? " Some decision records are invalid; consult diagnostics before assuming all constraints were retrieved." : "")
1567
+ hint: (Object.keys(decisions).length ? DECISION_GUIDANCE + " " : "") + trustHint([...Object.values(features).map((e) => e.trust), ...Object.values(flows).map((e) => e.trust), ...Object.values(decisions).flatMap((d) => [d.trust, ...d.pendingProposal ? [d.pendingProposal.trust] : []])]) + (store.diagnostics.length ? " Some decision records are invalid; consult diagnostics before assuming all constraints were retrieved." : "")
1528
1568
  };
1529
1569
  }
1530
1570
  function matchDecisions(allDecisions, taskTokens, anchorBoost, matchedEntryFiles, decisionDrift) {
1531
1571
  const scored = allDecisions.filter((d) => d.status === "active").map((d) => {
1532
- let score = scoreEntry(taskTokens, {
1533
- name: d.title,
1534
- description: d.body,
1535
- files: d.files
1536
- }) + anchorBoost(d.files);
1537
- if (d.files.some((f) => [...matchedEntryFiles].some((file) => anchorMatches(f, file)))) {
1538
- score += DECISION_FEATURE_OVERLAP_BOOST;
1539
- }
1572
+ const scoreRevision = (revision) => scoreEntry(taskTokens, { name: revision.title, description: revision.body, files: revision.files }) + anchorBoost(revision.files) + (revision.files.some((f) => [...matchedEntryFiles].some((file) => anchorMatches(f, file))) ? DECISION_FEATURE_OVERLAP_BOOST : 0);
1573
+ const score = Math.max(scoreRevision(effectiveDecision(d)), scoreRevision(d));
1540
1574
  return { d, score };
1541
1575
  }).filter((e) => e.score > 0).sort((a, b) => b.score - a.score).slice(0, MAX_DECISIONS);
1542
1576
  const result = {};
1543
1577
  for (const { d, score } of scored) {
1544
1578
  result[d.id] = {
1545
- title: d.title,
1546
- body: d.body,
1547
- category: d.category,
1548
- files: d.files,
1579
+ ...decisionKnowledge(d, decisionDrift.freshness?.[d.id] ?? "unknown", decisionDrift.pendingProposals?.[d.id]?.freshness ?? "unknown"),
1549
1580
  score,
1550
- stale: decisionDrift.freshness?.[d.id] !== "current",
1551
- ...decisionProvenance(d, decisionDrift.freshness?.[d.id] ?? "unknown"),
1552
- trust: decisionTrust(d, decisionDrift.freshness?.[d.id] ?? "unknown")
1581
+ stale: decisionDrift.freshness?.[d.id] !== "current"
1553
1582
  };
1554
1583
  }
1555
1584
  return result;
@@ -3017,24 +3046,26 @@ async function reviewState(root, record) {
3017
3046
  getWorkingTree(root),
3018
3047
  getChangesWithStatus(root, record.refreshedHash)
3019
3048
  ]);
3020
- const committed = (changes ?? []).filter((c) => [c.path, ...c.previousPath ? [c.previousPath] : []].some((file) => record.files.some((anchor) => anchorMatches(anchor, file))));
3049
+ const anchors = decisionAnchors(record);
3050
+ const committed = (changes ?? []).filter((c) => [c.path, ...c.previousPath ? [c.previousPath] : []].some((file) => anchors.some((anchor) => anchorMatches(anchor, file))));
3021
3051
  const evidence = {
3022
3052
  baseHash: record.refreshedHash,
3023
3053
  headHash,
3024
3054
  historyAvailable: changes !== null,
3025
3055
  changedFiles: touchedPaths(committed),
3026
- localChanges: matchingPaths(record.files, workingTree.changedFiles)
3056
+ localChanges: matchingPaths(anchors, workingTree.changedFiles)
3027
3057
  };
3028
3058
  const reviewToken = createHash2("sha256").update(JSON.stringify({ record, evidence, workingTreeAvailable: workingTree.available })).digest("hex");
3029
3059
  return { reviewToken, evidence, committed, workingTreeAvailable: workingTree.available };
3030
3060
  }
3031
3061
  async function previews(root, record, state) {
3032
3062
  const access = await createFileAccess(root);
3063
+ const anchors = decisionAnchors(record);
3033
3064
  const candidates = [.../* @__PURE__ */ new Set([
3034
3065
  ...state.evidence.changedFiles,
3035
3066
  ...state.evidence.localChanges,
3036
- ...record.files,
3037
- ...(await access.list()).filter((file) => record.files.some((anchor) => anchorMatches(anchor, file)))
3067
+ ...anchors,
3068
+ ...(await access.list()).filter((file) => anchors.some((anchor) => anchorMatches(anchor, file)))
3038
3069
  ])];
3039
3070
  const files = [];
3040
3071
  const omittedFiles = [];
@@ -3077,14 +3108,16 @@ async function reviewDecision(root, input) {
3077
3108
  const { record, diagnostics } = await read();
3078
3109
  if (!record) return { status: "error", error: `No readable decision with id "${request.id}"`, diagnostics };
3079
3110
  const state = await reviewState(root, record);
3111
+ const operativeDecision = effectiveDecision(record);
3080
3112
  return {
3081
3113
  status: "prepared",
3082
3114
  record,
3115
+ ...operativeDecision !== record ? { operativeDecision } : {},
3083
3116
  provenance: decisionProvenance(record),
3084
3117
  ...state,
3085
3118
  diagnostics,
3086
3119
  previews: await previews(root, record, state),
3087
- hint: "Inspect the content, sources, history, and code changes. Only record acceptance or reaffirmation when the user or cited team review has authorized it. Supply that reviewer's identity, a reason, and this reviewToken. Do not invent identities or infer agreement from unchanged code. Acceptance and reaffirmation require committed anchor changes; retirement is available independently. Missing old history remains visible in the event even if a reviewer establishes a new baseline at HEAD. Saved reviews are local assertions for normal PR review, not authenticated approvals."
3120
+ hint: "Inspect the proposed content, operativeDecision, sources, history, and code changes. A pending proposal leaves the prior accepted revision operative; accepting it replaces that revision, and retirement withdraws the entire decision including its proposal. Evidence covers both revisions' anchors. Only record acceptance or reaffirmation when the user or cited team review has authorized it. Supply that reviewer's identity, a reason, and this reviewToken. Do not invent identities or infer agreement from unchanged code. Acceptance and reaffirmation require committed anchor changes; retirement is available independently. Missing old history remains visible in the event even if a reviewer establishes a new baseline at HEAD. Saved reviews are local assertions for normal PR review, not authenticated approvals."
3088
3121
  };
3089
3122
  }
3090
3123
  if (!request.reviewer || !request.note || !request.reviewToken) return { status: "error", error: "Prepare the review first, then provide reviewToken, reviewer, and note." };
@@ -3300,7 +3333,7 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
3300
3333
  - Task, bug, or change request \u2192 \`get_context\` with the task text and known files: matching decisions, related tests, impact, and any available map entries.
3301
3334
  - Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
3302
3335
  - Learned something the code cannot explain (a failed approach, an incident's cause, a workaround's reason, a review-settled convention) \u2192 \`save_decision\` with rationale, anchors, and any known owner, sources, and recorder. It creates a proposal immediately without setup or a map. Never invent attribution or record code-derivable facts, session trivia, or secrets.
3303
- - 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.
3336
+ - 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.
3304
3337
  - 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.
3305
3338
  - 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.
3306
3339
  - \`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.
@@ -3323,7 +3356,7 @@ var QUICKSTART_PLAYBOOK = `Start with the audit and review results included in t
3323
3356
 
3324
3357
  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.
3325
3358
  2. Address findings within the user's requested scope. Inspect relevant source and tests. Do not invent a decision just to populate the store.
3326
- 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. 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)\`.
3359
+ 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)\`.
3327
3360
 
3328
3361
  ${ASSISTANT_SETUP}
3329
3362
 
@@ -4255,11 +4288,14 @@ async function checkDecisionAnchors(ctx) {
4255
4288
  reason: "some decision base commits are unreachable (shallow clone?)"
4256
4289
  });
4257
4290
  }
4258
- const byId = new Map(records.map((r) => [r.id, r]));
4259
- for (const [id, changedFiles] of Object.entries(drift.staleDecisions)) {
4260
- const record = byId.get(id);
4261
- if (!record) continue;
4262
- const provenance = decisionProvenance(record, drift.freshness?.[id] ?? "unknown");
4291
+ const changed = records.flatMap((record) => [
4292
+ { record: effectiveDecision(record), changedFiles: drift.staleDecisions[record.id] ?? [], freshness: drift.freshness?.[record.id] ?? "unknown" },
4293
+ { record, changedFiles: drift.pendingProposals?.[record.id]?.changedFiles ?? [], freshness: drift.pendingProposals?.[record.id]?.freshness ?? "unknown" }
4294
+ ]);
4295
+ for (const { record, changedFiles, freshness } of changed) {
4296
+ if (!changedFiles.length) continue;
4297
+ const id = record.id;
4298
+ const provenance = decisionProvenance(record, freshness);
4263
4299
  result.advisories.push({
4264
4300
  type: "decision-anchor-drift",
4265
4301
  message: `decision "${record.title}" (${provenance.approval}) has anchor files that changed since its evidence baseline \u2013 needs human review`,
@@ -4685,7 +4721,7 @@ function linkFinding(finding, changed, pairs, decisions, freshness) {
4685
4721
  if (changed.has(pair.source) && !relatedChangedFiles.some((f) => f.file === pair.source)) relatedChangedFiles.push({ file: pair.source, relationship: "paired-test", confidence: pair.confidence });
4686
4722
  }
4687
4723
  const allFiles = [.../* @__PURE__ */ new Set([...located, ...paired.map((pair) => pair.source)])];
4688
- const acceptedDecisions = decisions.filter((d) => d.status === "active" && decisionApproval(d) === "accepted").map((d) => ({ d, viaFiles: matchingPaths(d.files, allFiles) })).filter((match) => match.viaFiles.length).map(({ d, viaFiles }) => {
4724
+ const acceptedDecisions = decisions.map(effectiveDecision).filter((d) => d.status === "active" && decisionApproval(d) === "accepted").map((d) => ({ d, viaFiles: matchingPaths(d.files, allFiles) })).filter((match) => match.viaFiles.length).map(({ d, viaFiles }) => {
4689
4725
  const state = freshness[d.id] ?? "unknown", provenance = decisionProvenance(d, state);
4690
4726
  return { id: d.id, title: d.title, owner: provenance.owner, freshness: state, reviewRequired: provenance.reviewRequired, viaFiles };
4691
4727
  });
@@ -4947,16 +4983,17 @@ async function computeReview(rootDir, base, options = {}) {
4947
4983
  }
4948
4984
  const decisions = store.records;
4949
4985
  for (const record of decisions) {
4950
- if (record.status !== "active" || record.files.length === 0) continue;
4951
- const touched = anchorsTouched(record, changedFiles);
4952
- if (touched.length > 0) {
4986
+ if (record.status !== "active") continue;
4987
+ const effective = effectiveDecision(record);
4988
+ const touched = anchorsTouched(effective, changedFiles);
4989
+ const proposalTouched = effective !== record ? anchorsTouched(record, changedFiles) : [];
4990
+ if (touched.length > 0 || proposalTouched.length > 0) {
4991
+ const { pendingProposal, ...knowledge } = decisionKnowledge(record, decisionDrift.freshness?.[record.id] ?? "unknown", decisionDrift.pendingProposals?.[record.id]?.freshness ?? "unknown");
4953
4992
  report.touchedDecisions.push({
4954
- ...decisionProvenance(record, decisionDrift.freshness?.[record.id] ?? "unknown"),
4993
+ ...knowledge,
4994
+ ...pendingProposal ? { pendingProposal: { ...pendingProposal, touchedFiles: proposalTouched } } : {},
4955
4995
  id: record.id,
4956
- title: record.title,
4957
- body: record.body,
4958
- category: record.category,
4959
- anchors: record.files,
4996
+ anchors: effective.files,
4960
4997
  freshness: decisionDrift.freshness?.[record.id] ?? "unknown",
4961
4998
  touchedFiles: touched
4962
4999
  });
@@ -5028,7 +5065,8 @@ async function inspectOnboarding(root, base, evidence) {
5028
5065
  map: { status: map.status },
5029
5066
  decisions: {
5030
5067
  active: decisions.records.filter((record) => record.status === "active").length,
5031
- ...Object.fromEntries(["accepted", "proposed", "unreviewed"].map((approval) => [approval, decisions.records.filter((record) => record.status === "active" && decisionApproval(record) === approval).length]))
5068
+ ...Object.fromEntries(["accepted", "proposed", "unreviewed"].map((approval) => [approval, decisions.records.filter((record) => record.status === "active" && decisionApproval(effectiveDecision(record)) === approval).length])),
5069
+ pendingProposals: decisions.records.filter((record) => effectiveDecision(record) !== record).length
5032
5070
  },
5033
5071
  diagnostics: [...map.diagnostics, ...decisions.diagnostics]
5034
5072
  };
@@ -5292,7 +5330,7 @@ async function getSnapshot(dir) {
5292
5330
  const trust = {
5293
5331
  features: Object.fromEntries(Object.entries(snapshot.features).map(([name, entry]) => [name, assessTrust(entry, drift?.featureFreshness?.[name] ?? "unknown")])),
5294
5332
  flows: Object.fromEntries(Object.entries(snapshot.flows).map(([name, entry]) => [name, assessTrust(entry, drift?.flowFreshness?.[name] ?? "unknown")])),
5295
- decisions: Object.fromEntries(decisionRecords.filter((d) => d.status === "active").map((d) => [d.id, decisionTrust(d, decisionDrift.freshness?.[d.id] ?? "unknown")]))
5333
+ decisions: Object.fromEntries(decisionRecords.filter((d) => d.status === "active").map((d) => [d.id, decisionTrust(effectiveDecision(d), decisionDrift.freshness?.[d.id] ?? "unknown")]))
5296
5334
  };
5297
5335
  output.trust = trust;
5298
5336
  output.workingTree = drift?.workingTree;
@@ -5301,12 +5339,7 @@ async function getSnapshot(dir) {
5301
5339
  const compactDecisions = {};
5302
5340
  for (const d of decisionRecords) {
5303
5341
  if (d.status !== "active") continue;
5304
- compactDecisions[d.id] = {
5305
- ...decisionProvenance(d, decisionDrift.freshness?.[d.id] ?? "unknown"),
5306
- title: d.title,
5307
- category: d.category,
5308
- files: d.files
5309
- };
5342
+ compactDecisions[d.id] = compactDecisionKnowledge(d, decisionDrift.freshness?.[d.id] ?? "unknown", decisionDrift.pendingProposals?.[d.id]?.freshness ?? "unknown");
5310
5343
  }
5311
5344
  output.decisions = compactDecisions;
5312
5345
  output.decisionsHint = DECISION_GUIDANCE + " Full bodies via get_context; full history via review_decision.";
@@ -5930,7 +5963,7 @@ function createMcpServer() {
5930
5963
  const server = new McpServer(
5931
5964
  {
5932
5965
  name: "mason",
5933
- version: "0.10.0"
5966
+ version: "0.10.1"
5934
5967
  },
5935
5968
  {
5936
5969
  instructions: 'Mason retrieves recorded decisions, file impact, and optional feature/flow maps. Use get_context with the task and known files, and get_impact before editing. Save learned rationale and constraints with save_decision; review and commit the local records through the project workflow. These tools work without initialization or a concept map. Consult trust and diagnostics: changed or unknown freshness needs source inspection, failed verification needs correction, and proposals are suggestions and legacy records are unreviewed. Accepted decisions are recorded team constraints, still subject to freshness checks. Use review_decision to prepare code evidence and record only authorized acceptance, reaffirmation, or retirement. Never invent a reviewer or treat these recorded identities as authenticated approval. mason_init returns documentation audit and committed-diff review findings with a quickstart guide. Use mode: "map" only when a full architecture map is requested. A missing map is not a setup failure; use decisions and source evidence. get_snapshot provides architecture navigation when a map is available. The mason-audit and mason-review CLIs also work without setup.'
@@ -6150,7 +6183,7 @@ function createMcpServer() {
6150
6183
  );
6151
6184
  server.tool(
6152
6185
  "save_decision",
6153
- "Capture or revise a decision proposal with rationale, anchors, optional owner, sources, and a known actor. No setup or map required. Writes a local record and preserves content history. Changes reset acceptance to proposed; unchanged content does not re-verify or refresh it. Use review_decision for authorized acceptance or reaffirmation. A proposal cannot supersede an accepted record; review its replacement and retire the original separately.",
6186
+ "Capture or revise a decision proposal with rationale, anchors, optional owner, sources, and a known actor. No setup or map required. Writes a local record and preserves content history. Changes create a pending proposal while the last accepted revision remains operative; unchanged content does not re-verify or refresh it. Use review_decision for authorized acceptance or reaffirmation. A proposal cannot supersede a record with an operative accepted revision; review its replacement and retire the original separately.",
6154
6187
  {
6155
6188
  dir: z9.string().describe("Absolute path to the project root directory"),
6156
6189
  title: z9.string().max(80).describe("Short, specific headline \u2014 becomes the stable record id"),
@@ -6158,7 +6191,7 @@ function createMcpServer() {
6158
6191
  category: z9.enum(["decision", "gotcha", "deprecation", "convention"]),
6159
6192
  files: z9.array(z9.string()).optional().describe("Repo-relative files or directory prefixes this applies to. Matching is shared by retrieval, hooks, review, and drift checking; changes flag the decision for re-verification."),
6160
6193
  id: z9.string().optional().describe("Existing id to revise. Changed content becomes a proposal; unchanged content leaves the review and freshness untouched."),
6161
- supersedes: z9.string().optional().describe("Id of an unreviewed record or proposal to replace. Accepted decisions require separate review and retirement."),
6194
+ supersedes: z9.string().optional().describe("Id of an unreviewed record or proposal with no accepted revision to replace. Operative accepted decisions require separate review and retirement."),
6162
6195
  owner: attributionSchema.shape.owner.describe("Responsible person or team, when known. Null clears it. Required for acceptance."),
6163
6196
  sources: attributionSchema.shape.sources.describe("Known PR, issue, incident, discussion, or document references. Omit to preserve; [] clears. At least one is required for acceptance."),
6164
6197
  actor: attributionSchema.shape.actor.describe("Known person or agent recording this revision. Omit if unknown; do not infer from Git identity."),
@@ -6182,7 +6215,7 @@ function createMcpServer() {
6182
6215
  );
6183
6216
  server.tool(
6184
6217
  "review_decision",
6185
- "Prepare a decision review: returns the full record and history, provenance, committed changes, local edits, bounded source/diff previews, and a reviewToken. Then record accept, reaffirm, or retire with that token, the authorized reviewer, and a reason. Acceptance requires owner, source, readable Git HEAD, and committed anchor changes. Changed records or code invalidate the token. Identities and approvals are recorded assertions for normal PR review, not authenticated proof.",
6218
+ "Prepare a decision review: returns the full record and history, any operative accepted revision, provenance, changes and previews for both sets of anchors, and a reviewToken. Then record accept, reaffirm, or retire with that token, the authorized reviewer, and a reason. Acceptance replaces the operative revision; retirement withdraws the entire record including its proposal. Acceptance requires owner, source, readable Git HEAD, and committed anchor changes. Changed records or code invalidate the token. Identities and approvals are recorded assertions for normal PR review, not authenticated proof.",
6186
6219
  {
6187
6220
  dir: z9.string().describe("Absolute path to the project root directory"),
6188
6221
  id: z9.string().regex(/^[a-zA-Z0-9_-]+$/).describe("Decision id from get_context or save_decision"),