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.
- package/CHANGELOG.md +29 -0
- package/README.md +10 -5
- package/dist/mason-audit.js +159 -95
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-drift.js +90 -53
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +97 -45
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +3425 -5255
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +180 -125
- package/dist/mason-review.js.map +1 -1
- package/dist/mason-runtime.js +13 -6
- package/dist/mason-runtime.js.map +1 -1
- package/dist/mason.js +1808 -1882
- package/dist/mason.js.map +1 -1
- package/package.json +1 -4
package/dist/mason-hook.js
CHANGED
|
@@ -43,7 +43,7 @@ function isStringArray(value) {
|
|
|
43
43
|
|
|
44
44
|
// src/hook/hook.ts
|
|
45
45
|
import fs7 from "fs/promises";
|
|
46
|
-
import
|
|
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
|
|
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
|
-
...
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 ? {
|
|
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
|
|
969
|
+
import { createHash as createHash5 } from "crypto";
|
|
945
970
|
|
|
946
971
|
// src/decisions/drift.ts
|
|
947
|
-
import
|
|
972
|
+
import path8 from "path";
|
|
948
973
|
|
|
949
974
|
// src/drift/drift.ts
|
|
950
975
|
import fs6 from "fs/promises";
|
|
951
|
-
import
|
|
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 =
|
|
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
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
|
1070
|
-
const
|
|
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 (
|
|
1075
|
-
(report.pendingProposals ??= {})[record.id] = await inspect(
|
|
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(
|
|
1104
|
-
if (await exists(
|
|
1105
|
-
const parent =
|
|
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 =
|
|
1174
|
-
const root = await findMasonRoot(
|
|
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
|
-
|
|
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 =
|
|
1232
|
+
const stateFile = path9.join(
|
|
1181
1233
|
stateDir,
|
|
1182
|
-
`mason-hook-${
|
|
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}:${
|
|
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
|
});
|