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/CHANGELOG.md +9 -0
- package/README.md +8 -4
- package/dist/mason-audit.js +36 -13
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-drift.js +33 -8
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +74 -22
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +98 -65
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +70 -18
- package/dist/mason-review.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
1157
|
-
if (record.
|
|
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
|
-
|
|
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).
|
|
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).
|
|
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)
|
|
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
|
-
|
|
1533
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
...
|
|
3037
|
-
...(await access.list()).filter((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
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
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"
|
|
4951
|
-
const
|
|
4952
|
-
|
|
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
|
-
...
|
|
4993
|
+
...knowledge,
|
|
4994
|
+
...pendingProposal ? { pendingProposal: { ...pendingProposal, touchedFiles: proposalTouched } } : {},
|
|
4955
4995
|
id: record.id,
|
|
4956
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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,
|
|
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"),
|