mason-context 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +10 -5
- package/dist/mason-audit.js +159 -95
- package/dist/mason-audit.js.map +1 -1
- package/dist/mason-drift.js +90 -53
- package/dist/mason-drift.js.map +1 -1
- package/dist/mason-hook.js +97 -45
- package/dist/mason-hook.js.map +1 -1
- package/dist/mason-mcp.js +3425 -5255
- package/dist/mason-mcp.js.map +1 -1
- package/dist/mason-review.js +180 -125
- package/dist/mason-review.js.map +1 -1
- package/dist/mason-runtime.js +13 -6
- package/dist/mason-runtime.js.map +1 -1
- package/dist/mason.js +1808 -1882
- package/dist/mason.js.map +1 -1
- package/package.json +1 -4
package/dist/mason-review.js
CHANGED
|
@@ -31,14 +31,14 @@ var init_types = __esm({
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
// src/review/cli.ts
|
|
34
|
-
import
|
|
34
|
+
import path12 from "path";
|
|
35
35
|
import { stripVTControlCharacters } from "util";
|
|
36
36
|
|
|
37
37
|
// src/review/review.ts
|
|
38
38
|
import fs7 from "fs/promises";
|
|
39
|
-
import
|
|
40
|
-
import { execFile as
|
|
41
|
-
import { promisify as
|
|
39
|
+
import path11 from "path";
|
|
40
|
+
import { execFile as execFile4 } from "child_process";
|
|
41
|
+
import { promisify as promisify4 } from "util";
|
|
42
42
|
|
|
43
43
|
// src/utils/git-read.ts
|
|
44
44
|
import { execFile } from "child_process";
|
|
@@ -91,10 +91,7 @@ function execGit(args, options) {
|
|
|
91
91
|
|
|
92
92
|
// src/drift/drift.ts
|
|
93
93
|
import fs4 from "fs/promises";
|
|
94
|
-
import
|
|
95
|
-
|
|
96
|
-
// src/snapshot/snapshot.ts
|
|
97
|
-
import path5 from "path";
|
|
94
|
+
import path4 from "path";
|
|
98
95
|
|
|
99
96
|
// src/utils/validation.ts
|
|
100
97
|
function isRecord(value) {
|
|
@@ -394,11 +391,13 @@ async function storePath(root, relative, createParents = false) {
|
|
|
394
391
|
}
|
|
395
392
|
return current;
|
|
396
393
|
}
|
|
397
|
-
|
|
394
|
+
var STORE_MAX_BYTES = 10 * 1024 * 1024;
|
|
395
|
+
async function readStoreJson(root, relative, maxBytes = STORE_MAX_BYTES) {
|
|
398
396
|
try {
|
|
399
397
|
const file = await storePath(root, relative);
|
|
400
|
-
const raw = await readBoundedFile(file,
|
|
401
|
-
if (raw === null)
|
|
398
|
+
const raw = await readBoundedFile(file, maxBytes);
|
|
399
|
+
if (raw === null)
|
|
400
|
+
throw new Error(`file is not regular or exceeds ${maxBytes / 1024 / 1024} MiB`);
|
|
402
401
|
const parsed = JSON.parse(raw);
|
|
403
402
|
if (parsed === null) throw new Error("expected a JSON object, received null");
|
|
404
403
|
return parsed;
|
|
@@ -420,76 +419,6 @@ import fs3 from "fs/promises";
|
|
|
420
419
|
import os from "os";
|
|
421
420
|
var lockOwnerSchema = z.object({ host: z.string(), pid: z.number().int().positive() });
|
|
422
421
|
|
|
423
|
-
// src/test-map.ts
|
|
424
|
-
import path4 from "path";
|
|
425
|
-
async function buildTestMap(dir) {
|
|
426
|
-
const rootDir = path4.resolve(dir);
|
|
427
|
-
const access = await createFileAccess(rootDir);
|
|
428
|
-
const testPatterns = [
|
|
429
|
-
"**/*.test.*",
|
|
430
|
-
"**/*.spec.*",
|
|
431
|
-
"**/*Test.kt",
|
|
432
|
-
"**/*Test.java",
|
|
433
|
-
"**/*Tests.kt",
|
|
434
|
-
"**/*Tests.java",
|
|
435
|
-
"**/test_*.py",
|
|
436
|
-
"**/*_test.py",
|
|
437
|
-
"**/*_test.go",
|
|
438
|
-
"**/*Tests.swift",
|
|
439
|
-
"**/*Test.swift",
|
|
440
|
-
"**/*_test.rs"
|
|
441
|
-
];
|
|
442
|
-
const testFiles = await access.list(testPatterns);
|
|
443
|
-
const sourceFiles = await access.list();
|
|
444
|
-
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
445
|
-
for (const file of sourceFiles) {
|
|
446
|
-
if (testFiles.includes(file)) continue;
|
|
447
|
-
const baseName = path4.basename(file).replace(/\.[^.]+$/, "");
|
|
448
|
-
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
449
|
-
existing.push(file);
|
|
450
|
-
sourceByBaseName.set(baseName, existing);
|
|
451
|
-
}
|
|
452
|
-
const paired = [];
|
|
453
|
-
const unmatched = [];
|
|
454
|
-
for (const testFile of testFiles) {
|
|
455
|
-
const testBaseName = path4.basename(testFile).replace(/\.[^.]+$/, "");
|
|
456
|
-
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
457
|
-
if (!sourceName) {
|
|
458
|
-
unmatched.push(testFile);
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
const candidates = sourceByBaseName.get(sourceName);
|
|
462
|
-
if (candidates && candidates.length > 0) {
|
|
463
|
-
const testDir = path4.dirname(testFile);
|
|
464
|
-
const bestMatch = candidates.reduce((best, candidate) => {
|
|
465
|
-
const candidateDir = path4.dirname(candidate);
|
|
466
|
-
const bestDir = path4.dirname(best);
|
|
467
|
-
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
468
|
-
const bestOverlap = commonSegments(testDir, bestDir);
|
|
469
|
-
return candidateOverlap > bestOverlap ? candidate : best;
|
|
470
|
-
});
|
|
471
|
-
paired.push({
|
|
472
|
-
test: testFile,
|
|
473
|
-
source: bestMatch,
|
|
474
|
-
confidence: candidates.length === 1 ? "exact" : "best-guess"
|
|
475
|
-
});
|
|
476
|
-
} else {
|
|
477
|
-
unmatched.push(testFile);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
return { totalTestFiles: testFiles.length, paired, unmatched };
|
|
481
|
-
}
|
|
482
|
-
function commonSegments(pathA, pathB) {
|
|
483
|
-
const segsA = pathA.split("/");
|
|
484
|
-
const segsB = pathB.split("/");
|
|
485
|
-
let count2 = 0;
|
|
486
|
-
for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
|
|
487
|
-
if (segsA[i] === segsB[i]) count2++;
|
|
488
|
-
else break;
|
|
489
|
-
}
|
|
490
|
-
return count2;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
422
|
// src/snapshot/snapshot.ts
|
|
494
423
|
var repoPath = z2.string().refine((value) => normalizeRepoPath(value) !== null, "Expected a relative repository path");
|
|
495
424
|
var verificationFields = {
|
|
@@ -600,11 +529,11 @@ import fg3 from "fast-glob";
|
|
|
600
529
|
// src/audit/inspection.ts
|
|
601
530
|
import fg2 from "fast-glob";
|
|
602
531
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
603
|
-
import
|
|
532
|
+
import path5 from "path";
|
|
604
533
|
var inspections = new AsyncLocalStorage2();
|
|
605
534
|
function inspectionRead(root, key, read) {
|
|
606
535
|
const inspection = inspections.getStore();
|
|
607
|
-
if (!inspection || inspection.root !==
|
|
536
|
+
if (!inspection || inspection.root !== path5.resolve(root)) return read();
|
|
608
537
|
if (!inspection.reads.has(key)) {
|
|
609
538
|
const pending = read();
|
|
610
539
|
inspection.reads.set(key, pending);
|
|
@@ -645,7 +574,12 @@ async function captureAnchorScopes(root, scopes) {
|
|
|
645
574
|
);
|
|
646
575
|
const inventory = access.inventory();
|
|
647
576
|
if (!inventory) throw new Error("Git anchor inventory unavailable");
|
|
648
|
-
const
|
|
577
|
+
const visible = indexMatchingPaths(
|
|
578
|
+
union.map((anchor) => [anchor]),
|
|
579
|
+
inventory
|
|
580
|
+
);
|
|
581
|
+
const hidden = new Set(union.filter((_, index2) => !visible[index2].length));
|
|
582
|
+
const ignored = hidden.size ? (await inspectionGit(
|
|
649
583
|
[
|
|
650
584
|
"ls-files",
|
|
651
585
|
"-z",
|
|
@@ -653,13 +587,11 @@ async function captureAnchorScopes(root, scopes) {
|
|
|
653
587
|
"--ignored",
|
|
654
588
|
"--exclude-standard",
|
|
655
589
|
"--",
|
|
656
|
-
...
|
|
590
|
+
...[...hidden].map((anchor) => `:(literal)${anchor}`)
|
|
657
591
|
],
|
|
658
592
|
{ cwd: root, maxBuffer: 8 * 1024 * 1024 }
|
|
659
|
-
);
|
|
660
|
-
const files = [
|
|
661
|
-
.../* @__PURE__ */ new Set([...inventory, ...ignored.stdout.split("\0").filter(Boolean)])
|
|
662
|
-
].sort();
|
|
593
|
+
)).stdout.split("\0").filter(Boolean) : [];
|
|
594
|
+
const files = [.../* @__PURE__ */ new Set([...inventory, ...ignored])].sort();
|
|
663
595
|
const patterns = await Promise.all(
|
|
664
596
|
union.map(async (anchor) => {
|
|
665
597
|
const literal = fg3.escapePath(anchor);
|
|
@@ -675,7 +607,7 @@ async function captureAnchorScopes(root, scopes) {
|
|
|
675
607
|
const filesByScope = new Map(
|
|
676
608
|
scopes.map((scope, index2) => [JSON.stringify([...new Set(scope)].sort()), indexed[index2]])
|
|
677
609
|
);
|
|
678
|
-
return { filesByScope, eligible };
|
|
610
|
+
return { filesByScope, eligible, hidden, ignored: new Set(ignored) };
|
|
679
611
|
};
|
|
680
612
|
return Promise.all(
|
|
681
613
|
scopes.map((scope) => {
|
|
@@ -687,8 +619,10 @@ async function captureAnchorScopes(root, scopes) {
|
|
|
687
619
|
const capture = { anchors, files: {}, complete: true };
|
|
688
620
|
if (!anchors.length) return capture;
|
|
689
621
|
try {
|
|
690
|
-
const { eligible, filesByScope } = await (context ??= prepareContext());
|
|
691
|
-
const files = filesByScope.get(JSON.stringify(anchors))
|
|
622
|
+
const { eligible, filesByScope, hidden, ignored } = await (context ??= prepareContext());
|
|
623
|
+
const files = filesByScope.get(JSON.stringify(anchors)).filter(
|
|
624
|
+
(file) => !ignored.has(file) || anchors.some((anchor) => hidden.has(anchor) && anchorMatches(anchor, file))
|
|
625
|
+
);
|
|
692
626
|
if (files.length > 2e3) return { ...capture, complete: false };
|
|
693
627
|
for (const file of files) {
|
|
694
628
|
const evidence = await inspectionRead(
|
|
@@ -847,6 +781,7 @@ var currentSchema = legacySchema.extend({
|
|
|
847
781
|
inspector: text(200),
|
|
848
782
|
note: text(1500),
|
|
849
783
|
outcome: z4.literal("no_contradiction_found"),
|
|
784
|
+
scope: z4.literal("operative").optional(),
|
|
850
785
|
decisionDigest: z4.string().regex(/^[a-f0-9]{64}$/),
|
|
851
786
|
headHash: z4.string(),
|
|
852
787
|
capture: anchorCaptureSchema
|
|
@@ -957,8 +892,19 @@ function decisionTrust(record, freshness) {
|
|
|
957
892
|
freshness
|
|
958
893
|
);
|
|
959
894
|
}
|
|
960
|
-
function decisionInspectionDigest(record) {
|
|
895
|
+
function decisionInspectionDigest(record, scope = "all") {
|
|
961
896
|
const effective = effectiveDecision(record);
|
|
897
|
+
if (scope === "operative")
|
|
898
|
+
return createHash2("sha256").update(
|
|
899
|
+
JSON.stringify([
|
|
900
|
+
"operative",
|
|
901
|
+
record.status,
|
|
902
|
+
decisionApproval(effective),
|
|
903
|
+
effective.version === 2 ? effective.revision : 0,
|
|
904
|
+
decisionContent(effective),
|
|
905
|
+
effective.refreshedHash
|
|
906
|
+
])
|
|
907
|
+
).digest("hex");
|
|
962
908
|
return createHash2("sha256").update(
|
|
963
909
|
JSON.stringify([
|
|
964
910
|
record.status,
|
|
@@ -970,8 +916,10 @@ function decisionInspectionDigest(record) {
|
|
|
970
916
|
])
|
|
971
917
|
).digest("hex");
|
|
972
918
|
}
|
|
973
|
-
function latestDecisionInspection(record) {
|
|
974
|
-
return record.version === 2 ? record.inspections?.filter(
|
|
919
|
+
function latestDecisionInspection(record, scope = "all") {
|
|
920
|
+
return record.version === 2 ? record.inspections?.filter(
|
|
921
|
+
(i) => (scope === "operative" || i.scope !== "operative") && i.decisionDigest === decisionInspectionDigest(record, i.scope ?? "all")
|
|
922
|
+
).at(-1) : void 0;
|
|
975
923
|
}
|
|
976
924
|
function revisionKnowledge(record, freshness) {
|
|
977
925
|
return {
|
|
@@ -982,7 +930,10 @@ function revisionKnowledge(record, freshness) {
|
|
|
982
930
|
}
|
|
983
931
|
function decisionKnowledge(record, freshness = "unknown", proposalFreshness = "unknown") {
|
|
984
932
|
const effective = effectiveDecision(record);
|
|
985
|
-
const inspection = latestDecisionInspection(
|
|
933
|
+
const inspection = latestDecisionInspection(
|
|
934
|
+
record,
|
|
935
|
+
decisionApproval(effective) === "accepted" ? "operative" : "all"
|
|
936
|
+
);
|
|
986
937
|
return {
|
|
987
938
|
...revisionKnowledge(effective, freshness),
|
|
988
939
|
...inspection ? {
|
|
@@ -992,21 +943,27 @@ function decisionKnowledge(record, freshness = "unknown", proposalFreshness = "u
|
|
|
992
943
|
inspector: inspection.inspector,
|
|
993
944
|
note: inspection.note,
|
|
994
945
|
outcome: inspection.outcome,
|
|
946
|
+
scope: inspection.scope ?? "all",
|
|
995
947
|
headHash: inspection.headHash
|
|
996
948
|
}
|
|
997
949
|
} : {},
|
|
998
|
-
...effective !== record ? {
|
|
950
|
+
...effective !== record ? {
|
|
951
|
+
pendingProposal: {
|
|
952
|
+
...revisionKnowledge(record, proposalFreshness),
|
|
953
|
+
assessment: latestDecisionInspection(record) ? "no_contradiction_found" : "not_assessed"
|
|
954
|
+
}
|
|
955
|
+
} : {}
|
|
999
956
|
};
|
|
1000
957
|
}
|
|
1001
958
|
|
|
1002
959
|
// src/decisions/decisions.ts
|
|
1003
960
|
import fs6 from "fs/promises";
|
|
1004
|
-
import
|
|
961
|
+
import path7 from "path";
|
|
1005
962
|
import { createHash as createHash3 } from "crypto";
|
|
1006
963
|
import { z as z5 } from "zod";
|
|
1007
964
|
|
|
1008
965
|
// src/context/lexical.ts
|
|
1009
|
-
import
|
|
966
|
+
import path6 from "path";
|
|
1010
967
|
|
|
1011
968
|
// src/decisions/decisions.ts
|
|
1012
969
|
var TITLE_MAX_CHARS = 80;
|
|
@@ -1085,9 +1042,31 @@ async function loadDecisionStore(rootDir) {
|
|
|
1085
1042
|
}
|
|
1086
1043
|
|
|
1087
1044
|
// src/decisions/drift.ts
|
|
1088
|
-
import
|
|
1045
|
+
import path8 from "path";
|
|
1046
|
+
|
|
1047
|
+
// src/decisions/excerpts.ts
|
|
1048
|
+
import { createHash as createHash4 } from "crypto";
|
|
1049
|
+
import { execFile as execFile2 } from "child_process";
|
|
1050
|
+
import { promisify as promisify2 } from "util";
|
|
1051
|
+
var exec = promisify2(execFile2);
|
|
1052
|
+
function driftBaseline(record) {
|
|
1053
|
+
const effective = effectiveDecision(record);
|
|
1054
|
+
const inspection = latestDecisionInspection(
|
|
1055
|
+
record,
|
|
1056
|
+
decisionApproval(effective) === "accepted" ? "operative" : "all"
|
|
1057
|
+
);
|
|
1058
|
+
return {
|
|
1059
|
+
record: effective,
|
|
1060
|
+
capture: inspection?.capture ?? (effective.version === 2 ? effective.capture : void 0),
|
|
1061
|
+
commits: [inspection?.headHash, effective.refreshedHash].filter(
|
|
1062
|
+
(hash) => !!hash && /^[a-f0-9]{40,64}$/.test(hash)
|
|
1063
|
+
)
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/decisions/drift.ts
|
|
1089
1068
|
async function computeDecisionDrift(rootDir, decisions) {
|
|
1090
|
-
const resolvedRoot =
|
|
1069
|
+
const resolvedRoot = path8.resolve(rootDir);
|
|
1091
1070
|
const store = decisions ? { records: decisions, diagnostics: [] } : await loadDecisionStore(resolvedRoot);
|
|
1092
1071
|
const report = {
|
|
1093
1072
|
historyAvailable: true,
|
|
@@ -1102,11 +1081,15 @@ async function computeDecisionDrift(rootDir, decisions) {
|
|
|
1102
1081
|
]);
|
|
1103
1082
|
const changesByHash = /* @__PURE__ */ new Map();
|
|
1104
1083
|
const baselines = store.records.filter((record) => record.status === "active").flatMap((record) => {
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1084
|
+
const effective = effectiveDecision(record);
|
|
1085
|
+
return [.../* @__PURE__ */ new Set([effective, record])].flatMap((candidate) => {
|
|
1086
|
+
const inspection = latestDecisionInspection(
|
|
1087
|
+
record,
|
|
1088
|
+
candidate === effective && decisionApproval(effective) === "accepted" ? "operative" : "all"
|
|
1089
|
+
);
|
|
1090
|
+
const capture = inspection?.capture ?? (candidate.version === 2 ? candidate.capture : void 0);
|
|
1091
|
+
return capture ? [capture] : [];
|
|
1092
|
+
});
|
|
1110
1093
|
});
|
|
1111
1094
|
const captures = await captureAnchorScopes(
|
|
1112
1095
|
resolvedRoot,
|
|
@@ -1144,21 +1127,23 @@ async function computeDecisionDrift(rootDir, decisions) {
|
|
|
1144
1127
|
};
|
|
1145
1128
|
for (const record of store.records) {
|
|
1146
1129
|
if (record.status !== "active") continue;
|
|
1147
|
-
const
|
|
1148
|
-
const
|
|
1149
|
-
const state = await inspect(effective, inspection?.capture);
|
|
1130
|
+
const baseline = driftBaseline(record);
|
|
1131
|
+
const state = await inspect(baseline.record, baseline.capture);
|
|
1150
1132
|
report.freshness[record.id] = state.freshness;
|
|
1151
1133
|
if (state.changedFiles.length) report.staleDecisions[record.id] = state.changedFiles;
|
|
1152
|
-
if (
|
|
1153
|
-
(report.pendingProposals ??= {})[record.id] = await inspect(
|
|
1134
|
+
if (baseline.record !== record)
|
|
1135
|
+
(report.pendingProposals ??= {})[record.id] = await inspect(
|
|
1136
|
+
record,
|
|
1137
|
+
latestDecisionInspection(record)?.capture
|
|
1138
|
+
);
|
|
1154
1139
|
}
|
|
1155
1140
|
return report;
|
|
1156
1141
|
}
|
|
1157
1142
|
|
|
1158
1143
|
// src/review/cochange.ts
|
|
1159
|
-
import { execFile as
|
|
1160
|
-
import { promisify as
|
|
1161
|
-
var
|
|
1144
|
+
import { execFile as execFile3 } from "child_process";
|
|
1145
|
+
import { promisify as promisify3 } from "util";
|
|
1146
|
+
var exec2 = promisify3(execFile3);
|
|
1162
1147
|
var HISTORY_COMMITS = 1500;
|
|
1163
1148
|
var MIN_FILE_COMMITS = 5;
|
|
1164
1149
|
var MIN_SHARED_COMMITS = 4;
|
|
@@ -1166,11 +1151,11 @@ var MIN_COCHANGE_RATE = 0.6;
|
|
|
1166
1151
|
var MAX_COMMIT_FILES = 30;
|
|
1167
1152
|
async function buildMatrix(resolvedRoot) {
|
|
1168
1153
|
try {
|
|
1169
|
-
const { stdout: shallow } = await
|
|
1154
|
+
const { stdout: shallow } = await exec2("git", ["rev-parse", "--is-shallow-repository"], {
|
|
1170
1155
|
cwd: resolvedRoot
|
|
1171
1156
|
});
|
|
1172
1157
|
if (shallow.trim() === "true") return null;
|
|
1173
|
-
const { stdout } = await
|
|
1158
|
+
const { stdout } = await exec2(
|
|
1174
1159
|
"git",
|
|
1175
1160
|
["log", `-n${HISTORY_COMMITS}`, "--format=%x01", "--name-only", "-M"],
|
|
1176
1161
|
{ cwd: resolvedRoot, maxBuffer: 100 * 1024 * 1024 }
|
|
@@ -1228,9 +1213,79 @@ async function findMissingPartners(resolvedRoot, changedFiles, existsOnDisk, all
|
|
|
1228
1213
|
}
|
|
1229
1214
|
|
|
1230
1215
|
// src/review/evidence.ts
|
|
1231
|
-
import
|
|
1216
|
+
import path10 from "path";
|
|
1232
1217
|
import { z as z9 } from "zod";
|
|
1233
1218
|
|
|
1219
|
+
// src/test-map.ts
|
|
1220
|
+
import path9 from "path";
|
|
1221
|
+
async function buildTestMap(dir) {
|
|
1222
|
+
const rootDir = path9.resolve(dir);
|
|
1223
|
+
const access = await createFileAccess(rootDir);
|
|
1224
|
+
const testPatterns = [
|
|
1225
|
+
"**/*.test.*",
|
|
1226
|
+
"**/*.spec.*",
|
|
1227
|
+
"**/*Test.kt",
|
|
1228
|
+
"**/*Test.java",
|
|
1229
|
+
"**/*Tests.kt",
|
|
1230
|
+
"**/*Tests.java",
|
|
1231
|
+
"**/test_*.py",
|
|
1232
|
+
"**/*_test.py",
|
|
1233
|
+
"**/*_test.go",
|
|
1234
|
+
"**/*Tests.swift",
|
|
1235
|
+
"**/*Test.swift",
|
|
1236
|
+
"**/*_test.rs"
|
|
1237
|
+
];
|
|
1238
|
+
const testFiles = await access.list(testPatterns);
|
|
1239
|
+
const sourceFiles = await access.list();
|
|
1240
|
+
const sourceByBaseName = /* @__PURE__ */ new Map();
|
|
1241
|
+
for (const file of sourceFiles) {
|
|
1242
|
+
if (testFiles.includes(file)) continue;
|
|
1243
|
+
const baseName = path9.basename(file).replace(/\.[^.]+$/, "");
|
|
1244
|
+
const existing = sourceByBaseName.get(baseName) ?? [];
|
|
1245
|
+
existing.push(file);
|
|
1246
|
+
sourceByBaseName.set(baseName, existing);
|
|
1247
|
+
}
|
|
1248
|
+
const paired = [];
|
|
1249
|
+
const unmatched = [];
|
|
1250
|
+
for (const testFile of testFiles) {
|
|
1251
|
+
const testBaseName = path9.basename(testFile).replace(/\.[^.]+$/, "");
|
|
1252
|
+
const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
|
|
1253
|
+
if (!sourceName) {
|
|
1254
|
+
unmatched.push(testFile);
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
const candidates = sourceByBaseName.get(sourceName);
|
|
1258
|
+
if (candidates && candidates.length > 0) {
|
|
1259
|
+
const testDir = path9.dirname(testFile);
|
|
1260
|
+
const bestMatch = candidates.reduce((best, candidate) => {
|
|
1261
|
+
const candidateDir = path9.dirname(candidate);
|
|
1262
|
+
const bestDir = path9.dirname(best);
|
|
1263
|
+
const candidateOverlap = commonSegments(testDir, candidateDir);
|
|
1264
|
+
const bestOverlap = commonSegments(testDir, bestDir);
|
|
1265
|
+
return candidateOverlap > bestOverlap ? candidate : best;
|
|
1266
|
+
});
|
|
1267
|
+
paired.push({
|
|
1268
|
+
test: testFile,
|
|
1269
|
+
source: bestMatch,
|
|
1270
|
+
confidence: candidates.length === 1 ? "exact" : "best-guess"
|
|
1271
|
+
});
|
|
1272
|
+
} else {
|
|
1273
|
+
unmatched.push(testFile);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return { totalTestFiles: testFiles.length, paired, unmatched };
|
|
1277
|
+
}
|
|
1278
|
+
function commonSegments(pathA, pathB) {
|
|
1279
|
+
const segsA = pathA.split("/");
|
|
1280
|
+
const segsB = pathB.split("/");
|
|
1281
|
+
let count2 = 0;
|
|
1282
|
+
for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
|
|
1283
|
+
if (segsA[i] === segsB[i]) count2++;
|
|
1284
|
+
else break;
|
|
1285
|
+
}
|
|
1286
|
+
return count2;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1234
1289
|
// src/review/evidence/vitest.ts
|
|
1235
1290
|
import { z as z6 } from "zod";
|
|
1236
1291
|
|
|
@@ -1622,7 +1677,7 @@ var checkSchema = z9.object({
|
|
|
1622
1677
|
}).optional()
|
|
1623
1678
|
});
|
|
1624
1679
|
function relativeArtifact(root, file) {
|
|
1625
|
-
const relative =
|
|
1680
|
+
const relative = path10.isAbsolute(file) ? path10.relative(root, file) : file;
|
|
1626
1681
|
const normalized = normalizeRepoPath(relative);
|
|
1627
1682
|
if (!normalized) throw new Error(`Evidence artifact must be inside the repository: ${file}`);
|
|
1628
1683
|
return normalized;
|
|
@@ -1751,7 +1806,7 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
1751
1806
|
const report = await readStoreJson(root, file);
|
|
1752
1807
|
if (report === null) throw new Error(`Report artifact is missing: ${file}`);
|
|
1753
1808
|
const sourceRoot = check.sourceRoot ?? root;
|
|
1754
|
-
if (!
|
|
1809
|
+
if (!path10.posix.isAbsolute(sourceRoot) && !/^[A-Za-z]:[\\/]/.test(sourceRoot))
|
|
1755
1810
|
throw new Error(
|
|
1756
1811
|
"sourceRoot must identify the absolute checkout root on the check runner."
|
|
1757
1812
|
);
|
|
@@ -1828,11 +1883,11 @@ async function collectReviewEvidence(root, manifests, changedFiles, decisions, d
|
|
|
1828
1883
|
}
|
|
1829
1884
|
|
|
1830
1885
|
// src/review/review.ts
|
|
1831
|
-
var
|
|
1886
|
+
var exec3 = promisify4(execFile4);
|
|
1832
1887
|
var MAX_ANALYZED_FILES = 50;
|
|
1833
1888
|
async function resolveMergeBase(resolvedRoot, base, head) {
|
|
1834
1889
|
try {
|
|
1835
|
-
const { stdout } = await
|
|
1890
|
+
const { stdout } = await exec3("git", ["merge-base", base, head], {
|
|
1836
1891
|
cwd: resolvedRoot
|
|
1837
1892
|
});
|
|
1838
1893
|
return stdout.trim() || null;
|
|
@@ -1843,7 +1898,7 @@ async function resolveMergeBase(resolvedRoot, base, head) {
|
|
|
1843
1898
|
async function defaultBase(resolvedRoot) {
|
|
1844
1899
|
for (const ref of ["origin/HEAD", "origin/main", "origin/master", "main"]) {
|
|
1845
1900
|
try {
|
|
1846
|
-
await
|
|
1901
|
+
await exec3("git", ["rev-parse", "--verify", "--quiet", ref], {
|
|
1847
1902
|
cwd: resolvedRoot
|
|
1848
1903
|
});
|
|
1849
1904
|
return ref;
|
|
@@ -1856,7 +1911,7 @@ function anchorsTouched(record, changedFiles) {
|
|
|
1856
1911
|
return matchingPaths(record.files, changedFiles);
|
|
1857
1912
|
}
|
|
1858
1913
|
async function computeReview(rootDir, base, options = {}) {
|
|
1859
|
-
const resolvedRoot =
|
|
1914
|
+
const resolvedRoot = path11.resolve(rootDir);
|
|
1860
1915
|
const head = await getCurrentGitHash(resolvedRoot);
|
|
1861
1916
|
if (head === "unknown") return null;
|
|
1862
1917
|
const mergeBase = await resolveMergeBase(resolvedRoot, base, head);
|
|
@@ -1917,7 +1972,7 @@ async function computeReview(rootDir, base, options = {}) {
|
|
|
1917
1972
|
analyzed,
|
|
1918
1973
|
async (relPath) => {
|
|
1919
1974
|
try {
|
|
1920
|
-
await fs7.access(
|
|
1975
|
+
await fs7.access(path11.join(resolvedRoot, relPath));
|
|
1921
1976
|
return true;
|
|
1922
1977
|
} catch {
|
|
1923
1978
|
return false;
|
|
@@ -2152,7 +2207,7 @@ async function runReviewCli(argv, io = {
|
|
|
2152
2207
|
io.out(USAGE);
|
|
2153
2208
|
return 0;
|
|
2154
2209
|
}
|
|
2155
|
-
const rootDir =
|
|
2210
|
+
const rootDir = path12.resolve(args.dir);
|
|
2156
2211
|
const base = args.base ?? await defaultBase(rootDir);
|
|
2157
2212
|
if (!base) {
|
|
2158
2213
|
io.err(
|