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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.0 — 2026-09-16
|
|
4
|
+
|
|
5
|
+
- Fix overlapping snapshot writes restoring old descriptions and deleted flows. Snapshot saves, verdicts, and partial writes share a per-checkout cross-process lock; reads, replacement, and consolidation cleanup stay within the protected operation.
|
|
6
|
+
- Bind map verification to entry kind, content, and bounded source evidence through review tokens. Changed or deleted entries return conflicts; older calls without tokens request a fresh review without recording a verdict. Existing snapshot files remain compatible; callers must include the returned entry kind and review token to record a verdict. Restart all Mason MCP processes for a checkout after upgrading; older binaries do not honor the new snapshot lock.
|
|
7
|
+
- Recheck saved map verification evidence on retrieval. Changed evidence reports stale verification; unavailable evidence and legacy verdicts without tokens report unknown. Preserve historical verdicts and failure reasons, including when reviewed local edits are reverted to a clean checkout.
|
|
8
|
+
- Diagnose incomplete lock owners and interrupted recovery guards, document safe manual cleanup after writers stop, and cover both interruption windows with controlled process-termination tests.
|
|
9
|
+
- Cover the reported overwrite with a controlled regression, exercise both call orders through one MCP session and separate processes, and test interrupted writers, contention, partial cleanup, and stale review evidence. Run the snapshot regressions in the existing macOS, Linux, and Windows distribution matrix before publishing.
|
|
10
|
+
|
|
11
|
+
Run `mason upgrade` for standalone installations, or `npm install -g mason-context@0.18.0` for npm installations. Restart **every Mason MCP process** sharing a checkout after upgrading: older processes do not participate in the new snapshot lock. Existing decisions and snapshot files are preserved. Older snapshot verdicts remain visible but report unknown verification until reviewed again with an evidence token. Existing integrations do not need setup again.
|
|
12
|
+
|
|
3
13
|
## 0.17.4 — 2026-09-15
|
|
4
14
|
|
|
5
15
|
- Share Git document/source inventories within each automation inspection, with a separate fresh inventory before publication. Batch module ignore checks by directory level and reuse only identical scoped history queries.
|
package/dist/mason-audit.js
CHANGED
|
@@ -234,6 +234,102 @@ var init_storage = __esm({
|
|
|
234
234
|
}
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
+
// src/utils/store-lock.ts
|
|
238
|
+
import fs3 from "fs/promises";
|
|
239
|
+
import os from "os";
|
|
240
|
+
async function lockDiagnostic(file) {
|
|
241
|
+
let reason;
|
|
242
|
+
try {
|
|
243
|
+
const owner = JSON.parse(await fs3.readFile(file, "utf8"));
|
|
244
|
+
if (!owner || typeof owner.host !== "string" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
|
|
245
|
+
reason = "Lock owner data is incomplete or malformed.";
|
|
246
|
+
} else if (owner.host !== os.hostname()) {
|
|
247
|
+
reason = "The lock belongs to another host; its process cannot be checked locally.";
|
|
248
|
+
} else {
|
|
249
|
+
try {
|
|
250
|
+
process.kill(owner.pid, 0);
|
|
251
|
+
reason = `Local owner PID ${owner.pid} is still running.`;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
reason = 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.`;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
reason = error instanceof SyntaxError ? "Lock owner data is incomplete or malformed." : "Lock owner data could not be read; the lock may have changed.";
|
|
258
|
+
}
|
|
259
|
+
if (await fs3.lstat(file + ".reclaim").then(() => true, () => false)) {
|
|
260
|
+
reason += " A recovery guard also needs inspection: " + file + ".reclaim.";
|
|
261
|
+
}
|
|
262
|
+
return reason + " 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.";
|
|
263
|
+
}
|
|
264
|
+
async function withStoreLock(root, directory, run, waitMs = 5e3, label = "Mason store") {
|
|
265
|
+
const file = await storePath(root, directory + "/lock", true);
|
|
266
|
+
const deadline = Date.now() + waitMs;
|
|
267
|
+
let handle;
|
|
268
|
+
while (!handle) {
|
|
269
|
+
try {
|
|
270
|
+
handle = await fs3.open(file, "wx", 384);
|
|
271
|
+
try {
|
|
272
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
|
|
273
|
+
} catch (error) {
|
|
274
|
+
await handle.close().catch(() => {
|
|
275
|
+
});
|
|
276
|
+
handle = void 0;
|
|
277
|
+
await fs3.rm(file, { force: true }).catch(() => {
|
|
278
|
+
});
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (error.code !== "EEXIST") throw error;
|
|
283
|
+
try {
|
|
284
|
+
const owner = JSON.parse(await fs3.readFile(file, "utf8"));
|
|
285
|
+
if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
286
|
+
try {
|
|
287
|
+
process.kill(owner.pid, 0);
|
|
288
|
+
} catch (probe) {
|
|
289
|
+
if (probe.code === "ESRCH") {
|
|
290
|
+
const reclaim = file + ".reclaim";
|
|
291
|
+
let guard;
|
|
292
|
+
try {
|
|
293
|
+
guard = await fs3.open(reclaim, "wx", 384);
|
|
294
|
+
const current = JSON.parse(await fs3.readFile(file, "utf8"));
|
|
295
|
+
if (current.pid === owner.pid && current.host === owner.host) await fs3.unlink(file);
|
|
296
|
+
} finally {
|
|
297
|
+
if (guard) {
|
|
298
|
+
await guard.close();
|
|
299
|
+
await fs3.rm(reclaim, { force: true });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
if (Date.now() >= deadline) throw new Error(label + " is busy or its lock needs inspection: " + file + ". " + await lockDiagnostic(file));
|
|
308
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
return await run();
|
|
313
|
+
} finally {
|
|
314
|
+
await handle.close();
|
|
315
|
+
await fs3.unlink(file);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
var init_store_lock = __esm({
|
|
319
|
+
"src/utils/store-lock.ts"() {
|
|
320
|
+
"use strict";
|
|
321
|
+
init_storage();
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// src/snapshot/lock.ts
|
|
326
|
+
var init_lock = __esm({
|
|
327
|
+
"src/snapshot/lock.ts"() {
|
|
328
|
+
"use strict";
|
|
329
|
+
init_store_lock();
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
237
333
|
// src/test-map.ts
|
|
238
334
|
import path4 from "path";
|
|
239
335
|
var init_test_map = __esm({
|
|
@@ -264,6 +360,7 @@ var init_snapshot = __esm({
|
|
|
264
360
|
init_files();
|
|
265
361
|
init_files();
|
|
266
362
|
init_storage();
|
|
363
|
+
init_lock();
|
|
267
364
|
init_paths();
|
|
268
365
|
init_test_map();
|
|
269
366
|
repoPath = z.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
@@ -271,6 +368,7 @@ var init_snapshot = __esm({
|
|
|
271
368
|
refreshedHash: z.string().optional(),
|
|
272
369
|
verifiedAt: z.string().optional(),
|
|
273
370
|
verifiedHash: z.string().optional(),
|
|
371
|
+
verificationToken: z.string().optional(),
|
|
274
372
|
verificationFailed: z.boolean().optional(),
|
|
275
373
|
verificationNote: z.string().optional()
|
|
276
374
|
};
|
|
@@ -294,7 +392,7 @@ var init_snapshot = __esm({
|
|
|
294
392
|
});
|
|
295
393
|
|
|
296
394
|
// src/drift/drift.ts
|
|
297
|
-
import
|
|
395
|
+
import fs4 from "fs/promises";
|
|
298
396
|
import path6 from "path";
|
|
299
397
|
function parseChanges(output) {
|
|
300
398
|
const fields = output.split("\0");
|
|
@@ -855,12 +953,12 @@ var init_git = __esm({
|
|
|
855
953
|
});
|
|
856
954
|
|
|
857
955
|
// src/audit/inputs.ts
|
|
858
|
-
import
|
|
956
|
+
import fs5 from "fs/promises";
|
|
859
957
|
import * as nativeFs from "fs";
|
|
860
958
|
import path9 from "path";
|
|
861
959
|
import fg2 from "fast-glob";
|
|
862
960
|
async function auditInputPath(root, file) {
|
|
863
|
-
if (file === ".") return
|
|
961
|
+
if (file === ".") return fs5.realpath(root);
|
|
864
962
|
try {
|
|
865
963
|
return await storePath(root, file);
|
|
866
964
|
} catch (error) {
|
|
@@ -965,7 +1063,7 @@ var init_inputs = __esm({
|
|
|
965
1063
|
});
|
|
966
1064
|
|
|
967
1065
|
// src/audit/docs.ts
|
|
968
|
-
import
|
|
1066
|
+
import fs6 from "fs/promises";
|
|
969
1067
|
import path10 from "path";
|
|
970
1068
|
import fg3 from "fast-glob";
|
|
971
1069
|
async function dirtyDocs(resolvedRoot, files) {
|
|
@@ -1037,7 +1135,7 @@ async function localDocPaths(root) {
|
|
|
1037
1135
|
for (const dir of [".", ".claude"]) {
|
|
1038
1136
|
let entries;
|
|
1039
1137
|
try {
|
|
1040
|
-
entries = await
|
|
1138
|
+
entries = await fs6.readdir(await auditInputPath(root, dir), { withFileTypes: true });
|
|
1041
1139
|
} catch (error) {
|
|
1042
1140
|
if (error.code === "ENOENT") continue;
|
|
1043
1141
|
throw error;
|
|
@@ -1116,7 +1214,7 @@ var init_types = __esm({
|
|
|
1116
1214
|
});
|
|
1117
1215
|
|
|
1118
1216
|
// src/audit/scope.ts
|
|
1119
|
-
import
|
|
1217
|
+
import fs7 from "fs/promises";
|
|
1120
1218
|
import path11 from "path";
|
|
1121
1219
|
function scopedPath(directory, value) {
|
|
1122
1220
|
if (path11.posix.isAbsolute(value) || value.includes("\\") || /[\x00-\x1f]/.test(value)) return null;
|
|
@@ -1136,7 +1234,7 @@ function pathClaimScope(doc, claim) {
|
|
|
1136
1234
|
}
|
|
1137
1235
|
async function pathExists(root, file) {
|
|
1138
1236
|
try {
|
|
1139
|
-
await
|
|
1237
|
+
await fs7.access(await auditInputPath(root, file));
|
|
1140
1238
|
return true;
|
|
1141
1239
|
} catch (error) {
|
|
1142
1240
|
if (["ENOENT", "ENOTDIR"].includes(error.code ?? "")) return false;
|
|
@@ -1226,7 +1324,7 @@ var init_deleted_reference = __esm({
|
|
|
1226
1324
|
|
|
1227
1325
|
// src/audit/checks/new-module.ts
|
|
1228
1326
|
import fg4 from "fast-glob";
|
|
1229
|
-
import
|
|
1327
|
+
import fs8 from "fs/promises";
|
|
1230
1328
|
import path13 from "path";
|
|
1231
1329
|
function escapeRegExp(text2) {
|
|
1232
1330
|
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1278,7 +1376,7 @@ async function collectModuleCandidates(root, combinedDocs) {
|
|
|
1278
1376
|
const byParent = /* @__PURE__ */ new Map();
|
|
1279
1377
|
for (let offset = 0; offset < parents.length; offset += 16) {
|
|
1280
1378
|
await Promise.all(parents.slice(offset, offset + 16).map(async (dir) => {
|
|
1281
|
-
const entries = await
|
|
1379
|
+
const entries = await fs8.readdir(await auditInputPath(root, dir), { withFileTypes: true });
|
|
1282
1380
|
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));
|
|
1283
1381
|
byParent.set(dir, dirs);
|
|
1284
1382
|
}));
|
|
@@ -1942,7 +2040,7 @@ var init_provenance = __esm({
|
|
|
1942
2040
|
});
|
|
1943
2041
|
|
|
1944
2042
|
// src/decisions/decisions.ts
|
|
1945
|
-
import
|
|
2043
|
+
import fs9 from "fs/promises";
|
|
1946
2044
|
import path17 from "path";
|
|
1947
2045
|
import { createHash } from "crypto";
|
|
1948
2046
|
import { z as z3 } from "zod";
|
|
@@ -1951,7 +2049,7 @@ async function loadDecisionStore(rootDir) {
|
|
|
1951
2049
|
const diagnostics = [];
|
|
1952
2050
|
let entries;
|
|
1953
2051
|
try {
|
|
1954
|
-
entries = await
|
|
2052
|
+
entries = await fs9.readdir(await storePath(rootDir, ".mason/decisions"));
|
|
1955
2053
|
} catch (error) {
|
|
1956
2054
|
if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
|
|
1957
2055
|
return { records, diagnostics };
|
|
@@ -2219,68 +2317,15 @@ var init_findings = __esm({
|
|
|
2219
2317
|
});
|
|
2220
2318
|
|
|
2221
2319
|
// src/automation/store.ts
|
|
2222
|
-
import fs9 from "fs/promises";
|
|
2223
|
-
import os from "os";
|
|
2224
2320
|
import { z as z5 } from "zod";
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
const deadline = Date.now() + waitMs;
|
|
2228
|
-
let handle;
|
|
2229
|
-
while (!handle) {
|
|
2230
|
-
try {
|
|
2231
|
-
handle = await fs9.open(file, "wx", 384);
|
|
2232
|
-
try {
|
|
2233
|
-
await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
|
|
2234
|
-
} catch (error) {
|
|
2235
|
-
await handle.close().catch(() => {
|
|
2236
|
-
});
|
|
2237
|
-
handle = void 0;
|
|
2238
|
-
await fs9.rm(file, { force: true }).catch(() => {
|
|
2239
|
-
});
|
|
2240
|
-
throw error;
|
|
2241
|
-
}
|
|
2242
|
-
} catch (error) {
|
|
2243
|
-
if (error.code !== "EEXIST") throw error;
|
|
2244
|
-
try {
|
|
2245
|
-
const owner = JSON.parse(await fs9.readFile(file, "utf8"));
|
|
2246
|
-
if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
2247
|
-
try {
|
|
2248
|
-
process.kill(owner.pid, 0);
|
|
2249
|
-
} catch (probe) {
|
|
2250
|
-
if (probe.code === "ESRCH") {
|
|
2251
|
-
const reclaim = file + ".reclaim";
|
|
2252
|
-
let guard;
|
|
2253
|
-
try {
|
|
2254
|
-
guard = await fs9.open(reclaim, "wx", 384);
|
|
2255
|
-
const current = JSON.parse(await fs9.readFile(file, "utf8"));
|
|
2256
|
-
if (current.pid === owner.pid && current.host === owner.host) await fs9.unlink(file);
|
|
2257
|
-
} finally {
|
|
2258
|
-
if (guard) {
|
|
2259
|
-
await guard.close();
|
|
2260
|
-
await fs9.rm(reclaim, { force: true });
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
} catch {
|
|
2267
|
-
}
|
|
2268
|
-
if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
|
|
2269
|
-
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
2270
|
-
}
|
|
2271
|
-
}
|
|
2272
|
-
try {
|
|
2273
|
-
return await run();
|
|
2274
|
-
} finally {
|
|
2275
|
-
await handle.close();
|
|
2276
|
-
await fs9.unlink(file);
|
|
2277
|
-
}
|
|
2321
|
+
function withLock(root, directory, run, waitMs = 5e3) {
|
|
2322
|
+
return withStoreLock(root, directory, run, waitMs, "Automation");
|
|
2278
2323
|
}
|
|
2279
2324
|
var hostSchema, stateSchema;
|
|
2280
2325
|
var init_store = __esm({
|
|
2281
2326
|
"src/automation/store.ts"() {
|
|
2282
2327
|
"use strict";
|
|
2283
|
-
|
|
2328
|
+
init_store_lock();
|
|
2284
2329
|
hostSchema = z5.enum(["claude", "codex"]);
|
|
2285
2330
|
stateSchema = z5.object({
|
|
2286
2331
|
version: z5.literal(1),
|