mason-context 0.17.2 → 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 CHANGED
@@ -1,5 +1,14 @@
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
+
3
12
  ## 0.17.2 — 2026-09-15
4
13
 
5
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.
@@ -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 isDirty(resolvedRoot, relPath) {
930
+ async function dirtyDocs(resolvedRoot, files) {
931
+ if (!files.length) return /* @__PURE__ */ new Set();
909
932
  try {
910
- const { stdout } = await exec5(
911
- "git",
912
- ["status", "--porcelain", "--", `:(literal)${relPath}`],
913
- { cwd: resolvedRoot }
914
- );
915
- return stdout.trim().length > 0;
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 true;
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
- for (const candidate of await discoverDocPaths(resolvedRoot)) {
1000
- const content = await readAuditInput(resolvedRoot, candidate);
1001
- if (content === null) throw new Error("Document disappeared during discovery: " + candidate);
1002
- const [lastCommit, dirty] = await Promise.all([lastCommitOf(resolvedRoot, candidate), isDirty(resolvedRoot, candidate)]);
1003
- docs.push({
1004
- path: candidate,
1005
- content,
1006
- scope: documentScope(candidate),
1007
- kind: /^readme\.md$/i.test(path9.posix.basename(candidate)) ? "readme" : "instructions",
1008
- lineCount: content.split("\n").length,
1009
- lastCommit,
1010
- dirty,
1011
- claims: extractClaims(content)
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
- verifyRepair: () => verifyRepair
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
- changesSinceDoc.set(
2749
- doc.path,
2750
- doc.lastCommit ? await getChangesWithStatus(resolvedRoot, doc.lastCommit.hash) : null
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();