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 +9 -0
- package/dist/mason-audit.js +127 -34
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +241 -96
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-mcp.js +230 -93
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason.js +250 -105
- package/dist/mason.js.map +1 -1
- package/package.json +1 -1
package/dist/mason-auto.js
CHANGED
|
@@ -346,6 +346,39 @@ var init_store = __esm({
|
|
|
346
346
|
}
|
|
347
347
|
});
|
|
348
348
|
|
|
349
|
+
// src/utils/profile.ts
|
|
350
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
351
|
+
async function profilePhase(label, run) {
|
|
352
|
+
const phases = profiles.getStore();
|
|
353
|
+
if (!phases) return run();
|
|
354
|
+
const start = performance.now();
|
|
355
|
+
try {
|
|
356
|
+
return await run();
|
|
357
|
+
} finally {
|
|
358
|
+
const timing = phases[label] ??= { calls: 0, totalMs: 0 };
|
|
359
|
+
timing.calls++;
|
|
360
|
+
timing.totalMs += performance.now() - start;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function withProfile(run, emit) {
|
|
364
|
+
const phases = {};
|
|
365
|
+
const start = performance.now();
|
|
366
|
+
return profiles.run(phases, async () => {
|
|
367
|
+
try {
|
|
368
|
+
return await run();
|
|
369
|
+
} finally {
|
|
370
|
+
emit(JSON.stringify({ kind: "mason-profile", version: 1, totalMs: performance.now() - start, phases }));
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
var profiles;
|
|
375
|
+
var init_profile = __esm({
|
|
376
|
+
"src/utils/profile.ts"() {
|
|
377
|
+
"use strict";
|
|
378
|
+
profiles = new AsyncLocalStorage();
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
349
382
|
// src/audit/commands.ts
|
|
350
383
|
import path4 from "path";
|
|
351
384
|
function splitCommands(line) {
|
|
@@ -949,16 +982,27 @@ import path6 from "path";
|
|
|
949
982
|
import { execFile as execFile4 } from "child_process";
|
|
950
983
|
import { promisify as promisify4 } from "util";
|
|
951
984
|
import fg3 from "fast-glob";
|
|
952
|
-
async function
|
|
985
|
+
async function dirtyDocs(resolvedRoot, files) {
|
|
986
|
+
if (!files.length) return /* @__PURE__ */ new Set();
|
|
953
987
|
try {
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
988
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
989
|
+
for (let offset = 0; offset < files.length; offset += 256) {
|
|
990
|
+
const { stdout } = await exec3(
|
|
991
|
+
"git",
|
|
992
|
+
["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...files.slice(offset, offset + 256).map((file) => `:(literal)${file}`)],
|
|
993
|
+
{ cwd: resolvedRoot, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 }
|
|
994
|
+
);
|
|
995
|
+
const entries = stdout.split("\0");
|
|
996
|
+
for (let i = 0; i < entries.length; i++) {
|
|
997
|
+
if (!entries[i]) continue;
|
|
998
|
+
const status = entries[i].slice(0, 2);
|
|
999
|
+
dirty.add(entries[i].slice(3));
|
|
1000
|
+
if (/[RC]/.test(status) && entries[i + 1]) dirty.add(entries[++i]);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return dirty;
|
|
960
1004
|
} catch {
|
|
961
|
-
return
|
|
1005
|
+
return new Set(files);
|
|
962
1006
|
}
|
|
963
1007
|
}
|
|
964
1008
|
async function discoverDocPaths(root) {
|
|
@@ -1039,21 +1083,30 @@ function documentScope(file) {
|
|
|
1039
1083
|
return path6.posix.basename(parent).toLowerCase() === ".claude" ? path6.posix.dirname(parent) : parent;
|
|
1040
1084
|
}
|
|
1041
1085
|
async function discoverDocs(resolvedRoot) {
|
|
1086
|
+
return profilePhase("audit.documents", () => readDocs(resolvedRoot));
|
|
1087
|
+
}
|
|
1088
|
+
async function readDocs(resolvedRoot) {
|
|
1042
1089
|
const docs = [];
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1090
|
+
const files = await discoverDocPaths(resolvedRoot);
|
|
1091
|
+
const dirty = await dirtyDocs(resolvedRoot, files);
|
|
1092
|
+
for (let offset = 0; offset < files.length; offset += 4) {
|
|
1093
|
+
docs.push(...await Promise.all(files.slice(offset, offset + 4).map(async (candidate) => {
|
|
1094
|
+
const [content2, lastCommit] = await Promise.all([
|
|
1095
|
+
readAuditInput(resolvedRoot, candidate),
|
|
1096
|
+
profilePhase("audit.document-history", () => lastCommitOf(resolvedRoot, candidate))
|
|
1097
|
+
]);
|
|
1098
|
+
if (content2 === null) throw new Error("Document disappeared during discovery: " + candidate);
|
|
1099
|
+
return {
|
|
1100
|
+
path: candidate,
|
|
1101
|
+
content: content2,
|
|
1102
|
+
scope: documentScope(candidate),
|
|
1103
|
+
kind: /^readme\.md$/i.test(path6.posix.basename(candidate)) ? "readme" : "instructions",
|
|
1104
|
+
lineCount: content2.split("\n").length,
|
|
1105
|
+
lastCommit,
|
|
1106
|
+
dirty: dirty.has(candidate),
|
|
1107
|
+
claims: extractClaims(content2)
|
|
1108
|
+
};
|
|
1109
|
+
})));
|
|
1057
1110
|
}
|
|
1058
1111
|
return docs;
|
|
1059
1112
|
}
|
|
@@ -1061,6 +1114,7 @@ var exec3, DOC_CANDIDATES;
|
|
|
1061
1114
|
var init_docs = __esm({
|
|
1062
1115
|
"src/audit/docs.ts"() {
|
|
1063
1116
|
"use strict";
|
|
1117
|
+
init_profile();
|
|
1064
1118
|
init_claims();
|
|
1065
1119
|
init_git();
|
|
1066
1120
|
init_inputs();
|
|
@@ -2586,6 +2640,9 @@ var init_advisory_review = __esm({
|
|
|
2586
2640
|
import fs11 from "fs/promises";
|
|
2587
2641
|
import path18 from "path";
|
|
2588
2642
|
async function computeAudit(rootDir, options = {}) {
|
|
2643
|
+
return profilePhase("audit.current", () => auditCurrent(rootDir, options));
|
|
2644
|
+
}
|
|
2645
|
+
async function auditCurrent(rootDir, options) {
|
|
2589
2646
|
const resolvedRoot = path18.resolve(rootDir);
|
|
2590
2647
|
const [docs, headHash] = await Promise.all([discoverDocs(resolvedRoot), getCurrentGitHash(resolvedRoot)]);
|
|
2591
2648
|
if (docs.length === 0) return null;
|
|
@@ -2612,11 +2669,11 @@ async function computeAudit(rootDir, options = {}) {
|
|
|
2612
2669
|
};
|
|
2613
2670
|
if (!report.gitAvailable) return report;
|
|
2614
2671
|
const changesSinceDoc = /* @__PURE__ */ new Map();
|
|
2672
|
+
const changesByCommit = /* @__PURE__ */ new Map();
|
|
2615
2673
|
for (const doc of docs) {
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
);
|
|
2674
|
+
const hash2 = doc.lastCommit?.hash;
|
|
2675
|
+
if (hash2 && !changesByCommit.has(hash2)) changesByCommit.set(hash2, await getChangesWithStatus(resolvedRoot, hash2));
|
|
2676
|
+
changesSinceDoc.set(doc.path, hash2 ? changesByCommit.get(hash2) : null);
|
|
2620
2677
|
}
|
|
2621
2678
|
let decisionsPresent = false;
|
|
2622
2679
|
try {
|
|
@@ -2650,6 +2707,7 @@ async function computeAudit(rootDir, options = {}) {
|
|
|
2650
2707
|
var init_audit = __esm({
|
|
2651
2708
|
"src/audit/audit.ts"() {
|
|
2652
2709
|
"use strict";
|
|
2710
|
+
init_profile();
|
|
2653
2711
|
init_drift();
|
|
2654
2712
|
init_snapshot();
|
|
2655
2713
|
init_docs();
|
|
@@ -2685,6 +2743,9 @@ async function docState(root) {
|
|
|
2685
2743
|
return digest(docs);
|
|
2686
2744
|
}
|
|
2687
2745
|
async function stableAudit(root, checks, options = {}) {
|
|
2746
|
+
return profilePhase("repair.audit", () => readStableAudit(root, checks, options));
|
|
2747
|
+
}
|
|
2748
|
+
async function readStableAudit(root, checks, options) {
|
|
2688
2749
|
const head = await getCurrentGitHash(root);
|
|
2689
2750
|
const before = await docState(root);
|
|
2690
2751
|
const report = await computeAudit(root, { ...options, checks });
|
|
@@ -2693,10 +2754,44 @@ async function stableAudit(root, checks, options = {}) {
|
|
|
2693
2754
|
}
|
|
2694
2755
|
return report;
|
|
2695
2756
|
}
|
|
2696
|
-
async function
|
|
2757
|
+
async function withRepairSession(root, options, run, cachedAudit) {
|
|
2758
|
+
const before = await Promise.all([getCurrentGitHash(root), docState(root), advisoryReviewInventory(root).then(digest)]);
|
|
2759
|
+
const audits = /* @__PURE__ */ new Map();
|
|
2760
|
+
const documents = /* @__PURE__ */ new Map();
|
|
2761
|
+
const history = /* @__PURE__ */ new Map();
|
|
2762
|
+
const assessments = /* @__PURE__ */ new Map();
|
|
2763
|
+
const memo = (cache, key, read) => {
|
|
2764
|
+
if (!cache.has(key)) cache.set(key, read());
|
|
2765
|
+
return cache.get(key);
|
|
2766
|
+
};
|
|
2767
|
+
const session = {
|
|
2768
|
+
audit: (checks) => memo(audits, [...checks].sort().join(","), async () => {
|
|
2769
|
+
if (cachedAudit && cachedAudit.headHash === before[0] && cachedAudit.root === root && digest([...checks].sort()) === digest([...cachedAudit.checksRun ?? []].sort())) {
|
|
2770
|
+
const current = structuredClone(cachedAudit);
|
|
2771
|
+
const reviews = (await Promise.all(allFindings(current).map((f) => assessAdvisory(root, f, before[0])))).filter((r) => r !== null);
|
|
2772
|
+
delete current.advisoryReviews;
|
|
2773
|
+
if (reviews.length) current.advisoryReviews = reviews;
|
|
2774
|
+
return current;
|
|
2775
|
+
}
|
|
2776
|
+
return computeAudit(root, { ...options, checks });
|
|
2777
|
+
}),
|
|
2778
|
+
document: (file) => memo(documents, file, async () => readBoundedFile(await storePath(root, file), 10 * 1024 * 1024)),
|
|
2779
|
+
history: (head) => memo(history, head, () => getChangesWithStatus(root, head)),
|
|
2780
|
+
assessment: (dir, finding, head) => memo(assessments, digest([dir, finding, head]), () => assessAdvisory(dir, finding, head))
|
|
2781
|
+
};
|
|
2782
|
+
const result = await run({
|
|
2783
|
+
prepare: () => prepareOriginal(root, ALL_CHECKS, options, session),
|
|
2784
|
+
verify: (baseline) => profilePhase("repair.verify", () => verifyOriginal(root, baseline, options, session)),
|
|
2785
|
+
currentAudit: () => session.audit(ALL_CHECKS)
|
|
2786
|
+
});
|
|
2787
|
+
const after = await Promise.all([getCurrentGitHash(root), docState(root), advisoryReviewInventory(root).then(digest)]);
|
|
2788
|
+
if (digest(before) !== digest(after)) throw new Error("HEAD, context files or advisory review records changed during verification; retry against stable evidence.");
|
|
2789
|
+
return result;
|
|
2790
|
+
}
|
|
2791
|
+
async function prepareOriginal(rootDir, checks, options, session) {
|
|
2697
2792
|
const root = await fs12.realpath(rootDir);
|
|
2698
2793
|
const selected = z7.array(checkSchema).nonempty().parse(checks);
|
|
2699
|
-
const report = await stableAudit(root, selected, options);
|
|
2794
|
+
const report = await (session ? session.audit(selected) : stableAudit(root, selected, options));
|
|
2700
2795
|
if (!report) throw new Error("No context files found to prepare a repair.");
|
|
2701
2796
|
if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
|
|
2702
2797
|
const storedReport = reportSchema.parse(report);
|
|
@@ -2721,24 +2816,24 @@ async function loadRepairBaseline(rootDir, baselinePath) {
|
|
|
2721
2816
|
const original = stored.report;
|
|
2722
2817
|
return { root, relative, original };
|
|
2723
2818
|
}
|
|
2724
|
-
async function
|
|
2819
|
+
async function verifyOriginal(rootDir, baselinePath, options, session) {
|
|
2725
2820
|
const { root, relative, original } = await loadRepairBaseline(rootDir, baselinePath);
|
|
2726
2821
|
const diagnostics = [];
|
|
2727
2822
|
let reviewDigest;
|
|
2728
2823
|
let current = null;
|
|
2729
2824
|
try {
|
|
2730
|
-
reviewDigest = digest(await advisoryReviewInventory(root));
|
|
2731
|
-
current = await stableAudit(root, original.checksRun, options);
|
|
2825
|
+
if (!session) reviewDigest = digest(await advisoryReviewInventory(root));
|
|
2826
|
+
current = await (session ? session.audit(original.checksRun) : stableAudit(root, original.checksRun, options));
|
|
2732
2827
|
if (!current) diagnostics.push("No context files remain available to audit.");
|
|
2733
2828
|
else if (!current.gitAvailable) diagnostics.push("Git history is unavailable.");
|
|
2734
2829
|
for (const doc of original.docs) {
|
|
2735
2830
|
if (!allFindings(original).some((f) => f.anchor.doc === doc.path) || !current?.docs.some((d) => d.path === doc.path)) continue;
|
|
2736
|
-
const content2 = await readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024);
|
|
2831
|
+
const content2 = await (session ? session.document(doc.path) : readBoundedFile(await storePath(root, doc.path), 10 * 1024 * 1024));
|
|
2737
2832
|
if (content2 === null || !content2.trim()) {
|
|
2738
2833
|
diagnostics.push("Original context file " + doc.path + " is empty or unreadable; losing its claims does not verify a repair.");
|
|
2739
2834
|
}
|
|
2740
2835
|
}
|
|
2741
|
-
if (await getChangesWithStatus(root, original.headHash) === null) {
|
|
2836
|
+
if (await (session ? session.history(original.headHash) : getChangesWithStatus(root, original.headHash)) === null) {
|
|
2742
2837
|
diagnostics.push("The original audit commit is unavailable; repair history cannot be verified.");
|
|
2743
2838
|
}
|
|
2744
2839
|
} catch (error) {
|
|
@@ -2763,7 +2858,7 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
2763
2858
|
if (!current.checksRun?.includes(finding.type) || skipped.length) {
|
|
2764
2859
|
return { ...base, status: "unverified", reason: skipped.map((s) => s.reason).join("; ") || "The original check did not run." };
|
|
2765
2860
|
}
|
|
2766
|
-
const review = await assessAdvisory(root, now ?? finding, original.headHash);
|
|
2861
|
+
const review = await (session?.assessment ?? assessAdvisory)(root, now ?? finding, original.headHash);
|
|
2767
2862
|
if (review) {
|
|
2768
2863
|
if (review.status === "unverified") return { ...base, review, status: "unverified", reason: review.reason };
|
|
2769
2864
|
if (review.status === "current") return { ...base, review, status: "resolved", reason: review.reason };
|
|
@@ -2778,7 +2873,7 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
2778
2873
|
}
|
|
2779
2874
|
return { ...base, status: "resolved", reason: "The original check ran and no longer reports this claim. Inspect the edit for semantic correctness." };
|
|
2780
2875
|
}));
|
|
2781
|
-
try {
|
|
2876
|
+
if (!session) try {
|
|
2782
2877
|
if (reviewDigest !== digest(await advisoryReviewInventory(root)) || current?.headHash !== await getCurrentGitHash(root)) {
|
|
2783
2878
|
diagnostics.push("HEAD or advisory review records changed during verification; repeat against stable evidence.");
|
|
2784
2879
|
for (const finding of findings) {
|
|
@@ -2818,6 +2913,7 @@ var count2, reportSchema, baselineSchema, repairFindingSchema;
|
|
|
2818
2913
|
var init_repair = __esm({
|
|
2819
2914
|
"src/audit/repair.ts"() {
|
|
2820
2915
|
"use strict";
|
|
2916
|
+
init_profile();
|
|
2821
2917
|
init_audit();
|
|
2822
2918
|
init_docs();
|
|
2823
2919
|
init_snapshot();
|
|
@@ -2878,20 +2974,20 @@ import { promisify as promisify9 } from "util";
|
|
|
2878
2974
|
import fg5 from "fast-glob";
|
|
2879
2975
|
import { z as z8 } from "zod";
|
|
2880
2976
|
async function git3(root, ...args) {
|
|
2881
|
-
return (await exec7("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout;
|
|
2977
|
+
return profilePhase("automation.git", async () => (await exec7("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout);
|
|
2882
2978
|
}
|
|
2883
2979
|
async function workspace(dir) {
|
|
2884
2980
|
const root = await fs13.realpath((await git3(dir, "rev-parse", "--show-toplevel")).trim());
|
|
2885
|
-
const gitDir = await
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
} catch {
|
|
2890
|
-
branch = "detached";
|
|
2891
|
-
}
|
|
2981
|
+
const [gitDir, branch] = await Promise.all([
|
|
2982
|
+
git3(root, "rev-parse", "--absolute-git-dir").then((value) => fs13.realpath(value.trim())),
|
|
2983
|
+
git3(root, "symbolic-ref", "--quiet", "HEAD").then((value) => value.trim(), () => "detached")
|
|
2984
|
+
]);
|
|
2892
2985
|
return { root, gitDir, branch, directory: ".mason/reports/automation/" + hash([root, gitDir, branch]).slice(0, 24) };
|
|
2893
2986
|
}
|
|
2894
2987
|
async function readInputs(root) {
|
|
2988
|
+
return profilePhase("automation.inputs", () => collectInputs(root));
|
|
2989
|
+
}
|
|
2990
|
+
async function collectInputs(root) {
|
|
2895
2991
|
const docPaths = await discoverDocPaths(root);
|
|
2896
2992
|
const [headText, docStatus, shallowPath, replacements] = await Promise.all([
|
|
2897
2993
|
git3(root, "rev-parse", "HEAD"),
|
|
@@ -2908,21 +3004,29 @@ async function readInputs(root) {
|
|
|
2908
3004
|
}
|
|
2909
3005
|
const docs = {};
|
|
2910
3006
|
const docContents = [];
|
|
3007
|
+
const parsedDocs = [];
|
|
2911
3008
|
const claims = [];
|
|
3009
|
+
const presence = /* @__PURE__ */ new Map();
|
|
3010
|
+
const exists = (file) => {
|
|
3011
|
+
if (!presence.has(file)) presence.set(file, pathExists(root, file));
|
|
3012
|
+
return presence.get(file);
|
|
3013
|
+
};
|
|
2912
3014
|
for (const file of docPaths) {
|
|
2913
3015
|
const text2 = await content(root, file);
|
|
2914
3016
|
docs[file] = text2 === null ? null : hash(text2);
|
|
2915
3017
|
docContents.push([file, text2]);
|
|
2916
|
-
|
|
3018
|
+
const parsed = { path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") };
|
|
3019
|
+
parsedDocs.push(parsed);
|
|
3020
|
+
for (const claim of parsed.claims.paths) {
|
|
2917
3021
|
const scope = pathClaimScope(file, claim);
|
|
2918
3022
|
if (scope?.candidates.some(gitMetadataPath)) continue;
|
|
2919
3023
|
for (const candidate of scope?.candidates ?? []) {
|
|
2920
3024
|
if (optionalMasonPath(candidate)) continue;
|
|
2921
|
-
|
|
3025
|
+
const [filePresent, parentPresent] = await Promise.all([exists(candidate), exists(path20.posix.dirname(candidate))]);
|
|
3026
|
+
claims.push([candidate, filePresent, parentPresent]);
|
|
2922
3027
|
}
|
|
2923
3028
|
}
|
|
2924
3029
|
}
|
|
2925
|
-
const parsedDocs = docContents.map(([file, text2]) => ({ path: file, content: text2 ?? "", claims: extractClaims(text2 ?? "") }));
|
|
2926
3030
|
const combinedDocs = moduleDocumentation(parsedDocs);
|
|
2927
3031
|
const countClaims = parsedDocs.flatMap((doc) => doc.claims.counts.map((claim) => ({ doc: doc.path, claim })));
|
|
2928
3032
|
const decisionDirectory = await storePath(root, ".mason/decisions");
|
|
@@ -2944,7 +3048,7 @@ async function readInputs(root) {
|
|
|
2944
3048
|
git3(root, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ".", ":(exclude).mason/reports"),
|
|
2945
3049
|
git3(root, "ls-files", "--stage", "-z", "--", ".", ":(exclude).mason/reports")
|
|
2946
3050
|
]) : ["", ""];
|
|
2947
|
-
const common = [
|
|
3051
|
+
const common = [5, engineVersion, head, shallow, replacements, docContents];
|
|
2948
3052
|
const keys = {
|
|
2949
3053
|
"deleted-reference": hash([common, claims, docStatus]),
|
|
2950
3054
|
"new-module": hash([common, modules]),
|
|
@@ -2969,21 +3073,29 @@ function checkCache(raw, inputs) {
|
|
|
2969
3073
|
reused.add(name);
|
|
2970
3074
|
return structuredClone(entries[name].result);
|
|
2971
3075
|
}
|
|
2972
|
-
const result = await CHECKS[name](ctx);
|
|
3076
|
+
const result = await profilePhase("check." + name, () => CHECKS[name](ctx));
|
|
2973
3077
|
ran.add(name);
|
|
2974
3078
|
if (!result.skipped.length) entries[name] = { key: inputs.keys[name], result };
|
|
2975
3079
|
else delete entries[name];
|
|
2976
3080
|
return result;
|
|
2977
3081
|
} };
|
|
2978
|
-
return {
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
3082
|
+
return {
|
|
3083
|
+
options,
|
|
3084
|
+
ran,
|
|
3085
|
+
reused,
|
|
3086
|
+
diagnostic,
|
|
3087
|
+
has: (name) => entries[name]?.key === inputs.keys[name] && !entries[name].result.skipped.length,
|
|
3088
|
+
serialize: () => {
|
|
3089
|
+
const canonical = cacheSchema.shape.entries.parse(entries);
|
|
3090
|
+
return { version: 1, entries: canonical, digest: hash(canonical) };
|
|
3091
|
+
}
|
|
3092
|
+
};
|
|
2982
3093
|
}
|
|
2983
3094
|
var exec7, engineVersion, hash, content, cacheSchema;
|
|
2984
3095
|
var init_evidence = __esm({
|
|
2985
3096
|
"src/automation/evidence.ts"() {
|
|
2986
3097
|
"use strict";
|
|
3098
|
+
init_profile();
|
|
2987
3099
|
init_docs();
|
|
2988
3100
|
init_scope();
|
|
2989
3101
|
init_claims();
|
|
@@ -2996,7 +3108,7 @@ var init_evidence = __esm({
|
|
|
2996
3108
|
init_storage();
|
|
2997
3109
|
init_advisory_review();
|
|
2998
3110
|
exec7 = promisify9(execFile9);
|
|
2999
|
-
engineVersion = true ? "0.17.
|
|
3111
|
+
engineVersion = true ? "0.17.3" : "development";
|
|
3000
3112
|
hash = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
3001
3113
|
content = readAuditInput;
|
|
3002
3114
|
cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
|
|
@@ -3190,7 +3302,7 @@ var init_execution = __esm({
|
|
|
3190
3302
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
3191
3303
|
import { z as z10 } from "zod";
|
|
3192
3304
|
async function observeReadOnlyTool(dir, event) {
|
|
3193
|
-
const ws = await workspace(dir);
|
|
3305
|
+
const ws = await profilePhase("automation.workspace", () => workspace(dir));
|
|
3194
3306
|
await withLock(ws.root, ws.directory, async () => {
|
|
3195
3307
|
const statePath = ws.directory + "/state.json";
|
|
3196
3308
|
const raw = await readStoreJson(ws.root, statePath);
|
|
@@ -3237,24 +3349,41 @@ async function loadState(ws) {
|
|
|
3237
3349
|
}
|
|
3238
3350
|
return state;
|
|
3239
3351
|
}
|
|
3240
|
-
async function
|
|
3352
|
+
async function matchingAnalysis(ws, state, inputs) {
|
|
3241
3353
|
const pointer = state.analysis;
|
|
3242
|
-
if (!pointer || pointer.fingerprint !== inputs.fingerprint
|
|
3243
|
-
|
|
3354
|
+
if (!pointer || pointer.fingerprint !== inputs.fingerprint) return { analysis: null };
|
|
3355
|
+
if (pointer.path !== ws.directory + "/checks/analysis-" + inputs.fingerprint + ".json") {
|
|
3356
|
+
throw new Error("Invalid automation analysis cache path; inspect the retained state.");
|
|
3357
|
+
}
|
|
3358
|
+
const invalid = { analysis: null, diagnostic: "Discarded an invalid current-audit cache; audit preparation and original verification were repeated." };
|
|
3359
|
+
let raw;
|
|
3360
|
+
try {
|
|
3361
|
+
raw = await readStoreJson(ws.root, pointer.path);
|
|
3362
|
+
} catch (error) {
|
|
3363
|
+
const cause = error.cause;
|
|
3364
|
+
if (cause instanceof SyntaxError || cause instanceof Error && cause.message === "expected a JSON object, received null") return invalid;
|
|
3365
|
+
throw error;
|
|
3366
|
+
}
|
|
3367
|
+
if (raw === null) return { analysis: null };
|
|
3368
|
+
const parsed = sharedSchema.safeParse(raw);
|
|
3369
|
+
if (!parsed.success) return invalid;
|
|
3370
|
+
const stored = parsed.data;
|
|
3244
3371
|
const a = stored.analysis;
|
|
3245
3372
|
if (stored.digest !== hash(a) || a.id !== pointer.id || a.fingerprint !== inputs.fingerprint || a.completedAt !== pointer.completedAt || a.report.root !== ws.root || a.report.branch !== ws.branch || a.report.head !== inputs.head || hash(a.report.baselinePaths) !== hash(state.baselines.map((b) => b.path))) {
|
|
3246
|
-
|
|
3373
|
+
return invalid;
|
|
3247
3374
|
}
|
|
3248
3375
|
if (a.baselineDigest !== await baselineDigest(ws, state)) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
3249
|
-
return a;
|
|
3376
|
+
return { analysis: a };
|
|
3250
3377
|
}
|
|
3251
3378
|
async function verifyInputs(ws, inputs) {
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3379
|
+
return profilePhase("automation.revalidate", async () => {
|
|
3380
|
+
const [after, currentWs] = await Promise.all([readInputs(ws.root), workspace(ws.root)]);
|
|
3381
|
+
if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
|
|
3382
|
+
throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
|
|
3383
|
+
}
|
|
3384
|
+
});
|
|
3256
3385
|
}
|
|
3257
|
-
async function computeAnalysis(ws, state, inputs, event) {
|
|
3386
|
+
async function computeAnalysis(ws, state, inputs, event, previousAudit, cacheDiagnostic) {
|
|
3258
3387
|
const baselines = [...state.baselines];
|
|
3259
3388
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3260
3389
|
const empty = {
|
|
@@ -3283,7 +3412,7 @@ async function computeAnalysis(ws, state, inputs, event) {
|
|
|
3283
3412
|
}
|
|
3284
3413
|
}
|
|
3285
3414
|
let cached = null;
|
|
3286
|
-
const diagnostics = [];
|
|
3415
|
+
const diagnostics = cacheDiagnostic ? [cacheDiagnostic] : [];
|
|
3287
3416
|
try {
|
|
3288
3417
|
cached = await readStoreJson(ws.root, ws.directory + "/cache.json");
|
|
3289
3418
|
} catch {
|
|
@@ -3291,19 +3420,26 @@ async function computeAnalysis(ws, state, inputs, event) {
|
|
|
3291
3420
|
}
|
|
3292
3421
|
const cache = checkCache(cached, inputs);
|
|
3293
3422
|
if (cache.diagnostic) diagnostics.push(cache.diagnostic);
|
|
3294
|
-
const
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3423
|
+
const cachedAudit = previousAudit?.gitAvailable && previousAudit.headHash === inputs.head && previousAudit.root === ws.root && !previousAudit.skippedChecks.length && ALL_CHECKS.every((name) => previousAudit.checksRun?.includes(name) && cache.has(name)) ? previousAudit : void 0;
|
|
3424
|
+
if (cachedAudit) for (const name of ALL_CHECKS) cache.reused.add(name);
|
|
3425
|
+
let currentAudit = null;
|
|
3426
|
+
const verifications = await withRepairSession(ws.root, cache.options, async (repairs) => {
|
|
3427
|
+
const saveBaseline = async () => {
|
|
3428
|
+
if (baselines.length >= 128) throw new Error("128 retained baselines need review; automatic capture stopped without discarding original evidence.");
|
|
3429
|
+
const prepared = await repairs.prepare();
|
|
3430
|
+
baselines.push({ path: prepared.baselinePath, at: now, event: event.event, fingerprint: inputs.fingerprint });
|
|
3431
|
+
};
|
|
3432
|
+
if (!baselines.length) await saveBaseline();
|
|
3433
|
+
const verifications2 = [];
|
|
3434
|
+
for (const baseline of baselines) verifications2.push(await repairs.verify(baseline.path));
|
|
3435
|
+
const known = new Set(verifications2.flatMap((v) => v.findings.map((f) => f.id)));
|
|
3436
|
+
if (verifications2.some((v) => v.newFindings.some((f) => !known.has(findingId(f))))) {
|
|
3437
|
+
await saveBaseline();
|
|
3438
|
+
verifications2.push(await repairs.verify(baselines.at(-1).path));
|
|
3439
|
+
}
|
|
3440
|
+
currentAudit = await repairs.currentAudit();
|
|
3441
|
+
return verifications2;
|
|
3442
|
+
}, cachedAudit);
|
|
3307
3443
|
const merged = /* @__PURE__ */ new Map();
|
|
3308
3444
|
for (const verification of verifications) {
|
|
3309
3445
|
for (const finding of verification.findings) {
|
|
@@ -3324,20 +3460,20 @@ async function computeAnalysis(ws, state, inputs, event) {
|
|
|
3324
3460
|
reportPath: "",
|
|
3325
3461
|
findings: [...merged.values()],
|
|
3326
3462
|
diagnostics: [...new Set(diagnostics)],
|
|
3327
|
-
checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: verifications.at(-1).currentAudit?.skippedChecks ?? [] },
|
|
3463
|
+
checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: verifications.at(-1).currentAudit?.skippedChecks ?? [], ...cachedAudit ? { auditReused: true } : {} },
|
|
3328
3464
|
counts,
|
|
3329
3465
|
capture: "unknown",
|
|
3330
3466
|
scope: SCOPE
|
|
3331
3467
|
};
|
|
3332
|
-
return { baselines, report, cache: cache.ran.size || cached === null || cache.diagnostic ? cache.serialize() : null };
|
|
3468
|
+
return { baselines, report, currentAudit, cache: cache.ran.size || cached === null || cache.diagnostic ? cache.serialize() : null };
|
|
3333
3469
|
}
|
|
3334
3470
|
async function analyze(ws, inputs, event, startedAt) {
|
|
3335
3471
|
return withLock(ws.root, ws.directory + "/analysis/" + inputs.fingerprint, async () => {
|
|
3336
3472
|
const before = await loadState(ws);
|
|
3337
|
-
const
|
|
3338
|
-
if (
|
|
3473
|
+
const { analysis: previous, diagnostic } = await matchingAnalysis(ws, before, inputs);
|
|
3474
|
+
if (previous && previous.completedAt >= startedAt) return { analysis: previous, shared: true };
|
|
3339
3475
|
const originalDigest = await baselineDigest(ws, before);
|
|
3340
|
-
const computed = await computeAnalysis(ws, before, inputs, event);
|
|
3476
|
+
const computed = await computeAnalysis(ws, before, inputs, event, previous?.currentAudit, diagnostic);
|
|
3341
3477
|
await verifyInputs(ws, inputs);
|
|
3342
3478
|
if (originalDigest !== await baselineDigest(ws, before)) throw new Error("Repair baseline was modified during automation; original evidence must be inspected.");
|
|
3343
3479
|
const id = randomUUID4();
|
|
@@ -3346,7 +3482,8 @@ async function analyze(ws, inputs, event, startedAt) {
|
|
|
3346
3482
|
fingerprint: inputs.fingerprint,
|
|
3347
3483
|
completedAt: Date.now(),
|
|
3348
3484
|
baselineDigest: await baselineDigest(ws, { ...before, baselines: computed.baselines }),
|
|
3349
|
-
report: computed.report
|
|
3485
|
+
report: computed.report,
|
|
3486
|
+
currentAudit: computed.currentAudit
|
|
3350
3487
|
});
|
|
3351
3488
|
const analysisPath = ws.directory + "/checks/analysis-" + inputs.fingerprint + ".json";
|
|
3352
3489
|
await withLock(ws.root, ws.directory, async () => {
|
|
@@ -3365,17 +3502,17 @@ async function analyze(ws, inputs, event, startedAt) {
|
|
|
3365
3502
|
}
|
|
3366
3503
|
async function automate(dir, event) {
|
|
3367
3504
|
const startedAt = Date.now();
|
|
3368
|
-
const ws = await workspace(dir);
|
|
3505
|
+
const ws = await profilePhase("automation.workspace", () => workspace(dir));
|
|
3369
3506
|
return recordExecution(ws.root, ws.directory, event.event, async () => {
|
|
3370
3507
|
const inputs = await readInputs(ws.root);
|
|
3371
|
-
const result = await analyze(ws, inputs, event, startedAt);
|
|
3508
|
+
const result = await profilePhase("automation.analysis", () => analyze(ws, inputs, event, startedAt));
|
|
3372
3509
|
if (result.shared) {
|
|
3373
3510
|
await verifyInputs(ws, inputs);
|
|
3374
3511
|
if (result.analysis.baselineDigest !== await baselineDigest(ws, await loadState(ws))) {
|
|
3375
3512
|
throw new Error("Repair baseline was modified during shared automation; original evidence must be inspected.");
|
|
3376
3513
|
}
|
|
3377
3514
|
}
|
|
3378
|
-
return withLock(ws.root, ws.directory, async () => {
|
|
3515
|
+
return profilePhase("automation.publish", () => withLock(ws.root, ws.directory, async () => {
|
|
3379
3516
|
const state = await loadState(ws);
|
|
3380
3517
|
if (state.analysis?.id !== result.analysis.id) throw new Error("Automation evidence changed during publication; retry against the current inputs.");
|
|
3381
3518
|
const report = structuredClone(result.analysis.report);
|
|
@@ -3438,11 +3575,11 @@ async function automate(dir, event) {
|
|
|
3438
3575
|
if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
|
|
3439
3576
|
await writeStoreJson(ws.root, ws.directory + "/state.json", state);
|
|
3440
3577
|
return { report, message: notify ? summarize(report) : null, continueOnce };
|
|
3441
|
-
});
|
|
3578
|
+
}));
|
|
3442
3579
|
});
|
|
3443
3580
|
}
|
|
3444
3581
|
async function automationStatus(dir) {
|
|
3445
|
-
const ws = await workspace(dir);
|
|
3582
|
+
const ws = await profilePhase("automation.workspace", () => workspace(dir));
|
|
3446
3583
|
const execution = await executionStatus(ws.root, ws.directory);
|
|
3447
3584
|
const notifications = await failureNotifications(ws.root, ws.directory);
|
|
3448
3585
|
const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
|
|
@@ -3476,6 +3613,7 @@ var SCOPE, priority, cleanText, analysisSchema, sharedSchema, baselineDigest;
|
|
|
3476
3613
|
var init_runtime = __esm({
|
|
3477
3614
|
"src/automation/runtime.ts"() {
|
|
3478
3615
|
"use strict";
|
|
3616
|
+
init_profile();
|
|
3479
3617
|
init_execution();
|
|
3480
3618
|
init_repair();
|
|
3481
3619
|
init_types();
|
|
@@ -3490,6 +3628,7 @@ var init_runtime = __esm({
|
|
|
3490
3628
|
fingerprint: z10.string(),
|
|
3491
3629
|
completedAt: z10.number(),
|
|
3492
3630
|
baselineDigest: z10.string(),
|
|
3631
|
+
currentAudit: reportSchema.nullable().optional(),
|
|
3493
3632
|
report: z10.object({
|
|
3494
3633
|
version: z10.literal(1),
|
|
3495
3634
|
status: z10.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
|
|
@@ -3503,7 +3642,8 @@ var init_runtime = __esm({
|
|
|
3503
3642
|
checks: z10.object({
|
|
3504
3643
|
ran: z10.array(z10.string()),
|
|
3505
3644
|
reused: z10.array(z10.string()),
|
|
3506
|
-
skipped: z10.array(z10.object({ check: z10.string(), reason: z10.string(), doc: z10.string().optional() }))
|
|
3645
|
+
skipped: z10.array(z10.object({ check: z10.string(), reason: z10.string(), doc: z10.string().optional() })),
|
|
3646
|
+
auditReused: z10.boolean().optional()
|
|
3507
3647
|
}),
|
|
3508
3648
|
counts: z10.object({ resolved: z10.number(), unresolved: z10.number(), "review-required": z10.number(), unverified: z10.number() }),
|
|
3509
3649
|
capture: z10.enum(["observed", "unknown"]),
|
|
@@ -3511,9 +3651,9 @@ var init_runtime = __esm({
|
|
|
3511
3651
|
})
|
|
3512
3652
|
});
|
|
3513
3653
|
sharedSchema = z10.object({ analysis: analysisSchema, digest: z10.string() });
|
|
3514
|
-
baselineDigest = async (ws, state) => hash(await Promise.all(
|
|
3654
|
+
baselineDigest = async (ws, state) => profilePhase("automation.baselines", async () => hash(await Promise.all(
|
|
3515
3655
|
state.baselines.map(async (b) => [b.path, await readStoreJson(ws.root, b.path)])
|
|
3516
|
-
));
|
|
3656
|
+
)));
|
|
3517
3657
|
}
|
|
3518
3658
|
});
|
|
3519
3659
|
|
|
@@ -3650,7 +3790,7 @@ var init_launcher = __esm({
|
|
|
3650
3790
|
"use strict";
|
|
3651
3791
|
init_evidence();
|
|
3652
3792
|
init_model();
|
|
3653
|
-
executingVersion = () => true ? "0.17.
|
|
3793
|
+
executingVersion = () => true ? "0.17.3" : "development";
|
|
3654
3794
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
3655
3795
|
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
3656
3796
|
}
|
|
@@ -5663,6 +5803,7 @@ function createProgress(options = {}) {
|
|
|
5663
5803
|
}
|
|
5664
5804
|
|
|
5665
5805
|
// src/automation/cli.ts
|
|
5806
|
+
init_profile();
|
|
5666
5807
|
var TEARDOWN_USAGE = `Usage: mason teardown [options]
|
|
5667
5808
|
|
|
5668
5809
|
Disconnect Mason from this Git project. Defaults to all project hosts.
|
|
@@ -5690,6 +5831,7 @@ var USAGE = `Usage: mason-auto <setup|teardown|install|config|status|check|hook>
|
|
|
5690
5831
|
--command <prefix> Installed executable prefix for install/config
|
|
5691
5832
|
--dry-run Preview teardown without writing
|
|
5692
5833
|
--json Machine-readable output (status also uses JSON when piped)
|
|
5834
|
+
--profile Local phase timings on stderr for check/hook; no file contents
|
|
5693
5835
|
|
|
5694
5836
|
check exits 0 for verified checks, 1 for issues, 2 for incomplete/unavailable.
|
|
5695
5837
|
teardown exits 0 for complete/no-op, 2 when cleanup needs attention (also in dry runs).
|
|
@@ -5701,7 +5843,8 @@ var parseCli = (argv2) => parseArgs({ args: argv2, allowPositionals: true, optio
|
|
|
5701
5843
|
command: { type: "string" },
|
|
5702
5844
|
json: { type: "boolean" },
|
|
5703
5845
|
help: { type: "boolean", short: "h" },
|
|
5704
|
-
"dry-run": { type: "boolean" }
|
|
5846
|
+
"dry-run": { type: "boolean" },
|
|
5847
|
+
profile: { type: "boolean" }
|
|
5705
5848
|
} });
|
|
5706
5849
|
function isHookCommand(argv2) {
|
|
5707
5850
|
try {
|
|
@@ -5726,6 +5869,8 @@ async function runAutomationCli(argv2, stdin = "", io = defaultIo, options = {})
|
|
|
5726
5869
|
}
|
|
5727
5870
|
if (positionals.length !== 1) throw new Error("Expected one command.");
|
|
5728
5871
|
[action] = positionals;
|
|
5872
|
+
if (values.profile && !["check", "hook"].includes(action)) throw new Error("--profile applies only to check/hook.");
|
|
5873
|
+
const measured = (run) => values.profile ? withProfile(run, io.err) : run();
|
|
5729
5874
|
const dir = values.dir ?? process.cwd();
|
|
5730
5875
|
if (values["dry-run"] !== void 0 && action !== "teardown") throw new Error("--dry-run applies only to teardown.");
|
|
5731
5876
|
if (action === "teardown") {
|
|
@@ -5738,7 +5883,7 @@ async function runAutomationCli(argv2, stdin = "", io = defaultIo, options = {})
|
|
|
5738
5883
|
if (action === "hook") {
|
|
5739
5884
|
const host = hostSchema.parse(values.host);
|
|
5740
5885
|
if (options.managedHost && options.managedHost !== host) throw new Error("Managed hook --host does not match its launcher.");
|
|
5741
|
-
const output = await runAutomationHook(host, stdin, { managed: !!options.managedHost });
|
|
5886
|
+
const output = await measured(() => runAutomationHook(host, stdin, { managed: !!options.managedHost }));
|
|
5742
5887
|
if (output) io.out(JSON.stringify(output));
|
|
5743
5888
|
return 0;
|
|
5744
5889
|
}
|
|
@@ -5769,7 +5914,7 @@ async function runAutomationCli(argv2, stdin = "", io = defaultIo, options = {})
|
|
|
5769
5914
|
return 0;
|
|
5770
5915
|
}
|
|
5771
5916
|
if (action !== "check") throw new Error("Unknown automation command: " + action);
|
|
5772
|
-
const { report } = await automate(dir, { event: "task_end" });
|
|
5917
|
+
const { report } = await measured(() => automate(dir, { event: "task_end" }));
|
|
5773
5918
|
io.out(values.json ? JSON.stringify(report, null, 2) : summarize(report));
|
|
5774
5919
|
return report.status === "verified" ? 0 : report.status === "issues-remain" ? 1 : 2;
|
|
5775
5920
|
} catch (error) {
|