mason-context 0.17.4 → 0.18.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 +10 -0
- package/dist/mason-audit.js +112 -67
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +56 -11
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-drift.js +9 -4
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +14 -9
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +416 -227
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +10 -5
- package/dist/mason-review.js.map +1 -1
- package/dist/mason.js +59 -14
- package/dist/mason.js.map +1 -1
- package/package.json +1 -1
package/dist/mason-mcp.js
CHANGED
|
@@ -1098,6 +1098,105 @@ var init_scope = __esm({
|
|
|
1098
1098
|
}
|
|
1099
1099
|
});
|
|
1100
1100
|
|
|
1101
|
+
// src/utils/store-lock.ts
|
|
1102
|
+
import fs6 from "fs/promises";
|
|
1103
|
+
import os from "os";
|
|
1104
|
+
async function lockDiagnostic(file) {
|
|
1105
|
+
let reason2;
|
|
1106
|
+
try {
|
|
1107
|
+
const owner = JSON.parse(await fs6.readFile(file, "utf8"));
|
|
1108
|
+
if (!owner || typeof owner.host !== "string" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
|
|
1109
|
+
reason2 = "Lock owner data is incomplete or malformed.";
|
|
1110
|
+
} else if (owner.host !== os.hostname()) {
|
|
1111
|
+
reason2 = "The lock belongs to another host; its process cannot be checked locally.";
|
|
1112
|
+
} else {
|
|
1113
|
+
try {
|
|
1114
|
+
process.kill(owner.pid, 0);
|
|
1115
|
+
reason2 = `Local owner PID ${owner.pid} is still running.`;
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
reason2 = error.code === "ESRCH" ? `Local owner PID ${owner.pid} has exited, but lock recovery has not completed.` : `Local owner PID ${owner.pid} could not be checked.`;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
reason2 = error instanceof SyntaxError ? "Lock owner data is incomplete or malformed." : "Lock owner data could not be read; the lock may have changed.";
|
|
1122
|
+
}
|
|
1123
|
+
if (await fs6.lstat(file + ".reclaim").then(() => true, () => false)) {
|
|
1124
|
+
reason2 += " A recovery guard also needs inspection: " + file + ".reclaim.";
|
|
1125
|
+
}
|
|
1126
|
+
return reason2 + " Retry if a writer is active. For manual recovery, stop all Mason writers for this checkout, confirm they have exited (including other hosts), then remove only confirmed abandoned lock and lock.reclaim files. See docs/reference.md#snapshot-repairs-and-verification.";
|
|
1127
|
+
}
|
|
1128
|
+
async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason store") {
|
|
1129
|
+
const file = await storePath(root, directory + "/lock", true);
|
|
1130
|
+
const deadline = Date.now() + waitMs;
|
|
1131
|
+
let handle;
|
|
1132
|
+
while (!handle) {
|
|
1133
|
+
try {
|
|
1134
|
+
handle = await fs6.open(file, "wx", 384);
|
|
1135
|
+
try {
|
|
1136
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
await handle.close().catch(() => {
|
|
1139
|
+
});
|
|
1140
|
+
handle = void 0;
|
|
1141
|
+
await fs6.rm(file, { force: true }).catch(() => {
|
|
1142
|
+
});
|
|
1143
|
+
throw error;
|
|
1144
|
+
}
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
if (error.code !== "EEXIST") throw error;
|
|
1147
|
+
try {
|
|
1148
|
+
const owner = JSON.parse(await fs6.readFile(file, "utf8"));
|
|
1149
|
+
if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
1150
|
+
try {
|
|
1151
|
+
process.kill(owner.pid, 0);
|
|
1152
|
+
} catch (probe) {
|
|
1153
|
+
if (probe.code === "ESRCH") {
|
|
1154
|
+
const reclaim = file + ".reclaim";
|
|
1155
|
+
let guard;
|
|
1156
|
+
try {
|
|
1157
|
+
guard = await fs6.open(reclaim, "wx", 384);
|
|
1158
|
+
const current = JSON.parse(await fs6.readFile(file, "utf8"));
|
|
1159
|
+
if (current.pid === owner.pid && current.host === owner.host) await fs6.unlink(file);
|
|
1160
|
+
} finally {
|
|
1161
|
+
if (guard) {
|
|
1162
|
+
await guard.close();
|
|
1163
|
+
await fs6.rm(reclaim, { force: true });
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
} catch {
|
|
1170
|
+
}
|
|
1171
|
+
if (Date.now() >= deadline) throw new Error(label + " is busy or its lock needs inspection: " + file + ". " + await lockDiagnostic(file));
|
|
1172
|
+
await new Promise((resolve2) => setTimeout(resolve2, 40));
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
try {
|
|
1176
|
+
return await run();
|
|
1177
|
+
} finally {
|
|
1178
|
+
await handle.close();
|
|
1179
|
+
await fs6.unlink(file);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
var init_store_lock = __esm({
|
|
1183
|
+
"src/utils/store-lock.ts"() {
|
|
1184
|
+
"use strict";
|
|
1185
|
+
init_storage();
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
// src/snapshot/lock.ts
|
|
1190
|
+
function withSnapshotWrite(root, run, waitMs = 5e3) {
|
|
1191
|
+
return withStoreLock(root, ".mason/local/snapshot-write", run, waitMs, "Snapshot store");
|
|
1192
|
+
}
|
|
1193
|
+
var init_lock = __esm({
|
|
1194
|
+
"src/snapshot/lock.ts"() {
|
|
1195
|
+
"use strict";
|
|
1196
|
+
init_store_lock();
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1101
1200
|
// src/test-map.ts
|
|
1102
1201
|
var test_map_exports = {};
|
|
1103
1202
|
__export(test_map_exports, {
|
|
@@ -1203,8 +1302,15 @@ async function inspectSnapshot(rootDir) {
|
|
|
1203
1302
|
}] };
|
|
1204
1303
|
}
|
|
1205
1304
|
}
|
|
1206
|
-
async function
|
|
1207
|
-
|
|
1305
|
+
async function updateSnapshot(rootDir, update) {
|
|
1306
|
+
return withSnapshotWrite(rootDir, async () => {
|
|
1307
|
+
const change = await update(await loadSnapshot(rootDir));
|
|
1308
|
+
if (change.snapshot) {
|
|
1309
|
+
await writeStoreJson(rootDir, ".mason/snapshot.json", snapshotSchema.parse(change.snapshot));
|
|
1310
|
+
await change.afterSave?.();
|
|
1311
|
+
}
|
|
1312
|
+
return change.result;
|
|
1313
|
+
});
|
|
1208
1314
|
}
|
|
1209
1315
|
async function getCurrentGitHash(rootDir) {
|
|
1210
1316
|
try {
|
|
@@ -1277,6 +1383,7 @@ var init_snapshot = __esm({
|
|
|
1277
1383
|
init_files();
|
|
1278
1384
|
init_files();
|
|
1279
1385
|
init_storage();
|
|
1386
|
+
init_lock();
|
|
1280
1387
|
init_paths();
|
|
1281
1388
|
init_test_map();
|
|
1282
1389
|
repoPath = z.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
@@ -1284,6 +1391,7 @@ var init_snapshot = __esm({
|
|
|
1284
1391
|
refreshedHash: z.string().optional(),
|
|
1285
1392
|
verifiedAt: z.string().optional(),
|
|
1286
1393
|
verifiedHash: z.string().optional(),
|
|
1394
|
+
verificationToken: z.string().optional(),
|
|
1287
1395
|
verificationFailed: z.boolean().optional(),
|
|
1288
1396
|
verificationNote: z.string().optional()
|
|
1289
1397
|
};
|
|
@@ -1311,7 +1419,7 @@ var init_snapshot = __esm({
|
|
|
1311
1419
|
});
|
|
1312
1420
|
|
|
1313
1421
|
// src/drift/drift.ts
|
|
1314
|
-
import
|
|
1422
|
+
import fs7 from "fs/promises";
|
|
1315
1423
|
import path11 from "path";
|
|
1316
1424
|
function parseChanges(output) {
|
|
1317
1425
|
const fields = output.split("\0");
|
|
@@ -1379,7 +1487,7 @@ async function findGhostFiles(resolvedRoot, mappedFiles) {
|
|
|
1379
1487
|
const ghosts = [];
|
|
1380
1488
|
for (const file of mappedFiles) {
|
|
1381
1489
|
try {
|
|
1382
|
-
await
|
|
1490
|
+
await fs7.access(path11.join(resolvedRoot, file));
|
|
1383
1491
|
} catch {
|
|
1384
1492
|
ghosts.push(file);
|
|
1385
1493
|
}
|
|
@@ -1561,7 +1669,7 @@ var init_deleted_reference = __esm({
|
|
|
1561
1669
|
|
|
1562
1670
|
// src/audit/checks/new-module.ts
|
|
1563
1671
|
import fg4 from "fast-glob";
|
|
1564
|
-
import
|
|
1672
|
+
import fs8 from "fs/promises";
|
|
1565
1673
|
import path13 from "path";
|
|
1566
1674
|
function escapeRegExp(text2) {
|
|
1567
1675
|
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1613,7 +1721,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1613
1721
|
const byParent = /* @__PURE__ */ new Map();
|
|
1614
1722
|
for (let offset = 0; offset < parents.length; offset += 16) {
|
|
1615
1723
|
await Promise.all(parents.slice(offset, offset + 16).map(async (dir) => {
|
|
1616
|
-
const entries = await
|
|
1724
|
+
const entries = await fs8.readdir(await auditInputPath(root, dir), { withFileTypes: true });
|
|
1617
1725
|
const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink() && sources.some((file) => file.startsWith(path13.posix.join(dir, entry.name) + "/"))) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path13.posix.join(dir, entry.name));
|
|
1618
1726
|
byParent.set(dir, dirs);
|
|
1619
1727
|
}));
|
|
@@ -2236,7 +2344,9 @@ function assessTrust(entry, freshness) {
|
|
|
2236
2344
|
}
|
|
2237
2345
|
function trustHint(states) {
|
|
2238
2346
|
const parts = [];
|
|
2239
|
-
if (states.some((s) => s.verification === "failed")) parts.push("Verification failed for returned entries; do not rely on those descriptions until corrected.");
|
|
2347
|
+
if (states.some((s) => s.verification === "failed" || s.recordedVerdict === "failed")) parts.push("Verification failed for returned entries; do not rely on those descriptions until corrected.");
|
|
2348
|
+
if (states.some((s) => s.verification === "stale")) parts.push("Recorded verification evidence changed; use verify_snapshot and review the current evidence before reusing those verdicts.");
|
|
2349
|
+
if (states.some((s) => s.verification === "unknown")) parts.push("Recorded verification evidence cannot be confirmed; inspect the files and obtain a fresh review before relying on those verdicts.");
|
|
2240
2350
|
if (states.some((s) => s.freshness === "unknown")) parts.push("Freshness is unknown for some returned entries; inspect their files before relying on them.");
|
|
2241
2351
|
if (states.some((s) => s.freshness === "changed")) parts.push("Some returned entries have changed files, including possible local edits; verify against the current code.");
|
|
2242
2352
|
if (!parts.length) parts.push("No changes detected in the returned anchors. This does not prove the descriptions are correct.");
|
|
@@ -2461,7 +2571,7 @@ __export(decisions_exports, {
|
|
|
2461
2571
|
upsertDecision: () => upsertDecision,
|
|
2462
2572
|
withDecisionWrite: () => withDecisionWrite
|
|
2463
2573
|
});
|
|
2464
|
-
import
|
|
2574
|
+
import fs9 from "fs/promises";
|
|
2465
2575
|
import path17 from "path";
|
|
2466
2576
|
import { createHash } from "crypto";
|
|
2467
2577
|
import { z as z3 } from "zod";
|
|
@@ -2470,7 +2580,7 @@ async function loadDecisionStore(rootDir) {
|
|
|
2470
2580
|
const diagnostics = [];
|
|
2471
2581
|
let entries;
|
|
2472
2582
|
try {
|
|
2473
|
-
entries = await
|
|
2583
|
+
entries = await fs9.readdir(await storePath(rootDir, ".mason/decisions"));
|
|
2474
2584
|
} catch (error) {
|
|
2475
2585
|
if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
|
|
2476
2586
|
return { records, diagnostics };
|
|
@@ -2529,7 +2639,7 @@ async function withDecisionWrite(root, operation) {
|
|
|
2529
2639
|
const lockPath = await storePath(root, ".mason/decisions/.write-lock", true);
|
|
2530
2640
|
let lock;
|
|
2531
2641
|
try {
|
|
2532
|
-
lock = await
|
|
2642
|
+
lock = await fs9.open(lockPath, "wx", 384);
|
|
2533
2643
|
} catch (error) {
|
|
2534
2644
|
if (error.code === "EEXIST") return { status: "error", error: "Decision store is locked by another write. Retry after it finishes; an abandoned .mason/decisions/.write-lock must be removed only after confirming no writer is running." };
|
|
2535
2645
|
throw error;
|
|
@@ -2538,7 +2648,7 @@ async function withDecisionWrite(root, operation) {
|
|
|
2538
2648
|
return await operation();
|
|
2539
2649
|
} finally {
|
|
2540
2650
|
await lock.close();
|
|
2541
|
-
await
|
|
2651
|
+
await fs9.unlink(lockPath);
|
|
2542
2652
|
}
|
|
2543
2653
|
}
|
|
2544
2654
|
async function upsertDecision(rootDir, input) {
|
|
@@ -2560,7 +2670,7 @@ async function upsertDecision(rootDir, input) {
|
|
|
2560
2670
|
if (input.files && files.length < input.files.length) warnings.push("some anchor paths were outside the repo or duplicated and were dropped");
|
|
2561
2671
|
for (const file of files) {
|
|
2562
2672
|
try {
|
|
2563
|
-
await
|
|
2673
|
+
await fs9.access(path17.join(rootDir, file));
|
|
2564
2674
|
} catch {
|
|
2565
2675
|
warnings.push(`anchor file does not exist on disk: ${file}`);
|
|
2566
2676
|
}
|
|
@@ -2910,8 +3020,6 @@ var init_findings = __esm({
|
|
|
2910
3020
|
});
|
|
2911
3021
|
|
|
2912
3022
|
// src/automation/store.ts
|
|
2913
|
-
import fs9 from "fs/promises";
|
|
2914
|
-
import os from "os";
|
|
2915
3023
|
import { z as z5 } from "zod";
|
|
2916
3024
|
function parseState(raw) {
|
|
2917
3025
|
try {
|
|
@@ -2920,65 +3028,14 @@ function parseState(raw) {
|
|
|
2920
3028
|
throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
|
|
2921
3029
|
}
|
|
2922
3030
|
}
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
const deadline = Date.now() + waitMs;
|
|
2926
|
-
let handle;
|
|
2927
|
-
while (!handle) {
|
|
2928
|
-
try {
|
|
2929
|
-
handle = await fs9.open(file, "wx", 384);
|
|
2930
|
-
try {
|
|
2931
|
-
await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
|
|
2932
|
-
} catch (error) {
|
|
2933
|
-
await handle.close().catch(() => {
|
|
2934
|
-
});
|
|
2935
|
-
handle = void 0;
|
|
2936
|
-
await fs9.rm(file, { force: true }).catch(() => {
|
|
2937
|
-
});
|
|
2938
|
-
throw error;
|
|
2939
|
-
}
|
|
2940
|
-
} catch (error) {
|
|
2941
|
-
if (error.code !== "EEXIST") throw error;
|
|
2942
|
-
try {
|
|
2943
|
-
const owner = JSON.parse(await fs9.readFile(file, "utf8"));
|
|
2944
|
-
if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
2945
|
-
try {
|
|
2946
|
-
process.kill(owner.pid, 0);
|
|
2947
|
-
} catch (probe) {
|
|
2948
|
-
if (probe.code === "ESRCH") {
|
|
2949
|
-
const reclaim = file + ".reclaim";
|
|
2950
|
-
let guard;
|
|
2951
|
-
try {
|
|
2952
|
-
guard = await fs9.open(reclaim, "wx", 384);
|
|
2953
|
-
const current = JSON.parse(await fs9.readFile(file, "utf8"));
|
|
2954
|
-
if (current.pid === owner.pid && current.host === owner.host) await fs9.unlink(file);
|
|
2955
|
-
} finally {
|
|
2956
|
-
if (guard) {
|
|
2957
|
-
await guard.close();
|
|
2958
|
-
await fs9.rm(reclaim, { force: true });
|
|
2959
|
-
}
|
|
2960
|
-
}
|
|
2961
|
-
}
|
|
2962
|
-
}
|
|
2963
|
-
}
|
|
2964
|
-
} catch {
|
|
2965
|
-
}
|
|
2966
|
-
if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
|
|
2967
|
-
await new Promise((resolve2) => setTimeout(resolve2, 40));
|
|
2968
|
-
}
|
|
2969
|
-
}
|
|
2970
|
-
try {
|
|
2971
|
-
return await run();
|
|
2972
|
-
} finally {
|
|
2973
|
-
await handle.close();
|
|
2974
|
-
await fs9.unlink(file);
|
|
2975
|
-
}
|
|
3031
|
+
function withLock(root, directory, run, waitMs = 5e3) {
|
|
3032
|
+
return withStoreLock(root, directory, run, waitMs, "Automation");
|
|
2976
3033
|
}
|
|
2977
3034
|
var hostSchema, events, stateSchema;
|
|
2978
3035
|
var init_store = __esm({
|
|
2979
3036
|
"src/automation/store.ts"() {
|
|
2980
3037
|
"use strict";
|
|
2981
|
-
|
|
3038
|
+
init_store_lock();
|
|
2982
3039
|
hostSchema = z5.enum(["claude", "codex"]);
|
|
2983
3040
|
events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
|
|
2984
3041
|
stateSchema = z5.object({
|
|
@@ -3761,7 +3818,7 @@ var init_evidence = __esm({
|
|
|
3761
3818
|
init_dead_command();
|
|
3762
3819
|
init_storage();
|
|
3763
3820
|
init_advisory_review();
|
|
3764
|
-
engineVersion = true ? "0.
|
|
3821
|
+
engineVersion = true ? "0.18.0" : "development";
|
|
3765
3822
|
hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
3766
3823
|
content = readAuditInput;
|
|
3767
3824
|
cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
|
|
@@ -3853,12 +3910,124 @@ var init_launcher = __esm({
|
|
|
3853
3910
|
"use strict";
|
|
3854
3911
|
init_evidence();
|
|
3855
3912
|
init_model();
|
|
3856
|
-
executingVersion = () => true ? "0.
|
|
3913
|
+
executingVersion = () => true ? "0.18.0" : "development";
|
|
3857
3914
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
3858
3915
|
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
3859
3916
|
}
|
|
3860
3917
|
});
|
|
3861
3918
|
|
|
3919
|
+
// src/snapshot/review.ts
|
|
3920
|
+
import { createHash as createHash5 } from "crypto";
|
|
3921
|
+
function snapshotEntryContent(entry) {
|
|
3922
|
+
return {
|
|
3923
|
+
description: entry.description,
|
|
3924
|
+
files: "files" in entry ? entry.files : void 0,
|
|
3925
|
+
chain: "chain" in entry ? entry.chain : void 0,
|
|
3926
|
+
tests: "files" in entry ? entry.tests : void 0,
|
|
3927
|
+
type: "files" in entry ? normalizeFeatureType(entry.type) : void 0
|
|
3928
|
+
};
|
|
3929
|
+
}
|
|
3930
|
+
function reviewToken(kind, name, entry, evidence) {
|
|
3931
|
+
return createHash5("sha256").update(JSON.stringify({
|
|
3932
|
+
version: 1,
|
|
3933
|
+
kind,
|
|
3934
|
+
name,
|
|
3935
|
+
entry: snapshotEntryContent(entry),
|
|
3936
|
+
evidence
|
|
3937
|
+
})).digest("hex");
|
|
3938
|
+
}
|
|
3939
|
+
async function prepareSnapshotReview(access, kind, name, entry) {
|
|
3940
|
+
const files = "files" in entry ? entry.files : entry.chain;
|
|
3941
|
+
const evidence = [];
|
|
3942
|
+
const skeletons = [];
|
|
3943
|
+
for (const file of files.slice(0, VERIFY_MAX_FILES_PER_ENTRY)) {
|
|
3944
|
+
const full = await access.read(file);
|
|
3945
|
+
evidence.push({ path: file, hash: full ? sourceHash(full.content) : null });
|
|
3946
|
+
skeletons.push(full ? { path: full.path, content: full.content.slice(0, VERIFY_SKELETON_CHARS) } : { path: file, missing: true });
|
|
3947
|
+
}
|
|
3948
|
+
return { reviewToken: reviewToken(kind, name, entry, evidence), skeletons, truncated: files.length > VERIFY_MAX_FILES_PER_ENTRY };
|
|
3949
|
+
}
|
|
3950
|
+
function createSnapshotEvidenceReader(access) {
|
|
3951
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
3952
|
+
return async (kind, name, entry) => {
|
|
3953
|
+
const files = "files" in entry ? entry.files : entry.chain;
|
|
3954
|
+
const evidence = [];
|
|
3955
|
+
for (const file of files.slice(0, VERIFY_MAX_FILES_PER_ENTRY)) {
|
|
3956
|
+
let hash2 = hashes.get(file);
|
|
3957
|
+
if (!hash2) {
|
|
3958
|
+
hash2 = access.read(file).then((full) => full ? sourceHash(full.content) : null);
|
|
3959
|
+
hashes.set(file, hash2);
|
|
3960
|
+
}
|
|
3961
|
+
evidence.push({ path: file, hash: await hash2 });
|
|
3962
|
+
}
|
|
3963
|
+
return {
|
|
3964
|
+
reviewToken: reviewToken(kind, name, entry, evidence),
|
|
3965
|
+
evidenceAvailable: evidence.length > 0 && evidence.every((file) => file.hash !== null)
|
|
3966
|
+
};
|
|
3967
|
+
};
|
|
3968
|
+
}
|
|
3969
|
+
var VERIFY_MAX_FILES_PER_ENTRY, VERIFY_SKELETON_CHARS, sourceHash;
|
|
3970
|
+
var init_review = __esm({
|
|
3971
|
+
"src/snapshot/review.ts"() {
|
|
3972
|
+
"use strict";
|
|
3973
|
+
init_snapshot();
|
|
3974
|
+
VERIFY_MAX_FILES_PER_ENTRY = 8;
|
|
3975
|
+
VERIFY_SKELETON_CHARS = 500;
|
|
3976
|
+
sourceHash = (content2) => createHash5("sha256").update(content2).digest("hex");
|
|
3977
|
+
}
|
|
3978
|
+
});
|
|
3979
|
+
|
|
3980
|
+
// src/snapshot/trust.ts
|
|
3981
|
+
function createSnapshotTrustReader(root) {
|
|
3982
|
+
let reader;
|
|
3983
|
+
return async (kind, name, entry, freshness) => {
|
|
3984
|
+
const trust = assessTrust(entry, freshness);
|
|
3985
|
+
const verdict = entry.verificationFailed ? "failed" : entry.verifiedAt ? "passed" : void 0;
|
|
3986
|
+
if (!verdict) return trust;
|
|
3987
|
+
trust.recordedVerdict = verdict;
|
|
3988
|
+
const unknown = (reason2) => {
|
|
3989
|
+
trust.verification = "unknown";
|
|
3990
|
+
trust.reasons.push(reason2);
|
|
3991
|
+
return trust;
|
|
3992
|
+
};
|
|
3993
|
+
if (!entry.verificationToken || !/^[a-f0-9]{64}$/.test(entry.verificationToken)) return unknown("The recorded verdict has no supported evidence token; obtain a fresh verify_snapshot review.");
|
|
3994
|
+
try {
|
|
3995
|
+
reader ??= createFileAccess(root).then(createSnapshotEvidenceReader).catch(() => null);
|
|
3996
|
+
const readReview = await reader;
|
|
3997
|
+
if (!readReview) return unknown("Verification source evidence could not be read under the current file policy.");
|
|
3998
|
+
const review = await readReview(kind, name, entry);
|
|
3999
|
+
if (!review.evidenceAvailable) {
|
|
4000
|
+
return unknown("Verification source evidence is missing, excluded, unreadable, or empty; the recorded verdict cannot be confirmed.");
|
|
4001
|
+
}
|
|
4002
|
+
if (review.reviewToken !== entry.verificationToken) {
|
|
4003
|
+
trust.verification = "stale";
|
|
4004
|
+
trust.reasons.push("The entry or sampled source contents differ from the recorded verification evidence; obtain a fresh review.");
|
|
4005
|
+
}
|
|
4006
|
+
return trust;
|
|
4007
|
+
} catch {
|
|
4008
|
+
return unknown("Verification source evidence could not be read under the current file policy.");
|
|
4009
|
+
}
|
|
4010
|
+
};
|
|
4011
|
+
}
|
|
4012
|
+
async function readSnapshotTrustIndex(readTrust, snapshot, drift) {
|
|
4013
|
+
const features = /* @__PURE__ */ Object.create(null), flows = /* @__PURE__ */ Object.create(null);
|
|
4014
|
+
for (const [name, entry] of Object.entries(snapshot.features)) {
|
|
4015
|
+
features[name] = await readTrust("feature", name, entry, drift?.featureFreshness?.[name] ?? "unknown");
|
|
4016
|
+
}
|
|
4017
|
+
for (const [name, entry] of Object.entries(snapshot.flows)) {
|
|
4018
|
+
flows[name] = await readTrust("flow", name, entry, drift?.flowFreshness?.[name] ?? "unknown");
|
|
4019
|
+
}
|
|
4020
|
+
return { features, flows };
|
|
4021
|
+
}
|
|
4022
|
+
var init_trust2 = __esm({
|
|
4023
|
+
"src/snapshot/trust.ts"() {
|
|
4024
|
+
"use strict";
|
|
4025
|
+
init_trust();
|
|
4026
|
+
init_files();
|
|
4027
|
+
init_review();
|
|
4028
|
+
}
|
|
4029
|
+
});
|
|
4030
|
+
|
|
3862
4031
|
// src/setup/files.ts
|
|
3863
4032
|
var files_exports = {};
|
|
3864
4033
|
__export(files_exports, {
|
|
@@ -4893,7 +5062,7 @@ Mason provides recorded decisions and file impact over MCP. A concept map is opt
|
|
|
4893
5062
|
- Before editing a file \u2192 \`get_impact\` to check references, tests, and historical change partners.
|
|
4894
5063
|
- Investigation or correction reveals a reusable project lesson \u2192 \`save_decision\` before finishing. Capture only the lasting constraint and reason, with anchors and known sources, owner, and recorder. This creates a proposal; never infer approval. Preserve uncertain causes and unknown attribution. Route project lessons here; keep personal preferences in agent memory. Skip code summaries, current checkout status, temporary task permissions, session trivia, and secrets. No qualifying lesson means no record.
|
|
4895
5064
|
- Before saving, compare the lesson with matching \`get_context\` records and any pending proposal. If new evidence changes the same lesson's assumptions, scope, or recommended action, revise it with \`save_decision(id: existingId)\`, including when it is accepted. Preserve supported rationale and sources, replace obsolete instructions, and add the new evidence. Create a separate record for a genuinely distinct lesson; skip unchanged restatements. Revision preserves history and leaves the accepted version operative until authorized review.
|
|
4896
|
-
- Consult trust metadata before relying on entries: unknown or changed freshness requires inspection, and failed
|
|
5065
|
+
- Consult trust metadata before relying on entries: unknown or changed freshness requires inspection, stale or unknown map verification requires a fresh evidence review, and a recorded failed verdict means the description must be corrected. Check approval too: proposals are suggestions, legacy records are unreviewed, and accepted decisions are recorded constraints subject to freshness checks. An accepted revision remains operative while a pending proposal is reviewed; keep both versions and their freshness distinct.
|
|
4897
5066
|
- Asked to review or re-verify a decision \u2192 \`review_decision\` first to inspect content, sources, history, and code changes. Record acceptance, reaffirmation, or retirement only when authorized by the user or a cited team review, with the actual reviewer and reason. Never infer approval from unchanged code. Review and commit the local record through the normal project workflow.
|
|
4898
5067
|
- For an architectural overview, use \`get_snapshot\` if a map is available. If \`map.status\` is missing or invalid, use available decisions and source evidence; do not start building a map unless requested.
|
|
4899
5068
|
- \`mason_init\` returns documentation audit and committed-diff review results, plus a short setup guide. Pass \`evidence\` with local CI manifest paths to include test and analysis results; the CLI equivalent is \`mason-review --evidence <manifest>\`. State skipped, unavailable, stale, or unknown checks explicitly. Related accepted decisions identify review context, not proven violations.
|
|
@@ -5666,7 +5835,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
5666
5835
|
return finalize();
|
|
5667
5836
|
}
|
|
5668
5837
|
var exec5, MAX_ANALYZED_FILES;
|
|
5669
|
-
var
|
|
5838
|
+
var init_review2 = __esm({
|
|
5670
5839
|
"src/review/review.ts"() {
|
|
5671
5840
|
"use strict";
|
|
5672
5841
|
init_drift();
|
|
@@ -5755,7 +5924,7 @@ var init_onboarding = __esm({
|
|
|
5755
5924
|
"src/mcp/onboarding.ts"() {
|
|
5756
5925
|
"use strict";
|
|
5757
5926
|
init_audit();
|
|
5758
|
-
|
|
5927
|
+
init_review2();
|
|
5759
5928
|
init_drift();
|
|
5760
5929
|
init_snapshot();
|
|
5761
5930
|
init_provenance();
|
|
@@ -6171,6 +6340,7 @@ async function assembleContext(rootDir, task, files) {
|
|
|
6171
6340
|
};
|
|
6172
6341
|
}
|
|
6173
6342
|
const drift = await computeDrift(resolvedRoot);
|
|
6343
|
+
const readTrust = createSnapshotTrustReader(resolvedRoot);
|
|
6174
6344
|
const featureScores = Object.entries(snapshot.features).map(([name, feat]) => ({
|
|
6175
6345
|
name,
|
|
6176
6346
|
feat,
|
|
@@ -6197,14 +6367,7 @@ async function assembleContext(rootDir, task, files) {
|
|
|
6197
6367
|
Object.assign(bundle, await collectImpact(resolvedRoot, [...anchorFiles, ...Object.values(decisions).flatMap((d) => [...d.files, ...d.pendingProposal?.files ?? []])]));
|
|
6198
6368
|
bundle.diagnostics = store.diagnostics;
|
|
6199
6369
|
bundle.freshness = drift;
|
|
6200
|
-
bundle.trust =
|
|
6201
|
-
features: Object.fromEntries(Object.entries(snapshot.features).map(
|
|
6202
|
-
([name, entry]) => [name, assessTrust(entry, drift?.featureFreshness?.[name] ?? "unknown")]
|
|
6203
|
-
)),
|
|
6204
|
-
flows: Object.fromEntries(Object.entries(snapshot.flows).map(
|
|
6205
|
-
([name, entry]) => [name, assessTrust(entry, drift?.flowFreshness?.[name] ?? "unknown")]
|
|
6206
|
-
))
|
|
6207
|
-
};
|
|
6370
|
+
bundle.trust = await readSnapshotTrustIndex(readTrust, snapshot, drift);
|
|
6208
6371
|
bundle.hint += " " + trustHint([
|
|
6209
6372
|
...Object.values(bundle.trust.features),
|
|
6210
6373
|
...Object.values(bundle.trust.flows),
|
|
@@ -6217,7 +6380,7 @@ async function assembleContext(rootDir, task, files) {
|
|
|
6217
6380
|
const features = {};
|
|
6218
6381
|
const staleMatches = [];
|
|
6219
6382
|
for (const { name, feat, score } of featureScores) {
|
|
6220
|
-
const trust =
|
|
6383
|
+
const trust = await readTrust("feature", name, feat, drift?.featureFreshness?.[name] ?? "unknown");
|
|
6221
6384
|
const stale2 = trust.freshness !== "current";
|
|
6222
6385
|
if (stale2) staleMatches.push(name);
|
|
6223
6386
|
features[name] = {
|
|
@@ -6232,7 +6395,7 @@ async function assembleContext(rootDir, task, files) {
|
|
|
6232
6395
|
}
|
|
6233
6396
|
const flows = {};
|
|
6234
6397
|
for (const { name, flow, score } of flowScores) {
|
|
6235
|
-
const trust =
|
|
6398
|
+
const trust = await readTrust("flow", name, flow, drift?.flowFreshness?.[name] ?? "unknown");
|
|
6236
6399
|
const stale2 = trust.freshness !== "current";
|
|
6237
6400
|
if (stale2) staleMatches.push(name);
|
|
6238
6401
|
flows[name] = {
|
|
@@ -6317,6 +6480,7 @@ var init_assemble = __esm({
|
|
|
6317
6480
|
"use strict";
|
|
6318
6481
|
init_snapshot();
|
|
6319
6482
|
init_files();
|
|
6483
|
+
init_trust2();
|
|
6320
6484
|
init_drift();
|
|
6321
6485
|
init_impact();
|
|
6322
6486
|
init_lexical();
|
|
@@ -6979,9 +7143,9 @@ var init_renderer = __esm({
|
|
|
6979
7143
|
// src/confluence/diff.ts
|
|
6980
7144
|
import fs24 from "fs/promises";
|
|
6981
7145
|
import path33 from "path";
|
|
6982
|
-
import { createHash as
|
|
7146
|
+
import { createHash as createHash6 } from "crypto";
|
|
6983
7147
|
function hashDescription(description) {
|
|
6984
|
-
return
|
|
7148
|
+
return createHash6("sha256").update(description, "utf8").digest("hex");
|
|
6985
7149
|
}
|
|
6986
7150
|
function syncStateDir(rootDir) {
|
|
6987
7151
|
return path33.join(rootDir, ".mason");
|
|
@@ -8207,8 +8371,8 @@ async function reviewState(root, record) {
|
|
|
8207
8371
|
changedFiles: touchedPaths(committed),
|
|
8208
8372
|
localChanges: matchingPaths(anchors, workingTree.changedFiles)
|
|
8209
8373
|
};
|
|
8210
|
-
const
|
|
8211
|
-
return { reviewToken, evidence, committed, workingTreeAvailable: workingTree.available };
|
|
8374
|
+
const reviewToken2 = createHash4("sha256").update(JSON.stringify({ record, evidence, workingTreeAvailable: workingTree.available })).digest("hex");
|
|
8375
|
+
return { reviewToken: reviewToken2, evidence, committed, workingTreeAvailable: workingTree.available };
|
|
8212
8376
|
}
|
|
8213
8377
|
async function previews(root, record, state) {
|
|
8214
8378
|
const access = await createFileAccess(root);
|
|
@@ -8314,7 +8478,9 @@ async function reviewDecision(root, input) {
|
|
|
8314
8478
|
// src/mcp/tools.ts
|
|
8315
8479
|
init_drift2();
|
|
8316
8480
|
init_snapshot();
|
|
8481
|
+
init_trust2();
|
|
8317
8482
|
init_drift();
|
|
8483
|
+
init_review();
|
|
8318
8484
|
|
|
8319
8485
|
// src/snapshot/prompt.ts
|
|
8320
8486
|
var BATCH_SYSTEM_PROMPT = `You are Mason, building one piece of a larger concept-to-files map via a Map-Reduce pattern.
|
|
@@ -8411,6 +8577,7 @@ ${JSON.stringify({ partials }, null, 2)}`;
|
|
|
8411
8577
|
}
|
|
8412
8578
|
|
|
8413
8579
|
// src/snapshot/partials.ts
|
|
8580
|
+
init_lock();
|
|
8414
8581
|
init_storage();
|
|
8415
8582
|
init_snapshot();
|
|
8416
8583
|
import fs16 from "fs/promises";
|
|
@@ -8426,7 +8593,7 @@ var partialSchema = z11.object({
|
|
|
8426
8593
|
var scopeSchema2 = z11.object({ files: z11.array(z11.string()), savedAt: z11.string() });
|
|
8427
8594
|
async function savePartial(rootDir, partial) {
|
|
8428
8595
|
if (!/^[a-zA-Z0-9_-]+$/.test(partial.batchId)) throw new Error(`Invalid batchId: ${partial.batchId}`);
|
|
8429
|
-
await writeStoreJson(rootDir, `${DIRECTORY}/${partial.batchId}.json`, partialSchema.parse(partial));
|
|
8596
|
+
await withSnapshotWrite(rootDir, () => writeStoreJson(rootDir, `${DIRECTORY}/${partial.batchId}.json`, partialSchema.parse(partial)));
|
|
8430
8597
|
}
|
|
8431
8598
|
async function loadAllPartials(rootDir) {
|
|
8432
8599
|
let entries;
|
|
@@ -8446,14 +8613,16 @@ async function loadAllPartials(rootDir) {
|
|
|
8446
8613
|
return partials.sort((a, b) => a.offset - b.offset);
|
|
8447
8614
|
}
|
|
8448
8615
|
async function saveScope(rootDir, files) {
|
|
8449
|
-
await writeStoreJson(rootDir, `${DIRECTORY}/scope.json`, { files, savedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
8616
|
+
await withSnapshotWrite(rootDir, () => writeStoreJson(rootDir, `${DIRECTORY}/scope.json`, { files, savedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
8450
8617
|
}
|
|
8451
8618
|
async function loadScope(rootDir) {
|
|
8452
8619
|
const raw = await readStoreJson(rootDir, `${DIRECTORY}/scope.json`);
|
|
8453
8620
|
return raw === null ? null : scopeSchema2.parse(raw).files;
|
|
8454
8621
|
}
|
|
8455
8622
|
async function clearScope(rootDir) {
|
|
8456
|
-
await
|
|
8623
|
+
await withSnapshotWrite(rootDir, async () => {
|
|
8624
|
+
await fs16.rm(await storePath(rootDir, `${DIRECTORY}/scope.json`), { force: true });
|
|
8625
|
+
});
|
|
8457
8626
|
}
|
|
8458
8627
|
async function clearAllPartials(rootDir) {
|
|
8459
8628
|
await fs16.rm(await storePath(rootDir, DIRECTORY), { recursive: true, force: true });
|
|
@@ -8719,9 +8888,9 @@ async function getSnapshot(dir) {
|
|
|
8719
8888
|
const store = await loadDecisionStore2(rootDir);
|
|
8720
8889
|
const decisionRecords = store.records;
|
|
8721
8890
|
const decisionDrift = await computeDecisionDrift(rootDir, decisionRecords);
|
|
8891
|
+
const readTrust = createSnapshotTrustReader(rootDir);
|
|
8722
8892
|
const trust = {
|
|
8723
|
-
|
|
8724
|
-
flows: Object.fromEntries(Object.entries(snapshot.flows).map(([name, entry]) => [name, assessTrust(entry, drift?.flowFreshness?.[name] ?? "unknown")])),
|
|
8893
|
+
...await readSnapshotTrustIndex(readTrust, snapshot, drift),
|
|
8725
8894
|
decisions: Object.fromEntries(decisionRecords.filter((d) => d.status === "active").map((d) => [d.id, decisionTrust(effectiveDecision(d), decisionDrift.freshness?.[d.id] ?? "unknown")]))
|
|
8726
8895
|
};
|
|
8727
8896
|
output.trust = trust;
|
|
@@ -8981,86 +9150,82 @@ function sanitizePaths(rootDir, files) {
|
|
|
8981
9150
|
}
|
|
8982
9151
|
async function saveSnapshotData(dir, features, flows, removeFeatures = [], removeFlows = []) {
|
|
8983
9152
|
const rootDir = path34.resolve(dir);
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9153
|
+
return updateSnapshot(rootDir, async (previous) => {
|
|
9154
|
+
const gitHash = await getCurrentGitHash(rootDir);
|
|
9155
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9156
|
+
for (const feat of Object.values(features)) {
|
|
9157
|
+
feat.files = sanitizePaths(rootDir, feat.files);
|
|
9158
|
+
if (feat.tests) feat.tests = sanitizePaths(rootDir, feat.tests);
|
|
9159
|
+
feat.type = normalizeFeatureType(feat.type);
|
|
9160
|
+
}
|
|
9161
|
+
for (const flow of Object.values(flows)) {
|
|
9162
|
+
flow.chain = sanitizePaths(rootDir, flow.chain);
|
|
9163
|
+
}
|
|
9164
|
+
const partials = await loadAllPartials(rootDir);
|
|
9165
|
+
const replaceMode = partials.length > 0;
|
|
9166
|
+
const existing = replaceMode ? null : previous;
|
|
9167
|
+
const preserveVerification = (next, old) => {
|
|
9168
|
+
delete next.verifiedAt;
|
|
9169
|
+
delete next.verifiedHash;
|
|
9170
|
+
delete next.verificationToken;
|
|
9171
|
+
delete next.verificationFailed;
|
|
9172
|
+
delete next.verificationNote;
|
|
9173
|
+
if (!old) return;
|
|
9174
|
+
if (JSON.stringify(snapshotEntryContent(next)) !== JSON.stringify(snapshotEntryContent(old))) return;
|
|
9175
|
+
next.verificationToken = old.verificationToken;
|
|
9176
|
+
next.verifiedAt = old.verifiedAt;
|
|
9177
|
+
next.verifiedHash = old.verifiedHash;
|
|
9178
|
+
next.verificationFailed = old.verificationFailed;
|
|
9179
|
+
next.verificationNote = old.verificationNote;
|
|
9180
|
+
};
|
|
9181
|
+
for (const [name, entry] of Object.entries(features)) preserveVerification(entry, previous?.features[name]);
|
|
9182
|
+
for (const [name, entry] of Object.entries(flows)) preserveVerification(entry, previous?.flows[name]);
|
|
9183
|
+
if (existing) {
|
|
9184
|
+
if (existing.gitHash !== "unknown") {
|
|
9185
|
+
for (const feat of Object.values(existing.features)) {
|
|
9186
|
+
feat.refreshedHash ??= existing.gitHash;
|
|
9187
|
+
}
|
|
9188
|
+
for (const flow of Object.values(existing.flows)) {
|
|
9189
|
+
flow.refreshedHash ??= existing.gitHash;
|
|
9190
|
+
}
|
|
9019
9191
|
}
|
|
9020
|
-
|
|
9021
|
-
|
|
9192
|
+
const removedFeatures = removeFeatures.filter(
|
|
9193
|
+
(name) => name in existing.features
|
|
9194
|
+
);
|
|
9195
|
+
const removedFlows = removeFlows.filter((name) => name in existing.flows);
|
|
9196
|
+
for (const name of removedFeatures) delete existing.features[name];
|
|
9197
|
+
for (const name of removedFlows) delete existing.flows[name];
|
|
9198
|
+
if (gitHash !== "unknown") {
|
|
9199
|
+
for (const feat of Object.values(features)) feat.refreshedHash = gitHash;
|
|
9200
|
+
for (const flow of Object.values(flows)) flow.refreshedHash = gitHash;
|
|
9022
9201
|
}
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
version: 2,
|
|
9051
|
-
createdAt: now,
|
|
9052
|
-
updatedAt: now,
|
|
9053
|
-
gitHash,
|
|
9054
|
-
features,
|
|
9055
|
-
flows
|
|
9056
|
-
};
|
|
9057
|
-
await saveSnapshot(rootDir, snapshot);
|
|
9058
|
-
await clearAllPartials(rootDir);
|
|
9059
|
-
return JSON.stringify({
|
|
9060
|
-
status: replaceMode ? "replaced" : "created",
|
|
9061
|
-
mode: replaceMode ? "replaced-from-partials" : "fresh",
|
|
9062
|
-
features: Object.keys(features).length,
|
|
9063
|
-
flows: Object.keys(flows).length
|
|
9202
|
+
existing.features = { ...existing.features, ...features };
|
|
9203
|
+
existing.flows = { ...existing.flows, ...flows };
|
|
9204
|
+
existing.updatedAt = now;
|
|
9205
|
+
existing.gitHash = gitHash;
|
|
9206
|
+
return { snapshot: existing, afterSave: () => clearAllPartials(rootDir), result: JSON.stringify({
|
|
9207
|
+
status: "updated",
|
|
9208
|
+
mode: "merged",
|
|
9209
|
+
features: Object.keys(existing.features).length,
|
|
9210
|
+
flows: Object.keys(existing.flows).length,
|
|
9211
|
+
removedFeatures: removedFeatures.length,
|
|
9212
|
+
removedFlows: removedFlows.length
|
|
9213
|
+
}) };
|
|
9214
|
+
}
|
|
9215
|
+
const snapshot = {
|
|
9216
|
+
version: 2,
|
|
9217
|
+
createdAt: now,
|
|
9218
|
+
updatedAt: now,
|
|
9219
|
+
gitHash,
|
|
9220
|
+
features,
|
|
9221
|
+
flows
|
|
9222
|
+
};
|
|
9223
|
+
return { snapshot, afterSave: () => clearAllPartials(rootDir), result: JSON.stringify({
|
|
9224
|
+
status: replaceMode ? "replaced" : "created",
|
|
9225
|
+
mode: replaceMode ? "replaced-from-partials" : "fresh",
|
|
9226
|
+
features: Object.keys(features).length,
|
|
9227
|
+
flows: Object.keys(flows).length
|
|
9228
|
+
}) };
|
|
9064
9229
|
});
|
|
9065
9230
|
}
|
|
9066
9231
|
async function getImpact(dir, files) {
|
|
@@ -9070,8 +9235,6 @@ async function getImpact(dir, files) {
|
|
|
9070
9235
|
return JSON.stringify(result, null, 2);
|
|
9071
9236
|
}
|
|
9072
9237
|
var VERIFY_DEFAULT_SAMPLE = 5;
|
|
9073
|
-
var VERIFY_MAX_FILES_PER_ENTRY = 8;
|
|
9074
|
-
var VERIFY_SKELETON_CHARS = 500;
|
|
9075
9238
|
async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
|
|
9076
9239
|
const rootDir = path34.resolve(dir);
|
|
9077
9240
|
const snapshot = await loadSnapshot(rootDir);
|
|
@@ -9107,25 +9270,13 @@ async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
|
|
|
9107
9270
|
const picked = entries.slice(0, Math.max(1, sample));
|
|
9108
9271
|
const toVerify = [];
|
|
9109
9272
|
for (const entry of picked) {
|
|
9110
|
-
const
|
|
9111
|
-
for (const filePath of entry.files.slice(0, VERIFY_MAX_FILES_PER_ENTRY)) {
|
|
9112
|
-
const full = await access.read(filePath);
|
|
9113
|
-
if (full) {
|
|
9114
|
-
skeletons.push({
|
|
9115
|
-
path: full.path,
|
|
9116
|
-
content: full.content.slice(0, VERIFY_SKELETON_CHARS)
|
|
9117
|
-
});
|
|
9118
|
-
} else {
|
|
9119
|
-
skeletons.push({ path: filePath, missing: true });
|
|
9120
|
-
}
|
|
9121
|
-
}
|
|
9273
|
+
const stored = entry.kind === "feature" ? snapshot.features[entry.name] : snapshot.flows[entry.name];
|
|
9122
9274
|
toVerify.push({
|
|
9123
9275
|
name: entry.name,
|
|
9124
9276
|
kind: entry.kind,
|
|
9125
9277
|
description: entry.description,
|
|
9126
9278
|
lastVerified: entry.verifiedAt ?? "never",
|
|
9127
|
-
|
|
9128
|
-
truncated: entry.files.length > VERIFY_MAX_FILES_PER_ENTRY
|
|
9279
|
+
...await prepareSnapshotReview(access, entry.kind, entry.name, stored)
|
|
9129
9280
|
});
|
|
9130
9281
|
}
|
|
9131
9282
|
const neverVerified = entries.filter((e) => !e.verifiedAt).length;
|
|
@@ -9134,45 +9285,81 @@ async function verifySnapshot(dir, sample = VERIFY_DEFAULT_SAMPLE) {
|
|
|
9134
9285
|
totalEntries: entries.length,
|
|
9135
9286
|
neverVerified,
|
|
9136
9287
|
entries: toVerify,
|
|
9137
|
-
instructions: '
|
|
9288
|
+
instructions: 'Judge whether the files implement each claimed feature/flow, then call save_verification with verdicts: {"<entry name>": {"kind": "<feature|flow from the entry>", "reviewToken": "<token from the entry>", "ok": true|false, "note": "<required when ok is false>"}}. Tokens cover the entry and full contents of the previewed files (at most 8), including unavailable-file markers; previews themselves are truncated. This is a spot-check, not proof of unshown files. A conflict requires verify_snapshot and a fresh review; never reuse a verdict for changed content. If a feature and flow share a name, submit their verdicts in separate calls.'
|
|
9138
9289
|
});
|
|
9139
9290
|
}
|
|
9140
9291
|
async function saveVerification(dir, verdicts) {
|
|
9141
9292
|
const rootDir = path34.resolve(dir);
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9293
|
+
return updateSnapshot(rootDir, async (snapshot) => {
|
|
9294
|
+
if (!snapshot) {
|
|
9295
|
+
return { snapshot: null, result: JSON.stringify({ exists: false, stamped: [], hint: "No concept map exists. Build it and run verify_snapshot before recording verdicts." }) };
|
|
9296
|
+
}
|
|
9297
|
+
const stamped = [], unknown = [], failed = [];
|
|
9298
|
+
const conflicts = [], reviewRequired = [], invalid = [];
|
|
9299
|
+
const candidates = [];
|
|
9300
|
+
let access;
|
|
9301
|
+
for (const [name, verdict] of Object.entries(verdicts)) {
|
|
9302
|
+
if (!verdict.kind || !["feature", "flow"].includes(verdict.kind) || !/^[a-f0-9]{64}$/.test(verdict.reviewToken ?? "")) {
|
|
9303
|
+
reviewRequired.push(name);
|
|
9304
|
+
continue;
|
|
9305
|
+
}
|
|
9306
|
+
if (typeof verdict.ok !== "boolean" || !verdict.ok && !verdict.note?.trim()) {
|
|
9307
|
+
invalid.push(name);
|
|
9308
|
+
continue;
|
|
9309
|
+
}
|
|
9310
|
+
const collection = verdict.kind === "feature" ? snapshot.features : snapshot.flows;
|
|
9311
|
+
if (!Object.hasOwn(collection, name)) {
|
|
9312
|
+
unknown.push(name);
|
|
9313
|
+
conflicts.push(name);
|
|
9314
|
+
continue;
|
|
9315
|
+
}
|
|
9316
|
+
const entry = collection[name];
|
|
9317
|
+
access ??= await createFileAccess(rootDir);
|
|
9318
|
+
const current = await prepareSnapshotReview(access, verdict.kind, name, entry);
|
|
9319
|
+
if (current.reviewToken !== verdict.reviewToken) conflicts.push(name);
|
|
9320
|
+
else candidates.push({ name, verdict, entry });
|
|
9321
|
+
}
|
|
9322
|
+
const verifiedHash = candidates.length ? await getCurrentGitHash(rootDir) : "unknown";
|
|
9323
|
+
const freshAccess = candidates.length ? await createFileAccess(rootDir) : null;
|
|
9324
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9325
|
+
for (const { name, verdict, entry } of candidates) {
|
|
9326
|
+
const current = await prepareSnapshotReview(freshAccess, verdict.kind, name, entry);
|
|
9327
|
+
if (current.reviewToken !== verdict.reviewToken) {
|
|
9328
|
+
conflicts.push(name);
|
|
9329
|
+
continue;
|
|
9330
|
+
}
|
|
9331
|
+
entry.verifiedAt = now;
|
|
9332
|
+
entry.verifiedHash = verifiedHash;
|
|
9333
|
+
entry.verificationToken = verdict.reviewToken;
|
|
9334
|
+
if (verdict.ok) {
|
|
9335
|
+
delete entry.verificationFailed;
|
|
9336
|
+
delete entry.verificationNote;
|
|
9337
|
+
} else {
|
|
9338
|
+
entry.verificationFailed = true;
|
|
9339
|
+
entry.verificationNote = verdict.note.trim();
|
|
9340
|
+
failed.push(name);
|
|
9341
|
+
}
|
|
9342
|
+
stamped.push(name);
|
|
9343
|
+
}
|
|
9344
|
+
const rejected = conflicts.length + reviewRequired.length + invalid.length;
|
|
9345
|
+
const status = rejected ? stamped.length ? "partial" : conflicts.length ? "conflict" : reviewRequired.length ? "review_required" : "invalid" : stamped.length ? "saved" : "unchanged";
|
|
9346
|
+
const hints = [];
|
|
9347
|
+
if (conflicts.length) hints.push(`Entries [${conflicts.join(", ")}] changed or were deleted after review. Run verify_snapshot and review current evidence before retrying.`);
|
|
9348
|
+
if (reviewRequired.length) hints.push(`Entries [${reviewRequired.join(", ")}] need kind and reviewToken from verify_snapshot. Inspect that evidence before submitting a verdict.`);
|
|
9349
|
+
if (invalid.length) hints.push(`Entries [${invalid.join(", ")}] need a boolean ok and a non-empty note when ok is false.`);
|
|
9350
|
+
if (failed.length) hints.push(`Entries [${failed.join(", ")}] are mis-mapped. Re-map them: read their actual files, correct the entries, and call save_snapshot with only those entries (plus removeFeatures/removeFlows if a concept no longer exists). Then run verify_snapshot for fresh review tokens.`);
|
|
9351
|
+
if (!hints.length) hints.push(stamped.length ? "All submitted entries verified against the reviewed evidence. Re-run verify_snapshot periodically." : "No verdicts submitted; nothing changed.");
|
|
9352
|
+
if (stamped.length) snapshot.updatedAt = now;
|
|
9353
|
+
return { snapshot: stamped.length ? snapshot : null, result: JSON.stringify({
|
|
9354
|
+
status,
|
|
9355
|
+
stamped,
|
|
9356
|
+
unknown,
|
|
9357
|
+
failed,
|
|
9358
|
+
conflicts,
|
|
9359
|
+
reviewRequired,
|
|
9360
|
+
invalid,
|
|
9361
|
+
hint: hints.join(" ")
|
|
9362
|
+
}) };
|
|
9176
9363
|
});
|
|
9177
9364
|
}
|
|
9178
9365
|
async function saveDecision(dir, input) {
|
|
@@ -9402,10 +9589,10 @@ function createMcpServer() {
|
|
|
9402
9589
|
const server = new McpServer(
|
|
9403
9590
|
{
|
|
9404
9591
|
name: "mason",
|
|
9405
|
-
version: "0.
|
|
9592
|
+
version: "0.18.0"
|
|
9406
9593
|
},
|
|
9407
9594
|
{
|
|
9408
|
-
instructions: (setupNotice ? setupNotice + "\n\n" : "") + 'Mason retrieves recorded decisions, file impact, and optional feature/flow maps. Use get_context with the task and known files, and get_impact before editing. Save learned rationale and constraints with save_decision; review and commit the local records through the project workflow. These tools work without initialization or a concept map. Consult trust and diagnostics: changed or unknown freshness needs source inspection,
|
|
9595
|
+
instructions: (setupNotice ? setupNotice + "\n\n" : "") + 'Mason retrieves recorded decisions, file impact, and optional feature/flow maps. Use get_context with the task and known files, and get_impact before editing. Save learned rationale and constraints with save_decision; review and commit the local records through the project workflow. These tools work without initialization or a concept map. Consult trust and diagnostics: changed or unknown freshness needs source inspection, stale or unknown map verification needs a fresh review, recorded failed verdicts need correction, and proposals are suggestions and legacy records are unreviewed. Accepted decisions are recorded team constraints, still subject to freshness checks. Use review_decision to prepare code evidence and record only authorized acceptance, reaffirmation, or retirement. Never invent a reviewer or treat these recorded identities as authenticated approval. mason_init returns documentation audit and committed-diff review findings with a quickstart guide. Use mode: "map" only when a full architecture map is requested. A missing map is not a setup failure; use decisions and source evidence. get_snapshot provides architecture navigation when a map is available. The mason-audit and mason-review CLIs also work without setup.'
|
|
9409
9596
|
}
|
|
9410
9597
|
);
|
|
9411
9598
|
server.tool(
|
|
@@ -9527,7 +9714,7 @@ function createMcpServer() {
|
|
|
9527
9714
|
);
|
|
9528
9715
|
server.tool(
|
|
9529
9716
|
"get_snapshot",
|
|
9530
|
-
"Return the optional feature-to-file architecture map with drift and trust evidence. If no map exists, returns exists:false plus project structure, Git signals, and test pairs. Decision capture, get_context, and get_impact still work. No initialization required.",
|
|
9717
|
+
"Return the optional feature-to-file architecture map with drift and trust evidence. Map verification is rechecked against sampled source contents; stale or unknown verification requires a fresh review, and recordedVerdict preserves the historical verdict. If no map exists, returns exists:false plus project structure, Git signals, and test pairs. Decision capture, get_context, and get_impact still work. No initialization required.",
|
|
9531
9718
|
{
|
|
9532
9719
|
dir: z24.string().describe("Absolute path to the project root directory")
|
|
9533
9720
|
},
|
|
@@ -9540,7 +9727,7 @@ function createMcpServer() {
|
|
|
9540
9727
|
);
|
|
9541
9728
|
server.tool(
|
|
9542
9729
|
"get_context",
|
|
9543
|
-
"Assemble task context: matching decisions with rationale, approval, owner, sources, last review, and freshness, plus related tests, file impact, and any available map entries. Proposals are suggestions; legacy records are unreviewed; accepted decisions are constraints subject to freshness. No initialization or map required. Pass task and optional files. map.status and diagnostics preserve missing or invalid knowledge. Impact covers up to three unique files, expanding directory anchors.",
|
|
9730
|
+
"Assemble task context: matching decisions with rationale, approval, owner, sources, last review, and freshness, plus related tests, file impact, and any available map entries. Map verification is rechecked against sampled source contents; stale or unknown verification requires a fresh review, and recordedVerdict preserves the historical verdict. Proposals are suggestions; legacy records are unreviewed; accepted decisions are constraints subject to freshness. No initialization or map required. Pass task and optional files. map.status and diagnostics preserve missing or invalid knowledge. Impact covers up to three unique files, expanding directory anchors.",
|
|
9544
9731
|
{
|
|
9545
9732
|
dir: z24.string().describe("Absolute path to the project root directory"),
|
|
9546
9733
|
task: z24.string().describe("The task, bug, or change request in natural language \u2014 e.g. 'add rate limiting to the API client' or a ticket description"),
|
|
@@ -9716,7 +9903,7 @@ function createMcpServer() {
|
|
|
9716
9903
|
);
|
|
9717
9904
|
server.tool(
|
|
9718
9905
|
"verify_snapshot",
|
|
9719
|
-
"Spot-check the concept map's CORRECTNESS (drift checks freshness; this checks entries were right to begin with). Returns a sample of entries \u2014 always the never-verified and least-recently-verified first \u2014 with skeletons of their claimed files, for you to judge whether the files actually implement what the entry claims. Report verdicts back via save_verification. Run periodically, or after an automated refresh wrote entries no human reviewed.",
|
|
9906
|
+
"Spot-check the concept map's CORRECTNESS (drift checks freshness; this checks entries were right to begin with). Returns a sample of entries \u2014 always the never-verified and least-recently-verified first \u2014 with skeletons of their claimed files, for you to judge whether the files actually implement what the entry claims. Report verdicts back via save_verification with each entry\u2019s kind and reviewToken. Tokens bind the entry and bounded source evidence; changed evidence requires a fresh review. Run periodically, or after an automated refresh wrote entries no human reviewed.",
|
|
9720
9907
|
{
|
|
9721
9908
|
dir: z24.string().describe("Absolute path to the project root directory"),
|
|
9722
9909
|
sample: z24.number().int().optional().describe("Entries to sample (default 5)")
|
|
@@ -9728,11 +9915,13 @@ function createMcpServer() {
|
|
|
9728
9915
|
);
|
|
9729
9916
|
server.tool(
|
|
9730
9917
|
"save_verification",
|
|
9731
|
-
"Record verify_snapshot verdicts. Entries judged ok are stamped verifiedAt; failures are flagged verificationFailed with your note and surface in get_context, get_snapshot, and mason_check_drift until corrected. Verdict notes are required for failures.",
|
|
9918
|
+
"Record verify_snapshot verdicts using each entry\u2019s kind and reviewToken. Missing tokens request a fresh review; changed or deleted entries return conflicts without being stamped. Entries judged ok are stamped verifiedAt; failures are flagged verificationFailed with your note and surface in get_context, get_snapshot, and mason_check_drift until corrected. Verdict notes are required for failures.",
|
|
9732
9919
|
{
|
|
9733
9920
|
dir: z24.string().describe("Absolute path to the project root directory"),
|
|
9734
9921
|
verdicts: z24.record(
|
|
9735
9922
|
z24.object({
|
|
9923
|
+
kind: z24.enum(["feature", "flow"]).optional().describe("Entry kind returned by verify_snapshot; required to record a verdict"),
|
|
9924
|
+
reviewToken: z24.string().optional().describe("Copy the reviewToken from verify_snapshot after inspecting its evidence; required to record a verdict"),
|
|
9736
9925
|
ok: z24.boolean(),
|
|
9737
9926
|
note: z24.string().optional().describe("One line on what's wrong \u2014 required when ok is false")
|
|
9738
9927
|
})
|