mason-context 0.16.1 → 0.16.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/README.md +15 -8
- package/dist/mason-audit.js +86 -66
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-auto.js +989 -776
- package/dist/mason-auto.js.map +1 -1
- package/dist/mason-drift.js +13 -0
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +13 -0
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +1385 -1177
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +62 -49
- package/dist/mason-review.js.map +1 -1
- package/dist/mason.js +1060 -842
- package/dist/mason.js.map +1 -1
- package/package.json +2 -1
package/dist/mason-mcp.js
CHANGED
|
@@ -685,87 +685,435 @@ var init_snapshot = __esm({
|
|
|
685
685
|
}
|
|
686
686
|
});
|
|
687
687
|
|
|
688
|
-
// src/
|
|
689
|
-
import fs3 from "fs/promises";
|
|
688
|
+
// src/context/lexical.ts
|
|
690
689
|
import path6 from "path";
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
function parseChanges(output) {
|
|
694
|
-
const fields = output.split("\0");
|
|
695
|
-
const changes = [];
|
|
696
|
-
for (let i = 0; i < fields.length && fields[i]; ) {
|
|
697
|
-
const code = fields[i++];
|
|
698
|
-
const first = fields[i++];
|
|
699
|
-
if (!first) break;
|
|
700
|
-
const second = /^[RC]/.test(code) ? fields[i++] : void 0;
|
|
701
|
-
const change = second ? code.startsWith("R") ? { status: "renamed", path: second, previousPath: first } : { status: "added", path: second } : { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", path: first };
|
|
702
|
-
if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/"))) continue;
|
|
703
|
-
changes.push(change);
|
|
704
|
-
}
|
|
705
|
-
return changes;
|
|
690
|
+
function tokenize(text2) {
|
|
691
|
+
return text2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2 && !STOPWORDS.has(t));
|
|
706
692
|
}
|
|
707
|
-
function
|
|
708
|
-
return
|
|
693
|
+
function stem(token) {
|
|
694
|
+
return token.length > 3 && token.endsWith("s") ? token.slice(0, -1) : token;
|
|
709
695
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
696
|
+
function tokenSet(text2) {
|
|
697
|
+
return new Set(tokenize(text2).map(stem));
|
|
698
|
+
}
|
|
699
|
+
function scoreEntry(taskTokens, entry) {
|
|
700
|
+
const nameTokens = tokenSet(entry.name);
|
|
701
|
+
const descTokens = tokenSet(entry.description);
|
|
702
|
+
const fileTokens = tokenSet(entry.files.map((f) => path6.basename(f)).join(" "));
|
|
703
|
+
let score = 0;
|
|
704
|
+
for (const token of taskTokens) {
|
|
705
|
+
if (nameTokens.has(token)) score += 3;
|
|
706
|
+
else if (descTokens.has(token)) score += 1;
|
|
707
|
+
else if (fileTokens.has(token)) score += 1;
|
|
717
708
|
}
|
|
709
|
+
return score;
|
|
718
710
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
711
|
+
function jaccard(a, b) {
|
|
712
|
+
if (a.size === 0 && b.size === 0) return 0;
|
|
713
|
+
let intersection = 0;
|
|
714
|
+
for (const token of a) if (b.has(token)) intersection++;
|
|
715
|
+
return intersection / (a.size + b.size - intersection);
|
|
716
|
+
}
|
|
717
|
+
var STOPWORDS;
|
|
718
|
+
var init_lexical = __esm({
|
|
719
|
+
"src/context/lexical.ts"() {
|
|
720
|
+
"use strict";
|
|
721
|
+
STOPWORDS = /* @__PURE__ */ new Set([
|
|
722
|
+
"the",
|
|
723
|
+
"a",
|
|
724
|
+
"an",
|
|
725
|
+
"and",
|
|
726
|
+
"or",
|
|
727
|
+
"of",
|
|
728
|
+
"to",
|
|
729
|
+
"in",
|
|
730
|
+
"on",
|
|
731
|
+
"for",
|
|
732
|
+
"with",
|
|
733
|
+
"how",
|
|
734
|
+
"does",
|
|
735
|
+
"do",
|
|
736
|
+
"is",
|
|
737
|
+
"are",
|
|
738
|
+
"was",
|
|
739
|
+
"what",
|
|
740
|
+
"where",
|
|
741
|
+
"which",
|
|
742
|
+
"why",
|
|
743
|
+
"when",
|
|
744
|
+
"who",
|
|
745
|
+
"i",
|
|
746
|
+
"we",
|
|
747
|
+
"my",
|
|
748
|
+
"our",
|
|
749
|
+
"you",
|
|
750
|
+
"your",
|
|
751
|
+
"it",
|
|
752
|
+
"its",
|
|
753
|
+
"this",
|
|
754
|
+
"that",
|
|
755
|
+
"these",
|
|
756
|
+
"those",
|
|
757
|
+
"can",
|
|
758
|
+
"could",
|
|
759
|
+
"should",
|
|
760
|
+
"would",
|
|
761
|
+
"will",
|
|
762
|
+
"want",
|
|
763
|
+
"need",
|
|
764
|
+
"please",
|
|
765
|
+
"about",
|
|
766
|
+
"into",
|
|
767
|
+
"from",
|
|
768
|
+
"when",
|
|
769
|
+
"there",
|
|
770
|
+
"any",
|
|
771
|
+
"all",
|
|
772
|
+
"some",
|
|
773
|
+
"not",
|
|
774
|
+
"but",
|
|
775
|
+
"also",
|
|
776
|
+
"just",
|
|
777
|
+
"like",
|
|
778
|
+
"get",
|
|
779
|
+
"make",
|
|
780
|
+
"use",
|
|
781
|
+
"new",
|
|
782
|
+
"work",
|
|
783
|
+
"works",
|
|
784
|
+
"working",
|
|
785
|
+
"implement",
|
|
786
|
+
"implemented",
|
|
787
|
+
"change",
|
|
788
|
+
"changed",
|
|
789
|
+
"file",
|
|
790
|
+
"files",
|
|
791
|
+
"code"
|
|
724
792
|
]);
|
|
725
|
-
const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
|
|
726
|
-
return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
|
|
727
|
-
} catch {
|
|
728
|
-
return { available: false, changedFiles: [], untrackedFiles: [] };
|
|
729
793
|
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// src/decisions/decisions.ts
|
|
797
|
+
var decisions_exports = {};
|
|
798
|
+
__export(decisions_exports, {
|
|
799
|
+
BODY_MAX_CHARS: () => BODY_MAX_CHARS,
|
|
800
|
+
BODY_RECOMMENDED_CHARS: () => BODY_RECOMMENDED_CHARS,
|
|
801
|
+
MAX_ACTIVE_DECISIONS: () => MAX_ACTIVE_DECISIONS,
|
|
802
|
+
TITLE_MAX_CHARS: () => TITLE_MAX_CHARS,
|
|
803
|
+
decisionBodySchema: () => decisionBodySchema,
|
|
804
|
+
decisionIdFor: () => decisionIdFor,
|
|
805
|
+
decisionTitleSchema: () => decisionTitleSchema,
|
|
806
|
+
findNearDuplicate: () => findNearDuplicate,
|
|
807
|
+
loadDecisionStore: () => loadDecisionStore,
|
|
808
|
+
loadDecisions: () => loadDecisions,
|
|
809
|
+
saveDecisionRecord: () => saveDecisionRecord,
|
|
810
|
+
upsertDecision: () => upsertDecision,
|
|
811
|
+
withDecisionWrite: () => withDecisionWrite
|
|
812
|
+
});
|
|
813
|
+
import fs3 from "fs/promises";
|
|
814
|
+
import path7 from "path";
|
|
815
|
+
import { createHash } from "crypto";
|
|
816
|
+
import { z as z3 } from "zod";
|
|
817
|
+
async function loadDecisionStore(rootDir) {
|
|
818
|
+
const records = [];
|
|
819
|
+
const diagnostics = [];
|
|
820
|
+
let entries;
|
|
733
821
|
try {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
822
|
+
entries = await fs3.readdir(await storePath(rootDir, ".mason/decisions"));
|
|
823
|
+
} catch (error) {
|
|
824
|
+
if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
|
|
825
|
+
return { records, diagnostics };
|
|
826
|
+
}
|
|
827
|
+
for (const entry of entries.sort()) {
|
|
828
|
+
if (!entry.endsWith(".json")) continue;
|
|
829
|
+
const relative = `.mason/decisions/${entry}`;
|
|
830
|
+
try {
|
|
831
|
+
const record = decisionSchema.parse(await readStoreJson(rootDir, relative));
|
|
832
|
+
if (entry !== `${record.id}.json`) throw new Error("Record id does not match its filename");
|
|
833
|
+
records.push(record);
|
|
834
|
+
} catch (error) {
|
|
835
|
+
diagnostics.push({ path: relative, message: error instanceof Error ? error.message : String(error) });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return { records, diagnostics };
|
|
839
|
+
}
|
|
840
|
+
async function loadDecisions(rootDir) {
|
|
841
|
+
return (await loadDecisionStore(rootDir)).records;
|
|
842
|
+
}
|
|
843
|
+
async function saveDecisionRecord(rootDir, record) {
|
|
844
|
+
const validated = decisionSchema.parse(record);
|
|
845
|
+
await writeStoreJson(rootDir, `.mason/decisions/${validated.id}.json`, validated);
|
|
846
|
+
}
|
|
847
|
+
function decisionIdFor(title, body, existingIds) {
|
|
848
|
+
const fullSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
849
|
+
let slug = fullSlug.slice(0, 60);
|
|
850
|
+
if (fullSlug.length > 60 && fullSlug[60] !== "-") {
|
|
851
|
+
const boundary = slug.lastIndexOf("-");
|
|
852
|
+
if (boundary > 0) slug = slug.slice(0, boundary);
|
|
853
|
+
}
|
|
854
|
+
slug = slug.replace(/-+$/, "") || "decision";
|
|
855
|
+
if (!existingIds.has(slug)) return slug;
|
|
856
|
+
const suffix = createHash("sha1").update(title + body).digest("hex").slice(0, 6);
|
|
857
|
+
return `${slug}-${suffix}`;
|
|
858
|
+
}
|
|
859
|
+
function findNearDuplicate(candidate, existing) {
|
|
860
|
+
const candidateTokens = tokenSet(`${candidate.title} ${candidate.body}`);
|
|
861
|
+
const candidateFiles = new Set(candidate.files);
|
|
862
|
+
let best = null;
|
|
863
|
+
for (const record of existing) {
|
|
864
|
+
if (record.status !== "active") continue;
|
|
865
|
+
const similarity = jaccard(
|
|
866
|
+
candidateTokens,
|
|
867
|
+
tokenSet(`${record.title} ${record.body}`)
|
|
738
868
|
);
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
869
|
+
const sharesFile = record.files.some((f) => candidateFiles.has(f));
|
|
870
|
+
const threshold = sharesFile ? DUPLICATE_JACCARD_WITH_SHARED_FILE : DUPLICATE_JACCARD;
|
|
871
|
+
if (similarity >= threshold && (!best || similarity > best.similarity)) {
|
|
872
|
+
best = { record, similarity };
|
|
873
|
+
}
|
|
743
874
|
}
|
|
875
|
+
return best;
|
|
744
876
|
}
|
|
745
|
-
function
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
877
|
+
async function withDecisionWrite(root, operation) {
|
|
878
|
+
const lockPath = await storePath(root, ".mason/decisions/.write-lock", true);
|
|
879
|
+
let lock;
|
|
880
|
+
try {
|
|
881
|
+
lock = await fs3.open(lockPath, "wx", 384);
|
|
882
|
+
} catch (error) {
|
|
883
|
+
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." };
|
|
884
|
+
throw error;
|
|
750
885
|
}
|
|
751
|
-
|
|
752
|
-
|
|
886
|
+
try {
|
|
887
|
+
return await operation();
|
|
888
|
+
} finally {
|
|
889
|
+
await lock.close();
|
|
890
|
+
await fs3.unlink(lockPath);
|
|
753
891
|
}
|
|
754
|
-
return mappedFiles;
|
|
755
892
|
}
|
|
756
|
-
async function
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
893
|
+
async function upsertDecision(rootDir, input) {
|
|
894
|
+
const parsedTitle = decisionTitleSchema.safeParse(input.title), parsedBody = decisionBodySchema.safeParse(input.body);
|
|
895
|
+
if (!parsedTitle.success) return { status: "error", error: parsedTitle.error.issues[0].message };
|
|
896
|
+
if (!parsedBody.success) return { status: "error", error: parsedBody.error.issues[0].message };
|
|
897
|
+
const title = parsedTitle.data, body = parsedBody.data;
|
|
898
|
+
const attribution = attributionSchema.safeParse(input);
|
|
899
|
+
if (!attribution.success) return { status: "error", error: attribution.error.message };
|
|
900
|
+
if (input.id && input.supersedes) return { status: "error", error: "Use either id to revise or supersedes to replace a record, not both." };
|
|
901
|
+
return withDecisionWrite(rootDir, async () => {
|
|
902
|
+
const store = await loadDecisionStore(rootDir);
|
|
903
|
+
if (store.diagnostics.length) return { status: "error", error: "Repair malformed decision records before saving: " + store.diagnostics.map((d) => d.path).join(", ") };
|
|
904
|
+
const existing = store.records, byId = new Map(existing.map((r) => [r.id, r]));
|
|
905
|
+
const now = (/* @__PURE__ */ new Date()).toISOString(), head = await getCurrentGitHash(rootDir);
|
|
906
|
+
const warnings = [];
|
|
907
|
+
if (body.length > BODY_RECOMMENDED_CHARS) warnings.push(`body has ${body.length} characters, above the recommended ${BODY_RECOMMENDED_CHARS}; matching context and hooks include it in full. Keep the rule, reason, and exceptions concise; use sources for supporting references.`);
|
|
908
|
+
const files = sanitizeRepoPaths(input.files ?? []);
|
|
909
|
+
if (input.files && files.length < input.files.length) warnings.push("some anchor paths were outside the repo or duplicated and were dropped");
|
|
910
|
+
for (const file of files) {
|
|
911
|
+
try {
|
|
912
|
+
await fs3.access(path7.join(rootDir, file));
|
|
913
|
+
} catch {
|
|
914
|
+
warnings.push(`anchor file does not exist on disk: ${file}`);
|
|
915
|
+
}
|
|
763
916
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
917
|
+
const hint = "Saved locally for review and commit. Proposals are not accepted constraints; an existing accepted revision remains operative while its replacement is proposed. Use review_decision to inspect evidence and record an authorized acceptance or reaffirmation.";
|
|
918
|
+
if (input.id) {
|
|
919
|
+
const original = byId.get(input.id);
|
|
920
|
+
if (!original) return { status: "error", error: `no decision with id "${input.id}"` };
|
|
921
|
+
if (original.status !== "active") return { status: "error", error: "Archived records cannot be revised; create a new proposal." };
|
|
922
|
+
const record2 = importLegacy(original, now);
|
|
923
|
+
const content3 = decisionContent({
|
|
924
|
+
...record2,
|
|
925
|
+
title,
|
|
926
|
+
body,
|
|
927
|
+
category: input.category,
|
|
928
|
+
files: input.files !== void 0 ? files : record2.files,
|
|
929
|
+
owner: attribution.data.owner === void 0 ? record2.owner : attribution.data.owner ?? void 0,
|
|
930
|
+
sources: attribution.data.sources ?? record2.sources
|
|
931
|
+
});
|
|
932
|
+
if (JSON.stringify(content3) === JSON.stringify(decisionContent(record2))) {
|
|
933
|
+
return {
|
|
934
|
+
status: "unchanged",
|
|
935
|
+
id: record2.id,
|
|
936
|
+
totalActive: existing.filter((r) => r.status === "active").length,
|
|
937
|
+
approval: decisionApproval(original),
|
|
938
|
+
warnings,
|
|
939
|
+
hint: "Unchanged content; no review or freshness stamp was written. Use review_decision for explicit re-verification."
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
const revision = record2.revision + 1;
|
|
943
|
+
const updated = {
|
|
944
|
+
...record2,
|
|
945
|
+
...content3,
|
|
946
|
+
owner: content3.owner,
|
|
947
|
+
updatedAt: now,
|
|
948
|
+
revision,
|
|
949
|
+
approval: "proposed",
|
|
950
|
+
history: [...record2.history, { kind: "revised", at: now, actor: attribution.data.actor, revision, content: content3, approval: "proposed", status: "active", refreshedHash: record2.refreshedHash }]
|
|
951
|
+
};
|
|
952
|
+
await saveDecisionRecord(rootDir, updated);
|
|
953
|
+
return { status: "updated", id: record2.id, totalActive: existing.filter((r) => r.status === "active").length, approval: "proposed", warnings, hint };
|
|
954
|
+
}
|
|
955
|
+
const old = input.supersedes ? byId.get(input.supersedes) : void 0;
|
|
956
|
+
if (input.supersedes && !old) return { status: "error", error: `no decision with id "${input.supersedes}" to supersede` };
|
|
957
|
+
if (old && (old.status !== "active" || decisionApproval(effectiveDecision(old)) === "accepted")) {
|
|
958
|
+
return { status: "error", error: "A proposal cannot supersede an accepted or archived record. Create and review the replacement separately, then explicitly retire the old decision with review_decision." };
|
|
959
|
+
}
|
|
960
|
+
if (!input.force) {
|
|
961
|
+
const duplicate = findNearDuplicate({ title, body, files }, existing);
|
|
962
|
+
if (duplicate) return { status: "duplicate_suspected", existing: duplicate.record, hint: `A similar decision exists ("${duplicate.record.title}"). Call save_decision with id="${duplicate.record.id}" to revise it, or force:true if distinct.` };
|
|
963
|
+
}
|
|
964
|
+
const id = decisionIdFor(title, body, new Set(byId.keys()));
|
|
965
|
+
if (byId.has(id)) return { status: "error", error: `Decision id collision: ${id}. Choose a distinct title or revise the existing record.` };
|
|
966
|
+
const content2 = decisionContent({ title, body, category: input.category, files, owner: attribution.data.owner ?? void 0, sources: attribution.data.sources ?? [] });
|
|
967
|
+
const record = {
|
|
968
|
+
...content2,
|
|
969
|
+
version: 2,
|
|
970
|
+
id,
|
|
971
|
+
createdAt: now,
|
|
972
|
+
updatedAt: now,
|
|
973
|
+
refreshedHash: head,
|
|
974
|
+
status: "active",
|
|
975
|
+
approval: "proposed",
|
|
976
|
+
revision: 1,
|
|
977
|
+
history: [{ kind: "created", at: now, actor: attribution.data.actor, revision: 1, content: content2, approval: "proposed", status: "active", refreshedHash: head }]
|
|
978
|
+
};
|
|
979
|
+
await saveDecisionRecord(rootDir, record);
|
|
980
|
+
if (old) {
|
|
981
|
+
const imported = importLegacy(old, now);
|
|
982
|
+
await saveDecisionRecord(rootDir, {
|
|
983
|
+
...imported,
|
|
984
|
+
status: "superseded",
|
|
985
|
+
supersededBy: id,
|
|
986
|
+
updatedAt: now,
|
|
987
|
+
history: [...imported.history, {
|
|
988
|
+
kind: "superseded",
|
|
989
|
+
at: now,
|
|
990
|
+
actor: attribution.data.actor,
|
|
991
|
+
note: `Replaced by proposal ${id}`,
|
|
992
|
+
revision: imported.revision,
|
|
993
|
+
content: decisionContent(imported),
|
|
994
|
+
approval: imported.approval,
|
|
995
|
+
status: "superseded",
|
|
996
|
+
refreshedHash: imported.refreshedHash
|
|
997
|
+
}]
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
const totalActive = existing.filter((r) => r.status === "active").length + (old ? 0 : 1);
|
|
1001
|
+
const result = { status: old ? "superseded_and_created" : "created", id, totalActive, approval: "proposed", warnings, hint };
|
|
1002
|
+
if (totalActive > MAX_ACTIVE_DECISIONS) {
|
|
1003
|
+
result.pruneCandidates = existing.filter((r) => r.status !== "active").map((r) => r.id).slice(0, 10);
|
|
1004
|
+
warnings.push(`${totalActive} active decisions exceeds the soft cap of ${MAX_ACTIVE_DECISIONS} \u2014 consider a cleanup PR (archived records first)`);
|
|
1005
|
+
}
|
|
1006
|
+
return result;
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
var TITLE_MAX_CHARS, BODY_RECOMMENDED_CHARS, BODY_MAX_CHARS, MAX_ACTIVE_DECISIONS, boundedText, decisionTitleSchema, decisionBodySchema, DUPLICATE_JACCARD, DUPLICATE_JACCARD_WITH_SHARED_FILE;
|
|
1010
|
+
var init_decisions = __esm({
|
|
1011
|
+
"src/decisions/decisions.ts"() {
|
|
1012
|
+
"use strict";
|
|
1013
|
+
init_storage();
|
|
1014
|
+
init_paths();
|
|
1015
|
+
init_snapshot();
|
|
1016
|
+
init_lexical();
|
|
1017
|
+
init_provenance();
|
|
1018
|
+
TITLE_MAX_CHARS = 80;
|
|
1019
|
+
BODY_RECOMMENDED_CHARS = 1500;
|
|
1020
|
+
BODY_MAX_CHARS = 2500;
|
|
1021
|
+
MAX_ACTIVE_DECISIONS = 150;
|
|
1022
|
+
boundedText = (field, max, hint) => z3.string({
|
|
1023
|
+
errorMap: (issue, ctx) => ({ message: issue.code === "too_big" ? `${field} has ${ctx.data.length} characters; maximum is ${max} (${ctx.data.length - max} over). ${hint}` : ctx.defaultError })
|
|
1024
|
+
}).trim().min(1, `${field} must be non-empty`).max(max);
|
|
1025
|
+
decisionTitleSchema = boundedText("title", TITLE_MAX_CHARS, "Tighten it to a specific headline.");
|
|
1026
|
+
decisionBodySchema = boundedText(
|
|
1027
|
+
"body",
|
|
1028
|
+
BODY_MAX_CHARS,
|
|
1029
|
+
`Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer. Preserve the rule, reason, and exceptions; put supporting references in sources.`
|
|
1030
|
+
);
|
|
1031
|
+
DUPLICATE_JACCARD = 0.5;
|
|
1032
|
+
DUPLICATE_JACCARD_WITH_SHARED_FILE = 0.35;
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
// src/drift/drift.ts
|
|
1037
|
+
import fs4 from "fs/promises";
|
|
1038
|
+
import path8 from "path";
|
|
1039
|
+
import { execFile as execFile3 } from "child_process";
|
|
1040
|
+
import { promisify as promisify3 } from "util";
|
|
1041
|
+
function parseChanges(output) {
|
|
1042
|
+
const fields = output.split("\0");
|
|
1043
|
+
const changes = [];
|
|
1044
|
+
for (let i = 0; i < fields.length && fields[i]; ) {
|
|
1045
|
+
const code = fields[i++];
|
|
1046
|
+
const first = fields[i++];
|
|
1047
|
+
if (!first) break;
|
|
1048
|
+
const second = /^[RC]/.test(code) ? fields[i++] : void 0;
|
|
1049
|
+
const change = second ? code.startsWith("R") ? { status: "renamed", path: second, previousPath: first } : { status: "added", path: second } : { status: code === "A" ? "added" : code === "D" ? "deleted" : "modified", path: first };
|
|
1050
|
+
if (change.path.startsWith(".mason/") && (!change.previousPath || change.previousPath.startsWith(".mason/"))) continue;
|
|
1051
|
+
changes.push(change);
|
|
1052
|
+
}
|
|
1053
|
+
return changes;
|
|
1054
|
+
}
|
|
1055
|
+
function touchedPaths(changes) {
|
|
1056
|
+
return [...new Set(changes.flatMap((c) => c.previousPath ? [c.previousPath, c.path] : [c.path]))].sort();
|
|
1057
|
+
}
|
|
1058
|
+
async function getChangesWithStatus(resolvedRoot, fromHash, toHash = "HEAD") {
|
|
1059
|
+
if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-") || !toHash || toHash === "unknown" || toHash.startsWith("-")) return null;
|
|
1060
|
+
try {
|
|
1061
|
+
const { stdout } = await exec3("git", ["diff", "--name-status", "-z", "-M", fromHash, toHash, "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 });
|
|
1062
|
+
return parseChanges(stdout);
|
|
1063
|
+
} catch {
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
async function getWorkingTree(resolvedRoot) {
|
|
1068
|
+
try {
|
|
1069
|
+
const [diff, untracked] = await Promise.all([
|
|
1070
|
+
exec3("git", ["diff", "--name-status", "-z", "-M", "HEAD", "--"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 }),
|
|
1071
|
+
exec3("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: resolvedRoot, maxBuffer: 10 * 1024 * 1024 })
|
|
1072
|
+
]);
|
|
1073
|
+
const untrackedFiles = untracked.stdout.split("\0").filter((f) => f && !f.startsWith(".mason/"));
|
|
1074
|
+
return { available: true, changedFiles: [.../* @__PURE__ */ new Set([...touchedPaths(parseChanges(diff.stdout)), ...untrackedFiles])].sort(), untrackedFiles };
|
|
1075
|
+
} catch {
|
|
1076
|
+
return { available: false, changedFiles: [], untrackedFiles: [] };
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
async function countCommitsBehind(resolvedRoot, fromHash) {
|
|
1080
|
+
if (!fromHash || fromHash === "unknown" || fromHash.startsWith("-")) return null;
|
|
1081
|
+
try {
|
|
1082
|
+
const { stdout } = await exec3(
|
|
1083
|
+
"git",
|
|
1084
|
+
["rev-list", "--count", `${fromHash}..HEAD`],
|
|
1085
|
+
{ cwd: resolvedRoot }
|
|
1086
|
+
);
|
|
1087
|
+
const count3 = Number.parseInt(stdout.trim(), 10);
|
|
1088
|
+
return Number.isNaN(count3) ? null : count3;
|
|
1089
|
+
} catch {
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
function collectMappedFiles(snapshot) {
|
|
1094
|
+
const mappedFiles = /* @__PURE__ */ new Set();
|
|
1095
|
+
for (const feature of Object.values(snapshot.features)) {
|
|
1096
|
+
for (const f of feature.files) mappedFiles.add(f);
|
|
1097
|
+
for (const t of feature.tests ?? []) mappedFiles.add(t);
|
|
1098
|
+
}
|
|
1099
|
+
for (const flow of Object.values(snapshot.flows)) {
|
|
1100
|
+
for (const f of flow.chain) mappedFiles.add(f);
|
|
1101
|
+
}
|
|
1102
|
+
return mappedFiles;
|
|
1103
|
+
}
|
|
1104
|
+
async function findGhostFiles(resolvedRoot, mappedFiles) {
|
|
1105
|
+
const ghosts = [];
|
|
1106
|
+
for (const file of mappedFiles) {
|
|
1107
|
+
try {
|
|
1108
|
+
await fs4.access(path8.join(resolvedRoot, file));
|
|
1109
|
+
} catch {
|
|
1110
|
+
ghosts.push(file);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return ghosts.sort();
|
|
1114
|
+
}
|
|
767
1115
|
async function computeDrift(rootDir) {
|
|
768
|
-
const root =
|
|
1116
|
+
const root = path8.resolve(rootDir);
|
|
769
1117
|
const snapshot = await loadSnapshot(root);
|
|
770
1118
|
if (!snapshot) return null;
|
|
771
1119
|
const [headHash, workingTree] = await Promise.all([getCurrentGitHash(root), getWorkingTree(root)]);
|
|
@@ -1229,8 +1577,8 @@ var init_git = __esm({
|
|
|
1229
1577
|
});
|
|
1230
1578
|
|
|
1231
1579
|
// src/audit/docs.ts
|
|
1232
|
-
import
|
|
1233
|
-
import
|
|
1580
|
+
import fs5 from "fs/promises";
|
|
1581
|
+
import path9 from "path";
|
|
1234
1582
|
import { execFile as execFile5 } from "child_process";
|
|
1235
1583
|
import { promisify as promisify5 } from "util";
|
|
1236
1584
|
async function isDirty(resolvedRoot, relPath) {
|
|
@@ -1249,7 +1597,7 @@ async function discoverDocs(resolvedRoot) {
|
|
|
1249
1597
|
const docs = await Promise.all(DOC_CANDIDATES.map(async (candidate) => {
|
|
1250
1598
|
let content2;
|
|
1251
1599
|
try {
|
|
1252
|
-
content2 = await
|
|
1600
|
+
content2 = await fs5.readFile(path9.join(resolvedRoot, candidate), "utf-8");
|
|
1253
1601
|
} catch {
|
|
1254
1602
|
return null;
|
|
1255
1603
|
}
|
|
@@ -1297,11 +1645,11 @@ var init_types = __esm({
|
|
|
1297
1645
|
});
|
|
1298
1646
|
|
|
1299
1647
|
// src/audit/checks/deleted-reference.ts
|
|
1300
|
-
import
|
|
1301
|
-
import
|
|
1648
|
+
import fs6 from "fs/promises";
|
|
1649
|
+
import path10 from "path";
|
|
1302
1650
|
async function exists(absPath) {
|
|
1303
1651
|
try {
|
|
1304
|
-
await
|
|
1652
|
+
await fs6.access(absPath);
|
|
1305
1653
|
return true;
|
|
1306
1654
|
} catch {
|
|
1307
1655
|
return false;
|
|
@@ -1321,7 +1669,7 @@ async function checkDeletedReferences(ctx) {
|
|
|
1321
1669
|
if (claim.path === ".mason" || claim.path.startsWith(".mason/")) {
|
|
1322
1670
|
continue;
|
|
1323
1671
|
}
|
|
1324
|
-
if (await exists(
|
|
1672
|
+
if (await exists(path10.join(ctx.root, claim.path))) continue;
|
|
1325
1673
|
const anchor = { doc: doc.path, line: claim.line, excerpt: claim.excerpt };
|
|
1326
1674
|
const renamedTo = renames.get(claim.path) ?? null;
|
|
1327
1675
|
if (renamedTo) {
|
|
@@ -1357,14 +1705,14 @@ async function checkDeletedReferences(ctx) {
|
|
|
1357
1705
|
deletedInCommit: deleted,
|
|
1358
1706
|
everTracked: true,
|
|
1359
1707
|
parentDirExists: await exists(
|
|
1360
|
-
|
|
1708
|
+
path10.join(ctx.root, path10.dirname(claim.path))
|
|
1361
1709
|
)
|
|
1362
1710
|
}
|
|
1363
1711
|
});
|
|
1364
1712
|
continue;
|
|
1365
1713
|
}
|
|
1366
1714
|
const parentDirExists = await exists(
|
|
1367
|
-
|
|
1715
|
+
path10.join(ctx.root, path10.dirname(claim.path))
|
|
1368
1716
|
);
|
|
1369
1717
|
if (!parentDirExists) continue;
|
|
1370
1718
|
const issue = {
|
|
@@ -1395,14 +1743,14 @@ var init_deleted_reference = __esm({
|
|
|
1395
1743
|
});
|
|
1396
1744
|
|
|
1397
1745
|
// src/audit/inputs.ts
|
|
1398
|
-
import
|
|
1746
|
+
import fs7 from "fs/promises";
|
|
1399
1747
|
import * as nativeFs from "fs";
|
|
1400
|
-
import
|
|
1748
|
+
import path11 from "path";
|
|
1401
1749
|
import { execFile as execFile6 } from "child_process";
|
|
1402
1750
|
import { promisify as promisify6 } from "util";
|
|
1403
1751
|
import fg2 from "fast-glob";
|
|
1404
1752
|
async function auditInputPath(root, file) {
|
|
1405
|
-
if (file === ".") return
|
|
1753
|
+
if (file === ".") return fs7.realpath(root);
|
|
1406
1754
|
try {
|
|
1407
1755
|
return await storePath(root, file);
|
|
1408
1756
|
} catch (error) {
|
|
@@ -1426,7 +1774,7 @@ async function auditGlob(root, patterns, options) {
|
|
|
1426
1774
|
if (Array.isArray(patterns) && !patterns.length) return [];
|
|
1427
1775
|
const directories = /* @__PURE__ */ new Set();
|
|
1428
1776
|
const readdir2 = ((directory, opts, callback) => {
|
|
1429
|
-
const relative =
|
|
1777
|
+
const relative = path11.relative(root, directory).split(path11.sep).join("/") || ".";
|
|
1430
1778
|
directories.add(relative);
|
|
1431
1779
|
if (directories.size > MAX_INPUTS) {
|
|
1432
1780
|
callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
|
|
@@ -1437,7 +1785,7 @@ async function auditGlob(root, patterns, options) {
|
|
|
1437
1785
|
}, callback);
|
|
1438
1786
|
});
|
|
1439
1787
|
const lstat2 = ((file, callback) => {
|
|
1440
|
-
const relative =
|
|
1788
|
+
const relative = path11.relative(root, file).split(path11.sep).join("/") || ".";
|
|
1441
1789
|
auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
|
|
1442
1790
|
});
|
|
1443
1791
|
const stream = fg2.stream(patterns, {
|
|
@@ -1519,8 +1867,8 @@ var init_inputs = __esm({
|
|
|
1519
1867
|
|
|
1520
1868
|
// src/audit/checks/new-module.ts
|
|
1521
1869
|
import fg3 from "fast-glob";
|
|
1522
|
-
import
|
|
1523
|
-
import
|
|
1870
|
+
import fs8 from "fs/promises";
|
|
1871
|
+
import path12 from "path";
|
|
1524
1872
|
function escapeRegExp(text2) {
|
|
1525
1873
|
return text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1526
1874
|
}
|
|
@@ -1562,8 +1910,8 @@ async function moduleCandidates(root, combinedDocs) {
|
|
|
1562
1910
|
const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
|
|
1563
1911
|
const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
|
|
1564
1912
|
const listSubdirs = async (dir) => {
|
|
1565
|
-
const entries = await
|
|
1566
|
-
const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) =>
|
|
1913
|
+
const entries = await fs8.readdir(await auditInputPath(root, dir), { withFileTypes: true });
|
|
1914
|
+
const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path12.posix.join(dir, entry.name));
|
|
1567
1915
|
const ignored = await gitIgnoredPaths(root, dirs);
|
|
1568
1916
|
const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
|
|
1569
1917
|
return (await auditGlob(root, dir === "." ? "*" : `${fg3.escapePath(dir)}/*`, {
|
|
@@ -1571,7 +1919,7 @@ async function moduleCandidates(root, combinedDocs) {
|
|
|
1571
1919
|
onlyDirectories: true,
|
|
1572
1920
|
label: "Module directory discovery",
|
|
1573
1921
|
select: (file) => visible.has(file)
|
|
1574
|
-
})).map((file) =>
|
|
1922
|
+
})).map((file) => path12.posix.basename(file));
|
|
1575
1923
|
};
|
|
1576
1924
|
const countSourceFiles = async (dir) => {
|
|
1577
1925
|
const files = sources.filter((file) => file.startsWith(dir + "/"));
|
|
@@ -1640,7 +1988,7 @@ var init_new_module = __esm({
|
|
|
1640
1988
|
});
|
|
1641
1989
|
|
|
1642
1990
|
// src/audit/checks/stale-count.ts
|
|
1643
|
-
import
|
|
1991
|
+
import path13 from "path";
|
|
1644
1992
|
async function countGradleModules(root) {
|
|
1645
1993
|
for (const name of ["settings.gradle.kts", "settings.gradle"]) {
|
|
1646
1994
|
const content2 = await readAuditInput(root, name);
|
|
@@ -1674,7 +2022,7 @@ async function countNpmWorkspaces(root) {
|
|
|
1674
2022
|
return {
|
|
1675
2023
|
actual: matched.length,
|
|
1676
2024
|
countedFrom: "package.json workspaces",
|
|
1677
|
-
members: matched.map((m) =>
|
|
2025
|
+
members: matched.map((m) => path13.dirname(m)).sort()
|
|
1678
2026
|
};
|
|
1679
2027
|
}
|
|
1680
2028
|
}
|
|
@@ -1705,7 +2053,7 @@ async function countNpmWorkspaces(root) {
|
|
|
1705
2053
|
return {
|
|
1706
2054
|
actual: matched.length,
|
|
1707
2055
|
countedFrom: "pnpm-workspace.yaml",
|
|
1708
|
-
members: matched.map((m) =>
|
|
2056
|
+
members: matched.map((m) => path13.dirname(m)).sort()
|
|
1709
2057
|
};
|
|
1710
2058
|
}
|
|
1711
2059
|
}
|
|
@@ -1727,8 +2075,8 @@ async function countCargoCrates(root) {
|
|
|
1727
2075
|
ignore: ["**/target/**"],
|
|
1728
2076
|
label: "Cargo workspace discovery"
|
|
1729
2077
|
});
|
|
1730
|
-
for (const m of matched) members.add(
|
|
1731
|
-
} else if (await readAuditInput(root,
|
|
2078
|
+
for (const m of matched) members.add(path13.dirname(m));
|
|
2079
|
+
} else if (await readAuditInput(root, path13.posix.join(entry, "Cargo.toml")) !== null) {
|
|
1732
2080
|
members.add(entry);
|
|
1733
2081
|
}
|
|
1734
2082
|
}
|
|
@@ -1895,440 +2243,113 @@ function withoutAndroidReleaseValues(text2) {
|
|
|
1895
2243
|
}
|
|
1896
2244
|
async function releaseMetadataOnly(root, commit) {
|
|
1897
2245
|
if (!commit.files.length || !commit.files.every((file) => /(^|\/)build\.gradle(?:\.kts)?$/.test(file))) return false;
|
|
1898
|
-
try {
|
|
1899
|
-
const parents = (await git(root, ["rev-list", "--parents", "-n", "1", commit.hash])).trim().split(/\s+/);
|
|
1900
|
-
if (parents.length !== 2) return false;
|
|
1901
|
-
for (const file of commit.files) {
|
|
1902
|
-
const raw = await git(root, ["diff", "--raw", "-z", "--no-renames", "--no-ext-diff", "--no-textconv", parents[1], commit.hash, "--", file]);
|
|
1903
|
-
if (!/^:(100644|100755) \1 [a-f0-9]+ [a-f0-9]+ M\0/.test(raw)) return false;
|
|
1904
|
-
const [before, after] = await Promise.all([
|
|
1905
|
-
git(root, ["show", parents[1] + ":" + file]),
|
|
1906
|
-
git(root, ["show", commit.hash + ":" + file])
|
|
1907
|
-
]);
|
|
1908
|
-
const previous = withoutAndroidReleaseValues(before);
|
|
1909
|
-
if (previous === null || previous !== withoutAndroidReleaseValues(after)) return false;
|
|
1910
|
-
}
|
|
1911
|
-
return true;
|
|
1912
|
-
} catch {
|
|
1913
|
-
return false;
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
var exec6;
|
|
1917
|
-
var init_release_metadata = __esm({
|
|
1918
|
-
"src/audit/release-metadata.ts"() {
|
|
1919
|
-
"use strict";
|
|
1920
|
-
exec6 = promisify7(execFile7);
|
|
1921
|
-
}
|
|
1922
|
-
});
|
|
1923
|
-
|
|
1924
|
-
// src/audit/checks/deps-changed.ts
|
|
1925
|
-
async function checkDepsChanged(ctx) {
|
|
1926
|
-
const result = emptyResult();
|
|
1927
|
-
result.suppressedAdvisories = [];
|
|
1928
|
-
const releaseOnly = /* @__PURE__ */ new Map();
|
|
1929
|
-
for (const doc of ctx.docs) {
|
|
1930
|
-
if (!doc.lastCommit) {
|
|
1931
|
-
result.skipped.push({
|
|
1932
|
-
check: "deps-changed",
|
|
1933
|
-
doc: doc.path,
|
|
1934
|
-
reason: `${doc.path} has no commit history`
|
|
1935
|
-
});
|
|
1936
|
-
continue;
|
|
1937
|
-
}
|
|
1938
|
-
if (doc.dirty) {
|
|
1939
|
-
result.skipped.push({
|
|
1940
|
-
check: "deps-changed",
|
|
1941
|
-
doc: doc.path,
|
|
1942
|
-
reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
|
|
1943
|
-
});
|
|
1944
|
-
}
|
|
1945
|
-
const range = await commitsTouchingSince(
|
|
1946
|
-
ctx.root,
|
|
1947
|
-
doc.lastCommit.hash,
|
|
1948
|
-
MANIFEST_PATHSPECS
|
|
1949
|
-
);
|
|
1950
|
-
if (range === null) {
|
|
1951
|
-
result.skipped.push({
|
|
1952
|
-
check: "deps-changed",
|
|
1953
|
-
doc: doc.path,
|
|
1954
|
-
reason: `${doc.path}: commit range unreachable (shallow clone?)`
|
|
1955
|
-
});
|
|
1956
|
-
continue;
|
|
1957
|
-
}
|
|
1958
|
-
const relevant = [];
|
|
1959
|
-
for (const commit of range.commits) {
|
|
1960
|
-
if (!releaseOnly.has(commit.hash) && releaseOnly.size < 100) {
|
|
1961
|
-
releaseOnly.set(commit.hash, await releaseMetadataOnly(ctx.root, commit));
|
|
1962
|
-
}
|
|
1963
|
-
if (!releaseOnly.get(commit.hash)) relevant.push(commit);
|
|
1964
|
-
}
|
|
1965
|
-
range.commits = relevant;
|
|
1966
|
-
range.total = relevant.length;
|
|
1967
|
-
if (range.total === 0) continue;
|
|
1968
|
-
const latest = range.commits[0];
|
|
1969
|
-
(doc.dirty ? result.suppressedAdvisories : result.advisories).push({
|
|
1970
|
-
type: "deps-changed",
|
|
1971
|
-
message: `dependency manifests touched by ${range.total} commit${range.total === 1 ? "" : "s"} since ${doc.path} was last committed (latest: ${latest.hash.slice(0, 7)} "${latest.subject}")`,
|
|
1972
|
-
anchor: { doc: doc.path, line: null, excerpt: null },
|
|
1973
|
-
evidence: {
|
|
1974
|
-
kind: "doc-behind-manifests",
|
|
1975
|
-
docLastCommit: doc.lastCommit,
|
|
1976
|
-
manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
|
|
1977
|
-
totalCommits: range.total
|
|
1978
|
-
}
|
|
1979
|
-
});
|
|
1980
|
-
}
|
|
1981
|
-
return result;
|
|
1982
|
-
}
|
|
1983
|
-
var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
|
|
1984
|
-
var init_deps_changed = __esm({
|
|
1985
|
-
"src/audit/checks/deps-changed.ts"() {
|
|
1986
|
-
"use strict";
|
|
1987
|
-
init_release_metadata();
|
|
1988
|
-
init_git();
|
|
1989
|
-
init_checks();
|
|
1990
|
-
MANIFEST_COMMITS_CAP = 10;
|
|
1991
|
-
MANIFEST_PATHSPECS = [
|
|
1992
|
-
":(glob)**/package.json",
|
|
1993
|
-
":(glob)**/build.gradle.kts",
|
|
1994
|
-
":(glob)**/build.gradle",
|
|
1995
|
-
"settings.gradle.kts",
|
|
1996
|
-
"settings.gradle",
|
|
1997
|
-
"gradle/libs.versions.toml",
|
|
1998
|
-
":(glob)**/Cargo.toml",
|
|
1999
|
-
"go.mod",
|
|
2000
|
-
"pyproject.toml",
|
|
2001
|
-
"requirements.txt",
|
|
2002
|
-
"Gemfile",
|
|
2003
|
-
"composer.json"
|
|
2004
|
-
];
|
|
2005
|
-
}
|
|
2006
|
-
});
|
|
2007
|
-
|
|
2008
|
-
// src/context/lexical.ts
|
|
2009
|
-
import path12 from "path";
|
|
2010
|
-
function tokenize(text2) {
|
|
2011
|
-
return text2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2 && !STOPWORDS.has(t));
|
|
2012
|
-
}
|
|
2013
|
-
function stem(token) {
|
|
2014
|
-
return token.length > 3 && token.endsWith("s") ? token.slice(0, -1) : token;
|
|
2015
|
-
}
|
|
2016
|
-
function tokenSet(text2) {
|
|
2017
|
-
return new Set(tokenize(text2).map(stem));
|
|
2018
|
-
}
|
|
2019
|
-
function scoreEntry(taskTokens, entry) {
|
|
2020
|
-
const nameTokens = tokenSet(entry.name);
|
|
2021
|
-
const descTokens = tokenSet(entry.description);
|
|
2022
|
-
const fileTokens = tokenSet(entry.files.map((f) => path12.basename(f)).join(" "));
|
|
2023
|
-
let score = 0;
|
|
2024
|
-
for (const token of taskTokens) {
|
|
2025
|
-
if (nameTokens.has(token)) score += 3;
|
|
2026
|
-
else if (descTokens.has(token)) score += 1;
|
|
2027
|
-
else if (fileTokens.has(token)) score += 1;
|
|
2028
|
-
}
|
|
2029
|
-
return score;
|
|
2030
|
-
}
|
|
2031
|
-
function jaccard(a, b) {
|
|
2032
|
-
if (a.size === 0 && b.size === 0) return 0;
|
|
2033
|
-
let intersection = 0;
|
|
2034
|
-
for (const token of a) if (b.has(token)) intersection++;
|
|
2035
|
-
return intersection / (a.size + b.size - intersection);
|
|
2036
|
-
}
|
|
2037
|
-
var STOPWORDS;
|
|
2038
|
-
var init_lexical = __esm({
|
|
2039
|
-
"src/context/lexical.ts"() {
|
|
2040
|
-
"use strict";
|
|
2041
|
-
STOPWORDS = /* @__PURE__ */ new Set([
|
|
2042
|
-
"the",
|
|
2043
|
-
"a",
|
|
2044
|
-
"an",
|
|
2045
|
-
"and",
|
|
2046
|
-
"or",
|
|
2047
|
-
"of",
|
|
2048
|
-
"to",
|
|
2049
|
-
"in",
|
|
2050
|
-
"on",
|
|
2051
|
-
"for",
|
|
2052
|
-
"with",
|
|
2053
|
-
"how",
|
|
2054
|
-
"does",
|
|
2055
|
-
"do",
|
|
2056
|
-
"is",
|
|
2057
|
-
"are",
|
|
2058
|
-
"was",
|
|
2059
|
-
"what",
|
|
2060
|
-
"where",
|
|
2061
|
-
"which",
|
|
2062
|
-
"why",
|
|
2063
|
-
"when",
|
|
2064
|
-
"who",
|
|
2065
|
-
"i",
|
|
2066
|
-
"we",
|
|
2067
|
-
"my",
|
|
2068
|
-
"our",
|
|
2069
|
-
"you",
|
|
2070
|
-
"your",
|
|
2071
|
-
"it",
|
|
2072
|
-
"its",
|
|
2073
|
-
"this",
|
|
2074
|
-
"that",
|
|
2075
|
-
"these",
|
|
2076
|
-
"those",
|
|
2077
|
-
"can",
|
|
2078
|
-
"could",
|
|
2079
|
-
"should",
|
|
2080
|
-
"would",
|
|
2081
|
-
"will",
|
|
2082
|
-
"want",
|
|
2083
|
-
"need",
|
|
2084
|
-
"please",
|
|
2085
|
-
"about",
|
|
2086
|
-
"into",
|
|
2087
|
-
"from",
|
|
2088
|
-
"when",
|
|
2089
|
-
"there",
|
|
2090
|
-
"any",
|
|
2091
|
-
"all",
|
|
2092
|
-
"some",
|
|
2093
|
-
"not",
|
|
2094
|
-
"but",
|
|
2095
|
-
"also",
|
|
2096
|
-
"just",
|
|
2097
|
-
"like",
|
|
2098
|
-
"get",
|
|
2099
|
-
"make",
|
|
2100
|
-
"use",
|
|
2101
|
-
"new",
|
|
2102
|
-
"work",
|
|
2103
|
-
"works",
|
|
2104
|
-
"working",
|
|
2105
|
-
"implement",
|
|
2106
|
-
"implemented",
|
|
2107
|
-
"change",
|
|
2108
|
-
"changed",
|
|
2109
|
-
"file",
|
|
2110
|
-
"files",
|
|
2111
|
-
"code"
|
|
2112
|
-
]);
|
|
2113
|
-
}
|
|
2114
|
-
});
|
|
2115
|
-
|
|
2116
|
-
// src/decisions/decisions.ts
|
|
2117
|
-
var decisions_exports = {};
|
|
2118
|
-
__export(decisions_exports, {
|
|
2119
|
-
BODY_MAX_CHARS: () => BODY_MAX_CHARS,
|
|
2120
|
-
MAX_ACTIVE_DECISIONS: () => MAX_ACTIVE_DECISIONS,
|
|
2121
|
-
TITLE_MAX_CHARS: () => TITLE_MAX_CHARS,
|
|
2122
|
-
decisionIdFor: () => decisionIdFor,
|
|
2123
|
-
findNearDuplicate: () => findNearDuplicate,
|
|
2124
|
-
loadDecisionStore: () => loadDecisionStore,
|
|
2125
|
-
loadDecisions: () => loadDecisions,
|
|
2126
|
-
saveDecisionRecord: () => saveDecisionRecord,
|
|
2127
|
-
upsertDecision: () => upsertDecision,
|
|
2128
|
-
withDecisionWrite: () => withDecisionWrite
|
|
2129
|
-
});
|
|
2130
|
-
import fs8 from "fs/promises";
|
|
2131
|
-
import path13 from "path";
|
|
2132
|
-
import { createHash } from "crypto";
|
|
2133
|
-
async function loadDecisionStore(rootDir) {
|
|
2134
|
-
const records = [];
|
|
2135
|
-
const diagnostics = [];
|
|
2136
|
-
let entries;
|
|
2137
|
-
try {
|
|
2138
|
-
entries = await fs8.readdir(await storePath(rootDir, ".mason/decisions"));
|
|
2139
|
-
} catch (error) {
|
|
2140
|
-
if (error.code !== "ENOENT") diagnostics.push({ path: ".mason/decisions", message: String(error) });
|
|
2141
|
-
return { records, diagnostics };
|
|
2142
|
-
}
|
|
2143
|
-
for (const entry of entries.sort()) {
|
|
2144
|
-
if (!entry.endsWith(".json")) continue;
|
|
2145
|
-
const relative = `.mason/decisions/${entry}`;
|
|
2146
|
-
try {
|
|
2147
|
-
const record = decisionSchema.parse(await readStoreJson(rootDir, relative));
|
|
2148
|
-
if (entry !== `${record.id}.json`) throw new Error("Record id does not match its filename");
|
|
2149
|
-
records.push(record);
|
|
2150
|
-
} catch (error) {
|
|
2151
|
-
diagnostics.push({ path: relative, message: error instanceof Error ? error.message : String(error) });
|
|
2152
|
-
}
|
|
2153
|
-
}
|
|
2154
|
-
return { records, diagnostics };
|
|
2155
|
-
}
|
|
2156
|
-
async function loadDecisions(rootDir) {
|
|
2157
|
-
return (await loadDecisionStore(rootDir)).records;
|
|
2158
|
-
}
|
|
2159
|
-
async function saveDecisionRecord(rootDir, record) {
|
|
2160
|
-
const validated = decisionSchema.parse(record);
|
|
2161
|
-
await writeStoreJson(rootDir, `.mason/decisions/${validated.id}.json`, validated);
|
|
2162
|
-
}
|
|
2163
|
-
function decisionIdFor(title, body, existingIds) {
|
|
2164
|
-
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/, "");
|
|
2165
|
-
if (!existingIds.has(slug)) return slug || "decision";
|
|
2166
|
-
const suffix = createHash("sha1").update(title + body).digest("hex").slice(0, 6);
|
|
2167
|
-
return `${slug}-${suffix}`;
|
|
2168
|
-
}
|
|
2169
|
-
function findNearDuplicate(candidate, existing) {
|
|
2170
|
-
const candidateTokens = tokenSet(`${candidate.title} ${candidate.body}`);
|
|
2171
|
-
const candidateFiles = new Set(candidate.files);
|
|
2172
|
-
let best = null;
|
|
2173
|
-
for (const record of existing) {
|
|
2174
|
-
if (record.status !== "active") continue;
|
|
2175
|
-
const similarity = jaccard(
|
|
2176
|
-
candidateTokens,
|
|
2177
|
-
tokenSet(`${record.title} ${record.body}`)
|
|
2178
|
-
);
|
|
2179
|
-
const sharesFile = record.files.some((f) => candidateFiles.has(f));
|
|
2180
|
-
const threshold = sharesFile ? DUPLICATE_JACCARD_WITH_SHARED_FILE : DUPLICATE_JACCARD;
|
|
2181
|
-
if (similarity >= threshold && (!best || similarity > best.similarity)) {
|
|
2182
|
-
best = { record, similarity };
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
return best;
|
|
2186
|
-
}
|
|
2187
|
-
async function withDecisionWrite(root, operation) {
|
|
2188
|
-
const lockPath = await storePath(root, ".mason/decisions/.write-lock", true);
|
|
2189
|
-
let lock;
|
|
2190
|
-
try {
|
|
2191
|
-
lock = await fs8.open(lockPath, "wx", 384);
|
|
2192
|
-
} catch (error) {
|
|
2193
|
-
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." };
|
|
2194
|
-
throw error;
|
|
2195
|
-
}
|
|
2196
|
-
try {
|
|
2197
|
-
return await operation();
|
|
2198
|
-
} finally {
|
|
2199
|
-
await lock.close();
|
|
2200
|
-
await fs8.unlink(lockPath);
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
async function upsertDecision(rootDir, input) {
|
|
2204
|
-
const title = input.title.trim(), body = input.body.trim();
|
|
2205
|
-
if (!title || !body) return { status: "error", error: "title and body must be non-empty" };
|
|
2206
|
-
if (title.length > TITLE_MAX_CHARS) return { status: "error", error: `title exceeds ${TITLE_MAX_CHARS} chars \u2014 tighten it to a specific headline` };
|
|
2207
|
-
if (body.length > BODY_MAX_CHARS) return { status: "error", error: `body exceeds ${BODY_MAX_CHARS} chars \u2014 record the decision, not the transcript` };
|
|
2208
|
-
const attribution = attributionSchema.safeParse(input);
|
|
2209
|
-
if (!attribution.success) return { status: "error", error: attribution.error.message };
|
|
2210
|
-
if (input.id && input.supersedes) return { status: "error", error: "Use either id to revise or supersedes to replace a record, not both." };
|
|
2211
|
-
return withDecisionWrite(rootDir, async () => {
|
|
2212
|
-
const store = await loadDecisionStore(rootDir);
|
|
2213
|
-
if (store.diagnostics.length) return { status: "error", error: "Repair malformed decision records before saving: " + store.diagnostics.map((d) => d.path).join(", ") };
|
|
2214
|
-
const existing = store.records, byId = new Map(existing.map((r) => [r.id, r]));
|
|
2215
|
-
const now = (/* @__PURE__ */ new Date()).toISOString(), head = await getCurrentGitHash(rootDir);
|
|
2216
|
-
const warnings = [];
|
|
2217
|
-
const files = sanitizeRepoPaths(input.files ?? []);
|
|
2218
|
-
if (input.files && files.length < input.files.length) warnings.push("some anchor paths were outside the repo or duplicated and were dropped");
|
|
2219
|
-
for (const file of files) {
|
|
2220
|
-
try {
|
|
2221
|
-
await fs8.access(path13.join(rootDir, file));
|
|
2222
|
-
} catch {
|
|
2223
|
-
warnings.push(`anchor file does not exist on disk: ${file}`);
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
const hint = "Saved locally for review and commit. Proposals are not accepted constraints; an existing accepted revision remains operative while its replacement is proposed. Use review_decision to inspect evidence and record an authorized acceptance or reaffirmation.";
|
|
2227
|
-
if (input.id) {
|
|
2228
|
-
const original = byId.get(input.id);
|
|
2229
|
-
if (!original) return { status: "error", error: `no decision with id "${input.id}"` };
|
|
2230
|
-
if (original.status !== "active") return { status: "error", error: "Archived records cannot be revised; create a new proposal." };
|
|
2231
|
-
const record2 = importLegacy(original, now);
|
|
2232
|
-
const content3 = decisionContent({
|
|
2233
|
-
...record2,
|
|
2234
|
-
title,
|
|
2235
|
-
body,
|
|
2236
|
-
category: input.category,
|
|
2237
|
-
files: input.files !== void 0 ? files : record2.files,
|
|
2238
|
-
owner: attribution.data.owner === void 0 ? record2.owner : attribution.data.owner ?? void 0,
|
|
2239
|
-
sources: attribution.data.sources ?? record2.sources
|
|
2240
|
-
});
|
|
2241
|
-
if (JSON.stringify(content3) === JSON.stringify(decisionContent(record2))) {
|
|
2242
|
-
return {
|
|
2243
|
-
status: "unchanged",
|
|
2244
|
-
id: record2.id,
|
|
2245
|
-
totalActive: existing.filter((r) => r.status === "active").length,
|
|
2246
|
-
approval: decisionApproval(original),
|
|
2247
|
-
warnings,
|
|
2248
|
-
hint: "Unchanged content; no review or freshness stamp was written. Use review_decision for explicit re-verification."
|
|
2249
|
-
};
|
|
2250
|
-
}
|
|
2251
|
-
const revision = record2.revision + 1;
|
|
2252
|
-
const updated = {
|
|
2253
|
-
...record2,
|
|
2254
|
-
...content3,
|
|
2255
|
-
owner: content3.owner,
|
|
2256
|
-
updatedAt: now,
|
|
2257
|
-
revision,
|
|
2258
|
-
approval: "proposed",
|
|
2259
|
-
history: [...record2.history, { kind: "revised", at: now, actor: attribution.data.actor, revision, content: content3, approval: "proposed", status: "active", refreshedHash: record2.refreshedHash }]
|
|
2260
|
-
};
|
|
2261
|
-
await saveDecisionRecord(rootDir, updated);
|
|
2262
|
-
return { status: "updated", id: record2.id, totalActive: existing.filter((r) => r.status === "active").length, approval: "proposed", warnings, hint };
|
|
2246
|
+
try {
|
|
2247
|
+
const parents = (await git(root, ["rev-list", "--parents", "-n", "1", commit.hash])).trim().split(/\s+/);
|
|
2248
|
+
if (parents.length !== 2) return false;
|
|
2249
|
+
for (const file of commit.files) {
|
|
2250
|
+
const raw = await git(root, ["diff", "--raw", "-z", "--no-renames", "--no-ext-diff", "--no-textconv", parents[1], commit.hash, "--", file]);
|
|
2251
|
+
if (!/^:(100644|100755) \1 [a-f0-9]+ [a-f0-9]+ M\0/.test(raw)) return false;
|
|
2252
|
+
const [before, after] = await Promise.all([
|
|
2253
|
+
git(root, ["show", parents[1] + ":" + file]),
|
|
2254
|
+
git(root, ["show", commit.hash + ":" + file])
|
|
2255
|
+
]);
|
|
2256
|
+
const previous = withoutAndroidReleaseValues(before);
|
|
2257
|
+
if (previous === null || previous !== withoutAndroidReleaseValues(after)) return false;
|
|
2263
2258
|
}
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2259
|
+
return true;
|
|
2260
|
+
} catch {
|
|
2261
|
+
return false;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
var exec6;
|
|
2265
|
+
var init_release_metadata = __esm({
|
|
2266
|
+
"src/audit/release-metadata.ts"() {
|
|
2267
|
+
"use strict";
|
|
2268
|
+
exec6 = promisify7(execFile7);
|
|
2269
|
+
}
|
|
2270
|
+
});
|
|
2271
|
+
|
|
2272
|
+
// src/audit/checks/deps-changed.ts
|
|
2273
|
+
async function checkDepsChanged(ctx) {
|
|
2274
|
+
const result = emptyResult();
|
|
2275
|
+
result.suppressedAdvisories = [];
|
|
2276
|
+
const releaseOnly = /* @__PURE__ */ new Map();
|
|
2277
|
+
for (const doc of ctx.docs) {
|
|
2278
|
+
if (!doc.lastCommit) {
|
|
2279
|
+
result.skipped.push({
|
|
2280
|
+
check: "deps-changed",
|
|
2281
|
+
doc: doc.path,
|
|
2282
|
+
reason: `${doc.path} has no commit history`
|
|
2283
|
+
});
|
|
2284
|
+
continue;
|
|
2268
2285
|
}
|
|
2269
|
-
if (
|
|
2270
|
-
|
|
2271
|
-
|
|
2286
|
+
if (doc.dirty) {
|
|
2287
|
+
result.skipped.push({
|
|
2288
|
+
check: "deps-changed",
|
|
2289
|
+
doc: doc.path,
|
|
2290
|
+
reason: `${doc.path} has uncommitted edits \u2013 suppressed while in flight`
|
|
2291
|
+
});
|
|
2272
2292
|
}
|
|
2273
|
-
const
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
status: "active",
|
|
2284
|
-
approval: "proposed",
|
|
2285
|
-
revision: 1,
|
|
2286
|
-
history: [{ kind: "created", at: now, actor: attribution.data.actor, revision: 1, content: content2, approval: "proposed", status: "active", refreshedHash: head }]
|
|
2287
|
-
};
|
|
2288
|
-
await saveDecisionRecord(rootDir, record);
|
|
2289
|
-
if (old) {
|
|
2290
|
-
const imported = importLegacy(old, now);
|
|
2291
|
-
await saveDecisionRecord(rootDir, {
|
|
2292
|
-
...imported,
|
|
2293
|
-
status: "superseded",
|
|
2294
|
-
supersededBy: id,
|
|
2295
|
-
updatedAt: now,
|
|
2296
|
-
history: [...imported.history, {
|
|
2297
|
-
kind: "superseded",
|
|
2298
|
-
at: now,
|
|
2299
|
-
actor: attribution.data.actor,
|
|
2300
|
-
note: `Replaced by proposal ${id}`,
|
|
2301
|
-
revision: imported.revision,
|
|
2302
|
-
content: decisionContent(imported),
|
|
2303
|
-
approval: imported.approval,
|
|
2304
|
-
status: "superseded",
|
|
2305
|
-
refreshedHash: imported.refreshedHash
|
|
2306
|
-
}]
|
|
2293
|
+
const range = await commitsTouchingSince(
|
|
2294
|
+
ctx.root,
|
|
2295
|
+
doc.lastCommit.hash,
|
|
2296
|
+
MANIFEST_PATHSPECS
|
|
2297
|
+
);
|
|
2298
|
+
if (range === null) {
|
|
2299
|
+
result.skipped.push({
|
|
2300
|
+
check: "deps-changed",
|
|
2301
|
+
doc: doc.path,
|
|
2302
|
+
reason: `${doc.path}: commit range unreachable (shallow clone?)`
|
|
2307
2303
|
});
|
|
2304
|
+
continue;
|
|
2308
2305
|
}
|
|
2309
|
-
const
|
|
2310
|
-
const
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2306
|
+
const relevant = [];
|
|
2307
|
+
for (const commit of range.commits) {
|
|
2308
|
+
if (!releaseOnly.has(commit.hash) && releaseOnly.size < 100) {
|
|
2309
|
+
releaseOnly.set(commit.hash, await releaseMetadataOnly(ctx.root, commit));
|
|
2310
|
+
}
|
|
2311
|
+
if (!releaseOnly.get(commit.hash)) relevant.push(commit);
|
|
2314
2312
|
}
|
|
2315
|
-
|
|
2316
|
-
|
|
2313
|
+
range.commits = relevant;
|
|
2314
|
+
range.total = relevant.length;
|
|
2315
|
+
if (range.total === 0) continue;
|
|
2316
|
+
const latest = range.commits[0];
|
|
2317
|
+
(doc.dirty ? result.suppressedAdvisories : result.advisories).push({
|
|
2318
|
+
type: "deps-changed",
|
|
2319
|
+
message: `dependency manifests touched by ${range.total} commit${range.total === 1 ? "" : "s"} since ${doc.path} was last committed (latest: ${latest.hash.slice(0, 7)} "${latest.subject}")`,
|
|
2320
|
+
anchor: { doc: doc.path, line: null, excerpt: null },
|
|
2321
|
+
evidence: {
|
|
2322
|
+
kind: "doc-behind-manifests",
|
|
2323
|
+
docLastCommit: doc.lastCommit,
|
|
2324
|
+
manifestCommits: range.commits.slice(0, MANIFEST_COMMITS_CAP),
|
|
2325
|
+
totalCommits: range.total
|
|
2326
|
+
}
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
return result;
|
|
2317
2330
|
}
|
|
2318
|
-
var
|
|
2319
|
-
var
|
|
2320
|
-
"src/
|
|
2331
|
+
var MANIFEST_COMMITS_CAP, MANIFEST_PATHSPECS;
|
|
2332
|
+
var init_deps_changed = __esm({
|
|
2333
|
+
"src/audit/checks/deps-changed.ts"() {
|
|
2321
2334
|
"use strict";
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2335
|
+
init_release_metadata();
|
|
2336
|
+
init_git();
|
|
2337
|
+
init_checks();
|
|
2338
|
+
MANIFEST_COMMITS_CAP = 10;
|
|
2339
|
+
MANIFEST_PATHSPECS = [
|
|
2340
|
+
":(glob)**/package.json",
|
|
2341
|
+
":(glob)**/build.gradle.kts",
|
|
2342
|
+
":(glob)**/build.gradle",
|
|
2343
|
+
"settings.gradle.kts",
|
|
2344
|
+
"settings.gradle",
|
|
2345
|
+
"gradle/libs.versions.toml",
|
|
2346
|
+
":(glob)**/Cargo.toml",
|
|
2347
|
+
"go.mod",
|
|
2348
|
+
"pyproject.toml",
|
|
2349
|
+
"requirements.txt",
|
|
2350
|
+
"Gemfile",
|
|
2351
|
+
"composer.json"
|
|
2352
|
+
];
|
|
2332
2353
|
}
|
|
2333
2354
|
});
|
|
2334
2355
|
|
|
@@ -2527,7 +2548,7 @@ var init_audit = __esm({
|
|
|
2527
2548
|
import fs10 from "fs/promises";
|
|
2528
2549
|
import path16 from "path";
|
|
2529
2550
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
2530
|
-
import { z as
|
|
2551
|
+
import { z as z4 } from "zod";
|
|
2531
2552
|
function findingId(finding) {
|
|
2532
2553
|
const e = finding.evidence;
|
|
2533
2554
|
let key;
|
|
@@ -2581,7 +2602,7 @@ async function stableAudit(root, checks, options = {}) {
|
|
|
2581
2602
|
}
|
|
2582
2603
|
async function prepareRepair(rootDir, checks = ALL_CHECKS, options = {}) {
|
|
2583
2604
|
const root = await fs10.realpath(rootDir);
|
|
2584
|
-
const selected =
|
|
2605
|
+
const selected = z4.array(checkSchema).nonempty().parse(checks);
|
|
2585
2606
|
const report = await stableAudit(root, selected, options);
|
|
2586
2607
|
if (!report) throw new Error("No context files found to prepare a repair.");
|
|
2587
2608
|
if (!report.gitAvailable) throw new Error("Readable Git history is required to prepare a repair.");
|
|
@@ -2672,7 +2693,7 @@ async function verifyRepair(rootDir, baselinePath, options = {}) {
|
|
|
2672
2693
|
scope: "Original audit checks over current context files and repository evidence. Resolved means no longer detected by that check. Advisories require separate review; this is not a certification of documentation or application correctness."
|
|
2673
2694
|
};
|
|
2674
2695
|
}
|
|
2675
|
-
var checkSchema, commitSchema, anchorSchema, count, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema, reportSchema, baselineSchema, digest;
|
|
2696
|
+
var checkSchema, commitSchema, anchorSchema, count, evidenceSchema, findingSchema, issueSchema, advisorySchema, checkResultSchema, reportSchema, baselineSchema, digest, repairFindingSchema;
|
|
2676
2697
|
var init_repair = __esm({
|
|
2677
2698
|
"src/audit/repair.ts"() {
|
|
2678
2699
|
"use strict";
|
|
@@ -2684,95 +2705,102 @@ var init_repair = __esm({
|
|
|
2684
2705
|
init_files();
|
|
2685
2706
|
init_paths();
|
|
2686
2707
|
init_types();
|
|
2687
|
-
checkSchema =
|
|
2688
|
-
commitSchema =
|
|
2689
|
-
anchorSchema =
|
|
2690
|
-
count =
|
|
2691
|
-
evidenceSchema =
|
|
2692
|
-
|
|
2693
|
-
kind:
|
|
2694
|
-
claimed:
|
|
2695
|
-
renamedTo:
|
|
2708
|
+
checkSchema = z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"]);
|
|
2709
|
+
commitSchema = z4.object({ hash: z4.string().regex(/^[a-f0-9]{40,64}$/), date: z4.string(), subject: z4.string() });
|
|
2710
|
+
anchorSchema = z4.object({ doc: z4.string(), line: z4.number().int().positive().nullable(), excerpt: z4.string().nullable() });
|
|
2711
|
+
count = z4.number().int().nonnegative();
|
|
2712
|
+
evidenceSchema = z4.discriminatedUnion("kind", [
|
|
2713
|
+
z4.object({
|
|
2714
|
+
kind: z4.literal("missing-path"),
|
|
2715
|
+
claimed: z4.string(),
|
|
2716
|
+
renamedTo: z4.string().nullable(),
|
|
2696
2717
|
deletedInCommit: commitSchema.nullable(),
|
|
2697
|
-
everTracked:
|
|
2698
|
-
parentDirExists:
|
|
2718
|
+
everTracked: z4.boolean(),
|
|
2719
|
+
parentDirExists: z4.boolean()
|
|
2699
2720
|
}),
|
|
2700
|
-
|
|
2701
|
-
kind:
|
|
2702
|
-
dir:
|
|
2721
|
+
z4.object({
|
|
2722
|
+
kind: z4.literal("unmentioned-dir"),
|
|
2723
|
+
dir: z4.string(),
|
|
2703
2724
|
sourceFileCount: count,
|
|
2704
2725
|
firstCommit: commitSchema.nullable(),
|
|
2705
|
-
checkedDocs:
|
|
2726
|
+
checkedDocs: z4.array(z4.string())
|
|
2706
2727
|
}),
|
|
2707
|
-
|
|
2708
|
-
kind:
|
|
2728
|
+
z4.object({
|
|
2729
|
+
kind: z4.literal("count-mismatch"),
|
|
2709
2730
|
claimed: count,
|
|
2710
2731
|
actual: count,
|
|
2711
|
-
unit:
|
|
2712
|
-
countedFrom:
|
|
2713
|
-
members:
|
|
2732
|
+
unit: z4.string(),
|
|
2733
|
+
countedFrom: z4.string(),
|
|
2734
|
+
members: z4.array(z4.string())
|
|
2714
2735
|
}),
|
|
2715
|
-
|
|
2716
|
-
kind:
|
|
2717
|
-
scriptName:
|
|
2718
|
-
invocation:
|
|
2719
|
-
manifestsChecked:
|
|
2720
|
-
availableScripts:
|
|
2736
|
+
z4.object({
|
|
2737
|
+
kind: z4.literal("missing-script"),
|
|
2738
|
+
scriptName: z4.string(),
|
|
2739
|
+
invocation: z4.string(),
|
|
2740
|
+
manifestsChecked: z4.array(z4.string()),
|
|
2741
|
+
availableScripts: z4.array(z4.string())
|
|
2721
2742
|
}),
|
|
2722
|
-
|
|
2723
|
-
kind:
|
|
2743
|
+
z4.object({
|
|
2744
|
+
kind: z4.literal("doc-behind-manifests"),
|
|
2724
2745
|
docLastCommit: commitSchema,
|
|
2725
|
-
manifestCommits:
|
|
2746
|
+
manifestCommits: z4.array(commitSchema.extend({ files: z4.array(z4.string()) })),
|
|
2726
2747
|
totalCommits: count
|
|
2727
2748
|
}),
|
|
2728
|
-
|
|
2729
|
-
kind:
|
|
2730
|
-
decisionId:
|
|
2731
|
-
title:
|
|
2732
|
-
changedFiles:
|
|
2733
|
-
refreshedHash:
|
|
2734
|
-
provenance:
|
|
2749
|
+
z4.object({
|
|
2750
|
+
kind: z4.literal("decision-anchor"),
|
|
2751
|
+
decisionId: z4.string(),
|
|
2752
|
+
title: z4.string(),
|
|
2753
|
+
changedFiles: z4.array(z4.string()),
|
|
2754
|
+
refreshedHash: z4.string(),
|
|
2755
|
+
provenance: z4.object({}).passthrough().optional()
|
|
2735
2756
|
})
|
|
2736
2757
|
]);
|
|
2737
|
-
findingSchema =
|
|
2758
|
+
findingSchema = z4.object({ message: z4.string(), anchor: anchorSchema, evidence: evidenceSchema });
|
|
2738
2759
|
issueSchema = findingSchema.extend({
|
|
2739
|
-
type:
|
|
2740
|
-
confidence:
|
|
2760
|
+
type: z4.enum(["deleted-reference", "new-module", "stale-count", "dead-command"]),
|
|
2761
|
+
confidence: z4.enum(["certain", "likely"])
|
|
2741
2762
|
});
|
|
2742
|
-
advisorySchema = findingSchema.extend({ type:
|
|
2743
|
-
checkResultSchema =
|
|
2744
|
-
issues:
|
|
2745
|
-
advisories:
|
|
2746
|
-
suppressedAdvisories:
|
|
2747
|
-
skipped:
|
|
2763
|
+
advisorySchema = findingSchema.extend({ type: z4.enum(["deps-changed", "decision-anchor-drift"]) });
|
|
2764
|
+
checkResultSchema = z4.object({
|
|
2765
|
+
issues: z4.array(issueSchema),
|
|
2766
|
+
advisories: z4.array(advisorySchema),
|
|
2767
|
+
suppressedAdvisories: z4.array(advisorySchema).optional(),
|
|
2768
|
+
skipped: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
|
|
2748
2769
|
});
|
|
2749
|
-
reportSchema =
|
|
2750
|
-
version:
|
|
2751
|
-
root:
|
|
2752
|
-
gitAvailable:
|
|
2770
|
+
reportSchema = z4.object({
|
|
2771
|
+
version: z4.literal(1),
|
|
2772
|
+
root: z4.string(),
|
|
2773
|
+
gitAvailable: z4.literal(true),
|
|
2753
2774
|
headHash: commitSchema.shape.hash,
|
|
2754
|
-
checksRun:
|
|
2755
|
-
docs:
|
|
2756
|
-
path:
|
|
2775
|
+
checksRun: z4.array(checkSchema).nonempty(),
|
|
2776
|
+
docs: z4.array(z4.object({
|
|
2777
|
+
path: z4.enum(DOC_CANDIDATES),
|
|
2757
2778
|
lastCommit: commitSchema.nullable(),
|
|
2758
|
-
dirty:
|
|
2779
|
+
dirty: z4.boolean(),
|
|
2759
2780
|
lineCount: count
|
|
2760
2781
|
})).nonempty(),
|
|
2761
|
-
decisionsChecked:
|
|
2762
|
-
clean:
|
|
2763
|
-
issues:
|
|
2764
|
-
advisories:
|
|
2765
|
-
suppressedAdvisories:
|
|
2766
|
-
skippedChecks:
|
|
2782
|
+
decisionsChecked: z4.boolean(),
|
|
2783
|
+
clean: z4.boolean(),
|
|
2784
|
+
issues: z4.array(issueSchema),
|
|
2785
|
+
advisories: z4.array(advisorySchema),
|
|
2786
|
+
suppressedAdvisories: z4.array(advisorySchema).optional(),
|
|
2787
|
+
skippedChecks: z4.array(z4.object({ check: z4.string(), reason: z4.string(), doc: z4.string().optional() }))
|
|
2767
2788
|
});
|
|
2768
|
-
baselineSchema =
|
|
2769
|
-
kind:
|
|
2770
|
-
version:
|
|
2771
|
-
createdAt:
|
|
2789
|
+
baselineSchema = z4.object({
|
|
2790
|
+
kind: z4.literal("mason-audit-repair"),
|
|
2791
|
+
version: z4.literal(1),
|
|
2792
|
+
createdAt: z4.string().datetime(),
|
|
2772
2793
|
report: reportSchema,
|
|
2773
|
-
digest:
|
|
2794
|
+
digest: z4.string().regex(/^[a-f0-9]{64}$/)
|
|
2774
2795
|
});
|
|
2775
2796
|
digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2797
|
+
repairFindingSchema = z4.object({
|
|
2798
|
+
id: z4.string(),
|
|
2799
|
+
original: z4.union([issueSchema, advisorySchema]),
|
|
2800
|
+
status: z4.enum(["resolved", "unresolved", "review-required", "unverified"]),
|
|
2801
|
+
reason: z4.string(),
|
|
2802
|
+
current: z4.union([issueSchema, advisorySchema]).optional()
|
|
2803
|
+
});
|
|
2776
2804
|
}
|
|
2777
2805
|
});
|
|
2778
2806
|
|
|
@@ -2865,134 +2893,101 @@ var init_files2 = __esm({
|
|
|
2865
2893
|
}
|
|
2866
2894
|
});
|
|
2867
2895
|
|
|
2868
|
-
// src/automation/
|
|
2869
|
-
|
|
2870
|
-
__export(execution_exports, {
|
|
2871
|
-
automationFailure: () => automationFailure,
|
|
2872
|
-
executionStatus: () => executionStatus,
|
|
2873
|
-
failureMessage: () => failureMessage,
|
|
2874
|
-
recordExecution: () => recordExecution
|
|
2875
|
-
});
|
|
2896
|
+
// src/automation/store.ts
|
|
2897
|
+
import fs14 from "fs/promises";
|
|
2876
2898
|
import os from "os";
|
|
2877
|
-
import {
|
|
2878
|
-
|
|
2879
|
-
function parseExecution(raw) {
|
|
2880
|
-
try {
|
|
2881
|
-
return executionSchema.parse(raw);
|
|
2882
|
-
} catch (error) {
|
|
2883
|
-
throw new Error("Invalid automation execution store; receipt history was retained.", { cause: error });
|
|
2884
|
-
}
|
|
2885
|
-
}
|
|
2886
|
-
function automationFailure(error) {
|
|
2887
|
-
const recorded = failureSchema.safeParse(error?.failure);
|
|
2888
|
-
if (recorded.success) return recorded.data;
|
|
2889
|
-
const message2 = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
|
|
2890
|
-
const codes = /* @__PURE__ */ new Set();
|
|
2891
|
-
let cause = error;
|
|
2892
|
-
for (let i = 0; cause && i < 8; i++) {
|
|
2893
|
-
codes.add(String(cause.code));
|
|
2894
|
-
cause = cause.cause;
|
|
2895
|
-
}
|
|
2896
|
-
const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z6.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
|
|
2897
|
-
return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
|
|
2898
|
-
}
|
|
2899
|
-
function failureMessage(error) {
|
|
2900
|
-
const failure = automationFailure(error);
|
|
2901
|
-
return `Mason automation unavailable [${failure.code}]; evidence capture/verification was not established. ${failure.message}` + (failure.receiptRecorded ? "" : " No durable failure receipt was recorded.");
|
|
2902
|
-
}
|
|
2903
|
-
async function recordExecution(root, directory, event, run) {
|
|
2904
|
-
const file = directory + "/execution.json";
|
|
2905
|
-
const raw = await readStoreJson(root, file);
|
|
2906
|
-
const log = raw === null ? { version: 1, attempts: [], discardedAttempts: 0 } : parseExecution(raw);
|
|
2907
|
-
for (const attempt2 of log.attempts) if (attempt2.status === "running") attempt2.status = "unknown";
|
|
2908
|
-
log.discardedAttempts += Math.max(0, log.attempts.length - 31);
|
|
2909
|
-
log.attempts = log.attempts.slice(-31);
|
|
2910
|
-
const started = performance.now();
|
|
2911
|
-
const attempt = {
|
|
2912
|
-
id: randomUUID4(),
|
|
2913
|
-
event,
|
|
2914
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2915
|
-
pid: process.pid,
|
|
2916
|
-
host: os.hostname(),
|
|
2917
|
-
status: "running"
|
|
2918
|
-
};
|
|
2919
|
-
log.attempts.push(attempt);
|
|
2920
|
-
await writeStoreJson(root, file, log);
|
|
2899
|
+
import { z as z7 } from "zod";
|
|
2900
|
+
function parseState(raw) {
|
|
2921
2901
|
try {
|
|
2922
|
-
|
|
2923
|
-
Object.assign(attempt, {
|
|
2924
|
-
status: "completed",
|
|
2925
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2926
|
-
durationMs: performance.now() - started,
|
|
2927
|
-
verificationStatus: result.report.status,
|
|
2928
|
-
reportPath: result.report.reportPath
|
|
2929
|
-
});
|
|
2930
|
-
await writeStoreJson(root, file, log);
|
|
2931
|
-
return result;
|
|
2902
|
+
return stateSchema.parse(raw);
|
|
2932
2903
|
} catch (error) {
|
|
2933
|
-
|
|
2934
|
-
Object.assign(attempt, { status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: performance.now() - started, failure });
|
|
2935
|
-
delete attempt.verificationStatus;
|
|
2936
|
-
delete attempt.reportPath;
|
|
2937
|
-
try {
|
|
2938
|
-
await writeStoreJson(root, file, { ...log, attempts: log.attempts.map((a) => a === attempt ? { ...a, failure: { ...failure, receiptRecorded: true } } : a) });
|
|
2939
|
-
failure.receiptRecorded = true;
|
|
2940
|
-
} catch {
|
|
2941
|
-
}
|
|
2942
|
-
throw Object.assign(new Error(failure.message, { cause: error }), { failure });
|
|
2904
|
+
throw new Error("Invalid automation state; original evidence was retained.", { cause: error });
|
|
2943
2905
|
}
|
|
2944
2906
|
}
|
|
2945
|
-
async function
|
|
2946
|
-
const
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
if (log.attempts.some((attempt) => attempt.status === "running")) {
|
|
2907
|
+
async function withLock(root, directory, run, waitMs = 5e3) {
|
|
2908
|
+
const file = await storePath(root, directory + "/lock", true);
|
|
2909
|
+
const deadline = Date.now() + waitMs;
|
|
2910
|
+
let handle;
|
|
2911
|
+
while (!handle) {
|
|
2951
2912
|
try {
|
|
2952
|
-
|
|
2953
|
-
} catch {
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
for (const attempt of log.attempts) {
|
|
2957
|
-
if (attempt.status !== "running") continue;
|
|
2958
|
-
let alive = false;
|
|
2959
|
-
if (attempt.host === os.hostname() && lock?.pid === attempt.pid && lock.host === attempt.host) {
|
|
2913
|
+
handle = await fs14.open(file, "wx", 384);
|
|
2960
2914
|
try {
|
|
2961
|
-
|
|
2962
|
-
alive = true;
|
|
2915
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, host: os.hostname() }));
|
|
2963
2916
|
} catch (error) {
|
|
2964
|
-
|
|
2917
|
+
await handle.close().catch(() => {
|
|
2918
|
+
});
|
|
2919
|
+
handle = void 0;
|
|
2920
|
+
await fs14.rm(file, { force: true }).catch(() => {
|
|
2921
|
+
});
|
|
2922
|
+
throw error;
|
|
2923
|
+
}
|
|
2924
|
+
} catch (error) {
|
|
2925
|
+
if (error.code !== "EEXIST") throw error;
|
|
2926
|
+
try {
|
|
2927
|
+
const owner = JSON.parse(await fs14.readFile(file, "utf8"));
|
|
2928
|
+
if (owner.host === os.hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
2929
|
+
try {
|
|
2930
|
+
process.kill(owner.pid, 0);
|
|
2931
|
+
} catch (probe) {
|
|
2932
|
+
if (probe.code === "ESRCH") {
|
|
2933
|
+
const reclaim = file + ".reclaim";
|
|
2934
|
+
let guard;
|
|
2935
|
+
try {
|
|
2936
|
+
guard = await fs14.open(reclaim, "wx", 384);
|
|
2937
|
+
const current = JSON.parse(await fs14.readFile(file, "utf8"));
|
|
2938
|
+
if (current.pid === owner.pid && current.host === owner.host) await fs14.unlink(file);
|
|
2939
|
+
} finally {
|
|
2940
|
+
if (guard) {
|
|
2941
|
+
await guard.close();
|
|
2942
|
+
await fs14.rm(reclaim, { force: true });
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
} catch {
|
|
2965
2949
|
}
|
|
2950
|
+
if (Date.now() >= deadline) throw new Error("Automation is busy or its lock needs inspection: " + file);
|
|
2951
|
+
await new Promise((resolve2) => setTimeout(resolve2, 40));
|
|
2966
2952
|
}
|
|
2967
|
-
if (!alive) attempt.status = "unknown";
|
|
2968
2953
|
}
|
|
2969
|
-
|
|
2954
|
+
try {
|
|
2955
|
+
return await run();
|
|
2956
|
+
} finally {
|
|
2957
|
+
await handle.close();
|
|
2958
|
+
await fs14.unlink(file);
|
|
2959
|
+
}
|
|
2970
2960
|
}
|
|
2971
|
-
var
|
|
2972
|
-
var
|
|
2973
|
-
"src/automation/
|
|
2961
|
+
var hostSchema, events, stateSchema;
|
|
2962
|
+
var init_store = __esm({
|
|
2963
|
+
"src/automation/store.ts"() {
|
|
2974
2964
|
"use strict";
|
|
2975
2965
|
init_storage();
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2966
|
+
hostSchema = z7.enum(["claude", "codex"]);
|
|
2967
|
+
events = ["session_start", "turn_start", "before_tool", "after_tool", "task_end"];
|
|
2968
|
+
stateSchema = z7.object({
|
|
2969
|
+
version: z7.literal(1),
|
|
2970
|
+
root: z7.string(),
|
|
2971
|
+
gitDir: z7.string(),
|
|
2972
|
+
branch: z7.string(),
|
|
2973
|
+
baselines: z7.array(z7.object({ path: z7.string(), at: z7.string(), event: z7.string(), fingerprint: z7.string() })).max(128),
|
|
2974
|
+
sessions: z7.record(z7.object({
|
|
2975
|
+
host: hostSchema,
|
|
2976
|
+
seen: z7.string().nullable(),
|
|
2977
|
+
continued: z7.boolean(),
|
|
2978
|
+
initialIssues: z7.array(z7.string()),
|
|
2979
|
+
initialDocs: z7.record(z7.string().nullable()),
|
|
2980
|
+
lastUsed: z7.string(),
|
|
2981
|
+
mutationObserved: z7.boolean(),
|
|
2982
|
+
pending: z7.record(z7.string()),
|
|
2983
|
+
coverageGaps: z7.array(z7.string()),
|
|
2984
|
+
events: z7.record(z7.object({ at: z7.string(), count: z7.number().int().positive() }))
|
|
2985
|
+
})),
|
|
2986
|
+
updatedAt: z7.string(),
|
|
2987
|
+
fingerprint: z7.string().nullable(),
|
|
2988
|
+
latest: z7.string().nullable(),
|
|
2989
|
+
analysis: z7.object({ id: z7.string(), fingerprint: z7.string(), completedAt: z7.number(), path: z7.string() }).optional()
|
|
2994
2990
|
});
|
|
2995
|
-
executionSchema = z6.object({ version: z6.literal(1), attempts: z6.array(attemptSchema).max(32), discardedAttempts: z6.number().int().nonnegative().default(0) });
|
|
2996
2991
|
}
|
|
2997
2992
|
});
|
|
2998
2993
|
|
|
@@ -3005,19 +3000,19 @@ __export(evidence_exports, {
|
|
|
3005
3000
|
readInputs: () => readInputs,
|
|
3006
3001
|
workspace: () => workspace
|
|
3007
3002
|
});
|
|
3008
|
-
import
|
|
3003
|
+
import fs15 from "fs/promises";
|
|
3009
3004
|
import path20 from "path";
|
|
3010
3005
|
import { createHash as createHash4 } from "crypto";
|
|
3011
3006
|
import { execFile as execFile12 } from "child_process";
|
|
3012
3007
|
import { promisify as promisify12 } from "util";
|
|
3013
3008
|
import fg5 from "fast-glob";
|
|
3014
|
-
import { z as
|
|
3009
|
+
import { z as z8 } from "zod";
|
|
3015
3010
|
async function git2(root, ...args) {
|
|
3016
3011
|
return (await exec11("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024, timeout: 1e4 })).stdout;
|
|
3017
3012
|
}
|
|
3018
3013
|
async function workspace(dir) {
|
|
3019
|
-
const root = await
|
|
3020
|
-
const gitDir = await
|
|
3014
|
+
const root = await fs15.realpath((await git2(dir, "rev-parse", "--show-toplevel")).trim());
|
|
3015
|
+
const gitDir = await fs15.realpath((await git2(root, "rev-parse", "--absolute-git-dir")).trim());
|
|
3021
3016
|
let branch;
|
|
3022
3017
|
try {
|
|
3023
3018
|
branch = (await git2(root, "symbolic-ref", "--quiet", "HEAD")).trim();
|
|
@@ -3036,7 +3031,7 @@ async function readInputs(root) {
|
|
|
3036
3031
|
const head = headText.trim();
|
|
3037
3032
|
let shallow = null;
|
|
3038
3033
|
try {
|
|
3039
|
-
shallow = await
|
|
3034
|
+
shallow = await fs15.readFile(path20.resolve(root, shallowPath.trim()), "utf8");
|
|
3040
3035
|
} catch (error) {
|
|
3041
3036
|
if (error.code !== "ENOENT") throw error;
|
|
3042
3037
|
}
|
|
@@ -3049,7 +3044,7 @@ async function readInputs(root) {
|
|
|
3049
3044
|
docContents.push([file, text2]);
|
|
3050
3045
|
for (const claim of text2 ? extractClaims(text2).paths : []) {
|
|
3051
3046
|
if (internal(claim.path) || claim.path.startsWith(".mason/")) continue;
|
|
3052
|
-
const exists2 = async (p) =>
|
|
3047
|
+
const exists2 = async (p) => fs15.access(path20.resolve(root, p)).then(() => true, () => false);
|
|
3053
3048
|
claims.push([claim.path, await exists2(claim.path), await exists2(path20.dirname(claim.path))]);
|
|
3054
3049
|
}
|
|
3055
3050
|
}
|
|
@@ -3067,7 +3062,7 @@ async function readInputs(root) {
|
|
|
3067
3062
|
}
|
|
3068
3063
|
const needsWorkspaceScripts = rootScripts !== null && commandClaims.some((claim) => !rootScripts.includes(claim.scriptName));
|
|
3069
3064
|
const decisionDirectory = await storePath(root, ".mason/decisions");
|
|
3070
|
-
const decisionPresence = await
|
|
3065
|
+
const decisionPresence = await fs15.lstat(decisionDirectory).then((stat) => stat.isDirectory() ? "directory" : "file", (error) => {
|
|
3071
3066
|
if (error.code === "ENOENT") return "absent";
|
|
3072
3067
|
throw error;
|
|
3073
3068
|
});
|
|
@@ -3136,108 +3131,203 @@ var init_evidence = __esm({
|
|
|
3136
3131
|
init_dead_command();
|
|
3137
3132
|
init_storage();
|
|
3138
3133
|
exec11 = promisify12(execFile12);
|
|
3139
|
-
engineVersion = true ? "0.16.
|
|
3134
|
+
engineVersion = true ? "0.16.3" : "development";
|
|
3140
3135
|
hash = (value) => createHash4("sha256").update(JSON.stringify(value)).digest("hex");
|
|
3141
3136
|
content = readAuditInput;
|
|
3142
3137
|
internal = (file) => file === ".mason" || file === ".mason/reports" || file.startsWith(".mason/reports/");
|
|
3143
|
-
cacheSchema =
|
|
3138
|
+
cacheSchema = z8.object({ version: z8.literal(1), entries: z8.record(z8.object({ key: z8.string(), result: checkResultSchema })), digest: z8.string() });
|
|
3144
3139
|
}
|
|
3145
3140
|
});
|
|
3146
3141
|
|
|
3147
|
-
// src/automation/
|
|
3148
|
-
|
|
3142
|
+
// src/automation/execution.ts
|
|
3143
|
+
var execution_exports = {};
|
|
3144
|
+
__export(execution_exports, {
|
|
3145
|
+
automationFailure: () => automationFailure,
|
|
3146
|
+
executionStatus: () => executionStatus,
|
|
3147
|
+
failureMessage: () => failureMessage,
|
|
3148
|
+
failureNotifications: () => failureNotifications,
|
|
3149
|
+
hookFailureMessage: () => hookFailureMessage,
|
|
3150
|
+
recordExecution: () => recordExecution
|
|
3151
|
+
});
|
|
3149
3152
|
import os2 from "os";
|
|
3150
|
-
import
|
|
3151
|
-
|
|
3153
|
+
import fs16 from "fs/promises";
|
|
3154
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
3155
|
+
import { z as z9 } from "zod";
|
|
3156
|
+
function parseExecution(raw) {
|
|
3152
3157
|
try {
|
|
3153
|
-
return
|
|
3158
|
+
return executionSchema.parse(raw);
|
|
3154
3159
|
} catch (error) {
|
|
3155
|
-
throw new Error("Invalid automation
|
|
3160
|
+
throw new Error("Invalid automation execution store; receipt history was retained.", { cause: error });
|
|
3156
3161
|
}
|
|
3157
3162
|
}
|
|
3158
|
-
|
|
3159
|
-
const
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
+
function automationFailure(error) {
|
|
3164
|
+
const recorded = failureSchema.safeParse(error?.failure);
|
|
3165
|
+
if (recorded.success) return recorded.data;
|
|
3166
|
+
const message2 = (error instanceof Error ? error.message : String(error)).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 700);
|
|
3167
|
+
const codes = /* @__PURE__ */ new Set();
|
|
3168
|
+
let cause = error;
|
|
3169
|
+
for (let i = 0; cause && i < 8; i++) {
|
|
3170
|
+
codes.add(String(cause.code));
|
|
3171
|
+
cause = cause.cause;
|
|
3172
|
+
}
|
|
3173
|
+
const code = codes.has("ENOSPC") || codes.has("EDQUOT") ? "storage-full" : /changed during|changed while|changed between/.test(message2) ? "inputs-changed" : /Automation is busy/.test(message2) ? "busy" : /not a git repository|unknown revision|bad revision|different history|unreachable/.test(message2) ? "history-unavailable" : error instanceof z9.ZodError || error instanceof SyntaxError || /Hook input|Expected one command|Unknown automation command|--host/.test(message2) ? "invalid-input" : /store|baseline|modified|symbolic link|Symlink|automation state|state belongs/.test(message2) ? "invalid-evidence" : [...codes].some((c) => /^E[A-Z]+$/.test(c)) ? "io-error" : "internal";
|
|
3174
|
+
return { code, message: message2, retryable: ["inputs-changed", "storage-full", "busy", "io-error"].includes(code), receiptRecorded: false };
|
|
3175
|
+
}
|
|
3176
|
+
function failureMessage(error) {
|
|
3177
|
+
const failure = automationFailure(error);
|
|
3178
|
+
return `Mason automation unavailable [${failure.code}]; evidence capture/verification was not established. ${failure.message}` + (failure.receiptRecorded ? "" : " No durable failure receipt was recorded.");
|
|
3179
|
+
}
|
|
3180
|
+
async function failureNotifications(root, directory) {
|
|
3181
|
+
const raw = await readStoreJson(root, directory + "/notifications.json");
|
|
3182
|
+
return raw === null ? { version: 1, sessions: {} } : notificationsSchema.parse(raw);
|
|
3183
|
+
}
|
|
3184
|
+
async function hookFailureMessage(dir, host, sessionId, error) {
|
|
3185
|
+
const failure = automationFailure(error);
|
|
3186
|
+
if (!failure.receiptRecorded) return failureMessage(error);
|
|
3187
|
+
try {
|
|
3188
|
+
const ws = await workspace(dir);
|
|
3189
|
+
return await withLock(ws.root, ws.directory + "/notifications", async () => {
|
|
3190
|
+
const notices = await failureNotifications(ws.root, ws.directory);
|
|
3191
|
+
const key = hash([host, sessionId]), signature = hash([failure.code, failure.message]);
|
|
3192
|
+
const previous = notices.sessions[key];
|
|
3193
|
+
const execution = await executionStatus(ws.root, ws.directory);
|
|
3194
|
+
const recovered = previous && execution.attempts.some((a) => a.status === "completed" && a.startedAt > previous.lastAt);
|
|
3195
|
+
const repeat = previous?.signature === signature && !recovered;
|
|
3196
|
+
notices.sessions[key] = {
|
|
3197
|
+
signature,
|
|
3198
|
+
code: failure.code,
|
|
3199
|
+
message: failure.message,
|
|
3200
|
+
count: repeat ? previous.count + 1 : 1,
|
|
3201
|
+
lastAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3202
|
+
};
|
|
3203
|
+
notices.sessions = Object.fromEntries(Object.entries(notices.sessions).sort(([, a], [, b]) => b.lastAt.localeCompare(a.lastAt)).slice(0, 32));
|
|
3204
|
+
await writeStoreJson(ws.root, ws.directory + "/notifications.json", notices);
|
|
3205
|
+
return repeat ? null : failureMessage(error) + " Repeated failures in this session are recorded in mason status --json.";
|
|
3206
|
+
});
|
|
3207
|
+
} catch {
|
|
3208
|
+
return failureMessage(error);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
async function refreshRunning(root, directory, attempts) {
|
|
3212
|
+
for (const attempt of attempts) {
|
|
3213
|
+
if (attempt.status !== "running") continue;
|
|
3214
|
+
let alive = false;
|
|
3163
3215
|
try {
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
})
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
});
|
|
3173
|
-
throw error;
|
|
3216
|
+
const owner = await readStoreJson(root, attempt.lease ? leasePath(directory, attempt.id) : directory + "/lock");
|
|
3217
|
+
if (attempt.host === os2.hostname() && owner?.pid === attempt.pid && owner.host === attempt.host) {
|
|
3218
|
+
try {
|
|
3219
|
+
process.kill(attempt.pid, 0);
|
|
3220
|
+
alive = true;
|
|
3221
|
+
} catch (error) {
|
|
3222
|
+
alive = error.code === "EPERM";
|
|
3223
|
+
}
|
|
3174
3224
|
}
|
|
3225
|
+
} catch {
|
|
3226
|
+
}
|
|
3227
|
+
if (!alive) attempt.status = "unknown";
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
async function editExecution(root, directory, edit) {
|
|
3231
|
+
return withLock(root, directory + "/execution-lock", async () => {
|
|
3232
|
+
const file = directory + "/execution.json";
|
|
3233
|
+
const raw = await readStoreJson(root, file);
|
|
3234
|
+
const log = raw === null ? { version: 1, attempts: [], discardedAttempts: 0 } : parseExecution(raw);
|
|
3235
|
+
await refreshRunning(root, directory, log.attempts);
|
|
3236
|
+
edit(log);
|
|
3237
|
+
const finished = log.attempts.filter((a) => a.status !== "running");
|
|
3238
|
+
const discard = new Set(finished.slice(0, Math.max(0, finished.length - 32)).map((a) => a.id));
|
|
3239
|
+
log.discardedAttempts += discard.size;
|
|
3240
|
+
log.attempts = log.attempts.filter((a) => !discard.has(a.id));
|
|
3241
|
+
await writeStoreJson(root, file, log);
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
async function recordExecution(root, directory, event, run) {
|
|
3245
|
+
const started = performance.now();
|
|
3246
|
+
const attempt = {
|
|
3247
|
+
id: randomUUID4(),
|
|
3248
|
+
event,
|
|
3249
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3250
|
+
pid: process.pid,
|
|
3251
|
+
host: os2.hostname(),
|
|
3252
|
+
status: "running",
|
|
3253
|
+
lease: true
|
|
3254
|
+
};
|
|
3255
|
+
const owner = leasePath(directory, attempt.id);
|
|
3256
|
+
const finish = async (changes) => editExecution(root, directory, (log) => {
|
|
3257
|
+
const current = log.attempts.find((a) => a.id === attempt.id);
|
|
3258
|
+
if (!current) throw new Error("Automation execution receipt disappeared; completion could not be recorded.");
|
|
3259
|
+
Object.assign(current, changes, { finishedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: performance.now() - started });
|
|
3260
|
+
});
|
|
3261
|
+
await writeStoreJson(root, owner, { pid: attempt.pid, host: attempt.host });
|
|
3262
|
+
try {
|
|
3263
|
+
await editExecution(root, directory, (log) => {
|
|
3264
|
+
if (log.attempts.filter((a) => a.status === "running").length >= 128) throw new Error("Too many active automation executions; existing receipts were retained.");
|
|
3265
|
+
log.attempts.push(attempt);
|
|
3266
|
+
});
|
|
3267
|
+
try {
|
|
3268
|
+
const result = await run();
|
|
3269
|
+
await finish({ status: "completed", verificationStatus: result.report.status, reportPath: result.report.reportPath });
|
|
3270
|
+
return result;
|
|
3175
3271
|
} catch (error) {
|
|
3176
|
-
|
|
3272
|
+
const failure = automationFailure(error);
|
|
3177
3273
|
try {
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
try {
|
|
3181
|
-
process.kill(owner.pid, 0);
|
|
3182
|
-
} catch (probe) {
|
|
3183
|
-
if (probe.code === "ESRCH") {
|
|
3184
|
-
const reclaim = file + ".reclaim";
|
|
3185
|
-
let guard;
|
|
3186
|
-
try {
|
|
3187
|
-
guard = await fs15.open(reclaim, "wx", 384);
|
|
3188
|
-
const current = JSON.parse(await fs15.readFile(file, "utf8"));
|
|
3189
|
-
if (current.pid === owner.pid && current.host === owner.host) await fs15.unlink(file);
|
|
3190
|
-
} finally {
|
|
3191
|
-
if (guard) {
|
|
3192
|
-
await guard.close();
|
|
3193
|
-
await fs15.rm(reclaim, { force: true });
|
|
3194
|
-
}
|
|
3195
|
-
}
|
|
3196
|
-
}
|
|
3197
|
-
}
|
|
3198
|
-
}
|
|
3274
|
+
await finish({ status: "failed", failure: { ...failure, receiptRecorded: true } });
|
|
3275
|
+
failure.receiptRecorded = true;
|
|
3199
3276
|
} catch {
|
|
3200
3277
|
}
|
|
3201
|
-
|
|
3202
|
-
await new Promise((resolve2) => setTimeout(resolve2, 40));
|
|
3278
|
+
throw Object.assign(new Error(failure.message, { cause: error }), { failure });
|
|
3203
3279
|
}
|
|
3204
|
-
}
|
|
3205
|
-
try {
|
|
3206
|
-
return await run();
|
|
3207
3280
|
} finally {
|
|
3208
|
-
await
|
|
3209
|
-
|
|
3281
|
+
await fs16.rm(await storePath(root, owner), { force: true }).catch(() => {
|
|
3282
|
+
});
|
|
3210
3283
|
}
|
|
3211
3284
|
}
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3285
|
+
async function executionStatus(root, directory) {
|
|
3286
|
+
const raw = await readStoreJson(root, directory + "/execution.json");
|
|
3287
|
+
if (raw === null) return { status: "not-observed", attempts: [] };
|
|
3288
|
+
const log = parseExecution(raw);
|
|
3289
|
+
await refreshRunning(root, directory, log.attempts);
|
|
3290
|
+
const completed = [...log.attempts].reverse().find((a) => a.status === "completed");
|
|
3291
|
+
const outstanding = [...log.attempts].reverse().find((a) => ["failed", "unknown"].includes(a.status) && (!completed || (a.finishedAt ?? a.startedAt) >= completed.startedAt));
|
|
3292
|
+
const status = log.attempts.some((a) => a.status === "running") ? "running" : outstanding?.status ?? log.attempts.at(-1)?.status ?? "not-observed";
|
|
3293
|
+
return { status, attempts: log.attempts, discardedAttempts: log.discardedAttempts };
|
|
3294
|
+
}
|
|
3295
|
+
var failureSchema, attemptSchema, executionSchema, notificationsSchema, leasePath;
|
|
3296
|
+
var init_execution = __esm({
|
|
3297
|
+
"src/automation/execution.ts"() {
|
|
3215
3298
|
"use strict";
|
|
3299
|
+
init_store();
|
|
3300
|
+
init_evidence();
|
|
3216
3301
|
init_storage();
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
})),
|
|
3237
|
-
updatedAt: z8.string(),
|
|
3238
|
-
fingerprint: z8.string().nullable(),
|
|
3239
|
-
latest: z8.string().nullable()
|
|
3302
|
+
failureSchema = z9.object({
|
|
3303
|
+
code: z9.enum(["inputs-changed", "storage-full", "busy", "invalid-input", "history-unavailable", "invalid-evidence", "io-error", "internal"]),
|
|
3304
|
+
message: z9.string(),
|
|
3305
|
+
retryable: z9.boolean(),
|
|
3306
|
+
receiptRecorded: z9.boolean()
|
|
3307
|
+
});
|
|
3308
|
+
attemptSchema = z9.object({
|
|
3309
|
+
id: z9.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
3310
|
+
lease: z9.literal(true).optional(),
|
|
3311
|
+
event: z9.string(),
|
|
3312
|
+
startedAt: z9.string(),
|
|
3313
|
+
finishedAt: z9.string().optional(),
|
|
3314
|
+
durationMs: z9.number().nonnegative().optional(),
|
|
3315
|
+
pid: z9.number().int().positive(),
|
|
3316
|
+
host: z9.string(),
|
|
3317
|
+
status: z9.enum(["running", "completed", "failed", "unknown"]),
|
|
3318
|
+
verificationStatus: z9.string().optional(),
|
|
3319
|
+
reportPath: z9.string().optional(),
|
|
3320
|
+
failure: failureSchema.optional()
|
|
3240
3321
|
});
|
|
3322
|
+
executionSchema = z9.object({ version: z9.literal(1), attempts: z9.array(attemptSchema).max(160), discardedAttempts: z9.number().int().nonnegative().default(0) });
|
|
3323
|
+
notificationsSchema = z9.object({ version: z9.literal(1), sessions: z9.record(z9.object({
|
|
3324
|
+
signature: z9.string(),
|
|
3325
|
+
code: failureSchema.shape.code,
|
|
3326
|
+
message: z9.string(),
|
|
3327
|
+
count: z9.number().int().positive(),
|
|
3328
|
+
lastAt: z9.string()
|
|
3329
|
+
})) });
|
|
3330
|
+
leasePath = (directory, id) => `${directory}/executions/${id}.json`;
|
|
3241
3331
|
}
|
|
3242
3332
|
});
|
|
3243
3333
|
|
|
@@ -3246,9 +3336,30 @@ var runtime_exports = {};
|
|
|
3246
3336
|
__export(runtime_exports, {
|
|
3247
3337
|
automate: () => automate,
|
|
3248
3338
|
automationStatus: () => automationStatus,
|
|
3339
|
+
observeReadOnlyTool: () => observeReadOnlyTool,
|
|
3249
3340
|
summarize: () => summarize
|
|
3250
3341
|
});
|
|
3251
3342
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
3343
|
+
import { z as z10 } from "zod";
|
|
3344
|
+
async function observeReadOnlyTool(dir, event) {
|
|
3345
|
+
const ws = await workspace(dir);
|
|
3346
|
+
await withLock(ws.root, ws.directory, async () => {
|
|
3347
|
+
const statePath = ws.directory + "/state.json";
|
|
3348
|
+
const raw = await readStoreJson(ws.root, statePath);
|
|
3349
|
+
if (raw === null) return;
|
|
3350
|
+
const state = parseState(raw);
|
|
3351
|
+
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
|
|
3352
|
+
throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
|
|
3353
|
+
}
|
|
3354
|
+
const session = event.host && event.sessionId ? state.sessions[hash([event.host, event.sessionId])] : null;
|
|
3355
|
+
if (!session) return;
|
|
3356
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3357
|
+
session.lastUsed = now;
|
|
3358
|
+
session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
|
|
3359
|
+
await writeStoreJson(ws.root, statePath, state);
|
|
3360
|
+
});
|
|
3361
|
+
return { root: ws.root, directory: ws.directory };
|
|
3362
|
+
}
|
|
3252
3363
|
function summarize(report) {
|
|
3253
3364
|
const open = report.findings.filter((f) => f.status !== "resolved");
|
|
3254
3365
|
return [
|
|
@@ -3260,176 +3371,234 @@ function summarize(report) {
|
|
|
3260
3371
|
"Keep original evidence. Address findings relevant to the authorized task; report unrelated findings and unresolved advisories without approving them."
|
|
3261
3372
|
].join("\n");
|
|
3262
3373
|
}
|
|
3374
|
+
async function loadState(ws) {
|
|
3375
|
+
const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
|
|
3376
|
+
const state = raw === null ? {
|
|
3377
|
+
version: 1,
|
|
3378
|
+
root: ws.root,
|
|
3379
|
+
gitDir: ws.gitDir,
|
|
3380
|
+
branch: ws.branch,
|
|
3381
|
+
baselines: [],
|
|
3382
|
+
sessions: {},
|
|
3383
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3384
|
+
fingerprint: null,
|
|
3385
|
+
latest: null
|
|
3386
|
+
} : parseState(raw);
|
|
3387
|
+
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
|
|
3388
|
+
throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
|
|
3389
|
+
}
|
|
3390
|
+
return state;
|
|
3391
|
+
}
|
|
3392
|
+
async function sharedAnalysis(ws, state, inputs, startedAt) {
|
|
3393
|
+
const pointer = state.analysis;
|
|
3394
|
+
if (!pointer || pointer.fingerprint !== inputs.fingerprint || pointer.completedAt < startedAt) return null;
|
|
3395
|
+
const stored = sharedSchema.parse(await readStoreJson(ws.root, pointer.path));
|
|
3396
|
+
const a = stored.analysis;
|
|
3397
|
+
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))) {
|
|
3398
|
+
throw new Error("Invalid shared automation evidence; run a new check.");
|
|
3399
|
+
}
|
|
3400
|
+
if (a.baselineDigest !== await baselineDigest(ws, state)) throw new Error("Repair baseline was modified; use the original baseline.");
|
|
3401
|
+
return a;
|
|
3402
|
+
}
|
|
3403
|
+
async function verifyInputs(ws, inputs) {
|
|
3404
|
+
const [after, currentWs] = await Promise.all([readInputs(ws.root), workspace(ws.root)]);
|
|
3405
|
+
if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
|
|
3406
|
+
throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
async function computeAnalysis(ws, state, inputs, event) {
|
|
3410
|
+
const baselines = [...state.baselines];
|
|
3411
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3412
|
+
const empty = {
|
|
3413
|
+
version: 1,
|
|
3414
|
+
status: "unavailable",
|
|
3415
|
+
root: ws.root,
|
|
3416
|
+
branch: ws.branch,
|
|
3417
|
+
head: inputs.head,
|
|
3418
|
+
baselinePaths: [],
|
|
3419
|
+
reportPath: "",
|
|
3420
|
+
findings: [],
|
|
3421
|
+
diagnostics: ["No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md exists. Documentation capture is unavailable; other Mason tools remain usable."],
|
|
3422
|
+
checks: { ran: [], reused: [], skipped: [] },
|
|
3423
|
+
counts: { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 },
|
|
3424
|
+
capture: "unknown",
|
|
3425
|
+
scope: SCOPE
|
|
3426
|
+
};
|
|
3427
|
+
if (!baselines.length && Object.values(inputs.docs).every((value) => value === null)) return { baselines, report: empty, cache: null };
|
|
3428
|
+
if (ws.branch === "detached" && state.latest) {
|
|
3429
|
+
const previous = await readStoreJson(ws.root, state.latest);
|
|
3430
|
+
if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
|
|
3431
|
+
try {
|
|
3432
|
+
await git2(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
|
|
3433
|
+
} catch {
|
|
3434
|
+
throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
let cached = null;
|
|
3438
|
+
const diagnostics = [];
|
|
3439
|
+
try {
|
|
3440
|
+
cached = await readStoreJson(ws.root, ws.directory + "/cache.json");
|
|
3441
|
+
} catch {
|
|
3442
|
+
diagnostics.push("Unreadable automation cache; checks are being recomputed.");
|
|
3443
|
+
}
|
|
3444
|
+
const cache = checkCache(cached, inputs);
|
|
3445
|
+
if (cache.diagnostic) diagnostics.push(cache.diagnostic);
|
|
3446
|
+
const saveBaseline = async () => {
|
|
3447
|
+
if (baselines.length >= 128) throw new Error("128 retained baselines need review; automatic capture stopped without discarding original evidence.");
|
|
3448
|
+
const prepared = await prepareRepair(ws.root, ALL_CHECKS, cache.options);
|
|
3449
|
+
baselines.push({ path: prepared.baselinePath, at: now, event: event.event, fingerprint: inputs.fingerprint });
|
|
3450
|
+
};
|
|
3451
|
+
if (!baselines.length) await saveBaseline();
|
|
3452
|
+
const verifications = [];
|
|
3453
|
+
for (const baseline of baselines) verifications.push(await verifyRepair(ws.root, baseline.path, cache.options));
|
|
3454
|
+
const known = new Set(verifications.flatMap((v) => v.findings.map((f) => f.id)));
|
|
3455
|
+
if (verifications.some((v) => v.newFindings.some((f) => !known.has(findingId(f))))) {
|
|
3456
|
+
await saveBaseline();
|
|
3457
|
+
verifications.push(await verifyRepair(ws.root, baselines.at(-1).path, cache.options));
|
|
3458
|
+
}
|
|
3459
|
+
const merged = /* @__PURE__ */ new Map();
|
|
3460
|
+
for (const verification of verifications) {
|
|
3461
|
+
for (const finding of verification.findings) {
|
|
3462
|
+
const previous = merged.get(finding.id);
|
|
3463
|
+
if (!previous || priority[finding.status] > priority[previous.status]) merged.set(finding.id, finding);
|
|
3464
|
+
}
|
|
3465
|
+
diagnostics.push(...verification.diagnostics);
|
|
3466
|
+
}
|
|
3467
|
+
const counts = { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 };
|
|
3468
|
+
for (const finding of merged.values()) counts[finding.status]++;
|
|
3469
|
+
const report = {
|
|
3470
|
+
version: 1,
|
|
3471
|
+
status: diagnostics.length || verifications.some((v) => v.status === "incomplete") ? "incomplete" : counts.unresolved ? "issues-remain" : "verified",
|
|
3472
|
+
root: ws.root,
|
|
3473
|
+
branch: ws.branch,
|
|
3474
|
+
head: inputs.head,
|
|
3475
|
+
baselinePaths: baselines.map((b) => b.path),
|
|
3476
|
+
reportPath: "",
|
|
3477
|
+
findings: [...merged.values()],
|
|
3478
|
+
diagnostics: [...new Set(diagnostics)],
|
|
3479
|
+
checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: verifications.at(-1).currentAudit?.skippedChecks ?? [] },
|
|
3480
|
+
counts,
|
|
3481
|
+
capture: "unknown",
|
|
3482
|
+
scope: SCOPE
|
|
3483
|
+
};
|
|
3484
|
+
return { baselines, report, cache: cache.ran.size || cached === null || cache.diagnostic ? cache.serialize() : null };
|
|
3485
|
+
}
|
|
3486
|
+
async function analyze(ws, inputs, event, startedAt) {
|
|
3487
|
+
return withLock(ws.root, ws.directory + "/analysis/" + inputs.fingerprint, async () => {
|
|
3488
|
+
const before = await loadState(ws);
|
|
3489
|
+
const shared = await sharedAnalysis(ws, before, inputs, startedAt);
|
|
3490
|
+
if (shared) return { analysis: shared, shared: true };
|
|
3491
|
+
const originalDigest = await baselineDigest(ws, before);
|
|
3492
|
+
const computed = await computeAnalysis(ws, before, inputs, event);
|
|
3493
|
+
await verifyInputs(ws, inputs);
|
|
3494
|
+
if (originalDigest !== await baselineDigest(ws, before)) throw new Error("Repair baseline was modified during automation; original evidence must be inspected.");
|
|
3495
|
+
const id = randomUUID5();
|
|
3496
|
+
const analysis = analysisSchema.parse({
|
|
3497
|
+
id,
|
|
3498
|
+
fingerprint: inputs.fingerprint,
|
|
3499
|
+
completedAt: Date.now(),
|
|
3500
|
+
baselineDigest: await baselineDigest(ws, { ...before, baselines: computed.baselines }),
|
|
3501
|
+
report: computed.report
|
|
3502
|
+
});
|
|
3503
|
+
const analysisPath = ws.directory + "/checks/analysis-" + inputs.fingerprint + ".json";
|
|
3504
|
+
await withLock(ws.root, ws.directory, async () => {
|
|
3505
|
+
const state = await loadState(ws);
|
|
3506
|
+
if (hash(state.baselines) !== hash(before.baselines) || state.analysis?.id !== before.analysis?.id) {
|
|
3507
|
+
throw new Error("Automation evidence changed during analysis; retry against the retained baselines.");
|
|
3508
|
+
}
|
|
3509
|
+
await writeStoreJson(ws.root, analysisPath, { analysis, digest: hash(analysis) });
|
|
3510
|
+
if (computed.cache) await writeStoreJson(ws.root, ws.directory + "/cache.json", computed.cache);
|
|
3511
|
+
state.baselines = computed.baselines;
|
|
3512
|
+
state.analysis = { id, fingerprint: inputs.fingerprint, completedAt: analysis.completedAt, path: analysisPath };
|
|
3513
|
+
await writeStoreJson(ws.root, ws.directory + "/state.json", state);
|
|
3514
|
+
});
|
|
3515
|
+
return { analysis, shared: false };
|
|
3516
|
+
}, Math.max(0, 25e3 - (Date.now() - startedAt)));
|
|
3517
|
+
}
|
|
3263
3518
|
async function automate(dir, event) {
|
|
3519
|
+
const startedAt = Date.now();
|
|
3264
3520
|
const ws = await workspace(dir);
|
|
3265
|
-
return
|
|
3521
|
+
return recordExecution(ws.root, ws.directory, event.event, async () => {
|
|
3266
3522
|
const inputs = await readInputs(ws.root);
|
|
3267
|
-
const
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
root: ws.root,
|
|
3273
|
-
gitDir: ws.gitDir,
|
|
3274
|
-
branch: ws.branch,
|
|
3275
|
-
baselines: [],
|
|
3276
|
-
sessions: {},
|
|
3277
|
-
updatedAt: now,
|
|
3278
|
-
fingerprint: null,
|
|
3279
|
-
latest: null
|
|
3280
|
-
} : parseState(raw);
|
|
3281
|
-
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) {
|
|
3282
|
-
throw new Error("Automation state belongs to another branch or worktree; original evidence was retained.");
|
|
3283
|
-
}
|
|
3284
|
-
if (ws.branch === "detached" && state.latest) {
|
|
3285
|
-
const previous = await readStoreJson(ws.root, state.latest);
|
|
3286
|
-
if (!previous?.head || !/^[a-f0-9]{40,64}$/.test(previous.head)) throw new Error("The previous detached checkout evidence is unavailable.");
|
|
3287
|
-
try {
|
|
3288
|
-
await git2(ws.root, "merge-base", "--is-ancestor", previous.head, inputs.head);
|
|
3289
|
-
} catch {
|
|
3290
|
-
throw new Error("Detached checkout moved to a different history; original repair evidence was retained. Inspect that baseline explicitly.");
|
|
3523
|
+
const result = await analyze(ws, inputs, event, startedAt);
|
|
3524
|
+
if (result.shared) {
|
|
3525
|
+
await verifyInputs(ws, inputs);
|
|
3526
|
+
if (result.analysis.baselineDigest !== await baselineDigest(ws, await loadState(ws))) {
|
|
3527
|
+
throw new Error("Repair baseline was modified during shared automation; original evidence must be inspected.");
|
|
3291
3528
|
}
|
|
3292
3529
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
const
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3530
|
+
return withLock(ws.root, ws.directory, async () => {
|
|
3531
|
+
const state = await loadState(ws);
|
|
3532
|
+
if (state.analysis?.id !== result.analysis.id) throw new Error("Automation evidence changed during publication; retry against the current inputs.");
|
|
3533
|
+
const report = structuredClone(result.analysis.report);
|
|
3534
|
+
if (result.shared) report.checks = { ...report.checks, ran: [], reused: [.../* @__PURE__ */ new Set([...report.checks.ran, ...report.checks.reused])], shared: true };
|
|
3535
|
+
report.reportPath = ws.directory + "/checks/" + randomUUID5() + ".json";
|
|
3536
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3537
|
+
const key = event.host && event.sessionId ? hash([event.host, event.sessionId]) : null;
|
|
3538
|
+
const newSession = key !== null && !state.sessions[key];
|
|
3539
|
+
if (key && !state.sessions[key]) {
|
|
3540
|
+
const keys = Object.keys(state.sessions).sort((a, b) => state.sessions[a].lastUsed.localeCompare(state.sessions[b].lastUsed));
|
|
3541
|
+
const removable = keys.filter((k) => !Object.keys(state.sessions[k].pending).length);
|
|
3542
|
+
for (const expired of removable.slice(0, Math.max(0, keys.length - 31))) delete state.sessions[expired];
|
|
3543
|
+
if (Object.keys(state.sessions).length >= 128) throw new Error("Too many sessions with unfinished tool calls; original evidence was retained.");
|
|
3544
|
+
state.sessions[key] = {
|
|
3545
|
+
host: event.host,
|
|
3546
|
+
seen: null,
|
|
3547
|
+
continued: false,
|
|
3548
|
+
initialIssues: report.findings.filter((f) => f.status === "unresolved").map((f) => f.id),
|
|
3549
|
+
initialDocs: inputs.docs,
|
|
3550
|
+
lastUsed: now,
|
|
3551
|
+
mutationObserved: false,
|
|
3552
|
+
pending: {},
|
|
3553
|
+
coverageGaps: [],
|
|
3554
|
+
events: {}
|
|
3555
|
+
};
|
|
3318
3556
|
}
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3557
|
+
const session = key ? state.sessions[key] : null;
|
|
3558
|
+
if (session) {
|
|
3559
|
+
session.lastUsed = now;
|
|
3560
|
+
session.events[event.event] = { at: now, count: (session.events[event.event]?.count ?? 0) + 1 };
|
|
3561
|
+
if (event.event === "before_tool" && event.mutating && event.toolId) {
|
|
3562
|
+
if (Object.keys(session.pending).length >= 128) throw new Error("Too many unfinished tool calls to track pre-edit evidence.");
|
|
3563
|
+
session.pending[event.toolId] = inputs.fingerprint;
|
|
3324
3564
|
}
|
|
3325
|
-
if (event.
|
|
3565
|
+
if (event.event === "after_tool" && event.mutating) {
|
|
3566
|
+
session.mutationObserved = true;
|
|
3567
|
+
if (!event.toolId || !session.pending[event.toolId]) {
|
|
3568
|
+
const gap = "A tool completed without an observed matching pre-tool capture; pre-edit coverage is unknown.";
|
|
3569
|
+
if (!session.coverageGaps.includes(gap)) session.coverageGaps.push(gap);
|
|
3570
|
+
}
|
|
3571
|
+
if (event.toolId) delete session.pending[event.toolId];
|
|
3572
|
+
}
|
|
3573
|
+
report.diagnostics.push(...session.coverageGaps);
|
|
3574
|
+
if (session.coverageGaps.length && report.status !== "unavailable") report.status = "incomplete";
|
|
3575
|
+
report.capture = report.status !== "unavailable" && !session.coverageGaps.length && (session.events.session_start || session.events.before_tool) ? "observed" : "unknown";
|
|
3326
3576
|
}
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
const
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
head: inputs.head,
|
|
3335
|
-
baselinePaths: [],
|
|
3336
|
-
reportPath: ws.directory + "/checks/" + randomUUID5() + ".json",
|
|
3337
|
-
findings: [],
|
|
3338
|
-
diagnostics: ["No AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md exists. Documentation capture is unavailable; other Mason tools remain usable."],
|
|
3339
|
-
checks: { ran: [], reused: [], skipped: [] },
|
|
3340
|
-
counts: { resolved: 0, unresolved: 0, "review-required": 0, unverified: 0 },
|
|
3341
|
-
capture: "unknown",
|
|
3342
|
-
scope: SCOPE
|
|
3343
|
-
};
|
|
3344
|
-
const notify2 = !session || session.seen !== "no-docs";
|
|
3345
|
-
if (session) session.seen = "no-docs";
|
|
3346
|
-
state.fingerprint = inputs.fingerprint;
|
|
3347
|
-
state.latest = report2.reportPath;
|
|
3348
|
-
state.updatedAt = now;
|
|
3349
|
-
await writeStoreJson(ws.root, report2.reportPath, report2);
|
|
3350
|
-
await writeStoreJson(ws.root, statePath, state);
|
|
3351
|
-
return { report: report2, message: notify2 ? summarize(report2) : null, continueOnce: false };
|
|
3352
|
-
}
|
|
3353
|
-
let cached = null;
|
|
3354
|
-
const diagnostics = [];
|
|
3355
|
-
try {
|
|
3356
|
-
cached = await readStoreJson(ws.root, ws.directory + "/cache.json");
|
|
3357
|
-
} catch {
|
|
3358
|
-
diagnostics.push("Unreadable automation cache; checks are being recomputed.");
|
|
3359
|
-
}
|
|
3360
|
-
const cache = checkCache(cached, inputs);
|
|
3361
|
-
if (cache.diagnostic) diagnostics.push(cache.diagnostic);
|
|
3362
|
-
const saveBaseline = async () => {
|
|
3363
|
-
if (state.baselines.length >= 128) throw new Error("128 retained baselines need review; automatic capture stopped without discarding original evidence.");
|
|
3364
|
-
const prepared = await prepareRepair(ws.root, ALL_CHECKS, cache.options);
|
|
3365
|
-
state.baselines.push({ path: prepared.baselinePath, at: now, event: event.event, fingerprint: inputs.fingerprint });
|
|
3366
|
-
};
|
|
3367
|
-
if (!state.baselines.length) await saveBaseline();
|
|
3368
|
-
const verifications = [];
|
|
3369
|
-
for (const baseline of state.baselines) verifications.push(await verifyRepair(ws.root, baseline.path, cache.options));
|
|
3370
|
-
const known = new Set(verifications.flatMap((v) => v.findings.map((f) => f.id)));
|
|
3371
|
-
if (verifications.some((v) => v.newFindings.some((f) => !known.has(findingId(f))))) {
|
|
3372
|
-
await saveBaseline();
|
|
3373
|
-
verifications.push(await verifyRepair(ws.root, state.baselines.at(-1).path, cache.options));
|
|
3374
|
-
}
|
|
3375
|
-
const merged = /* @__PURE__ */ new Map();
|
|
3376
|
-
for (const verification of verifications) {
|
|
3377
|
-
for (const finding of verification.findings) {
|
|
3378
|
-
const previous = merged.get(finding.id);
|
|
3379
|
-
if (!previous || priority[finding.status] > priority[previous.status]) merged.set(finding.id, finding);
|
|
3577
|
+
const signature = hash([report.status, report.findings, report.diagnostics, report.checks.skipped]);
|
|
3578
|
+
const relevant = report.findings.some((f) => f.status === "unresolved" && session && (!session.initialIssues.includes(f.id) || session.initialDocs[f.original.anchor.doc] !== inputs.docs[f.original.anchor.doc]));
|
|
3579
|
+
const continueOnce = event.event === "task_end" && !!session?.mutationObserved && relevant && !session.continued && !event.stopHookActive;
|
|
3580
|
+
const notify = !session || newSession || signature !== session.seen || continueOnce;
|
|
3581
|
+
if (session) {
|
|
3582
|
+
session.seen = signature;
|
|
3583
|
+
if (continueOnce) session.continued = true;
|
|
3380
3584
|
}
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
if (after.fingerprint !== inputs.fingerprint || currentWs.directory !== ws.directory) {
|
|
3392
|
-
throw new Error("Repository inputs or branch changed during automation; no current verification was recorded. Retry on a stable checkout.");
|
|
3393
|
-
}
|
|
3394
|
-
const report = {
|
|
3395
|
-
version: 1,
|
|
3396
|
-
status: diagnostics.length || verifications.some((v) => v.status === "incomplete") ? "incomplete" : counts.unresolved ? "issues-remain" : "verified",
|
|
3397
|
-
root: ws.root,
|
|
3398
|
-
branch: ws.branch,
|
|
3399
|
-
head: inputs.head,
|
|
3400
|
-
baselinePaths: state.baselines.map((b) => b.path),
|
|
3401
|
-
reportPath: ws.directory + "/checks/" + randomUUID5() + ".json",
|
|
3402
|
-
findings: [...merged.values()],
|
|
3403
|
-
diagnostics: [...new Set(diagnostics)],
|
|
3404
|
-
checks: { ran: [...cache.ran], reused: [...cache.reused].filter((name) => !cache.ran.has(name)), skipped: current?.skippedChecks ?? [] },
|
|
3405
|
-
counts,
|
|
3406
|
-
capture,
|
|
3407
|
-
scope: SCOPE
|
|
3408
|
-
};
|
|
3409
|
-
const signature = hash([report.status, report.findings, report.diagnostics, report.checks.skipped]);
|
|
3410
|
-
const relevant = report.findings.some((f) => f.status === "unresolved" && session && (!session.initialIssues.includes(f.id) || session.initialDocs[f.original.anchor.doc] !== inputs.docs[f.original.anchor.doc]));
|
|
3411
|
-
const continueOnce = event.event === "task_end" && !!session?.mutationObserved && relevant && !session.continued && !event.stopHookActive;
|
|
3412
|
-
const notify = !session || newSession || signature !== session.seen || continueOnce;
|
|
3413
|
-
if (session) {
|
|
3414
|
-
session.seen = signature;
|
|
3415
|
-
if (continueOnce) session.continued = true;
|
|
3416
|
-
}
|
|
3417
|
-
const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || event.event === "task_end";
|
|
3418
|
-
if (!persistReport) report.reportPath = state.latest;
|
|
3419
|
-
state.updatedAt = now;
|
|
3420
|
-
state.fingerprint = inputs.fingerprint;
|
|
3421
|
-
state.latest = report.reportPath;
|
|
3422
|
-
if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
|
|
3423
|
-
if (cache.ran.size || cached === null || cache.diagnostic) await writeStoreJson(ws.root, ws.directory + "/cache.json", cache.serialize());
|
|
3424
|
-
await writeStoreJson(ws.root, statePath, state);
|
|
3425
|
-
return { report, message: notify ? summarize(report) : null, continueOnce };
|
|
3426
|
-
}));
|
|
3585
|
+
const persistReport = !state.latest || state.fingerprint !== inputs.fingerprint || notify || event.event === "task_end";
|
|
3586
|
+
if (!persistReport) report.reportPath = state.latest;
|
|
3587
|
+
state.updatedAt = now;
|
|
3588
|
+
state.fingerprint = inputs.fingerprint;
|
|
3589
|
+
state.latest = report.reportPath;
|
|
3590
|
+
if (persistReport) await writeStoreJson(ws.root, report.reportPath, report);
|
|
3591
|
+
await writeStoreJson(ws.root, ws.directory + "/state.json", state);
|
|
3592
|
+
return { report, message: notify ? summarize(report) : null, continueOnce };
|
|
3593
|
+
});
|
|
3594
|
+
});
|
|
3427
3595
|
}
|
|
3428
3596
|
async function automationStatus(dir) {
|
|
3429
3597
|
const ws = await workspace(dir);
|
|
3430
3598
|
const execution = await executionStatus(ws.root, ws.directory);
|
|
3599
|
+
const notifications = await failureNotifications(ws.root, ws.directory);
|
|
3431
3600
|
const raw = await readStoreJson(ws.root, ws.directory + "/state.json");
|
|
3432
|
-
if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution };
|
|
3601
|
+
if (raw === null) return { version: 1, status: execution.status === "not-observed" ? "not-observed" : "unavailable", root: ws.root, branch: ws.branch, baselinePaths: [], hosts: {}, execution, notifications };
|
|
3433
3602
|
const state = parseState(raw);
|
|
3434
3603
|
if (state.root !== ws.root || state.gitDir !== ws.gitDir || state.branch !== ws.branch) throw new Error("Automation state belongs to another workspace.");
|
|
3435
3604
|
const inputs = await readInputs(ws.root);
|
|
@@ -3451,10 +3620,11 @@ async function automationStatus(dir) {
|
|
|
3451
3620
|
verificationStatus: unfinished ? "unavailable" : latest?.status ?? "unavailable",
|
|
3452
3621
|
hosts,
|
|
3453
3622
|
execution,
|
|
3623
|
+
notifications,
|
|
3454
3624
|
note: "Observed events do not prove all tool paths are intercepted. Run check to verify the retained evidence."
|
|
3455
3625
|
};
|
|
3456
3626
|
}
|
|
3457
|
-
var SCOPE, priority, cleanText;
|
|
3627
|
+
var SCOPE, priority, cleanText, analysisSchema, sharedSchema, baselineDigest;
|
|
3458
3628
|
var init_runtime = __esm({
|
|
3459
3629
|
"src/automation/runtime.ts"() {
|
|
3460
3630
|
"use strict";
|
|
@@ -3467,13 +3637,49 @@ var init_runtime = __esm({
|
|
|
3467
3637
|
SCOPE = "Documentation audit evidence only. Hook receipts show observed events, not complete interception. Resolved claims no longer fail their checks; advisories need separate review. Repair only within the user's task authorization.";
|
|
3468
3638
|
priority = { resolved: 0, "review-required": 1, unresolved: 2, unverified: 3 };
|
|
3469
3639
|
cleanText = (text2) => text2.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 250);
|
|
3640
|
+
analysisSchema = z10.object({
|
|
3641
|
+
id: z10.string(),
|
|
3642
|
+
fingerprint: z10.string(),
|
|
3643
|
+
completedAt: z10.number(),
|
|
3644
|
+
baselineDigest: z10.string(),
|
|
3645
|
+
report: z10.object({
|
|
3646
|
+
version: z10.literal(1),
|
|
3647
|
+
status: z10.enum(["verified", "issues-remain", "incomplete", "unavailable"]),
|
|
3648
|
+
root: z10.string(),
|
|
3649
|
+
branch: z10.string(),
|
|
3650
|
+
head: z10.string(),
|
|
3651
|
+
baselinePaths: z10.array(z10.string()),
|
|
3652
|
+
reportPath: z10.string(),
|
|
3653
|
+
findings: z10.array(repairFindingSchema),
|
|
3654
|
+
diagnostics: z10.array(z10.string()),
|
|
3655
|
+
checks: z10.object({
|
|
3656
|
+
ran: z10.array(z10.string()),
|
|
3657
|
+
reused: z10.array(z10.string()),
|
|
3658
|
+
skipped: z10.array(z10.object({ check: z10.string(), reason: z10.string(), doc: z10.string().optional() }))
|
|
3659
|
+
}),
|
|
3660
|
+
counts: z10.object({ resolved: z10.number(), unresolved: z10.number(), "review-required": z10.number(), unverified: z10.number() }),
|
|
3661
|
+
capture: z10.enum(["observed", "unknown"]),
|
|
3662
|
+
scope: z10.string()
|
|
3663
|
+
})
|
|
3664
|
+
});
|
|
3665
|
+
sharedSchema = z10.object({ analysis: analysisSchema, digest: z10.string() });
|
|
3666
|
+
baselineDigest = async (ws, state) => hash(await Promise.all(
|
|
3667
|
+
state.baselines.map(async (b) => [b.path, await readStoreJson(ws.root, b.path)])
|
|
3668
|
+
));
|
|
3470
3669
|
}
|
|
3471
3670
|
});
|
|
3472
3671
|
|
|
3473
3672
|
// src/automation/adapters.ts
|
|
3474
|
-
import { z as
|
|
3673
|
+
import { z as z11 } from "zod";
|
|
3674
|
+
function managedHookCommand(host, platform = process.platform) {
|
|
3675
|
+
const command = `mason --setup-host ${host} auto hook --host ${host}`;
|
|
3676
|
+
return platform === "win32" ? `cmd.exe /d /s /c "where mason >nul 2>nul & if errorlevel 1 (exit /b 0) else (${command})"` : `if command -v mason >/dev/null 2>&1; then ${command}; fi`;
|
|
3677
|
+
}
|
|
3678
|
+
function knownManagedHookCommands(host) {
|
|
3679
|
+
return [managedHookCommand(host, "linux"), managedHookCommand(host, "win32"), `mason --setup-host ${host} auto hook --host ${host}`];
|
|
3680
|
+
}
|
|
3475
3681
|
function hookConfig(host, command = "npx --no-install --package mason-context mason-auto") {
|
|
3476
|
-
const handler = { type: "command", command: command + " hook --host " + host, timeout: 30 };
|
|
3682
|
+
const handler = { type: "command", command: command === `mason --setup-host ${host} auto` ? managedHookCommand(host) : command + " hook --host " + host, timeout: 30 };
|
|
3477
3683
|
return { hooks: Object.fromEntries(HOOK_EVENTS.map((name) => [
|
|
3478
3684
|
name,
|
|
3479
3685
|
[{ ...["PreToolUse", "PostToolUse"].includes(name) ? { matcher: ".*" } : {}, hooks: [{ ...handler }] }]
|
|
@@ -3486,15 +3692,15 @@ var init_adapters = __esm({
|
|
|
3486
3692
|
init_execution();
|
|
3487
3693
|
init_runtime();
|
|
3488
3694
|
init_store();
|
|
3489
|
-
inputSchema =
|
|
3490
|
-
cwd:
|
|
3491
|
-
session_id:
|
|
3492
|
-
hook_event_name:
|
|
3493
|
-
tool_name:
|
|
3494
|
-
tool_use_id:
|
|
3495
|
-
tool_input:
|
|
3496
|
-
stop_hook_active:
|
|
3497
|
-
permission_mode:
|
|
3695
|
+
inputSchema = z11.object({
|
|
3696
|
+
cwd: z11.string().min(1),
|
|
3697
|
+
session_id: z11.string().min(1).max(500),
|
|
3698
|
+
hook_event_name: z11.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"]),
|
|
3699
|
+
tool_name: z11.string().optional(),
|
|
3700
|
+
tool_use_id: z11.string().max(500).optional(),
|
|
3701
|
+
tool_input: z11.unknown().optional(),
|
|
3702
|
+
stop_hook_active: z11.boolean().optional(),
|
|
3703
|
+
permission_mode: z11.string().optional()
|
|
3498
3704
|
});
|
|
3499
3705
|
HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"];
|
|
3500
3706
|
}
|
|
@@ -3510,7 +3716,7 @@ __export(ownership_exports, {
|
|
|
3510
3716
|
ownershipEdit: () => ownershipEdit,
|
|
3511
3717
|
ownershipSchema: () => ownershipSchema
|
|
3512
3718
|
});
|
|
3513
|
-
import { z as
|
|
3719
|
+
import { z as z12 } from "zod";
|
|
3514
3720
|
import { parse } from "smol-toml";
|
|
3515
3721
|
function blockSpan(text2, start, end) {
|
|
3516
3722
|
const starts = text2.split(start).length - 1, ends = text2.split(end).length - 1;
|
|
@@ -3561,15 +3767,15 @@ var init_ownership = __esm({
|
|
|
3561
3767
|
["<!-- mason:agents:start -->", "<!-- mason:agents:end -->"],
|
|
3562
3768
|
["# mason:mcp:start", "# mason:mcp:end"]
|
|
3563
3769
|
];
|
|
3564
|
-
fileSchema =
|
|
3565
|
-
created:
|
|
3566
|
-
initialHash:
|
|
3567
|
-
blocks:
|
|
3568
|
-
mcpHash:
|
|
3569
|
-
hookCommands:
|
|
3570
|
-
hooks:
|
|
3770
|
+
fileSchema = z12.object({
|
|
3771
|
+
created: z12.boolean(),
|
|
3772
|
+
initialHash: z12.string(),
|
|
3773
|
+
blocks: z12.record(z12.string()),
|
|
3774
|
+
mcpHash: z12.string().optional(),
|
|
3775
|
+
hookCommands: z12.array(z12.string()).optional(),
|
|
3776
|
+
hooks: z12.record(z12.array(z12.string())).optional()
|
|
3571
3777
|
});
|
|
3572
|
-
ownershipSchema =
|
|
3778
|
+
ownershipSchema = z12.object({ version: z12.literal(1), files: z12.record(fileSchema) });
|
|
3573
3779
|
}
|
|
3574
3780
|
});
|
|
3575
3781
|
|
|
@@ -3584,7 +3790,7 @@ __export(install_exports, {
|
|
|
3584
3790
|
planAutomationInstall: () => planAutomationInstall,
|
|
3585
3791
|
recordSchema: () => recordSchema
|
|
3586
3792
|
});
|
|
3587
|
-
import { z as
|
|
3793
|
+
import { z as z13 } from "zod";
|
|
3588
3794
|
async function installationRecord(root) {
|
|
3589
3795
|
return recordSchema.parse(await readStoreJson(root, AUTOMATION_PATH) ?? { version: 1, hosts: {} });
|
|
3590
3796
|
}
|
|
@@ -3602,7 +3808,7 @@ async function planAutomationInstall(root, host, command) {
|
|
|
3602
3808
|
const previous = record.hosts[host]?.command;
|
|
3603
3809
|
const ownershipText = await readText(root, OWNERSHIP_PATH);
|
|
3604
3810
|
const pending = ownershipText === null ? [] : ownershipSchema.parse(JSON.parse(ownershipText)).files[file]?.hookCommands ?? [];
|
|
3605
|
-
const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand]);
|
|
3811
|
+
const owned = /* @__PURE__ */ new Set([previous, ...pending, newCommand, ...knownManagedHookCommands(host)]);
|
|
3606
3812
|
const hooks = existing.hooks ?? {};
|
|
3607
3813
|
for (const event of HOOK_EVENTS) {
|
|
3608
3814
|
hooks[event] = (hooks[event] ?? []).map((group) => ({
|
|
@@ -3665,9 +3871,9 @@ var init_install = __esm({
|
|
|
3665
3871
|
init_store();
|
|
3666
3872
|
init_files2();
|
|
3667
3873
|
init_ownership();
|
|
3668
|
-
groupSchema =
|
|
3669
|
-
automationConfigSchema =
|
|
3670
|
-
recordSchema =
|
|
3874
|
+
groupSchema = z13.object({ hooks: z13.array(z13.object({ type: z13.string(), command: z13.string().optional() }).passthrough()) }).passthrough();
|
|
3875
|
+
automationConfigSchema = z13.object({ hooks: z13.record(z13.array(groupSchema)).optional() }).passthrough();
|
|
3876
|
+
recordSchema = z13.object({ version: z13.literal(1), hosts: z13.record(z13.object({ command: z13.string() })) });
|
|
3671
3877
|
AUTOMATION_PATH = ".mason/local/automation.json";
|
|
3672
3878
|
configPath = (host) => host === "claude" ? ".claude/settings.json" : ".codex/hooks.json";
|
|
3673
3879
|
}
|
|
@@ -3682,7 +3888,7 @@ __export(model_exports, {
|
|
|
3682
3888
|
setupHostSchema: () => setupHostSchema,
|
|
3683
3889
|
setupSchema: () => setupSchema
|
|
3684
3890
|
});
|
|
3685
|
-
import { z as
|
|
3891
|
+
import { z as z14 } from "zod";
|
|
3686
3892
|
async function loadSetup(root) {
|
|
3687
3893
|
const raw = await readStoreJson(root, SETUP_PATH);
|
|
3688
3894
|
if (raw === null) return null;
|
|
@@ -3700,27 +3906,27 @@ var init_model = __esm({
|
|
|
3700
3906
|
"src/setup/model.ts"() {
|
|
3701
3907
|
"use strict";
|
|
3702
3908
|
init_storage();
|
|
3703
|
-
setupHostSchema =
|
|
3704
|
-
configuredVersion:
|
|
3705
|
-
revision:
|
|
3706
|
-
fingerprint:
|
|
3707
|
-
mcpFingerprint:
|
|
3708
|
-
instructions:
|
|
3909
|
+
setupHostSchema = z14.object({
|
|
3910
|
+
configuredVersion: z14.string(),
|
|
3911
|
+
revision: z14.string().uuid(),
|
|
3912
|
+
fingerprint: z14.string(),
|
|
3913
|
+
mcpFingerprint: z14.string(),
|
|
3914
|
+
instructions: z14.array(z14.string())
|
|
3709
3915
|
});
|
|
3710
|
-
setupSchema =
|
|
3916
|
+
setupSchema = z14.object({ version: z14.literal(2), hosts: z14.object({
|
|
3711
3917
|
codex: setupHostSchema.optional(),
|
|
3712
3918
|
claude: setupHostSchema.optional()
|
|
3713
3919
|
}) });
|
|
3714
3920
|
SETUP_PATH = ".mason/local/setup.json";
|
|
3715
|
-
receiptSchema =
|
|
3716
|
-
version:
|
|
3717
|
-
host:
|
|
3718
|
-
status:
|
|
3719
|
-
initialReportPath:
|
|
3720
|
-
initialBaselinePaths:
|
|
3721
|
-
root:
|
|
3722
|
-
revision:
|
|
3723
|
-
configuredAt:
|
|
3921
|
+
receiptSchema = z14.object({
|
|
3922
|
+
version: z14.literal(1),
|
|
3923
|
+
host: z14.enum(["codex", "claude"]),
|
|
3924
|
+
status: z14.enum(["installing", "configured"]),
|
|
3925
|
+
initialReportPath: z14.string(),
|
|
3926
|
+
initialBaselinePaths: z14.array(z14.string()),
|
|
3927
|
+
root: z14.string(),
|
|
3928
|
+
revision: z14.string().uuid(),
|
|
3929
|
+
configuredAt: z14.string().optional()
|
|
3724
3930
|
});
|
|
3725
3931
|
}
|
|
3726
3932
|
});
|
|
@@ -3728,6 +3934,7 @@ var init_model = __esm({
|
|
|
3728
3934
|
// src/setup/launcher.ts
|
|
3729
3935
|
import { execFile as execFile13 } from "child_process";
|
|
3730
3936
|
import { promisify as promisify13 } from "util";
|
|
3937
|
+
import fs17 from "fs/promises";
|
|
3731
3938
|
function globalCommand(args, platform = process.platform) {
|
|
3732
3939
|
return platform === "win32" ? { command: "cmd.exe", args: ["/d", "/s", "/c", "mason", ...args] } : { command: "mason", args };
|
|
3733
3940
|
}
|
|
@@ -3752,7 +3959,7 @@ var init_launcher = __esm({
|
|
|
3752
3959
|
"use strict";
|
|
3753
3960
|
init_evidence();
|
|
3754
3961
|
init_model();
|
|
3755
|
-
executingVersion = () => true ? "0.16.
|
|
3962
|
+
executingVersion = () => true ? "0.16.3" : "development";
|
|
3756
3963
|
mcpCommand = (host) => globalCommand(["--setup-host", host, "mcp"]);
|
|
3757
3964
|
hookCommand = (host) => `mason --setup-host ${host} auto`;
|
|
3758
3965
|
}
|
|
@@ -3770,7 +3977,7 @@ __export(config_exports, {
|
|
|
3770
3977
|
});
|
|
3771
3978
|
import { isDeepStrictEqual } from "util";
|
|
3772
3979
|
import { parse as parse2, stringify } from "smol-toml";
|
|
3773
|
-
import { z as
|
|
3980
|
+
import { z as z15 } from "zod";
|
|
3774
3981
|
async function instructionEdits(root, host) {
|
|
3775
3982
|
const found = await Promise.all(DOC_CANDIDATES.map(async (file) => ({ path: file, text: await readText(root, file) })));
|
|
3776
3983
|
const existing = found.find((f) => f.text !== null);
|
|
@@ -3886,7 +4093,7 @@ var init_config = __esm({
|
|
|
3886
4093
|
init_install();
|
|
3887
4094
|
init_files2();
|
|
3888
4095
|
init_launcher();
|
|
3889
|
-
object =
|
|
4096
|
+
object = z15.record(z15.unknown());
|
|
3890
4097
|
mcpPath = (host) => host === "codex" ? ".codex/config.toml" : ".mcp.json";
|
|
3891
4098
|
TOML_START = "# mason:mcp:start";
|
|
3892
4099
|
TOML_END = "# mason:mcp:end";
|
|
@@ -3896,7 +4103,7 @@ var init_config = __esm({
|
|
|
3896
4103
|
});
|
|
3897
4104
|
|
|
3898
4105
|
// src/mcp/init.ts
|
|
3899
|
-
import { z as
|
|
4106
|
+
import { z as z16 } from "zod";
|
|
3900
4107
|
async function loadProjectMarker(rootDir) {
|
|
3901
4108
|
const raw = await readStoreJson(rootDir, ".mason/local/project.json");
|
|
3902
4109
|
if (raw === null) return null;
|
|
@@ -3904,8 +4111,8 @@ async function loadProjectMarker(rootDir) {
|
|
|
3904
4111
|
return { ...marker, features: await loadProjectFeatures(rootDir) };
|
|
3905
4112
|
}
|
|
3906
4113
|
async function loadProjectFeatures(rootDir) {
|
|
3907
|
-
const config =
|
|
3908
|
-
return
|
|
4114
|
+
const config = z16.record(z16.unknown()).parse(await readStoreJson(rootDir, ".mason/config.json") ?? {});
|
|
4115
|
+
return z16.object({ confluence: z16.boolean().optional() }).passthrough().parse(config.features ?? {});
|
|
3909
4116
|
}
|
|
3910
4117
|
async function saveProjectMarker(rootDir, marker) {
|
|
3911
4118
|
const { ancillaryEdits: ancillaryEdits2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
@@ -3917,8 +4124,8 @@ async function projectMarkerEdits(rootDir, marker) {
|
|
|
3917
4124
|
const edits = [{ path: file, before: await readText(rootDir, file), after: JSON.stringify(local, null, 2) + "\n" }];
|
|
3918
4125
|
if (features && Object.keys(features).length) {
|
|
3919
4126
|
const configPath2 = ".mason/config.json", before = await readText(rootDir, configPath2);
|
|
3920
|
-
const config =
|
|
3921
|
-
config.features = { ...
|
|
4127
|
+
const config = z16.record(z16.unknown()).parse(JSON.parse(before ?? "{}"));
|
|
4128
|
+
config.features = { ...z16.record(z16.unknown()).parse(config.features ?? {}), ...features };
|
|
3922
4129
|
edits.push({ path: configPath2, before, after: JSON.stringify(config, null, 2) + "\n" });
|
|
3923
4130
|
}
|
|
3924
4131
|
return edits;
|
|
@@ -3932,10 +4139,10 @@ var init_init = __esm({
|
|
|
3932
4139
|
"use strict";
|
|
3933
4140
|
init_storage();
|
|
3934
4141
|
init_files2();
|
|
3935
|
-
markerSchema =
|
|
3936
|
-
version:
|
|
3937
|
-
initializedAt:
|
|
3938
|
-
features:
|
|
4142
|
+
markerSchema = z16.object({
|
|
4143
|
+
version: z16.literal(1),
|
|
4144
|
+
initializedAt: z16.string(),
|
|
4145
|
+
features: z16.object({ confluence: z16.boolean().optional() }).passthrough().optional()
|
|
3939
4146
|
}).passthrough();
|
|
3940
4147
|
CLAUDE_MD_SECTION = `<!-- mason:start -->
|
|
3941
4148
|
## Mason project knowledge
|
|
@@ -4129,7 +4336,7 @@ var init_types2 = __esm({
|
|
|
4129
4336
|
});
|
|
4130
4337
|
|
|
4131
4338
|
// src/review/evidence/vitest.ts
|
|
4132
|
-
import { z as
|
|
4339
|
+
import { z as z17 } from "zod";
|
|
4133
4340
|
function parseVitest(raw, sourceRoot) {
|
|
4134
4341
|
const report = schema.parse(raw);
|
|
4135
4342
|
const findings = [], diagnostics = [];
|
|
@@ -4178,24 +4385,24 @@ var init_vitest = __esm({
|
|
|
4178
4385
|
"use strict";
|
|
4179
4386
|
init_paths2();
|
|
4180
4387
|
init_types2();
|
|
4181
|
-
count2 =
|
|
4182
|
-
schema =
|
|
4183
|
-
success:
|
|
4388
|
+
count2 = z17.number().int().nonnegative();
|
|
4389
|
+
schema = z17.object({
|
|
4390
|
+
success: z17.boolean(),
|
|
4184
4391
|
numTotalTests: count2,
|
|
4185
4392
|
numPassedTests: count2,
|
|
4186
4393
|
numFailedTests: count2,
|
|
4187
4394
|
numPendingTests: count2,
|
|
4188
4395
|
numTodoTests: count2,
|
|
4189
4396
|
numFailedTestSuites: count2,
|
|
4190
|
-
testResults:
|
|
4191
|
-
name:
|
|
4192
|
-
status:
|
|
4193
|
-
message:
|
|
4194
|
-
assertionResults:
|
|
4195
|
-
fullName:
|
|
4196
|
-
status:
|
|
4197
|
-
failureMessages:
|
|
4198
|
-
location:
|
|
4397
|
+
testResults: z17.array(z17.object({
|
|
4398
|
+
name: z17.string().min(1),
|
|
4399
|
+
status: z17.enum(["passed", "failed"]),
|
|
4400
|
+
message: z17.string().optional(),
|
|
4401
|
+
assertionResults: z17.array(z17.object({
|
|
4402
|
+
fullName: z17.string(),
|
|
4403
|
+
status: z17.enum(["passed", "failed", "pending", "skipped", "todo", "disabled"]),
|
|
4404
|
+
failureMessages: z17.array(z17.string()).nullable().optional(),
|
|
4405
|
+
location: z17.object({ line: count2, column: count2 }).nullable().optional()
|
|
4199
4406
|
}))
|
|
4200
4407
|
}))
|
|
4201
4408
|
});
|
|
@@ -4203,7 +4410,7 @@ var init_vitest = __esm({
|
|
|
4203
4410
|
});
|
|
4204
4411
|
|
|
4205
4412
|
// src/review/evidence/sarif.ts
|
|
4206
|
-
import { z as
|
|
4413
|
+
import { z as z18 } from "zod";
|
|
4207
4414
|
function parseSarif(raw, sourceRoot) {
|
|
4208
4415
|
const report = schema2.parse(raw), findings = [], diagnostics = [];
|
|
4209
4416
|
let active = 0, suppressed = 0, absent = 0, unresolved = 0;
|
|
@@ -4294,29 +4501,29 @@ var init_sarif = __esm({
|
|
|
4294
4501
|
"use strict";
|
|
4295
4502
|
init_paths2();
|
|
4296
4503
|
init_types2();
|
|
4297
|
-
index =
|
|
4298
|
-
artifact =
|
|
4299
|
-
message =
|
|
4300
|
-
level =
|
|
4301
|
-
location =
|
|
4302
|
-
schema2 =
|
|
4303
|
-
version:
|
|
4304
|
-
runs:
|
|
4305
|
-
tool:
|
|
4306
|
-
invocations:
|
|
4307
|
-
versionControlProvenance:
|
|
4308
|
-
originalUriBaseIds:
|
|
4309
|
-
artifacts:
|
|
4310
|
-
results:
|
|
4311
|
-
ruleId:
|
|
4504
|
+
index = z18.number().int().nonnegative();
|
|
4505
|
+
artifact = z18.object({ uri: z18.string().optional(), uriBaseId: z18.string().optional(), index: index.optional() });
|
|
4506
|
+
message = z18.object({ text: z18.string().optional(), markdown: z18.string().optional(), id: z18.string().optional(), arguments: z18.array(z18.string()).optional() });
|
|
4507
|
+
level = z18.enum(["error", "warning", "note", "none"]);
|
|
4508
|
+
location = z18.object({ physicalLocation: z18.object({ artifactLocation: artifact.optional(), region: z18.object({ startLine: index.optional(), startColumn: index.optional() }).optional() }).optional() });
|
|
4509
|
+
schema2 = z18.object({
|
|
4510
|
+
version: z18.literal("2.1.0"),
|
|
4511
|
+
runs: z18.array(z18.object({
|
|
4512
|
+
tool: z18.object({ driver: z18.object({ name: z18.string().min(1), rules: z18.array(z18.object({ id: z18.string(), defaultConfiguration: z18.object({ level: level.optional() }).optional(), messageStrings: z18.record(message).optional() })).optional(), globalMessageStrings: z18.record(message).optional() }) }),
|
|
4513
|
+
invocations: z18.array(z18.object({ executionSuccessful: z18.boolean(), commandLine: z18.string().optional(), toolExecutionNotifications: z18.array(z18.object({ level: level.optional(), message })).optional() })).optional(),
|
|
4514
|
+
versionControlProvenance: z18.array(z18.object({ revisionId: z18.string().optional() })).optional(),
|
|
4515
|
+
originalUriBaseIds: z18.record(artifact).optional(),
|
|
4516
|
+
artifacts: z18.array(z18.object({ location: artifact.optional() })).optional(),
|
|
4517
|
+
results: z18.array(z18.object({
|
|
4518
|
+
ruleId: z18.string().optional(),
|
|
4312
4519
|
ruleIndex: index.optional(),
|
|
4313
4520
|
message,
|
|
4314
4521
|
level: level.optional(),
|
|
4315
|
-
kind:
|
|
4316
|
-
baselineState:
|
|
4317
|
-
suppressions:
|
|
4318
|
-
locations:
|
|
4319
|
-
relatedLocations:
|
|
4522
|
+
kind: z18.enum(["fail", "pass", "open", "informational", "notApplicable", "review"]).optional(),
|
|
4523
|
+
baselineState: z18.enum(["new", "unchanged", "updated", "absent"]).optional(),
|
|
4524
|
+
suppressions: z18.array(z18.object({ kind: z18.enum(["inSource", "external"]), status: z18.enum(["accepted", "underReview", "rejected"]).optional() })).nullable().optional(),
|
|
4525
|
+
locations: z18.array(location).optional(),
|
|
4526
|
+
relatedLocations: z18.array(location).optional()
|
|
4320
4527
|
})).optional()
|
|
4321
4528
|
}))
|
|
4322
4529
|
});
|
|
@@ -4325,7 +4532,7 @@ var init_sarif = __esm({
|
|
|
4325
4532
|
|
|
4326
4533
|
// src/review/evidence.ts
|
|
4327
4534
|
import path21 from "path";
|
|
4328
|
-
import { z as
|
|
4535
|
+
import { z as z19 } from "zod";
|
|
4329
4536
|
function relativeArtifact(root, file) {
|
|
4330
4537
|
const relative = path21.isAbsolute(file) ? path21.relative(root, file) : file;
|
|
4331
4538
|
const normalized = normalizeRepoPath(relative);
|
|
@@ -4366,7 +4573,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
4366
4573
|
let manifest, raw;
|
|
4367
4574
|
try {
|
|
4368
4575
|
manifest = relativeArtifact(root, supplied);
|
|
4369
|
-
raw =
|
|
4576
|
+
raw = z19.object({ version: z19.literal(1), checks: z19.array(z19.unknown()) }).parse(await readStoreJson(root, manifest));
|
|
4370
4577
|
} catch (error) {
|
|
4371
4578
|
output.diagnostics.push(`${supplied}: ${String(error)}`);
|
|
4372
4579
|
continue;
|
|
@@ -4519,25 +4726,25 @@ var init_evidence2 = __esm({
|
|
|
4519
4726
|
init_vitest();
|
|
4520
4727
|
init_sarif();
|
|
4521
4728
|
init_types2();
|
|
4522
|
-
checkSchema2 =
|
|
4523
|
-
id:
|
|
4524
|
-
kind:
|
|
4525
|
-
tool:
|
|
4526
|
-
command:
|
|
4527
|
-
commit:
|
|
4528
|
-
workingTreeClean:
|
|
4529
|
-
source:
|
|
4530
|
-
sourceRoot:
|
|
4531
|
-
status:
|
|
4532
|
-
reason:
|
|
4533
|
-
exitCode:
|
|
4534
|
-
report:
|
|
4729
|
+
checkSchema2 = z19.object({
|
|
4730
|
+
id: z19.string().min(1).max(100),
|
|
4731
|
+
kind: z19.enum(["tests", "static-analysis", "security", "complexity", "duplication"]),
|
|
4732
|
+
tool: z19.string().min(1).max(200),
|
|
4733
|
+
command: z19.string().min(1).max(2e3),
|
|
4734
|
+
commit: z19.string().regex(/^(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$/).nullable().optional(),
|
|
4735
|
+
workingTreeClean: z19.boolean().optional(),
|
|
4736
|
+
source: z19.string().max(2e3).optional(),
|
|
4737
|
+
sourceRoot: z19.string().min(1).max(4e3).optional(),
|
|
4738
|
+
status: z19.enum(["completed", "skipped", "unavailable"]).default("completed"),
|
|
4739
|
+
reason: z19.string().min(1).max(2e3).optional(),
|
|
4740
|
+
exitCode: z19.number().int().nullable().optional(),
|
|
4741
|
+
report: z19.object({ format: z19.enum(["vitest-json", "sarif"]), path: z19.string().min(1).max(4e3) }).optional()
|
|
4535
4742
|
});
|
|
4536
4743
|
}
|
|
4537
4744
|
});
|
|
4538
4745
|
|
|
4539
4746
|
// src/review/review.ts
|
|
4540
|
-
import
|
|
4747
|
+
import fs18 from "fs/promises";
|
|
4541
4748
|
import path22 from "path";
|
|
4542
4749
|
import { execFile as execFile15 } from "child_process";
|
|
4543
4750
|
import { promisify as promisify15 } from "util";
|
|
@@ -4617,7 +4824,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
4617
4824
|
analyzed,
|
|
4618
4825
|
async (relPath) => {
|
|
4619
4826
|
try {
|
|
4620
|
-
await
|
|
4827
|
+
await fs18.access(path22.join(resolvedRoot, relPath));
|
|
4621
4828
|
return true;
|
|
4622
4829
|
} catch {
|
|
4623
4830
|
return false;
|
|
@@ -5093,12 +5300,12 @@ __export(assemble_exports, {
|
|
|
5093
5300
|
assembleContext: () => assembleContext
|
|
5094
5301
|
});
|
|
5095
5302
|
import path25 from "path";
|
|
5096
|
-
import
|
|
5303
|
+
import fs19 from "fs/promises";
|
|
5097
5304
|
async function collectImpact(root, candidates) {
|
|
5098
5305
|
const targets = /* @__PURE__ */ new Set();
|
|
5099
5306
|
let sourceFiles;
|
|
5100
5307
|
for (const candidate of sanitizeRepoPaths(candidates)) {
|
|
5101
|
-
const stat = await
|
|
5308
|
+
const stat = await fs19.stat(path25.join(root, candidate)).catch(() => null);
|
|
5102
5309
|
if (stat?.isDirectory()) {
|
|
5103
5310
|
sourceFiles ??= await (await createFileAccess(root)).list();
|
|
5104
5311
|
for (const file of sourceFiles) {
|
|
@@ -5325,8 +5532,8 @@ __export(observations_exports, {
|
|
|
5325
5532
|
observeActivation: () => observeActivation,
|
|
5326
5533
|
readObservation: () => readObservation
|
|
5327
5534
|
});
|
|
5328
|
-
import
|
|
5329
|
-
import { z as
|
|
5535
|
+
import fs20 from "fs/promises";
|
|
5536
|
+
import { z as z20 } from "zod";
|
|
5330
5537
|
function observationPath(directory, host, revision, version = executingVersion()) {
|
|
5331
5538
|
return `${directory}/activation/${host}-${revision}-${hash(version).slice(0, 12)}.json`;
|
|
5332
5539
|
}
|
|
@@ -5341,9 +5548,9 @@ async function observeActivation(dir, event, options = {}) {
|
|
|
5341
5548
|
const host = process.env.MASON_SETUP_HOST, revision = process.env.MASON_SETUP_REVISION;
|
|
5342
5549
|
if (host !== "codex" && host !== "claude" || !revision || !process.env.MASON_SETUP_ROOT) return null;
|
|
5343
5550
|
try {
|
|
5344
|
-
const capturedDirectory = options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
|
|
5345
|
-
const ws = capturedDirectory ? { root: await
|
|
5346
|
-
if (ws.root !== await
|
|
5551
|
+
const capturedDirectory = options.directory?.match(/^\.mason\/reports\/automation\/[a-f0-9]{24}$/)?.[0] ?? options.reportPath?.match(/^(\.mason\/reports\/automation\/[a-f0-9]{24})\/checks\//)?.[1];
|
|
5552
|
+
const ws = capturedDirectory ? { root: await fs20.realpath(dir), directory: capturedDirectory } : await workspace(dir);
|
|
5553
|
+
if (ws.root !== await fs20.realpath(process.env.MASON_SETUP_ROOT)) return null;
|
|
5347
5554
|
const setup = await loadSetup(ws.root);
|
|
5348
5555
|
if (setup?.hosts[host]?.revision !== revision) return "Mason setup changed; restart the assistant to observe the current integration.";
|
|
5349
5556
|
const directory = ws.directory + "/activation";
|
|
@@ -5391,19 +5598,19 @@ var init_observations = __esm({
|
|
|
5391
5598
|
init_storage();
|
|
5392
5599
|
init_model();
|
|
5393
5600
|
init_launcher();
|
|
5394
|
-
observationSchema =
|
|
5395
|
-
version:
|
|
5396
|
-
masonVersion:
|
|
5397
|
-
root:
|
|
5398
|
-
revision:
|
|
5399
|
-
host:
|
|
5400
|
-
contextCalls:
|
|
5401
|
-
lastContextAt:
|
|
5402
|
-
sessions:
|
|
5403
|
-
events:
|
|
5404
|
-
at:
|
|
5405
|
-
verificationStatus:
|
|
5406
|
-
reportPath:
|
|
5601
|
+
observationSchema = z20.object({
|
|
5602
|
+
version: z20.literal(1),
|
|
5603
|
+
masonVersion: z20.string(),
|
|
5604
|
+
root: z20.string(),
|
|
5605
|
+
revision: z20.string(),
|
|
5606
|
+
host: z20.enum(["codex", "claude"]),
|
|
5607
|
+
contextCalls: z20.number().int().nonnegative(),
|
|
5608
|
+
lastContextAt: z20.string().optional(),
|
|
5609
|
+
sessions: z20.record(z20.object({
|
|
5610
|
+
events: z20.array(z20.enum(events)),
|
|
5611
|
+
at: z20.string(),
|
|
5612
|
+
verificationStatus: z20.string().optional(),
|
|
5613
|
+
reportPath: z20.string().optional()
|
|
5407
5614
|
}))
|
|
5408
5615
|
});
|
|
5409
5616
|
}
|
|
@@ -5429,7 +5636,7 @@ async function setupStatus(dir) {
|
|
|
5429
5636
|
version: 1,
|
|
5430
5637
|
status: pending.length ? "incomplete" : "not-configured",
|
|
5431
5638
|
hosts: {},
|
|
5432
|
-
next: pending.length ? "Setup did not finish. Rerun mason-auto setup --host " + pending[0] + " to resume using the retained original evidence." : "Run mason
|
|
5639
|
+
next: pending.length ? "Setup did not finish. Rerun mason-auto setup --host " + pending[0] + " to resume using the retained original evidence." : "Project hooks are inactive until local setup. Run mason setup --host codex or --host claude."
|
|
5433
5640
|
};
|
|
5434
5641
|
}
|
|
5435
5642
|
const hosts = {};
|
|
@@ -5759,7 +5966,7 @@ __export(config_exports2, {
|
|
|
5759
5966
|
saveConfluenceConfig: () => saveConfluenceConfig,
|
|
5760
5967
|
validateProvider: () => validateProvider
|
|
5761
5968
|
});
|
|
5762
|
-
import
|
|
5969
|
+
import fs21 from "fs/promises";
|
|
5763
5970
|
import path26 from "path";
|
|
5764
5971
|
import os3 from "os";
|
|
5765
5972
|
import { execFile as execFile17 } from "child_process";
|
|
@@ -5772,15 +5979,15 @@ function configFile() {
|
|
|
5772
5979
|
}
|
|
5773
5980
|
async function loadConfig() {
|
|
5774
5981
|
try {
|
|
5775
|
-
const raw = await
|
|
5982
|
+
const raw = await fs21.readFile(configFile(), "utf-8");
|
|
5776
5983
|
return JSON.parse(raw);
|
|
5777
5984
|
} catch {
|
|
5778
5985
|
return null;
|
|
5779
5986
|
}
|
|
5780
5987
|
}
|
|
5781
5988
|
async function saveConfig(config) {
|
|
5782
|
-
await
|
|
5783
|
-
await
|
|
5989
|
+
await fs21.mkdir(configDir(), { recursive: true });
|
|
5990
|
+
await fs21.writeFile(configFile(), JSON.stringify(config, null, 2), "utf-8");
|
|
5784
5991
|
}
|
|
5785
5992
|
function getDefaultModel(provider) {
|
|
5786
5993
|
return DEFAULT_MODELS[provider];
|
|
@@ -5940,7 +6147,7 @@ var init_renderer = __esm({
|
|
|
5940
6147
|
});
|
|
5941
6148
|
|
|
5942
6149
|
// src/confluence/diff.ts
|
|
5943
|
-
import
|
|
6150
|
+
import fs22 from "fs/promises";
|
|
5944
6151
|
import path27 from "path";
|
|
5945
6152
|
import { createHash as createHash5 } from "crypto";
|
|
5946
6153
|
function hashDescription(description) {
|
|
@@ -5954,7 +6161,7 @@ function syncStatePath(rootDir) {
|
|
|
5954
6161
|
}
|
|
5955
6162
|
async function loadSyncState(rootDir) {
|
|
5956
6163
|
try {
|
|
5957
|
-
const raw = await
|
|
6164
|
+
const raw = await fs22.readFile(syncStatePath(rootDir), "utf-8");
|
|
5958
6165
|
const parsed = JSON.parse(raw);
|
|
5959
6166
|
if (parsed.version !== 2) return null;
|
|
5960
6167
|
return parsed;
|
|
@@ -5963,8 +6170,8 @@ async function loadSyncState(rootDir) {
|
|
|
5963
6170
|
}
|
|
5964
6171
|
}
|
|
5965
6172
|
async function saveSyncState(rootDir, state) {
|
|
5966
|
-
await
|
|
5967
|
-
await
|
|
6173
|
+
await fs22.mkdir(syncStateDir(rootDir), { recursive: true });
|
|
6174
|
+
await fs22.writeFile(
|
|
5968
6175
|
syncStatePath(rootDir),
|
|
5969
6176
|
JSON.stringify(state, null, 2),
|
|
5970
6177
|
"utf-8"
|
|
@@ -6512,13 +6719,14 @@ var init_sync = __esm({
|
|
|
6512
6719
|
|
|
6513
6720
|
// src/mcp/server.ts
|
|
6514
6721
|
init_provenance();
|
|
6722
|
+
init_decisions();
|
|
6515
6723
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6516
6724
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6517
|
-
import { z as
|
|
6725
|
+
import { z as z21 } from "zod";
|
|
6518
6726
|
|
|
6519
6727
|
// src/mcp/tools.ts
|
|
6520
6728
|
init_repair();
|
|
6521
|
-
import
|
|
6729
|
+
import fs23 from "fs/promises";
|
|
6522
6730
|
import path28 from "path";
|
|
6523
6731
|
import { execFile as execFile19 } from "child_process";
|
|
6524
6732
|
import { promisify as promisify19 } from "util";
|
|
@@ -7124,14 +7332,14 @@ init_files();
|
|
|
7124
7332
|
import { createHash as createHash3 } from "crypto";
|
|
7125
7333
|
import { execFile as execFile11 } from "child_process";
|
|
7126
7334
|
import { promisify as promisify11 } from "util";
|
|
7127
|
-
import { z as
|
|
7335
|
+
import { z as z5 } from "zod";
|
|
7128
7336
|
var exec10 = promisify11(execFile11);
|
|
7129
|
-
var requestSchema =
|
|
7130
|
-
id:
|
|
7131
|
-
action:
|
|
7132
|
-
reviewer:
|
|
7133
|
-
note:
|
|
7134
|
-
reviewToken:
|
|
7337
|
+
var requestSchema = z5.object({
|
|
7338
|
+
id: z5.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
7339
|
+
action: z5.enum(["prepare", "accept", "reaffirm", "retire"]).default("prepare"),
|
|
7340
|
+
reviewer: z5.string().trim().min(1).max(200).optional(),
|
|
7341
|
+
note: z5.string().trim().min(1).max(1500).optional(),
|
|
7342
|
+
reviewToken: z5.string().regex(/^[a-f0-9]{64}$/).optional()
|
|
7135
7343
|
});
|
|
7136
7344
|
async function reviewState(root, record) {
|
|
7137
7345
|
const [headHash, workingTree, changes] = await Promise.all([
|
|
@@ -7355,16 +7563,16 @@ ${JSON.stringify({ partials }, null, 2)}`;
|
|
|
7355
7563
|
init_storage();
|
|
7356
7564
|
init_snapshot();
|
|
7357
7565
|
import fs12 from "fs/promises";
|
|
7358
|
-
import { z as
|
|
7566
|
+
import { z as z6 } from "zod";
|
|
7359
7567
|
var DIRECTORY = ".mason/partial-snapshots";
|
|
7360
|
-
var partialSchema =
|
|
7361
|
-
batchId:
|
|
7362
|
-
offset:
|
|
7363
|
-
features:
|
|
7364
|
-
flows:
|
|
7365
|
-
savedAt:
|
|
7568
|
+
var partialSchema = z6.object({
|
|
7569
|
+
batchId: z6.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
7570
|
+
offset: z6.number().int().nonnegative(),
|
|
7571
|
+
features: z6.record(featureSchema),
|
|
7572
|
+
flows: z6.record(flowSchema),
|
|
7573
|
+
savedAt: z6.string()
|
|
7366
7574
|
});
|
|
7367
|
-
var scopeSchema =
|
|
7575
|
+
var scopeSchema = z6.object({ files: z6.array(z6.string()), savedAt: z6.string() });
|
|
7368
7576
|
async function savePartial(rootDir, partial) {
|
|
7369
7577
|
if (!/^[a-zA-Z0-9_-]+$/.test(partial.batchId)) throw new Error(`Invalid batchId: ${partial.batchId}`);
|
|
7370
7578
|
await writeStoreJson(rootDir, `${DIRECTORY}/${partial.batchId}.json`, partialSchema.parse(partial));
|
|
@@ -7469,7 +7677,7 @@ async function detectProjectSnapshot(rootDir) {
|
|
|
7469
7677
|
const present = [];
|
|
7470
7678
|
for (const file of buildFiles) {
|
|
7471
7679
|
try {
|
|
7472
|
-
await
|
|
7680
|
+
await fs23.access(path28.join(rootDir, file));
|
|
7473
7681
|
present.push(file);
|
|
7474
7682
|
} catch {
|
|
7475
7683
|
}
|
|
@@ -8335,7 +8543,7 @@ function createMcpServer() {
|
|
|
8335
8543
|
const server = new McpServer(
|
|
8336
8544
|
{
|
|
8337
8545
|
name: "mason",
|
|
8338
|
-
version: "0.16.
|
|
8546
|
+
version: "0.16.3"
|
|
8339
8547
|
},
|
|
8340
8548
|
{
|
|
8341
8549
|
instructions: '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, failed verification needs 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.'
|
|
@@ -8345,11 +8553,11 @@ function createMcpServer() {
|
|
|
8345
8553
|
"mason_init",
|
|
8346
8554
|
"Inspect this project now: returns documentation audit findings, committed-diff review findings, decision/map status, and a quickstart playbook. Quickstart and map modes are read-only and deterministic. Explicit mode: setup configures MCP, instructions and lifecycle hooks to use mason on PATH while retaining original audit evidence; use it only when the user requests setup. Optional host selects codex or claude. Optional base selects the review comparison; evidence imports CI manifests with check outcomes, commit freshness, and links to changed files and accepted decisions. mode: map returns the full Map-Reduce build workflow. Repeat calls refresh findings even after setup.",
|
|
8347
8555
|
{
|
|
8348
|
-
dir:
|
|
8349
|
-
mode:
|
|
8350
|
-
host:
|
|
8351
|
-
base:
|
|
8352
|
-
evidence:
|
|
8556
|
+
dir: z21.string().describe("Absolute path to the project root directory"),
|
|
8557
|
+
mode: z21.enum(["quickstart", "map", "setup"]).optional().default("quickstart").describe("Quickstart inspects without edits; map requests an architecture build; setup installs the shared onboarding flow."),
|
|
8558
|
+
host: z21.enum(["codex", "claude"]).optional().describe("Assistant to configure in setup mode; inferred only when unambiguous."),
|
|
8559
|
+
base: z21.string().optional().describe("Git ref for committed-diff review. Defaults to the first available main branch ref."),
|
|
8560
|
+
evidence: z21.array(z21.string()).max(10).optional().describe("Repository-local CI evidence manifests to include in the review. Imports Vitest JSON and SARIF without executing check commands.")
|
|
8353
8561
|
},
|
|
8354
8562
|
async ({ dir, mode, host, base, evidence }) => {
|
|
8355
8563
|
const result = await masonInit(dir, { mode, host, base, evidence });
|
|
@@ -8360,8 +8568,8 @@ function createMcpServer() {
|
|
|
8360
8568
|
"mason_automation",
|
|
8361
8569
|
"Inspect installed automation and observed host events, or resume/check retained documentation repair evidence across sessions. status is read-only; check saves local baselines and verification reports without editing source or approving advisories. Returns concise results with a full report path. Works without a map.",
|
|
8362
8570
|
{
|
|
8363
|
-
dir:
|
|
8364
|
-
action:
|
|
8571
|
+
dir: z21.string().describe("Absolute path to the project directory"),
|
|
8572
|
+
action: z21.enum(["status", "check"]).describe("Inspect configuration and receipts, or capture/resume and verify original audit evidence.")
|
|
8365
8573
|
},
|
|
8366
8574
|
async ({ dir, action }) => ({ content: [{ type: "text", text: await masonAutomation(dir, action) }] })
|
|
8367
8575
|
);
|
|
@@ -8369,10 +8577,10 @@ function createMcpServer() {
|
|
|
8369
8577
|
"mason_repair",
|
|
8370
8578
|
"Track documentation repairs against original audit evidence. prepare saves a local baseline and returns a scoped work order; verify reads that baseline and reports resolved, unresolved, review-required, unverified, and new findings. Suppressed advisories remain unresolved. Does not edit documentation or approve decisions. No map required.",
|
|
8371
8579
|
{
|
|
8372
|
-
dir:
|
|
8373
|
-
action:
|
|
8374
|
-
baselinePath:
|
|
8375
|
-
checks:
|
|
8580
|
+
dir: z21.string().describe("Absolute path to the project root directory"),
|
|
8581
|
+
action: z21.enum(["prepare", "verify"]),
|
|
8582
|
+
baselinePath: z21.string().optional().describe("Original baseline path returned by prepare; required for verify."),
|
|
8583
|
+
checks: z21.array(z21.enum(["deleted-reference", "new-module", "stale-count", "dead-command", "deps-changed", "decision-anchor-drift"])).min(1).optional().describe("Optional audit check subset for prepare. Verification always uses the original checks.")
|
|
8376
8584
|
},
|
|
8377
8585
|
async ({ dir, action, baselinePath, checks }) => {
|
|
8378
8586
|
const result = await masonRepair(dir, { action, baselinePath, checks });
|
|
@@ -8383,8 +8591,8 @@ function createMcpServer() {
|
|
|
8383
8591
|
"mason_complete_init",
|
|
8384
8592
|
"Record assistant instruction setup locally in ignored .mason/local/project.json, with feature settings in shared .mason/config.json. Other tools work without this marker. Repeated calls preserve the original setup time and existing settings; pass confluenceConfigured only to change that setting.",
|
|
8385
8593
|
{
|
|
8386
|
-
dir:
|
|
8387
|
-
confluenceConfigured:
|
|
8594
|
+
dir: z21.string().describe("Absolute path to the project root directory"),
|
|
8595
|
+
confluenceConfigured: z21.boolean().optional().describe("Set the Confluence setup status; omit to preserve the existing value")
|
|
8388
8596
|
},
|
|
8389
8597
|
async ({ dir, confluenceConfigured }) => {
|
|
8390
8598
|
const result = await masonCompleteInit(dir, { confluenceConfigured });
|
|
@@ -8395,11 +8603,11 @@ function createMcpServer() {
|
|
|
8395
8603
|
"mason_set_confluence",
|
|
8396
8604
|
"Configure Confluence credentials. Two-step flow: (1) call without `spaceKey` to validate the credentials and receive a list of available spaces \u2014 relay them to the user. (2) call again with the same `baseUrl`/`email`/`apiToken` plus the chosen `spaceKey` to persist. Credentials are stored in `~/.mason/config.json`. Warn the user that the API token will be visible in chat history before they paste it.",
|
|
8397
8605
|
{
|
|
8398
|
-
baseUrl:
|
|
8399
|
-
email:
|
|
8400
|
-
apiToken:
|
|
8401
|
-
spaceKey:
|
|
8402
|
-
parentPageId:
|
|
8606
|
+
baseUrl: z21.string().describe("Confluence base URL. Accepts `acme`, `acme.atlassian.net`, or `https://acme.atlassian.net` (normalized automatically)."),
|
|
8607
|
+
email: z21.string().describe("User's Atlassian account email"),
|
|
8608
|
+
apiToken: z21.string().describe("API token from id.atlassian.com/manage-profile/security/api-tokens"),
|
|
8609
|
+
spaceKey: z21.string().optional().describe("Confluence space key. Omit on the first call to list available spaces."),
|
|
8610
|
+
parentPageId: z21.string().optional().describe("Optional parent page ID under which Mason's index page is created")
|
|
8403
8611
|
},
|
|
8404
8612
|
async ({ baseUrl, email, apiToken, spaceKey, parentPageId }) => {
|
|
8405
8613
|
const result = await masonSetConfluence({
|
|
@@ -8416,7 +8624,7 @@ function createMcpServer() {
|
|
|
8416
8624
|
"full_analysis",
|
|
8417
8625
|
"One-shot orientation for a project WITHOUT a concept map (get_snapshot returned exists:false). Returns git history stats, project structure with file counts, curated code sample previews (~60 lines each), and test-to-source mapping. On a mapped project, prefer get_snapshot \u2014 it is cheaper and answers feature/architecture questions directly.",
|
|
8418
8626
|
{
|
|
8419
|
-
dir:
|
|
8627
|
+
dir: z21.string().describe("Absolute path to the project root directory")
|
|
8420
8628
|
},
|
|
8421
8629
|
async ({ dir }) => {
|
|
8422
8630
|
const result = await fullAnalysis(dir);
|
|
@@ -8429,7 +8637,7 @@ function createMcpServer() {
|
|
|
8429
8637
|
"analyze_project",
|
|
8430
8638
|
"Run git history analysis on a codebase. Returns commit convention patterns, stale directories, and frequently changed files. These are aggregate stats across hundreds of commits that would be expensive to compute manually.",
|
|
8431
8639
|
{
|
|
8432
|
-
dir:
|
|
8640
|
+
dir: z21.string().describe("Absolute path to the project root directory")
|
|
8433
8641
|
},
|
|
8434
8642
|
async ({ dir }) => {
|
|
8435
8643
|
const result = await analyzeProject(dir);
|
|
@@ -8442,8 +8650,8 @@ function createMcpServer() {
|
|
|
8442
8650
|
"get_code_samples",
|
|
8443
8651
|
"Get previews (first ~60 lines) of representative source files from the codebase. Includes entry points, config files, hot files (frequently changed), test examples, and one file per directory for breadth. Read files natively for full content.",
|
|
8444
8652
|
{
|
|
8445
|
-
dir:
|
|
8446
|
-
count:
|
|
8653
|
+
dir: z21.string().describe("Absolute path to the project root directory"),
|
|
8654
|
+
count: z21.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
|
|
8447
8655
|
},
|
|
8448
8656
|
async ({ dir, count: count3 }) => {
|
|
8449
8657
|
const result = await getCodeSamples(dir, count3);
|
|
@@ -8456,7 +8664,7 @@ function createMcpServer() {
|
|
|
8456
8664
|
"get_snapshot",
|
|
8457
8665
|
"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.",
|
|
8458
8666
|
{
|
|
8459
|
-
dir:
|
|
8667
|
+
dir: z21.string().describe("Absolute path to the project root directory")
|
|
8460
8668
|
},
|
|
8461
8669
|
async ({ dir }) => {
|
|
8462
8670
|
const result = await getSnapshot(dir);
|
|
@@ -8469,9 +8677,9 @@ function createMcpServer() {
|
|
|
8469
8677
|
"get_context",
|
|
8470
8678
|
"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.",
|
|
8471
8679
|
{
|
|
8472
|
-
dir:
|
|
8473
|
-
task:
|
|
8474
|
-
files:
|
|
8680
|
+
dir: z21.string().describe("Absolute path to the project root directory"),
|
|
8681
|
+
task: z21.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"),
|
|
8682
|
+
files: z21.array(z21.string()).optional().describe("Optional file paths already known to be involved (e.g. from a diff or stack trace). Entries containing them are boosted above pure text matches.")
|
|
8475
8683
|
},
|
|
8476
8684
|
async ({ dir, task, files }) => {
|
|
8477
8685
|
const result = await getContext(dir, task, files);
|
|
@@ -8487,10 +8695,10 @@ function createMcpServer() {
|
|
|
8487
8695
|
"generate_snapshot_batch",
|
|
8488
8696
|
"Map step of the concept-map build. Returns one batch of source files (skeletons of every file in the batch plus a few deeper-read bodies for grounding), along with a system prompt instructing you to derive features and flows for ONLY this batch. Call repeatedly with the returned `nextOffset` until it is null, calling `save_partial_snapshot` between each call. Use product-natural feature names so partials merge cleanly in the reduce step.",
|
|
8489
8697
|
{
|
|
8490
|
-
dir:
|
|
8491
|
-
offset:
|
|
8492
|
-
batchSize:
|
|
8493
|
-
files:
|
|
8698
|
+
dir: z21.string().describe("Absolute path to the project root directory"),
|
|
8699
|
+
offset: z21.number().int().optional().describe("0-indexed file offset to start the batch at. Omit on the first call; pass the `nextOffset` from the previous response for subsequent calls."),
|
|
8700
|
+
batchSize: z21.number().int().optional().describe("Files per batch. Defaults to 50."),
|
|
8701
|
+
files: z21.array(z21.string()).optional().describe("Scope the batch walk to this explicit file list \u2014 e.g. the drift set from mason_check_drift (changedFiles + unmappedFiles). Pass the SAME list on every batch call of one refresh run. Triggers refresh mode: reduce_snapshot will merge the partials into the existing map instead of rebuilding it.")
|
|
8494
8702
|
},
|
|
8495
8703
|
async ({ dir, offset, batchSize, files }) => {
|
|
8496
8704
|
const result = await generateSnapshotBatch(dir, offset, batchSize, files);
|
|
@@ -8503,23 +8711,23 @@ function createMcpServer() {
|
|
|
8503
8711
|
"save_partial_snapshot",
|
|
8504
8712
|
"Persist the partial concept map you derived for one batch. Call this once per batch, with the `batchId` from the `generate_snapshot_batch` response. Partials accumulate in `.mason/partial-snapshots/` and are merged in the reduce step.",
|
|
8505
8713
|
{
|
|
8506
|
-
dir:
|
|
8507
|
-
batchId:
|
|
8508
|
-
offset:
|
|
8509
|
-
features:
|
|
8510
|
-
|
|
8511
|
-
description:
|
|
8512
|
-
files:
|
|
8513
|
-
tests:
|
|
8514
|
-
type:
|
|
8714
|
+
dir: z21.string().describe("Absolute path to the project root directory"),
|
|
8715
|
+
batchId: z21.string().describe("The `batchId` returned by `generate_snapshot_batch`."),
|
|
8716
|
+
offset: z21.number().int().describe("The `offset` returned by `generate_snapshot_batch`. Used to order partials in the reduce step."),
|
|
8717
|
+
features: z21.record(
|
|
8718
|
+
z21.object({
|
|
8719
|
+
description: z21.string(),
|
|
8720
|
+
files: z21.array(z21.string()),
|
|
8721
|
+
tests: z21.array(z21.string()).optional(),
|
|
8722
|
+
type: z21.enum(["capability", "infrastructure"]).optional().describe(
|
|
8515
8723
|
'"capability" (user-facing functionality) or "infrastructure" (internal plumbing with no end user \u2014 DI/service wiring, config, logging, adapters). Defaults to "capability".'
|
|
8516
8724
|
)
|
|
8517
8725
|
})
|
|
8518
8726
|
).describe("Partial features for this batch only \u2014 files outside the batch will be added by other partials."),
|
|
8519
|
-
flows:
|
|
8520
|
-
|
|
8521
|
-
description:
|
|
8522
|
-
chain:
|
|
8727
|
+
flows: z21.record(
|
|
8728
|
+
z21.object({
|
|
8729
|
+
description: z21.string(),
|
|
8730
|
+
chain: z21.array(z21.string())
|
|
8523
8731
|
})
|
|
8524
8732
|
).describe("Partial flows whose entire chain is in this batch. Cross-batch flows are reconstructed in reduce.")
|
|
8525
8733
|
},
|
|
@@ -8534,7 +8742,7 @@ function createMcpServer() {
|
|
|
8534
8742
|
"reduce_snapshot",
|
|
8535
8743
|
"Reduce step of the concept-map build. Returns every partial snapshot plus a system prompt asking you to merge them into one coherent project-wide map. Resolve platform variants into single product features, dedupe near-duplicates, and ensure no file is dropped. After producing the unified map, call `save_snapshot` to persist it (this also clears the partials).",
|
|
8536
8744
|
{
|
|
8537
|
-
dir:
|
|
8745
|
+
dir: z21.string().describe("Absolute path to the project root directory")
|
|
8538
8746
|
},
|
|
8539
8747
|
async ({ dir }) => {
|
|
8540
8748
|
const result = await reduceSnapshot(dir);
|
|
@@ -8547,25 +8755,25 @@ function createMcpServer() {
|
|
|
8547
8755
|
"save_snapshot",
|
|
8548
8756
|
"Save a concept-to-files map as a persistent project snapshot. Maps feature names and data flows to the files that implement them. Persists across conversations \u2014 future sessions can call get_snapshot to instantly find relevant files. No API key needed \u2014 you are the LLM generating the map.",
|
|
8549
8757
|
{
|
|
8550
|
-
dir:
|
|
8551
|
-
features:
|
|
8552
|
-
|
|
8553
|
-
description:
|
|
8554
|
-
files:
|
|
8555
|
-
tests:
|
|
8556
|
-
type:
|
|
8758
|
+
dir: z21.string().describe("Absolute path to the project root directory"),
|
|
8759
|
+
features: z21.record(
|
|
8760
|
+
z21.object({
|
|
8761
|
+
description: z21.string().describe("One-line description of the feature"),
|
|
8762
|
+
files: z21.array(z21.string()).describe("File paths that implement this feature"),
|
|
8763
|
+
tests: z21.array(z21.string()).optional().describe("Test file paths for this feature"),
|
|
8764
|
+
type: z21.enum(["capability", "infrastructure"]).optional().describe(
|
|
8557
8765
|
'Classification: "capability" for user-facing functionality, "infrastructure" for internal plumbing with no end user (DI/service wiring, config, logging, adapters). Capabilities are published to Confluence; infrastructure stays in the AI concept map only. Defaults to "capability".'
|
|
8558
8766
|
)
|
|
8559
8767
|
})
|
|
8560
8768
|
).describe("Map of feature names to their implementing files"),
|
|
8561
|
-
flows:
|
|
8562
|
-
|
|
8563
|
-
description:
|
|
8564
|
-
chain:
|
|
8769
|
+
flows: z21.record(
|
|
8770
|
+
z21.object({
|
|
8771
|
+
description: z21.string().describe("One-line description of the flow"),
|
|
8772
|
+
chain: z21.array(z21.string()).describe("Ordered list of file paths showing data/call flow")
|
|
8565
8773
|
})
|
|
8566
8774
|
).describe("Map of flow names to ordered file chains"),
|
|
8567
|
-
removeFeatures:
|
|
8568
|
-
removeFlows:
|
|
8775
|
+
removeFeatures: z21.array(z21.string()).optional().describe("Feature names to delete from the existing map \u2014 for features that were renamed or no longer exist. Applied before merging; only meaningful on incremental saves."),
|
|
8776
|
+
removeFlows: z21.array(z21.string()).optional().describe("Flow names to delete from the existing map. Applied before merging; only meaningful on incremental saves.")
|
|
8569
8777
|
},
|
|
8570
8778
|
async ({ dir, features, flows, removeFeatures, removeFlows }) => {
|
|
8571
8779
|
const result = await saveSnapshotData(
|
|
@@ -8584,17 +8792,17 @@ function createMcpServer() {
|
|
|
8584
8792
|
"save_decision",
|
|
8585
8793
|
"Capture or revise a decision proposal with rationale, anchors, optional owner, sources, and a known actor. No setup or map required. Writes a local record and preserves content history. Changes create a pending proposal while the last accepted revision remains operative; unchanged content does not re-verify or refresh it. Use review_decision for authorized acceptance or reaffirmation. A proposal cannot supersede a record with an operative accepted revision; review its replacement and retire the original separately.",
|
|
8586
8794
|
{
|
|
8587
|
-
dir:
|
|
8588
|
-
title:
|
|
8589
|
-
body:
|
|
8590
|
-
category:
|
|
8591
|
-
files:
|
|
8592
|
-
id:
|
|
8593
|
-
supersedes:
|
|
8795
|
+
dir: z21.string().describe("Absolute path to the project root directory"),
|
|
8796
|
+
title: decisionTitleSchema.describe(`Short, specific headline (max ${TITLE_MAX_CHARS} characters). Generates a stable id on creation; revising the title keeps the existing id.`),
|
|
8797
|
+
body: decisionBodySchema.describe(`The knowledge itself: what was tried/decided, why, and what to avoid. Must contain information NOT derivable by reading the code. Aim for ${BODY_RECOMMENDED_CHARS} characters or fewer; up to ${BODY_MAX_CHARS} is accepted with a warning above the target. Matching context and hooks include the full body. Preserve exceptions; use sources for supporting references.`),
|
|
8798
|
+
category: z21.enum(["decision", "gotcha", "deprecation", "convention"]),
|
|
8799
|
+
files: z21.array(z21.string()).optional().describe("Repo-relative files or directory prefixes this applies to. Matching is shared by retrieval, hooks, review, and drift checking; changes flag the decision for re-verification."),
|
|
8800
|
+
id: z21.string().optional().describe("Existing id to revise. Changed content becomes a proposal; unchanged content leaves the review and freshness untouched."),
|
|
8801
|
+
supersedes: z21.string().optional().describe("Id of an unreviewed record or proposal with no accepted revision to replace. Operative accepted decisions require separate review and retirement."),
|
|
8594
8802
|
owner: attributionSchema.shape.owner.describe("Responsible person or team, when known. Null clears it. Required for acceptance."),
|
|
8595
8803
|
sources: attributionSchema.shape.sources.describe("Known PR, issue, incident, discussion, or document references. Omit to preserve; [] clears. At least one is required for acceptance."),
|
|
8596
8804
|
actor: attributionSchema.shape.actor.describe("Known person or agent recording this revision. Omit if unknown; do not infer from Git identity."),
|
|
8597
|
-
force:
|
|
8805
|
+
force: z21.boolean().optional().describe("Save even when a near-duplicate was detected")
|
|
8598
8806
|
},
|
|
8599
8807
|
async ({ dir, title, body, category, files, id, supersedes, force, owner, sources, actor }) => {
|
|
8600
8808
|
const result = await saveDecision(dir, {
|
|
@@ -8616,12 +8824,12 @@ function createMcpServer() {
|
|
|
8616
8824
|
"review_decision",
|
|
8617
8825
|
"Prepare a decision review: returns the full record and history, any operative accepted revision, provenance, changes and previews for both sets of anchors, and a reviewToken. Then record accept, reaffirm, or retire with that token, the authorized reviewer, and a reason. Acceptance replaces the operative revision; retirement withdraws the entire record including its proposal. Acceptance requires owner, source, readable Git HEAD, and committed anchor changes. Changed records or code invalidate the token. Identities and approvals are recorded assertions for normal PR review, not authenticated proof.",
|
|
8618
8826
|
{
|
|
8619
|
-
dir:
|
|
8620
|
-
id:
|
|
8621
|
-
action:
|
|
8622
|
-
reviewer:
|
|
8623
|
-
note:
|
|
8624
|
-
reviewToken:
|
|
8827
|
+
dir: z21.string().describe("Absolute path to the project root directory"),
|
|
8828
|
+
id: z21.string().regex(/^[a-zA-Z0-9_-]+$/).describe("Decision id from get_context or save_decision"),
|
|
8829
|
+
action: z21.enum(["prepare", "accept", "reaffirm", "retire"]).optional().default("prepare").describe("Prepare is read-only. Other actions record an explicitly authorized review."),
|
|
8830
|
+
reviewer: z21.string().trim().min(1).max(200).optional().describe("Identity of the actual reviewer; required for a verdict. Never invent one."),
|
|
8831
|
+
note: z21.string().trim().min(1).max(1500).optional().describe("Review rationale; required for a verdict. Cite evidence for the decision."),
|
|
8832
|
+
reviewToken: z21.string().regex(/^[a-f0-9]{64}$/).optional().describe("Token from the prepared review; rejects stale record or code revisions")
|
|
8625
8833
|
},
|
|
8626
8834
|
async ({ dir, ...input }) => {
|
|
8627
8835
|
const result = await reviewDecision2(dir, input);
|
|
@@ -8632,7 +8840,7 @@ function createMcpServer() {
|
|
|
8632
8840
|
"mason_check_drift",
|
|
8633
8841
|
"Check how far the concept map has drifted from HEAD. Deterministic (git + filesystem, no LLM). Returns which features/flows are stale and the changed files behind them, new source files not yet mapped, ghost files (mapped but deleted), renames, and a `recommendation`: `up-to-date` (nothing to do), `incremental` (update just the stale entries via save_snapshot), or `full-rebuild` (re-run the Map-Reduce build). Call this before trusting the map in a long session, or periodically to keep the map and any synced wikis fresh.",
|
|
8634
8842
|
{
|
|
8635
|
-
dir:
|
|
8843
|
+
dir: z21.string().describe("Absolute path to the project root directory")
|
|
8636
8844
|
},
|
|
8637
8845
|
async ({ dir }) => {
|
|
8638
8846
|
const result = await checkDrift(dir);
|
|
@@ -8645,8 +8853,8 @@ function createMcpServer() {
|
|
|
8645
8853
|
"verify_snapshot",
|
|
8646
8854
|
"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.",
|
|
8647
8855
|
{
|
|
8648
|
-
dir:
|
|
8649
|
-
sample:
|
|
8856
|
+
dir: z21.string().describe("Absolute path to the project root directory"),
|
|
8857
|
+
sample: z21.number().int().optional().describe("Entries to sample (default 5)")
|
|
8650
8858
|
},
|
|
8651
8859
|
async ({ dir, sample }) => {
|
|
8652
8860
|
const result = await verifySnapshot(dir, sample);
|
|
@@ -8657,11 +8865,11 @@ function createMcpServer() {
|
|
|
8657
8865
|
"save_verification",
|
|
8658
8866
|
"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.",
|
|
8659
8867
|
{
|
|
8660
|
-
dir:
|
|
8661
|
-
verdicts:
|
|
8662
|
-
|
|
8663
|
-
ok:
|
|
8664
|
-
note:
|
|
8868
|
+
dir: z21.string().describe("Absolute path to the project root directory"),
|
|
8869
|
+
verdicts: z21.record(
|
|
8870
|
+
z21.object({
|
|
8871
|
+
ok: z21.boolean(),
|
|
8872
|
+
note: z21.string().optional().describe("One line on what's wrong \u2014 required when ok is false")
|
|
8665
8873
|
})
|
|
8666
8874
|
).describe("Entry name \u2192 verdict, exactly as returned by verify_snapshot")
|
|
8667
8875
|
},
|
|
@@ -8674,8 +8882,8 @@ function createMcpServer() {
|
|
|
8674
8882
|
"get_impact",
|
|
8675
8883
|
"Trace historical co-change partners, related tests, and references with evidence: resolved imports, explicit paths, or uncertain name candidates. Candidates are not proven dependencies. Deterministic and read-only; no initialization or concept map required.",
|
|
8676
8884
|
{
|
|
8677
|
-
dir:
|
|
8678
|
-
files:
|
|
8885
|
+
dir: z21.string().describe("Absolute path to the project root directory"),
|
|
8886
|
+
files: z21.array(z21.string()).describe("File paths or names to analyze (e.g., ['WeatherRepository.kt'] or ['src/services/auth.ts'])")
|
|
8679
8887
|
},
|
|
8680
8888
|
async ({ dir, files }) => {
|
|
8681
8889
|
const result = await getImpact(dir, files);
|
|
@@ -8688,12 +8896,12 @@ function createMcpServer() {
|
|
|
8688
8896
|
"export_to_confluence",
|
|
8689
8897
|
"Sync the project's concept map to Confluence as product-readable wiki pages: an index page, one page per feature (PM-language descriptions, no file paths), and a changelog page. Mason replaces managed page bodies; manual edits to those bodies are overwritten. Requires `mason_set_confluence` to have been called first.",
|
|
8690
8898
|
{
|
|
8691
|
-
dir:
|
|
8692
|
-
spaceKey:
|
|
8693
|
-
parentPageId:
|
|
8694
|
-
indexPageTitle:
|
|
8695
|
-
changelogPageTitle:
|
|
8696
|
-
featurePagePrefix:
|
|
8899
|
+
dir: z21.string().describe("Absolute path to the project root directory"),
|
|
8900
|
+
spaceKey: z21.string().optional().describe("Override the configured space key"),
|
|
8901
|
+
parentPageId: z21.string().optional().describe("Override the configured parent page ID"),
|
|
8902
|
+
indexPageTitle: z21.string().optional().describe("Title of the index page (default: 'Mason \u2014 System Map')"),
|
|
8903
|
+
changelogPageTitle: z21.string().optional().describe("Title of the changelog page (default: 'Mason \u2014 Changelog')"),
|
|
8904
|
+
featurePagePrefix: z21.string().optional().describe("Prefix for each feature page title (default: 'Feature: ')")
|
|
8697
8905
|
},
|
|
8698
8906
|
async ({ dir, spaceKey, parentPageId, indexPageTitle, changelogPageTitle, featurePagePrefix }) => {
|
|
8699
8907
|
const result = await exportToConfluenceTool(dir, {
|