mason-context 0.17.1 → 0.17.3
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 +16 -0
- package/dist/mason-audit.js +127 -34
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +309 -99
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-mcp.js +235 -93
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason.js +342 -117
- package/dist/mason.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.3 — 2026-09-15
|
|
4
|
+
|
|
5
|
+
- Reduce repeated hook work by sharing the current audit across repair baselines and reusing complete current audits on unchanged sequential calls. Keep original history, review assessments, skipped-check retries and per-tool evidence receipts independent.
|
|
6
|
+
- Rebuild missing or damaged derived audit caches without clearing original repair evidence; retain failures for unsafe paths or damaged baselines.
|
|
7
|
+
- Batch document status checks, bound concurrent document-history reads, and reuse changes from shared document commits.
|
|
8
|
+
- Add opt-in `--profile` timing output for checks/hooks and larger hook benchmarks covering retained repairs, ignored build output, decision records and final-commit verification. See [hook performance](docs/hook-performance.md) for scope and measurements.
|
|
9
|
+
|
|
10
|
+
Run `mason upgrade` for standalone installations, or `npm install -g mason-context@0.17.3` for npm installations, then restart running assistants. Existing project integrations do not need setup again for this release. Decisions and retained repair evidence are preserved.
|
|
11
|
+
|
|
12
|
+
## 0.17.2 — 2026-09-15
|
|
13
|
+
|
|
14
|
+
- Show progress during standalone installation, upgrades, and project setup, with native download progress and elapsed-time spinners in interactive terminals. Keep redirected progress on stderr and JSON, MCP, and hook output unchanged.
|
|
15
|
+
- Offer Codex and Claude Code setup instructions without assuming an assistant. Show the previous and installed version after an update and remind users to restart running assistants.
|
|
16
|
+
|
|
17
|
+
Run `mason upgrade` for standalone installations, or `npm install -g mason-context@0.17.2` for npm installations, then restart running assistants. Existing project integrations do not need setup again for this release. The first upgrade from an older version still uses its bundled downloader; the new download display appears on subsequent upgrades and fresh installations.
|
|
18
|
+
|
|
3
19
|
## 0.17.1 — 2026-09-15
|
|
4
20
|
|
|
5
21
|
- Keep MCP available in fresh clones, with actionable setup instructions and inactive automatic hooks until local setup. Preserve corrupt-setup diagnostics and checkout-specific activation.
|
package/dist/mason-audit.js
CHANGED
|
@@ -9,6 +9,28 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// src/utils/profile.ts
|
|
13
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
14
|
+
async function profilePhase(label, run) {
|
|
15
|
+
const phases = profiles.getStore();
|
|
16
|
+
if (!phases) return run();
|
|
17
|
+
const start = performance.now();
|
|
18
|
+
try {
|
|
19
|
+
return await run();
|
|
20
|
+
} finally {
|
|
21
|
+
const timing = phases[label] ??= { calls: 0, totalMs: 0 };
|
|
22
|
+
timing.calls++;
|
|
23
|
+
timing.totalMs += performance.now() - start;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
var profiles;
|
|
27
|
+
var init_profile = __esm({
|
|
28
|
+
"src/utils/profile.ts"() {
|
|
29
|
+
"use strict";
|
|
30
|
+
profiles = new AsyncLocalStorage();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
12
34
|
// src/utils/paths.ts
|
|
13
35
|
import path from "path";
|
|
14
36
|
function normalizeRepoPath(value) {
|
|
@@ -905,16 +927,27 @@ import path9 from "path";
|
|
|
905
927
|
import { execFile as execFile6 } from "child_process";
|
|
906
928
|
import { promisify as promisify6 } from "util";
|
|
907
929
|
import fg3 from "fast-glob";
|
|
908
|
-
async function
|
|
930
|
+
async function dirtyDocs(resolvedRoot, files) {
|
|
931
|
+
if (!files.length) return /* @__PURE__ */ new Set();
|
|
909
932
|
try {
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
933
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
934
|
+
for (let offset = 0; offset < files.length; offset += 256) {
|
|
935
|
+
const { stdout } = await exec5(
|
|
936
|
+
"git",
|
|
937
|
+
["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...files.slice(offset, offset + 256).map((file) => `:(literal)${file}`)],
|
|
938
|
+
{ cwd: resolvedRoot, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 }
|
|
939
|
+
);
|
|
940
|
+
const entries = stdout.split("\0");
|
|
941
|
+
for (let i = 0; i < entries.length; i++) {
|
|
942
|
+
if (!entries[i]) continue;
|
|
943
|
+
const status = entries[i].slice(0, 2);
|
|
944
|
+
dirty.add(entries[i].slice(3));
|
|
945
|
+
if (/[RC]/.test(status) && entries[i + 1]) dirty.add(entries[++i]);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return dirty;
|
|
916
949
|
} catch {
|
|
917
|
-
return
|
|
950
|
+
return new Set(files);
|
|
918
951
|
}
|
|
919
952
|
}
|
|
920
953
|
async function discoverDocPaths(root) {
|
|
@@ -995,21 +1028,30 @@ function documentScope(file) {
|
|
|
995
1028
|
return path9.posix.basename(parent).toLowerCase() === ".claude" ? path9.posix.dirname(parent) : parent;
|
|
996
1029
|
}
|
|
997
1030
|
async function discoverDocs(resolvedRoot) {
|
|
1031
|
+
return profilePhase("audit.documents", () => readDocs(resolvedRoot));
|
|
1032
|
+
}
|
|
1033
|
+
async function readDocs(resolvedRoot) {
|
|
998
1034
|
const docs = [];
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1035
|
+
const files = await discoverDocPaths(resolvedRoot);
|
|
1036
|
+
const dirty = await dirtyDocs(resolvedRoot, files);
|
|
1037
|
+
for (let offset = 0; offset < files.length; offset += 4) {
|
|
1038
|
+
docs.push(...await Promise.all(files.slice(offset, offset + 4).map(async (candidate) => {
|
|
1039
|
+
const [content, lastCommit] = await Promise.all([
|
|
1040
|
+
readAuditInput(resolvedRoot, candidate),
|
|
1041
|
+
profilePhase("audit.document-history", () => lastCommitOf(resolvedRoot, candidate))
|
|
1042
|
+
]);
|
|
1043
|
+
if (content === null) throw new Error("Document disappeared during discovery: " + candidate);
|
|
1044
|
+
return {
|
|
1045
|
+
path: candidate,
|
|
1046
|
+
content,
|
|
1047
|
+
scope: documentScope(candidate),
|
|
1048
|
+
kind: /^readme\.md$/i.test(path9.posix.basename(candidate)) ? "readme" : "instructions",
|
|
1049
|
+
lineCount: content.split("\n").length,
|
|
1050
|
+
lastCommit,
|
|
1051
|
+
dirty: dirty.has(candidate),
|
|
1052
|
+
claims: extractClaims(content)
|
|
1053
|
+
};
|
|
1054
|
+
})));
|
|
1013
1055
|
}
|
|
1014
1056
|
return docs;
|
|
1015
1057
|
}
|
|
@@ -1017,6 +1059,7 @@ var exec5, DOC_CANDIDATES;
|
|
|
1017
1059
|
var init_docs = __esm({
|
|
1018
1060
|
"src/audit/docs.ts"() {
|
|
1019
1061
|
"use strict";
|
|
1062
|
+
init_profile();
|
|
1020
1063
|
init_claims();
|
|
1021
1064
|
init_git();
|
|
1022
1065
|
init_inputs();
|
|
@@ -2236,7 +2279,9 @@ __export(repair_exports, {
|
|
|
2236
2279
|
prepareRepair: () => prepareRepair,
|
|
2237
2280
|
repairExitCode: () => repairExitCode,
|
|
2238
2281
|
repairFindingSchema: () => repairFindingSchema,
|
|
2239
|
-
|
|
2282
|
+
reportSchema: () => reportSchema,
|
|
2283
|
+
verifyRepair: () => verifyRepair,
|
|
2284
|
+
withRepairSession: () => withRepairSession
|
|
2240
2285
|
});
|
|
2241
2286
|
import fs10 from "fs/promises";
|
|
2242
2287
|
import path18 from "path";
|
|
@@ -2263,6 +2308,9 @@ async function docState(root) {
|
|
|
2263
2308
|
return digest(docs);
|
|
2264
2309
|
}
|
|
2265
2310
|
async function stableAudit(root, checks, options = {}) {
|
|
2311
|
+
return profilePhase("repair.audit", () => readStableAudit(root, checks, options));
|
|
2312
|
+
}
|
|
2313
|
+
async function readStableAudit(root, checks, options) {
|
|
2266
2314
|
const head = await getCurrentGitHash(root);
|
|
2267
2315
|
const before = await docState(root);
|
|
2268
2316
|
const report = await computeAudit(root, { ...options, checks });
|
|
@@ -2272,9 +2320,46 @@ async function stableAudit(root, checks, options = {}) {
|
|
|
2272
2320
|
return report;
|
|
2273
2321
|
}
|
|
2274
2322
|
async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
|
|
2323
|
+
return prepareOriginal(rootDir, checks, options);
|
|
2324
|
+
}
|
|
2325
|
+
async function withRepairSession(root, options, run, cachedAudit) {
|
|
2326
|
+
const before = await Promise.all([getCurrentGitHash(root), docState(root), advisoryReviewInventory(root).then(digest)]);
|
|
2327
|
+
const audits = /* @__PURE__ */ new Map();
|
|
2328
|
+
const documents = /* @__PURE__ */ new Map();
|
|
2329
|
+
const history = /* @__PURE__ */ new Map();
|
|
2330
|
+
const assessments = /* @__PURE__ */ new Map();
|
|
2331
|
+
const memo = (cache, key, read) => {
|
|
2332
|
+
if (!cache.has(key)) cache.set(key, read());
|
|
2333
|
+
return cache.get(key);
|
|
2334
|
+
};
|
|
2335
|
+
const session = {
|
|
2336
|
+
audit: (checks) => memo(audits, [...checks].sort().join(","), async () => {
|
|
2337
|
+
if (cachedAudit && cachedAudit.headHash === before[0] && cachedAudit.root === root && digest([...checks].sort()) === digest([...cachedAudit.checksRun ?? []].sort())) {
|
|
2338
|
+
const current = structuredClone(cachedAudit);
|
|
2339
|
+
const reviews = (await Promise.all(allFindings(current).map((f) => assessAdvisory(root, f, before[0])))).filter((r) => r !== null);
|
|
2340
|
+
delete current.advisoryReviews;
|
|
2341
|
+
if (reviews.length) current.advisoryReviews = reviews;
|
|
2342
|
+
return current;
|
|
2343
|
+
}
|
|
2344
|
+
return computeAudit(root, { ...options, checks });
|
|
2345
|
+
}),
|
|
2346
|
+
document: (file) => memo(documents, file, async () => readBoundedFile(await storePath(root, file), 10 * 1024 * 1024)),
|
|
2347
|
+
history: (head) => memo(history, head, () => getChangesWithStatus(root, head)),
|
|
2348
|
+
assessment: (dir, finding, head) => memo(assessments, digest([dir, finding, head]), () => assessAdvisory(dir, finding, head))
|
|
2349
|
+
};
|
|
2350
|
+
const result = await run({
|
|
2351
|
+
prepare: () => prepareOriginal(root, ALL_CHECKS, options, session),
|
|
2352
|
+
verify: (baseline) => profilePhase("repair.verify", () => verifyOriginal(root, baseline, options, session)),
|
|
2353
|
+
currentAudit: () => session.audit(ALL_CHECKS)
|
|
2354
|
+
});
|
|
2355
|
+
const after = await Promise.all([getCurrentGitHash(root), docState(root), advisoryReviewInventory(root).then(digest)]);
|
|
2356
|
+
if (digest(before) !== digest(after)) throw new Error("HEAD, context files or advisory review records changed during verification; retry against stable evidence.");
|
|
2357
|
+
return result;
|
|
2358
|
+
}
|
|
2359
|
+
async function prepareOriginal(rootDir, checks, options, session) {
|
|
2275
2360
|
const root = await fs10.realpath(rootDir);
|
|
2276
2361
|
const selected = z6.array(checkSchema).nonempty().parse(checks);
|
|
2277
|
-
const report = await stableAudit(root, selected, options);
|
|
2362
|
+
const report = await (session ? session.audit(selected) : stableAudit(root, selected, options));
|
|
2278
2363
|
if (!report) throw new Error("No context files found to prepare a repair.");
|
|
2279
2364
|
if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
|
|
2280
2365
|
const storedReport = reportSchema.parse(report);
|
|
@@ -2300,23 +2385,26 @@ async function loadRepairBaseline(rootDir, baselinePath) {
|
|
|
2300
2385
|
return { root, relative, original };
|
|
2301
2386
|
}
|
|
2302
2387
|
async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
2388
|
+
return profilePhase("repair.verify", () => verifyOriginal(rootDir, baselinePath, options));
|
|
2389
|
+
}
|
|
2390
|
+
async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
2303
2391
|
const { root, relative, original } = await loadRepairBaseline(rootDir, baselinePath);
|
|
2304
2392
|
const diagnostics = [];
|
|
2305
2393
|
let reviewDigest;
|
|
2306
2394
|
let current = null;
|
|
2307
2395
|
try {
|
|
2308
|
-
reviewDigest = digest(await advisoryReviewInventory(root));
|
|
2309
|
-
current = await stableAudit(root, original.checksRun, options);
|
|
2396
|
+
if (!session) reviewDigest = digest(await advisoryReviewInventory(root));
|
|
2397
|
+
current = await (session ? session.audit(original.checksRun) : stableAudit(root, original.checksRun, options));
|
|
2310
2398
|
if (!current) diagnostics.push("No context files remain available to audit.");
|
|
2311
2399
|
else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
|
|
2312
2400
|
for (const doc of original.docs) {
|
|
2313
2401
|
if (!allFindings(original).some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
|
|
2314
|
-
const content = await readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024);
|
|
2402
|
+
const content = await (session ? session.document(doc.path) : readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024));
|
|
2315
2403
|
if (content === null || !content.trim()) {
|
|
2316
2404
|
diagnostics.push("Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair.");
|
|
2317
2405
|
}
|
|
2318
2406
|
}
|
|
2319
|
-
if (await getChangesWithStatus(root, original.headHash) === null) {
|
|
2407
|
+
if (await (session ? session.history(original.headHash) : getChangesWithStatus(root, original.headHash)) === null) {
|
|
2320
2408
|
diagnostics.push("The original audit commit is unavailable; repair history cannot be verified.");
|
|
2321
2409
|
}
|
|
2322
2410
|
} catch (error) {
|
|
@@ -2341,7 +2429,7 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
2341
2429
|
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
2342
2430
|
return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
|
|
2343
2431
|
}
|
|
2344
|
-
const review = await assessAdvisory(root, now ?? finding, original.headHash);
|
|
2432
|
+
const review = await (session?.assessment ?? assessAdvisory)(root, now ?? finding, original.headHash);
|
|
2345
2433
|
if (review) {
|
|
2346
2434
|
if (review.status === "unverified") return { ...base, review, status: "unverified", reason: review.reason };
|
|
2347
2435
|
if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
|
|
@@ -2356,7 +2444,7 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
2356
2444
|
}
|
|
2357
2445
|
return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
|
|
2358
2446
|
}));
|
|
2359
|
-
try {
|
|
2447
|
+
if (!session) try {
|
|
2360
2448
|
if (reviewDigest !== digest(await advisoryReviewInventory(root)) || current?.headHash !== await getCurrentGitHash(root)) {
|
|
2361
2449
|
diagnostics.push("HEAD or advisory review records changed during verification; repeat against stable evidence.");
|
|
2362
2450
|
for (const finding of findings) {
|
|
@@ -2409,6 +2497,7 @@ var count2, reportSchema, baselineSchema, repairFindingSchema;
|
|
|
2409
2497
|
var init_repair = __esm({
|
|
2410
2498
|
"src/audit/repair.ts"() {
|
|
2411
2499
|
"use strict";
|
|
2500
|
+
init_profile();
|
|
2412
2501
|
init_audit();
|
|
2413
2502
|
init_docs();
|
|
2414
2503
|
init_snapshot();
|
|
@@ -2718,6 +2807,9 @@ var init_advisory_review = __esm({
|
|
|
2718
2807
|
import fs12 from "fs/promises";
|
|
2719
2808
|
import path19 from "path";
|
|
2720
2809
|
async function computeAudit(rootDir, options = {}) {
|
|
2810
|
+
return profilePhase("audit.current", () => auditCurrent(rootDir, options));
|
|
2811
|
+
}
|
|
2812
|
+
async function auditCurrent(rootDir, options) {
|
|
2721
2813
|
const resolvedRoot = path19.resolve(rootDir);
|
|
2722
2814
|
const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
|
|
2723
2815
|
if (docs.length === 0) return null;
|
|
@@ -2744,11 +2836,11 @@ async function computeAudit(rootDir, options = {}) {
|
|
|
2744
2836
|
};
|
|
2745
2837
|
if (!report.gitAvailable) return report;
|
|
2746
2838
|
const changesSinceDoc = /* @__PURE__ */ new Map();
|
|
2839
|
+
const changesByCommit = /* @__PURE__ */ new Map();
|
|
2747
2840
|
for (const doc of docs) {
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
);
|
|
2841
|
+
const hash = doc.lastCommit?.hash;
|
|
2842
|
+
if (hash && !changesByCommit.has(hash)) changesByCommit.set(hash, await getChangesWithStatus(resolvedRoot, hash));
|
|
2843
|
+
changesSinceDoc.set(doc.path, hash ? changesByCommit.get(hash) : null);
|
|
2752
2844
|
}
|
|
2753
2845
|
let decisionsPresent = false;
|
|
2754
2846
|
try {
|
|
@@ -2782,6 +2874,7 @@ async function computeAudit(rootDir, options = {}) {
|
|
|
2782
2874
|
var init_audit = __esm({
|
|
2783
2875
|
"src/audit/audit.ts"() {
|
|
2784
2876
|
"use strict";
|
|
2877
|
+
init_profile();
|
|
2785
2878
|
init_drift();
|
|
2786
2879
|
init_snapshot();
|
|
2787
2880
|
init_docs();
|