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.
@@ -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
- for (const record of store.records) {
358
- if (record.status !== "active") continue;
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
- report.freshness[record.id] = touched === null || !workingTree.available ? "unknown" : hits.length || localHits.length ? "changed" : "current";
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.files.some((anchor) => anchorMatches(anchor, relPath));
453
+ return decisionAnchors(record).some((anchor) => anchorMatches(anchor, relPath));
403
454
  }
404
455
  function exactAnchor(record, relPath) {
405
- return record.files.some((a) => a.replace(/\/+$/, "") === relPath);
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, freshness) {
470
+ function formatContext(relPath, records, drift) {
420
471
  const lines = [];
421
472
  lines.push(
422
- `Mason: recorded knowledge anchored to ${relPath}. Accepted records are constraints subject to freshness; proposals are suggestions and legacy unreviewed records need confirmation. Retired or superseded records are no longer active. Do not modify decision records in .mason/decisions/.`
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
- for (const record of records) {
425
- const provenance = decisionProvenance(record, freshness[record.id] ?? "unknown");
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}] [${record.category}] ${record.title}: ${record.body} (anchors: ${record.files.join(", ")}; owner: ${provenance.owner ?? "unknown"}; sources: ${provenance.sources.slice(0, 2).map((s) => s.reference).join(", ") || "unrecorded"})${stale}`
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 fresh = matched.filter((r) => !injected.has(recordKey(r)) && (r.status === "active" || [...injected].some((key) => key === r.id || key.startsWith(`${r.id}:`))));
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, freshness)
529
+ additionalContext: formatContext(relPath, selected, drift)
478
530
  }
479
531
  });
480
532
  }