mason-context 0.21.0 → 0.22.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 +10 -5
- package/dist/mason-audit.js +89 -73
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-drift.js +52 -38
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +60 -30
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +2767 -5123
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +130 -100
- package/dist/mason-review.js.map +1 -1
- package/dist/mason-runtime.js +1 -0
- package/dist/mason-runtime.js.map +1 -1
- package/dist/mason.js +1055 -1703
- package/dist/mason.js.map +1 -1
- package/package.json +1 -4
package/dist/mason-review.js
CHANGED
|
@@ -31,12 +31,12 @@ var init_types = __esm({
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
// src/review/cli.ts
|
|
34
|
-
import
|
|
34
|
+
import path12 from "path";
|
|
35
35
|
import { stripVTControlCharacters } from "util";
|
|
36
36
|
|
|
37
37
|
// src/review/review.ts
|
|
38
38
|
import fs7 from "fs/promises";
|
|
39
|
-
import
|
|
39
|
+
import path11 from "path";
|
|
40
40
|
import { execFile as execFile3 } from "child_process";
|
|
41
41
|
import { promisify as promisify3 } from "util";
|
|
42
42
|
|
|
@@ -91,10 +91,7 @@ function execGit(args, options) {
|
|
|
91
91
|
|
|
92
92
|
// src/drift/drift.ts
|
|
93
93
|
import fs4 from "fs/promises";
|
|
94
|
-
import
|
|
95
|
-
|
|
96
|
-
// src/snapshot/snapshot.ts
|
|
97
|
-
import path5 from "path";
|
|
94
|
+
import path4 from "path";
|
|
98
95
|
|
|
99
96
|
// src/utils/validation.ts
|
|
100
97
|
function isRecord(value) {
|
|
@@ -420,76 +417,6 @@ import fs3 from "fs/promises";
|
|
|
420
417
|
import os from "os";
|
|
421
418
|
var lockOwnerSchema = z.object({ host: z.string(), pid: z.number().int().positive() });
|
|
422
419
|
|
|
423
|
-
// src/test-map.ts
|
|
424
|
-
import path4 from "path";
|
|
425
|
-
async function buildTestMap(dir) {
|
|
426
|
-
const rootDir = path4.resolve(dir);
|
|
427
|
-
const access = await createFileAccess(rootDir);
|
|
428
|
-
const testPatterns = [
|
|
429
|
-
"**/*.test.*",
|
|
430
|
-
"**/*.spec.*",
|
|
431
|
-
"**/*Test.kt",
|
|
432
|
-
"**/*Test.java",
|
|
433
|
-
"**/*Tests.kt",
|
|
434
|
-
"**/*Tests.java",
|
|
435
|
-
"**/test_*.py",
|
|
436
|
-
"**/*_test.py",
|
|
437
|
-
"**/*_test.go",
|
|
438
|
-
"**/*Tests.swift",
|
|
439
|
-
"**/*Test.swift",
|
|
440
|
-
"**/*_test.rs"
|
|
441
|
-
];
|
|
442
|
-
const testFiles = await access.list(testPatterns);
|
|
443
|
-
const sourceFiles = await access.list();
|
|
444
|
-
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
445
|
-
for (const file of sourceFiles) {
|
|
446
|
-
if (testFiles.includes(file)) continue;
|
|
447
|
-
const baseName = path4.basename(file).replace(/\.[^.]+$/, "");
|
|
448
|
-
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
449
|
-
existing.push(file);
|
|
450
|
-
sourceByBaseName.set(baseName, existing);
|
|
451
|
-
}
|
|
452
|
-
const paired = [];
|
|
453
|
-
const unmatched = [];
|
|
454
|
-
for (const testFile of testFiles) {
|
|
455
|
-
const testBaseName = path4.basename(testFile).replace(/\.[^.]+$/, "");
|
|
456
|
-
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
457
|
-
if (!sourceName) {
|
|
458
|
-
unmatched.push(testFile);
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
const candidates = sourceByBaseName.get(sourceName);
|
|
462
|
-
if (candidates && candidates.length > 0) {
|
|
463
|
-
const testDir = path4.dirname(testFile);
|
|
464
|
-
const bestMatch = candidates.reduce((best, candidate) => {
|
|
465
|
-
const candidateDir = path4.dirname(candidate);
|
|
466
|
-
const bestDir = path4.dirname(best);
|
|
467
|
-
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
468
|
-
const bestOverlap = commonSegments(testDir, bestDir);
|
|
469
|
-
return candidateOverlap > bestOverlap ? candidate : best;
|
|
470
|
-
});
|
|
471
|
-
paired.push({
|
|
472
|
-
test: testFile,
|
|
473
|
-
source: bestMatch,
|
|
474
|
-
confidence: candidates.length === 1 ? "exact" : "best-guess"
|
|
475
|
-
});
|
|
476
|
-
} else {
|
|
477
|
-
unmatched.push(testFile);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
return { totalTestFiles: testFiles.length, paired, unmatched };
|
|
481
|
-
}
|
|
482
|
-
function commonSegments(pathA, pathB) {
|
|
483
|
-
const segsA = pathA.split("/");
|
|
484
|
-
const segsB = pathB.split("/");
|
|
485
|
-
let count2 = 0;
|
|
486
|
-
for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
|
|
487
|
-
if (segsA[i] === segsB[i]) count2++;
|
|
488
|
-
else break;
|
|
489
|
-
}
|
|
490
|
-
return count2;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
420
|
// src/snapshot/snapshot.ts
|
|
494
421
|
var repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
495
422
|
var verificationFields = {
|
|
@@ -600,11 +527,11 @@ import fg3 from "fast-glob";
|
|
|
600
527
|
// src/audit/inspection.ts
|
|
601
528
|
import fg2 from "fast-glob";
|
|
602
529
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
603
|
-
import
|
|
530
|
+
import path5 from "path";
|
|
604
531
|
var inspections = new AsyncLocalStorage2();
|
|
605
532
|
function inspectionRead(root, key, read) {
|
|
606
533
|
const inspection = inspections.getStore();
|
|
607
|
-
if (!inspection || inspection.root !==
|
|
534
|
+
if (!inspection || inspection.root !== path5.resolve(root)) return read();
|
|
608
535
|
if (!inspection.reads.has(key)) {
|
|
609
536
|
const pending = read();
|
|
610
537
|
inspection.reads.set(key, pending);
|
|
@@ -847,6 +774,7 @@ var currentSchema = legacySchema.extend({
|
|
|
847
774
|
inspector: text(200),
|
|
848
775
|
note: text(1500),
|
|
849
776
|
outcome: z4.literal("no_contradiction_found"),
|
|
777
|
+
scope: z4.literal("operative").optional(),
|
|
850
778
|
decisionDigest: z4.string().regex(/^[a-f0-9]{64}$/),
|
|
851
779
|
headHash: z4.string(),
|
|
852
780
|
capture: anchorCaptureSchema
|
|
@@ -957,8 +885,19 @@ function decisionTrust(record, freshness) {
|
|
|
957
885
|
freshness
|
|
958
886
|
);
|
|
959
887
|
}
|
|
960
|
-
function decisionInspectionDigest(record) {
|
|
888
|
+
function decisionInspectionDigest(record, scope = "all") {
|
|
961
889
|
const effective = effectiveDecision(record);
|
|
890
|
+
if (scope === "operative")
|
|
891
|
+
return createHash2("sha256").update(
|
|
892
|
+
JSON.stringify([
|
|
893
|
+
"operative",
|
|
894
|
+
record.status,
|
|
895
|
+
decisionApproval(effective),
|
|
896
|
+
effective.version === 2 ? effective.revision : 0,
|
|
897
|
+
decisionContent(effective),
|
|
898
|
+
effective.refreshedHash
|
|
899
|
+
])
|
|
900
|
+
).digest("hex");
|
|
962
901
|
return createHash2("sha256").update(
|
|
963
902
|
JSON.stringify([
|
|
964
903
|
record.status,
|
|
@@ -970,8 +909,10 @@ function decisionInspectionDigest(record) {
|
|
|
970
909
|
])
|
|
971
910
|
).digest("hex");
|
|
972
911
|
}
|
|
973
|
-
function latestDecisionInspection(record) {
|
|
974
|
-
return record.version === 2 ? record.inspections?.filter(
|
|
912
|
+
function latestDecisionInspection(record, scope = "all") {
|
|
913
|
+
return record.version === 2 ? record.inspections?.filter(
|
|
914
|
+
(i) => (scope === "operative" || i.scope !== "operative") && i.decisionDigest === decisionInspectionDigest(record, i.scope ?? "all")
|
|
915
|
+
).at(-1) : void 0;
|
|
975
916
|
}
|
|
976
917
|
function revisionKnowledge(record, freshness) {
|
|
977
918
|
return {
|
|
@@ -982,7 +923,10 @@ function revisionKnowledge(record, freshness) {
|
|
|
982
923
|
}
|
|
983
924
|
function decisionKnowledge(record, freshness = "unknown", proposalFreshness = "unknown") {
|
|
984
925
|
const effective = effectiveDecision(record);
|
|
985
|
-
const inspection = latestDecisionInspection(
|
|
926
|
+
const inspection = latestDecisionInspection(
|
|
927
|
+
record,
|
|
928
|
+
decisionApproval(effective) === "accepted" ? "operative" : "all"
|
|
929
|
+
);
|
|
986
930
|
return {
|
|
987
931
|
...revisionKnowledge(effective, freshness),
|
|
988
932
|
...inspection ? {
|
|
@@ -992,21 +936,27 @@ function decisionKnowledge(record, freshness = "unknown", proposalFreshness = "u
|
|
|
992
936
|
inspector: inspection.inspector,
|
|
993
937
|
note: inspection.note,
|
|
994
938
|
outcome: inspection.outcome,
|
|
939
|
+
scope: inspection.scope ?? "all",
|
|
995
940
|
headHash: inspection.headHash
|
|
996
941
|
}
|
|
997
942
|
} : {},
|
|
998
|
-
...effective !== record ? {
|
|
943
|
+
...effective !== record ? {
|
|
944
|
+
pendingProposal: {
|
|
945
|
+
...revisionKnowledge(record, proposalFreshness),
|
|
946
|
+
assessment: latestDecisionInspection(record) ? "no_contradiction_found" : "not_assessed"
|
|
947
|
+
}
|
|
948
|
+
} : {}
|
|
999
949
|
};
|
|
1000
950
|
}
|
|
1001
951
|
|
|
1002
952
|
// src/decisions/decisions.ts
|
|
1003
953
|
import fs6 from "fs/promises";
|
|
1004
|
-
import
|
|
954
|
+
import path7 from "path";
|
|
1005
955
|
import { createHash as createHash3 } from "crypto";
|
|
1006
956
|
import { z as z5 } from "zod";
|
|
1007
957
|
|
|
1008
958
|
// src/context/lexical.ts
|
|
1009
|
-
import
|
|
959
|
+
import path6 from "path";
|
|
1010
960
|
|
|
1011
961
|
// src/decisions/decisions.ts
|
|
1012
962
|
var TITLE_MAX_CHARS = 80;
|
|
@@ -1085,9 +1035,9 @@ async function loadDecisionStore(rootDir) {
|
|
|
1085
1035
|
}
|
|
1086
1036
|
|
|
1087
1037
|
// src/decisions/drift.ts
|
|
1088
|
-
import
|
|
1038
|
+
import path8 from "path";
|
|
1089
1039
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
1090
|
-
const resolvedRoot =
|
|
1040
|
+
const resolvedRoot = path8.resolve(rootDir);
|
|
1091
1041
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
1092
1042
|
const report = {
|
|
1093
1043
|
historyAvailable: true,
|
|
@@ -1102,11 +1052,15 @@ async function computeDecisionDrift(rootDir, decisions) {
|
|
|
1102
1052
|
]);
|
|
1103
1053
|
const changesByHash = /* @__PURE__ */ new Map();
|
|
1104
1054
|
const baselines = store.records.filter((record) => record.status === "active").flatMap((record) => {
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1055
|
+
const effective = effectiveDecision(record);
|
|
1056
|
+
return [.../* @__PURE__ */ new Set([effective, record])].flatMap((candidate) => {
|
|
1057
|
+
const inspection = latestDecisionInspection(
|
|
1058
|
+
record,
|
|
1059
|
+
candidate === effective && decisionApproval(effective) === "accepted" ? "operative" : "all"
|
|
1060
|
+
);
|
|
1061
|
+
const capture = inspection?.capture ?? (candidate.version === 2 ? candidate.capture : void 0);
|
|
1062
|
+
return capture ? [capture] : [];
|
|
1063
|
+
});
|
|
1110
1064
|
});
|
|
1111
1065
|
const captures = await captureAnchorScopes(
|
|
1112
1066
|
resolvedRoot,
|
|
@@ -1145,12 +1099,18 @@ async function computeDecisionDrift(rootDir, decisions) {
|
|
|
1145
1099
|
for (const record of store.records) {
|
|
1146
1100
|
if (record.status !== "active") continue;
|
|
1147
1101
|
const effective = effectiveDecision(record);
|
|
1148
|
-
const inspection = latestDecisionInspection(
|
|
1102
|
+
const inspection = latestDecisionInspection(
|
|
1103
|
+
record,
|
|
1104
|
+
decisionApproval(effective) === "accepted" ? "operative" : "all"
|
|
1105
|
+
);
|
|
1149
1106
|
const state = await inspect(effective, inspection?.capture);
|
|
1150
1107
|
report.freshness[record.id] = state.freshness;
|
|
1151
1108
|
if (state.changedFiles.length) report.staleDecisions[record.id] = state.changedFiles;
|
|
1152
1109
|
if (effective !== record)
|
|
1153
|
-
(report.pendingProposals ??= {})[record.id] = await inspect(
|
|
1110
|
+
(report.pendingProposals ??= {})[record.id] = await inspect(
|
|
1111
|
+
record,
|
|
1112
|
+
latestDecisionInspection(record)?.capture
|
|
1113
|
+
);
|
|
1154
1114
|
}
|
|
1155
1115
|
return report;
|
|
1156
1116
|
}
|
|
@@ -1228,9 +1188,79 @@ async function findMissingPartners(resolvedRoot, changedFiles, existsOnDisk, all
|
|
|
1228
1188
|
}
|
|
1229
1189
|
|
|
1230
1190
|
// src/review/evidence.ts
|
|
1231
|
-
import
|
|
1191
|
+
import path10 from "path";
|
|
1232
1192
|
import { z as z9 } from "zod";
|
|
1233
1193
|
|
|
1194
|
+
// src/test-map.ts
|
|
1195
|
+
import path9 from "path";
|
|
1196
|
+
async function buildTestMap(dir) {
|
|
1197
|
+
const rootDir = path9.resolve(dir);
|
|
1198
|
+
const access = await createFileAccess(rootDir);
|
|
1199
|
+
const testPatterns = [
|
|
1200
|
+
"**/*.test.*",
|
|
1201
|
+
"**/*.spec.*",
|
|
1202
|
+
"**/*Test.kt",
|
|
1203
|
+
"**/*Test.java",
|
|
1204
|
+
"**/*Tests.kt",
|
|
1205
|
+
"**/*Tests.java",
|
|
1206
|
+
"**/test_*.py",
|
|
1207
|
+
"**/*_test.py",
|
|
1208
|
+
"**/*_test.go",
|
|
1209
|
+
"**/*Tests.swift",
|
|
1210
|
+
"**/*Test.swift",
|
|
1211
|
+
"**/*_test.rs"
|
|
1212
|
+
];
|
|
1213
|
+
const testFiles = await access.list(testPatterns);
|
|
1214
|
+
const sourceFiles = await access.list();
|
|
1215
|
+
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
1216
|
+
for (const file of sourceFiles) {
|
|
1217
|
+
if (testFiles.includes(file)) continue;
|
|
1218
|
+
const baseName = path9.basename(file).replace(/\.[^.]+$/, "");
|
|
1219
|
+
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
1220
|
+
existing.push(file);
|
|
1221
|
+
sourceByBaseName.set(baseName, existing);
|
|
1222
|
+
}
|
|
1223
|
+
const paired = [];
|
|
1224
|
+
const unmatched = [];
|
|
1225
|
+
for (const testFile of testFiles) {
|
|
1226
|
+
const testBaseName = path9.basename(testFile).replace(/\.[^.]+$/, "");
|
|
1227
|
+
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
1228
|
+
if (!sourceName) {
|
|
1229
|
+
unmatched.push(testFile);
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
const candidates = sourceByBaseName.get(sourceName);
|
|
1233
|
+
if (candidates && candidates.length > 0) {
|
|
1234
|
+
const testDir = path9.dirname(testFile);
|
|
1235
|
+
const bestMatch = candidates.reduce((best, candidate) => {
|
|
1236
|
+
const candidateDir = path9.dirname(candidate);
|
|
1237
|
+
const bestDir = path9.dirname(best);
|
|
1238
|
+
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
1239
|
+
const bestOverlap = commonSegments(testDir, bestDir);
|
|
1240
|
+
return candidateOverlap > bestOverlap ? candidate : best;
|
|
1241
|
+
});
|
|
1242
|
+
paired.push({
|
|
1243
|
+
test: testFile,
|
|
1244
|
+
source: bestMatch,
|
|
1245
|
+
confidence: candidates.length === 1 ? "exact" : "best-guess"
|
|
1246
|
+
});
|
|
1247
|
+
} else {
|
|
1248
|
+
unmatched.push(testFile);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return { totalTestFiles: testFiles.length, paired, unmatched };
|
|
1252
|
+
}
|
|
1253
|
+
function commonSegments(pathA, pathB) {
|
|
1254
|
+
const segsA = pathA.split("/");
|
|
1255
|
+
const segsB = pathB.split("/");
|
|
1256
|
+
let count2 = 0;
|
|
1257
|
+
for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
|
|
1258
|
+
if (segsA[i] === segsB[i]) count2++;
|
|
1259
|
+
else break;
|
|
1260
|
+
}
|
|
1261
|
+
return count2;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1234
1264
|
// src/review/evidence/vitest.ts
|
|
1235
1265
|
import { z as z6 } from "zod";
|
|
1236
1266
|
|
|
@@ -1622,7 +1652,7 @@ var checkSchema = z9.object({
|
|
|
1622
1652
|
}).optional()
|
|
1623
1653
|
});
|
|
1624
1654
|
function relativeArtifact(root, file) {
|
|
1625
|
-
const relative =
|
|
1655
|
+
const relative = path10.isAbsolute(file) ? path10.relative(root, file) : file;
|
|
1626
1656
|
const normalized = normalizeRepoPath(relative);
|
|
1627
1657
|
if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
|
|
1628
1658
|
return normalized;
|
|
@@ -1751,7 +1781,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
1751
1781
|
const report = await readStoreJson(root, file);
|
|
1752
1782
|
if (report === null) throw new Error(`Report artifact is missing: ${file}`);
|
|
1753
1783
|
const sourceRoot = check.sourceRoot ?? root;
|
|
1754
|
-
if (!
|
|
1784
|
+
if (!path10.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot))
|
|
1755
1785
|
throw new Error(
|
|
1756
1786
|
"sourceRoot must identify the absolute checkout root on the check runner."
|
|
1757
1787
|
);
|
|
@@ -1856,7 +1886,7 @@ function anchorsTouched(record, changedFiles) {
|
|
|
1856
1886
|
return matchingPaths(record.files, changedFiles);
|
|
1857
1887
|
}
|
|
1858
1888
|
async function computeReview(rootDir, base, options = {}) {
|
|
1859
|
-
const resolvedRoot =
|
|
1889
|
+
const resolvedRoot = path11.resolve(rootDir);
|
|
1860
1890
|
const head = await getCurrentGitHash(resolvedRoot);
|
|
1861
1891
|
if (head === "unknown") return null;
|
|
1862
1892
|
const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
|
|
@@ -1917,7 +1947,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
1917
1947
|
analyzed,
|
|
1918
1948
|
async (relPath) => {
|
|
1919
1949
|
try {
|
|
1920
|
-
await fs7.access(
|
|
1950
|
+
await fs7.access(path11.join(resolvedRoot, relPath));
|
|
1921
1951
|
return true;
|
|
1922
1952
|
} catch {
|
|
1923
1953
|
return false;
|
|
@@ -2152,7 +2182,7 @@ async function runReviewCli(argv, io = {
|
|
|
2152
2182
|
io.out(USAGE);
|
|
2153
2183
|
return 0;
|
|
2154
2184
|
}
|
|
2155
|
-
const rootDir =
|
|
2185
|
+
const rootDir = path12.resolve(args.dir);
|
|
2156
2186
|
const base = args.base ?? await defaultBase(rootDir);
|
|
2157
2187
|
if (!base) {
|
|
2158
2188
|
io.err(
|