mason-context 0.10.0 → 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 +19 -0
- package/README.md +32 -5
- package/dist/mason-audit.js +392 -36
- 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 +2631 -2220
- 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-hook.js
CHANGED
|
@@ -153,6 +153,19 @@ import path6 from "path";
|
|
|
153
153
|
|
|
154
154
|
// src/decisions/provenance.ts
|
|
155
155
|
import { z as z2 } from "zod";
|
|
156
|
+
|
|
157
|
+
// src/context/trust.ts
|
|
158
|
+
function assessTrust(entry, freshness) {
|
|
159
|
+
const verification = entry.verificationFailed ? "failed" : entry.verifiedAt ? "passed" : "unverified";
|
|
160
|
+
const reasons = [];
|
|
161
|
+
if (freshness === "unknown") reasons.push("Anchors, history, or working-tree evidence are unavailable; verify before relying on this entry.");
|
|
162
|
+
if (freshness === "changed") reasons.push("Anchored files changed; verify against current code before relying on this entry.");
|
|
163
|
+
if (verification === "failed") reasons.push(`Verification failed: ${entry.verificationNote ?? "re-map this entry before relying on it"}`);
|
|
164
|
+
if (verification === "unverified") reasons.push("No correctness verification has been recorded.");
|
|
165
|
+
return { freshness, verification, verifiedAt: entry.verifiedAt, verifiedHash: entry.verifiedHash, reasons };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/decisions/provenance.ts
|
|
156
169
|
var text = (max) => z2.string().trim().min(1).max(max);
|
|
157
170
|
var decisionSourceSchema = z2.object({
|
|
158
171
|
kind: z2.enum(["pull_request", "issue", "incident", "discussion", "document", "other"]),
|
|
@@ -257,6 +270,26 @@ function decisionContent(record) {
|
|
|
257
270
|
function decisionApproval(record) {
|
|
258
271
|
return record.version === 1 ? "unreviewed" : record.approval;
|
|
259
272
|
}
|
|
273
|
+
function effectiveDecision(record) {
|
|
274
|
+
if (record.version !== 2 || record.status !== "active" || record.approval !== "proposed") return record;
|
|
275
|
+
let index = record.history.length - 1;
|
|
276
|
+
while (index >= 0 && !["accepted", "reaffirmed"].includes(record.history[index].kind)) index--;
|
|
277
|
+
if (index < 0) return record;
|
|
278
|
+
const event = record.history[index];
|
|
279
|
+
return {
|
|
280
|
+
...record,
|
|
281
|
+
...event.content,
|
|
282
|
+
owner: event.content.owner,
|
|
283
|
+
approval: "accepted",
|
|
284
|
+
revision: event.revision,
|
|
285
|
+
refreshedHash: event.refreshedHash,
|
|
286
|
+
updatedAt: event.at,
|
|
287
|
+
history: record.history.slice(0, index + 1)
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function decisionAnchors(record) {
|
|
291
|
+
return [.../* @__PURE__ */ new Set([...effectiveDecision(record).files, ...record.files])];
|
|
292
|
+
}
|
|
260
293
|
function decisionProvenance(record, freshness = "unknown") {
|
|
261
294
|
const approval = decisionApproval(record);
|
|
262
295
|
const review = record.version === 2 ? [...record.history].reverse().find((e) => ["accepted", "reaffirmed"].includes(e.kind) && e.revision === record.revision) : void 0;
|
|
@@ -270,6 +303,21 @@ function decisionProvenance(record, freshness = "unknown") {
|
|
|
270
303
|
lastReview: review ? { reviewer: review.actor, at: review.at, note: review.note, gitHash: review.refreshedHash } : null
|
|
271
304
|
};
|
|
272
305
|
}
|
|
306
|
+
function decisionTrust(record, freshness) {
|
|
307
|
+
const review = decisionProvenance(record, freshness).lastReview;
|
|
308
|
+
return assessTrust(review ? { verifiedAt: review.at, verifiedHash: review.gitHash } : {}, freshness);
|
|
309
|
+
}
|
|
310
|
+
function revisionKnowledge(record, freshness) {
|
|
311
|
+
return { ...decisionContent(record), ...decisionProvenance(record, freshness), trust: decisionTrust(record, freshness) };
|
|
312
|
+
}
|
|
313
|
+
function decisionKnowledge(record, freshness = "unknown", proposalFreshness = "unknown") {
|
|
314
|
+
const effective = effectiveDecision(record);
|
|
315
|
+
return {
|
|
316
|
+
...revisionKnowledge(effective, freshness),
|
|
317
|
+
...effective !== record ? { pendingProposal: revisionKnowledge(record, proposalFreshness) } : {}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
var 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.";
|
|
273
321
|
|
|
274
322
|
// src/decisions/decisions.ts
|
|
275
323
|
async function loadDecisionStore(rootDir) {
|
|
@@ -354,12 +402,8 @@ async function computeDecisionDrift(rootDir, decisions) {
|
|
|
354
402
|
const report = { historyAvailable: true, totalDecisions: store.records.length, staleDecisions: {}, freshness: {}, diagnostics: store.diagnostics };
|
|
355
403
|
const [head, workingTree] = await Promise.all([getCurrentGitHash(resolvedRoot), getWorkingTree(resolvedRoot)]);
|
|
356
404
|
const changesByHash = /* @__PURE__ */ new Map();
|
|
357
|
-
|
|
358
|
-
if (record.
|
|
359
|
-
if (record.files.length === 0) {
|
|
360
|
-
report.freshness[record.id] = "unknown";
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
405
|
+
const inspect = async (record) => {
|
|
406
|
+
if (record.files.length === 0) return { freshness: "unknown", changedFiles: [] };
|
|
363
407
|
let touched = changesByHash.get(record.refreshedHash);
|
|
364
408
|
if (touched === void 0) {
|
|
365
409
|
const changes = record.refreshedHash === head && head !== "unknown" ? [] : await getChangesWithStatus(resolvedRoot, record.refreshedHash);
|
|
@@ -368,9 +412,16 @@ async function computeDecisionDrift(rootDir, decisions) {
|
|
|
368
412
|
}
|
|
369
413
|
if (touched === null) report.historyAvailable = false;
|
|
370
414
|
const hits = touched ? matchingPaths(record.files, touched) : [];
|
|
371
|
-
if (hits.length) report.staleDecisions[record.id] = hits;
|
|
372
415
|
const localHits = matchingPaths(record.files, workingTree.changedFiles);
|
|
373
|
-
|
|
416
|
+
return { freshness: touched === null || !workingTree.available ? "unknown" : hits.length || localHits.length ? "changed" : "current", changedFiles: hits };
|
|
417
|
+
};
|
|
418
|
+
for (const record of store.records) {
|
|
419
|
+
if (record.status !== "active") continue;
|
|
420
|
+
const effective = effectiveDecision(record);
|
|
421
|
+
const state = await inspect(effective);
|
|
422
|
+
report.freshness[record.id] = state.freshness;
|
|
423
|
+
if (state.changedFiles.length) report.staleDecisions[record.id] = state.changedFiles;
|
|
424
|
+
if (effective !== record) (report.pendingProposals ??= {})[record.id] = await inspect(record);
|
|
374
425
|
}
|
|
375
426
|
return report;
|
|
376
427
|
}
|
|
@@ -399,10 +450,10 @@ async function findMasonRoot(startDir) {
|
|
|
399
450
|
return null;
|
|
400
451
|
}
|
|
401
452
|
function anchorsCover(record, relPath) {
|
|
402
|
-
return record.
|
|
453
|
+
return decisionAnchors(record).some((anchor) => anchorMatches(anchor, relPath));
|
|
403
454
|
}
|
|
404
455
|
function exactAnchor(record, relPath) {
|
|
405
|
-
return record.
|
|
456
|
+
return decisionAnchors(record).some((a) => a.replace(/\/+$/, "") === relPath);
|
|
406
457
|
}
|
|
407
458
|
function stateKey(input) {
|
|
408
459
|
const raw = `${input.session_id ?? "nosession"}${input.agent_id ? `-${input.agent_id}` : ""}`;
|
|
@@ -416,18 +467,21 @@ async function loadInjected(stateFile) {
|
|
|
416
467
|
return /* @__PURE__ */ new Set();
|
|
417
468
|
}
|
|
418
469
|
}
|
|
419
|
-
function formatContext(relPath, records,
|
|
470
|
+
function formatContext(relPath, records, drift) {
|
|
420
471
|
const lines = [];
|
|
421
472
|
lines.push(
|
|
422
|
-
`Mason:
|
|
473
|
+
`Mason: decision knowledge relevant to ${relPath} or updated since this session saw it. This replaces earlier guidance for the same decision id. ${DECISION_GUIDANCE} Retired or superseded records are no longer active. Do not modify decision records in .mason/decisions/.`
|
|
423
474
|
);
|
|
424
|
-
|
|
425
|
-
const
|
|
426
|
-
const label = record.status === "active" ? provenance.approval : record.status;
|
|
427
|
-
const stale = freshness[record.id] === "current" ? "" : freshness[record.id] === "changed" ? " [recorded against changed files \u2013 verify against current code before relying on it]" : " [freshness unknown \u2013 verify against current code before relying on it]";
|
|
475
|
+
const append = (id, knowledge, label, freshness) => {
|
|
476
|
+
const stale = freshness === "current" ? "" : freshness === "changed" ? " [recorded against changed files \u2013 verify against current code before relying on it]" : " [freshness unknown \u2013 verify against current code before relying on it]";
|
|
428
477
|
lines.push(
|
|
429
|
-
`- [${label}] [${
|
|
478
|
+
`- [${label}] [${knowledge.category}] ${knowledge.title}: ${knowledge.body} (id: ${id}; revision: ${knowledge.revision}; anchors: ${knowledge.files.join(", ")}; owner: ${knowledge.owner ?? "unknown"}; sources: ${knowledge.sources.slice(0, 2).map((s) => s.reference).join(", ") || "unrecorded"})${stale}`
|
|
430
479
|
);
|
|
480
|
+
};
|
|
481
|
+
for (const record of records) {
|
|
482
|
+
const knowledge = decisionKnowledge(record, drift.freshness?.[record.id] ?? "unknown", drift.pendingProposals?.[record.id]?.freshness ?? "unknown");
|
|
483
|
+
append(record.id, knowledge, record.status === "active" ? knowledge.approval : record.status, knowledge.trust.freshness);
|
|
484
|
+
if (knowledge.pendingProposal) append(record.id, knowledge.pendingProposal, "proposed", knowledge.pendingProposal.trust.freshness);
|
|
431
485
|
}
|
|
432
486
|
return lines.join("\n");
|
|
433
487
|
}
|
|
@@ -448,13 +502,12 @@ async function runHook(stdinText, env = {}) {
|
|
|
448
502
|
const relPath = path10.relative(root, absPath).split(path10.sep).join("/");
|
|
449
503
|
if (relPath.startsWith("..")) return null;
|
|
450
504
|
const { records } = await loadDecisionStore(root);
|
|
451
|
-
const matched = records.filter((r) => anchorsCover(r, relPath));
|
|
452
|
-
if (matched.length === 0) return null;
|
|
453
505
|
const stateDir = env.stateDir ?? os.tmpdir();
|
|
454
506
|
const stateFile = path10.join(stateDir, `mason-hook-${createHash2("sha256").update(root).digest("hex").slice(0, 12)}-${stateKey(input)}.json`);
|
|
455
507
|
const injected = await loadInjected(stateFile);
|
|
456
508
|
const recordKey = (record) => `${record.id}:${createHash2("sha256").update(JSON.stringify(record)).digest("hex")}`;
|
|
457
|
-
const
|
|
509
|
+
const previouslyInjected = (record) => [...injected].some((key) => key === record.id || key.startsWith(`${record.id}:`));
|
|
510
|
+
const fresh = records.filter((r) => !injected.has(recordKey(r)) && (previouslyInjected(r) || r.status === "active" && anchorsCover(r, relPath)));
|
|
458
511
|
if (fresh.length === 0) return null;
|
|
459
512
|
fresh.sort((a, b) => {
|
|
460
513
|
const withdrawn = Number(b.status !== "active") - Number(a.status !== "active");
|
|
@@ -465,7 +518,6 @@ async function runHook(stdinText, env = {}) {
|
|
|
465
518
|
});
|
|
466
519
|
const selected = fresh.slice(0, MAX_INJECTED_DECISIONS);
|
|
467
520
|
const drift = await computeDecisionDrift(root, selected);
|
|
468
|
-
const freshness = drift.freshness ?? {};
|
|
469
521
|
for (const record of selected) injected.add(recordKey(record));
|
|
470
522
|
try {
|
|
471
523
|
await fs5.writeFile(stateFile, JSON.stringify([...injected]), "utf-8");
|
|
@@ -474,7 +526,7 @@ async function runHook(stdinText, env = {}) {
|
|
|
474
526
|
return JSON.stringify({
|
|
475
527
|
hookSpecificOutput: {
|
|
476
528
|
hookEventName: "PostToolUse",
|
|
477
|
-
additionalContext: formatContext(relPath, selected,
|
|
529
|
+
additionalContext: formatContext(relPath, selected, drift)
|
|
478
530
|
}
|
|
479
531
|
});
|
|
480
532
|
}
|