mason-context 0.21.0 → 0.23.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.
@@ -43,7 +43,7 @@ function isStringArray(value) {
43
43
 
44
44
  // src/hook/hook.ts
45
45
  import fs7 from "fs/promises";
46
- import path11 from "path";
46
+ import path9 from "path";
47
47
  import os2 from "os";
48
48
 
49
49
  // src/audit/inspection.ts
@@ -421,6 +421,7 @@ async function storePath(root, relative, createParents = false) {
421
421
  }
422
422
  return current;
423
423
  }
424
+ var STORE_MAX_BYTES = 10 * 1024 * 1024;
424
425
 
425
426
  // src/decisions/anchors.ts
426
427
  var repoPath = z.string().refine((p) => normalizeRepoPath(p) === p);
@@ -443,7 +444,12 @@ async function captureAnchorScopes(root, scopes) {
443
444
  );
444
445
  const inventory = access.inventory();
445
446
  if (!inventory) throw new Error("Git anchor inventory unavailable");
446
- const ignored = await inspectionGit(
447
+ const visible = indexMatchingPaths(
448
+ union.map((anchor) => [anchor]),
449
+ inventory
450
+ );
451
+ const hidden = new Set(union.filter((_, index) => !visible[index].length));
452
+ const ignored = hidden.size ? (await inspectionGit(
447
453
  [
448
454
  "ls-files",
449
455
  "-z",
@@ -451,13 +457,11 @@ async function captureAnchorScopes(root, scopes) {
451
457
  "--ignored",
452
458
  "--exclude-standard",
453
459
  "--",
454
- ...union.map((anchor) => `:(literal)${anchor}`)
460
+ ...[...hidden].map((anchor) => `:(literal)${anchor}`)
455
461
  ],
456
462
  { cwd: root, maxBuffer: 8 * 1024 * 1024 }
457
- );
458
- const files = [
459
- .../* @__PURE__ */ new Set([...inventory, ...ignored.stdout.split("\0").filter(Boolean)])
460
- ].sort();
463
+ )).stdout.split("\0").filter(Boolean) : [];
464
+ const files = [.../* @__PURE__ */ new Set([...inventory, ...ignored])].sort();
461
465
  const patterns = await Promise.all(
462
466
  union.map(async (anchor) => {
463
467
  const literal = fg3.escapePath(anchor);
@@ -473,7 +477,7 @@ async function captureAnchorScopes(root, scopes) {
473
477
  const filesByScope = new Map(
474
478
  scopes.map((scope, index) => [JSON.stringify([...new Set(scope)].sort()), indexed[index]])
475
479
  );
476
- return { filesByScope, eligible };
480
+ return { filesByScope, eligible, hidden, ignored: new Set(ignored) };
477
481
  };
478
482
  return Promise.all(
479
483
  scopes.map((scope) => {
@@ -485,8 +489,10 @@ async function captureAnchorScopes(root, scopes) {
485
489
  const capture = { anchors, files: {}, complete: true };
486
490
  if (!anchors.length) return capture;
487
491
  try {
488
- const { eligible, filesByScope } = await (context ??= prepareContext());
489
- const files = filesByScope.get(JSON.stringify(anchors));
492
+ const { eligible, filesByScope, hidden, ignored } = await (context ??= prepareContext());
493
+ const files = filesByScope.get(JSON.stringify(anchors)).filter(
494
+ (file) => !ignored.has(file) || anchors.some((anchor) => hidden.has(anchor) && anchorMatches(anchor, file))
495
+ );
490
496
  if (files.length > 2e3) return { ...capture, complete: false };
491
497
  for (const file of files) {
492
498
  const evidence = await inspectionRead(
@@ -543,12 +549,11 @@ function changedCaptureFiles(before, after) {
543
549
 
544
550
  // src/decisions/decisions.ts
545
551
  import fs5 from "fs/promises";
546
- import path8 from "path";
552
+ import path6 from "path";
547
553
  import { createHash as createHash3 } from "crypto";
548
554
  import { z as z5 } from "zod";
549
555
 
550
556
  // src/snapshot/snapshot.ts
551
- import path6 from "path";
552
557
  import { z as z3 } from "zod";
553
558
 
554
559
  // src/utils/store-lock.ts
@@ -557,9 +562,6 @@ import fs4 from "fs/promises";
557
562
  import os from "os";
558
563
  var lockOwnerSchema = z2.object({ host: z2.string(), pid: z2.number().int().positive() });
559
564
 
560
- // src/test-map.ts
561
- import path5 from "path";
562
-
563
565
  // src/snapshot/snapshot.ts
564
566
  var repoPath2 = z3.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
565
567
  var verificationFields = {
@@ -598,7 +600,7 @@ async function getCurrentGitHash(rootDir) {
598
600
  }
599
601
 
600
602
  // src/context/lexical.ts
601
- import path7 from "path";
603
+ import path5 from "path";
602
604
 
603
605
  // src/decisions/provenance.ts
604
606
  import { z as z4 } from "zod";
@@ -708,6 +710,7 @@ var currentSchema = legacySchema.extend({
708
710
  inspector: text(200),
709
711
  note: text(1500),
710
712
  outcome: z4.literal("no_contradiction_found"),
713
+ scope: z4.literal("operative").optional(),
711
714
  decisionDigest: z4.string().regex(/^[a-f0-9]{64}$/),
712
715
  headHash: z4.string(),
713
716
  capture: anchorCaptureSchema
@@ -821,8 +824,19 @@ function decisionTrust(record, freshness) {
821
824
  freshness
822
825
  );
823
826
  }
824
- function decisionInspectionDigest(record) {
827
+ function decisionInspectionDigest(record, scope = "all") {
825
828
  const effective = effectiveDecision(record);
829
+ if (scope === "operative")
830
+ return createHash2("sha256").update(
831
+ JSON.stringify([
832
+ "operative",
833
+ record.status,
834
+ decisionApproval(effective),
835
+ effective.version === 2 ? effective.revision : 0,
836
+ decisionContent(effective),
837
+ effective.refreshedHash
838
+ ])
839
+ ).digest("hex");
826
840
  return createHash2("sha256").update(
827
841
  JSON.stringify([
828
842
  record.status,
@@ -834,8 +848,10 @@ function decisionInspectionDigest(record) {
834
848
  ])
835
849
  ).digest("hex");
836
850
  }
837
- function latestDecisionInspection(record) {
838
- return record.version === 2 ? record.inspections?.filter((i) => i.decisionDigest === decisionInspectionDigest(record)).at(-1) : void 0;
851
+ function latestDecisionInspection(record, scope = "all") {
852
+ return record.version === 2 ? record.inspections?.filter(
853
+ (i) => (scope === "operative" || i.scope !== "operative") && i.decisionDigest === decisionInspectionDigest(record, i.scope ?? "all")
854
+ ).at(-1) : void 0;
839
855
  }
840
856
  function revisionKnowledge(record, freshness) {
841
857
  return {
@@ -846,7 +862,10 @@ function revisionKnowledge(record, freshness) {
846
862
  }
847
863
  function decisionKnowledge(record, freshness = "unknown", proposalFreshness = "unknown") {
848
864
  const effective = effectiveDecision(record);
849
- const inspection = latestDecisionInspection(record);
865
+ const inspection = latestDecisionInspection(
866
+ record,
867
+ decisionApproval(effective) === "accepted" ? "operative" : "all"
868
+ );
850
869
  return {
851
870
  ...revisionKnowledge(effective, freshness),
852
871
  ...inspection ? {
@@ -856,10 +875,16 @@ function decisionKnowledge(record, freshness = "unknown", proposalFreshness = "u
856
875
  inspector: inspection.inspector,
857
876
  note: inspection.note,
858
877
  outcome: inspection.outcome,
878
+ scope: inspection.scope ?? "all",
859
879
  headHash: inspection.headHash
860
880
  }
861
881
  } : {},
862
- ...effective !== record ? { pendingProposal: revisionKnowledge(record, proposalFreshness) } : {}
882
+ ...effective !== record ? {
883
+ pendingProposal: {
884
+ ...revisionKnowledge(record, proposalFreshness),
885
+ assessment: latestDecisionInspection(record) ? "no_contradiction_found" : "not_assessed"
886
+ }
887
+ } : {}
863
888
  };
864
889
  }
865
890
  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 prepare evidence and record a separate source inspection without human approval, or an authorized human review; identities and sources are recorded assertions, not authenticated proof.";
@@ -941,14 +966,14 @@ async function loadDecisionStore(rootDir) {
941
966
  }
942
967
 
943
968
  // src/hook/hook.ts
944
- import { createHash as createHash4 } from "crypto";
969
+ import { createHash as createHash5 } from "crypto";
945
970
 
946
971
  // src/decisions/drift.ts
947
- import path10 from "path";
972
+ import path8 from "path";
948
973
 
949
974
  // src/drift/drift.ts
950
975
  import fs6 from "fs/promises";
951
- import path9 from "path";
976
+ import path7 from "path";
952
977
  function parseChanges(output) {
953
978
  const fields = output.split("\0");
954
979
  const changes = [];
@@ -1007,9 +1032,29 @@ async function getWorkingTree(resolvedRoot) {
1007
1032
  }
1008
1033
  }
1009
1034
 
1035
+ // src/decisions/excerpts.ts
1036
+ import { createHash as createHash4 } from "crypto";
1037
+ import { execFile as execFile2 } from "child_process";
1038
+ import { promisify as promisify2 } from "util";
1039
+ var exec = promisify2(execFile2);
1040
+ function driftBaseline(record) {
1041
+ const effective = effectiveDecision(record);
1042
+ const inspection = latestDecisionInspection(
1043
+ record,
1044
+ decisionApproval(effective) === "accepted" ? "operative" : "all"
1045
+ );
1046
+ return {
1047
+ record: effective,
1048
+ capture: inspection?.capture ?? (effective.version === 2 ? effective.capture : void 0),
1049
+ commits: [inspection?.headHash, effective.refreshedHash].filter(
1050
+ (hash) => !!hash && /^[a-f0-9]{40,64}$/.test(hash)
1051
+ )
1052
+ };
1053
+ }
1054
+
1010
1055
  // src/decisions/drift.ts
1011
1056
  async function computeDecisionDrift(rootDir, decisions) {
1012
- const resolvedRoot = path10.resolve(rootDir);
1057
+ const resolvedRoot = path8.resolve(rootDir);
1013
1058
  const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
1014
1059
  const report = {
1015
1060
  historyAvailable: true,
@@ -1024,11 +1069,15 @@ async function computeDecisionDrift(rootDir, decisions) {
1024
1069
  ]);
1025
1070
  const changesByHash = /* @__PURE__ */ new Map();
1026
1071
  const baselines = store.records.filter((record) => record.status === "active").flatMap((record) => {
1027
- const inspection = latestDecisionInspection(record);
1028
- if (inspection) return [inspection.capture];
1029
- return [.../* @__PURE__ */ new Set([effectiveDecision(record), record])].flatMap(
1030
- (candidate) => candidate.version === 2 && candidate.capture ? [candidate.capture] : []
1031
- );
1072
+ const effective = effectiveDecision(record);
1073
+ return [.../* @__PURE__ */ new Set([effective, record])].flatMap((candidate) => {
1074
+ const inspection = latestDecisionInspection(
1075
+ record,
1076
+ candidate === effective && decisionApproval(effective) === "accepted" ? "operative" : "all"
1077
+ );
1078
+ const capture = inspection?.capture ?? (candidate.version === 2 ? candidate.capture : void 0);
1079
+ return capture ? [capture] : [];
1080
+ });
1032
1081
  });
1033
1082
  const captures = await captureAnchorScopes(
1034
1083
  resolvedRoot,
@@ -1066,13 +1115,15 @@ async function computeDecisionDrift(rootDir, decisions) {
1066
1115
  };
1067
1116
  for (const record of store.records) {
1068
1117
  if (record.status !== "active") continue;
1069
- const effective = effectiveDecision(record);
1070
- const inspection = latestDecisionInspection(record);
1071
- const state = await inspect(effective, inspection?.capture);
1118
+ const baseline = driftBaseline(record);
1119
+ const state = await inspect(baseline.record, baseline.capture);
1072
1120
  report.freshness[record.id] = state.freshness;
1073
1121
  if (state.changedFiles.length) report.staleDecisions[record.id] = state.changedFiles;
1074
- if (effective !== record)
1075
- (report.pendingProposals ??= {})[record.id] = await inspect(record, inspection?.capture);
1122
+ if (baseline.record !== record)
1123
+ (report.pendingProposals ??= {})[record.id] = await inspect(
1124
+ record,
1125
+ latestDecisionInspection(record)?.capture
1126
+ );
1076
1127
  }
1077
1128
  return report;
1078
1129
  }
@@ -1100,9 +1151,9 @@ async function exists(p) {
1100
1151
  async function findMasonRoot(startDir) {
1101
1152
  let dir = startDir;
1102
1153
  for (let i = 0; i < MAX_WALK_UP; i++) {
1103
- if (await exists(path11.join(dir, ".mason", "decisions"))) return dir;
1104
- if (await exists(path11.join(dir, ".git"))) return null;
1105
- const parent = path11.dirname(dir);
1154
+ if (await exists(path9.join(dir, ".mason", "decisions"))) return dir;
1155
+ if (await exists(path9.join(dir, ".git"))) return null;
1156
+ const parent = path9.dirname(dir);
1106
1157
  if (parent === dir) return null;
1107
1158
  dir = parent;
1108
1159
  }
@@ -1170,19 +1221,20 @@ async function runHook(stdinText, env = {}) {
1170
1221
  if (input.tool_name && !SUPPORTED_TOOLS.has(input.tool_name)) return null;
1171
1222
  const filePath = input.tool_input?.file_path;
1172
1223
  if (!filePath || typeof filePath !== "string") return null;
1173
- const absPath = path11.isAbsolute(filePath) ? filePath : path11.resolve(input.cwd ?? process.cwd(), filePath);
1174
- const root = await findMasonRoot(path11.dirname(absPath));
1224
+ const absPath = path9.isAbsolute(filePath) ? filePath : path9.resolve(input.cwd ?? process.cwd(), filePath);
1225
+ const root = await findMasonRoot(path9.dirname(absPath));
1175
1226
  if (!root) return null;
1176
- const relPath = path11.relative(root, absPath).split(path11.sep).join("/");
1227
+ if (env.rootDir && await fs7.realpath(root) !== await fs7.realpath(env.rootDir)) return null;
1228
+ const relPath = path9.relative(root, absPath).split(path9.sep).join("/");
1177
1229
  if (relPath.startsWith("..")) return null;
1178
1230
  const { records } = await loadDecisionStore(root);
1179
1231
  const stateDir = env.stateDir ?? os2.tmpdir();
1180
- const stateFile = path11.join(
1232
+ const stateFile = path9.join(
1181
1233
  stateDir,
1182
- `mason-hook-${createHash4("sha256").update(root).digest("hex").slice(0, 12)}-${stateKey(input)}.json`
1234
+ `mason-hook-${createHash5("sha256").update(root).digest("hex").slice(0, 12)}-${stateKey(input)}.json`
1183
1235
  );
1184
1236
  const injected = await loadInjected(stateFile);
1185
- const recordKey = (record) => `${record.id}:${createHash4("sha256").update(JSON.stringify(record)).digest("hex")}`;
1237
+ const recordKey = (record) => `${record.id}:${createHash5("sha256").update(JSON.stringify(record)).digest("hex")}`;
1186
1238
  const previouslyInjected = (record) => [...injected].some((key) => key === record.id || key.startsWith(`${record.id}:`));
1187
1239
  const fresh = records.filter(
1188
1240
  (r) => !injected.has(recordKey(r)) && (previouslyInjected(r) || r.status === "active" && anchorsCover(r, relPath))
@@ -1204,7 +1256,7 @@ async function runHook(stdinText, env = {}) {
1204
1256
  }
1205
1257
  return JSON.stringify({
1206
1258
  hookSpecificOutput: {
1207
- hookEventName: "PostToolUse",
1259
+ hookEventName: env.hookEventName ?? "PostToolUse",
1208
1260
  additionalContext: formatContext(relPath, selected, drift)
1209
1261
  }
1210
1262
  });