@bendyline/gezel 1.0.6 → 1.0.7

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.
@@ -1,3 +1,33 @@
1
+ // src/checks/workspace-exists.ts
2
+ function createCitedPathChecker(ws) {
3
+ let listing = null;
4
+ const probes = /* @__PURE__ */ new Map();
5
+ const loadListing = () => {
6
+ listing ??= ws.list().then((files) => new Set(files.map((f) => citedPathKey(f))));
7
+ return listing;
8
+ };
9
+ return async (cited) => {
10
+ const probe = cleanCitedPath(cited);
11
+ if (!probe) return false;
12
+ if ((await loadListing()).has(probe.toLowerCase())) return true;
13
+ let hit = probes.get(probe);
14
+ if (!hit) {
15
+ hit = ws.read(probe).then(
16
+ (content) => content !== null,
17
+ () => false
18
+ );
19
+ probes.set(probe, hit);
20
+ }
21
+ return hit;
22
+ };
23
+ }
24
+ function cleanCitedPath(p) {
25
+ return p.trim().replace(/^`+|`+$/g, "").replace(/[:#].*$/, "").replace(/^\.\//, "").replace(/^\/+/, "").replace(/^workspace\//i, "");
26
+ }
27
+ function citedPathKey(p) {
28
+ return cleanCitedPath(p).toLowerCase();
29
+ }
30
+
1
31
  // src/checks/html.ts
2
32
  var MIN_INLINE_JS_BYTES = 2048;
3
33
  var SCRIPT_RE = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi;
@@ -536,12 +566,24 @@ function valueGrounding(text, facts, opts = {}) {
536
566
  decoysDetected
537
567
  };
538
568
  }
539
- var DEFAULT_CITATION_RE = /\(source:\s*([^)\s]+)(?:\s+\[[^\]]*\])*\s*\)|\]\(\s*(?!#)([^)\s]+?)\s*\)|`([^`]*\/[^`]+)`/gi;
569
+ var DEFAULT_CITATION_RE = (
570
+ // The inline-path form excludes newlines AND spaces on purpose: a path
571
+ // contains neither, and without the exclusions two failure families
572
+ // appear (both wild-caught). An UNBALANCED backtick lets the span swallow
573
+ // sentences until the next stray backtick; and even with balanced spans,
574
+ // the CLOSER of one legitimate span pairs with the OPENER of the next, so
575
+ // the prose BETWEEN two `code` spans — which mentions a path — became one
576
+ // giant unresolvable "citation" and honest work read as fabricated.
577
+ /\(source:\s*([^)\s]+)(?:\s+\[[^\]]*\])*\s*\)|\]\(\s*(?!#)([^)\s]+?)\s*\)|`([^`\s]*\/[^`\s]+)`/gi
578
+ );
579
+ function stripFencedBlocks(text) {
580
+ return text.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1[ \t]*$/gm, "").replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*$/m, "");
581
+ }
540
582
  function extractCitations(text, re) {
541
583
  const flags = re.flags.includes("g") ? re.flags : `${re.flags}g`;
542
584
  const global = new RegExp(re.source, flags);
543
585
  const out = [];
544
- for (const m of text.matchAll(global)) {
586
+ for (const m of stripFencedBlocks(text).matchAll(global)) {
545
587
  const cap = m.slice(1).find((x) => x !== void 0) ?? m[0];
546
588
  if (cap) out.push(cap);
547
589
  }
@@ -550,8 +592,8 @@ function extractCitations(text, re) {
550
592
  function cleanCitation(raw) {
551
593
  return raw.trim().replace(/^[<'"`(]+/, "").replace(/[>'"`).,;:]+$/, "");
552
594
  }
553
- function normalizePath(p) {
554
- return p.trim().toLowerCase().replace(/^\.?\//, "").replace(/^workspace\//, "");
595
+ function normalizeForKnownMatch(p) {
596
+ return p.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\//, "").replace(/^workspace\//i, "").replace(/\/+$/, "").toLowerCase();
555
597
  }
556
598
  async function citationsResolve(ws, file, opts = {}) {
557
599
  const content = await ws.read(file);
@@ -574,38 +616,54 @@ async function citationsResolve(ws, file, opts = {}) {
574
616
  urls: []
575
617
  };
576
618
  }
577
- const cites = [...new Set(extractCitations(content, re).map(cleanCitation).filter(Boolean))];
619
+ const cites = [
620
+ ...new Set(
621
+ extractCitations(content, re).map(cleanCitation).filter(
622
+ (citation) => citation && (/^[a-z][\w+.-]*:\/\//i.test(citation) || !/[\\/]$/.test(citation))
623
+ )
624
+ )
625
+ ];
578
626
  const min = opts.minCitations ?? 1;
579
- const listing = new Set((await ws.list()).map(normalizePath));
627
+ const citedPathExists = createCitedPathChecker(ws);
580
628
  const corpus = opts.corpus ? new Set(opts.corpus.map((c) => c.toLowerCase())) : null;
629
+ const known = new Set((opts.knownPaths ?? []).map(normalizeForKnownMatch).filter(Boolean));
581
630
  const resolved = [];
582
631
  const unresolved = [];
583
632
  const urls = [];
633
+ const forgiven = [];
584
634
  for (const c of cites) {
585
635
  if (/^[a-z][\w+.-]*:\/\//i.test(c) || c.startsWith("mailto:")) {
586
636
  urls.push(c);
587
637
  if (corpus && !corpus.has(c.toLowerCase())) unresolved.push(c);
588
638
  continue;
589
639
  }
590
- if (listing.has(normalizePath(c)) || corpus?.has(c.toLowerCase())) resolved.push(c);
640
+ if (corpus?.has(c.toLowerCase()) || await citedPathExists(c)) resolved.push(c);
641
+ else if (known.has(normalizeForKnownMatch(c))) forgiven.push(c);
591
642
  else unresolved.push(c);
592
643
  }
593
- if (cites.length < min) {
644
+ if (cites.length - forgiven.length < min) {
594
645
  return {
595
646
  ok: false,
596
- detail: `${file} has ${cites.length} citation(s), need \u2265 ${min} \u2014 cite the source path/URL for each claim.`,
647
+ // Name the accepted FORMS, not just the rule: a model that wrote the
648
+ // right paths as plain prose ("File: src/pricing.js, line 8") reads
649
+ // "cite the source" as already satisfied and rewrites content instead
650
+ // of adding markup, looping to gate exhaustion (wild-caught:
651
+ // deepseek-v4 with a flawless diagnosis, three identical rejections).
652
+ detail: `${file} has ${cites.length - forgiven.length} recognizable citation(s), need \u2265 ${min}. Only these forms count as citations: a backticked path like \`src/file.js\`, a markdown link like [name](src/file.js), or (source: src/file.js). Plain prose paths are not counted \u2014 wrap each cited file path in backticks.`,
597
653
  resolved,
598
654
  unresolved,
599
- urls
655
+ urls,
656
+ ...forgiven.length > 0 ? { forgiven } : {}
600
657
  };
601
658
  }
602
659
  if (unresolved.length > 0) {
603
660
  return {
604
661
  ok: false,
605
- detail: `${file} cites ${unresolved.length} source(s) that do not exist: ${unresolved.slice(0, 5).join(", ")} \u2014 every cited path must resolve to a real file in the workspace${corpus ? "/corpus" : ""} (no fabricated citations).`,
662
+ detail: `${file} cites ${unresolved.length} source(s) that do not exist: ${unresolved.slice(0, 5).join(", ")}${unresolved.length > 5 ? ", \u2026" : ""} \u2014 every cited path must resolve to a real file in the workspace${corpus ? "/corpus" : ""} (no fabricated citations).`,
606
663
  resolved,
607
664
  unresolved,
608
- urls
665
+ urls,
666
+ ...forgiven.length > 0 ? { forgiven } : {}
609
667
  };
610
668
  }
611
669
  return {
@@ -613,7 +671,8 @@ async function citationsResolve(ws, file, opts = {}) {
613
671
  detail: `${file} cites ${resolved.length} resolvable source(s)${urls.length ? ` (+${urls.length} URL(s) not checked offline)` : ""}`,
614
672
  resolved,
615
673
  unresolved,
616
- urls
674
+ urls,
675
+ ...forgiven.length > 0 ? { forgiven } : {}
617
676
  };
618
677
  }
619
678
  function valuesSubsetOf(outputText, sourceTexts, spec) {
@@ -682,9 +741,6 @@ var VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "l
682
741
  function str(x) {
683
742
  return typeof x === "string" ? x.trim() : "";
684
743
  }
685
- function normalizePath2(p) {
686
- return p.trim().replace(/^`+|`+$/g, "").replace(/[:#].*$/, "").replace(/^\.\//, "").replace(/^\/+/, "").replace(/^workspace\//i, "").toLowerCase();
687
- }
688
744
  function escapeRe(s) {
689
745
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
690
746
  }
@@ -740,12 +796,12 @@ async function securityReport(ws, reportFile, opts = {}) {
740
796
  if (!arr) {
741
797
  return fail(`${findingsPath} must be a JSON array of findings (or { "findings": [...] }).`);
742
798
  }
743
- const listing = new Set((await ws.list()).map(normalizePath2));
799
+ const citedPathExists = createCitedPathChecker(ws);
744
800
  const problems = [];
745
801
  const fabricated = [];
746
802
  let critHigh = 0;
747
- arr.forEach((raw2, i) => {
748
- const f = raw2 ?? {};
803
+ for (const [i, rawFinding] of arr.entries()) {
804
+ const f = rawFinding ?? {};
749
805
  const file = str(f.file ?? f.path);
750
806
  const sev = str(f.severity).toLowerCase();
751
807
  const remediation = str(f.remediation ?? f.fix ?? f.recommendation);
@@ -753,13 +809,13 @@ async function securityReport(ws, reportFile, opts = {}) {
753
809
  const line = f.line ?? f.lineStart;
754
810
  const label = file || `#${i + 1}`;
755
811
  if (!file) problems.push(`finding #${i + 1} has no file`);
756
- else if (!listing.has(normalizePath2(file))) fabricated.push(file);
812
+ else if (!await citedPathExists(file)) fabricated.push(file);
757
813
  if (!VALID_SEVERITIES.has(sev)) problems.push(`${label} has an invalid severity "${sev}"`);
758
814
  if (!remediation) problems.push(`${label} has no remediation`);
759
815
  if (!title) problems.push(`${label} has no title/description`);
760
816
  if (typeof line !== "number") problems.push(`${label} is not pinned to a line`);
761
817
  if (sev === "critical" || sev === "high") critHigh++;
762
- });
818
+ }
763
819
  if (fabricated.length > 0) {
764
820
  return fail(
765
821
  `findings cite ${fabricated.length} file(s) that don't exist in the workspace: ${uniq(fabricated).slice(0, 5).join(", ")} \u2014 every finding must point at a real file:line (no fabricated citations).`,
@@ -818,6 +874,191 @@ async function securityReport(ws, reportFile, opts = {}) {
818
874
  };
819
875
  }
820
876
 
877
+ // src/checks/codebase-review-report.ts
878
+ var DEFAULT_SECTIONS2 = [
879
+ "Executive summary",
880
+ "Scorecard",
881
+ "Index coverage and method",
882
+ "Systemic themes",
883
+ "Findings",
884
+ "Quick wins",
885
+ "Strategic recommendations",
886
+ "Verified sound",
887
+ "Not assessed",
888
+ "Suggested deeper reviews"
889
+ ];
890
+ var VALID_SEVERITIES2 = /* @__PURE__ */ new Set(["critical", "high", "medium", "low", "info"]);
891
+ function str2(x) {
892
+ return typeof x === "string" ? x.trim() : "";
893
+ }
894
+ function escapeRe2(s) {
895
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
896
+ }
897
+ function hasSection2(content, name) {
898
+ return new RegExp(`^#{1,4}\\s+${escapeRe2(name)}\\b`, "im").test(content);
899
+ }
900
+ function sectionBody2(content, name) {
901
+ const m = new RegExp(`^(#{1,4})\\s+${escapeRe2(name)}\\b`, "im").exec(content);
902
+ if (!m) return "";
903
+ const level = m[1].length;
904
+ const start = m.index + m[0].length;
905
+ const rest = content.slice(start);
906
+ const next = new RegExp(`^#{1,${level}}\\s+\\S`, "m").exec(rest);
907
+ return next ? rest.slice(0, next.index) : rest;
908
+ }
909
+ function countThemeItems2(section) {
910
+ let n = 0;
911
+ for (const line of section.split(/\r?\n/)) {
912
+ if (/^\s*(#{3,4}\s+\S|[-*]\s+\S|\d+\.\s+\S|\*\*[^*]+\*\*)/.test(line)) n++;
913
+ }
914
+ return n;
915
+ }
916
+ function scorecardRows(section) {
917
+ const lines = section.split(/\r?\n/);
918
+ let headerAt = -1;
919
+ for (let i = 0; i < lines.length; i++) {
920
+ const line = lines[i];
921
+ if (/^\s*\|/.test(line) && /dimension/i.test(line) && /(grade|rating|score|health)/i.test(line)) {
922
+ headerAt = i;
923
+ break;
924
+ }
925
+ }
926
+ if (headerAt < 0) return -1;
927
+ let rows = 0;
928
+ for (let i = headerAt + 1; i < lines.length; i++) {
929
+ const line = lines[i];
930
+ if (!/^\s*\|/.test(line)) break;
931
+ if (/^\s*\|[\s:|-]+\|?\s*$/.test(line)) continue;
932
+ if (line.replace(/[|\s]/g, "").length === 0) continue;
933
+ rows++;
934
+ }
935
+ return rows;
936
+ }
937
+ function uniq2(xs) {
938
+ return [...new Set(xs)];
939
+ }
940
+ async function codebaseReviewReport(reports, workspace, reportFile, opts = {}) {
941
+ const fail = (detail, count = 0, fabricated2 = []) => ({
942
+ ok: false,
943
+ detail,
944
+ findingCount: count,
945
+ fabricated: fabricated2
946
+ });
947
+ const content = await reports.read(reportFile);
948
+ if (content === null) {
949
+ return fail(`${reportFile} not found \u2014 write the codebase review report before advancing.`);
950
+ }
951
+ const findingsPath = opts.findings ?? "codebase-review-findings.json";
952
+ const raw = await reports.read(findingsPath);
953
+ if (raw === null) {
954
+ return fail(
955
+ `${findingsPath} not found \u2014 emit a machine-readable findings JSON alongside the report.`
956
+ );
957
+ }
958
+ let parsed;
959
+ try {
960
+ parsed = JSON.parse(raw);
961
+ } catch (e) {
962
+ return fail(
963
+ `${findingsPath} is not valid JSON (${e instanceof Error ? e.message : "parse error"}). Emit a JSON array of findings.`
964
+ );
965
+ }
966
+ const arr = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.findings) ? parsed.findings : null;
967
+ if (!arr) {
968
+ return fail(`${findingsPath} must be a JSON array of findings (or { "findings": [...] }).`);
969
+ }
970
+ const citedPathExists = createCitedPathChecker(workspace);
971
+ const problems = [];
972
+ const fabricated = [];
973
+ let critHigh = 0;
974
+ for (const [i, rawFinding] of arr.entries()) {
975
+ const f = rawFinding ?? {};
976
+ const file = str2(f.file ?? f.path);
977
+ const sev = str2(f.severity).toLowerCase();
978
+ const remediation = str2(f.remediation ?? f.fix ?? f.recommendation);
979
+ const title = str2(f.title ?? f.description ?? f.summary);
980
+ const line = f.line ?? f.lineStart;
981
+ const label = file || `#${i + 1}`;
982
+ if (!file) problems.push(`finding #${i + 1} has no file`);
983
+ else if (!await citedPathExists(file)) fabricated.push(file);
984
+ if (!VALID_SEVERITIES2.has(sev)) problems.push(`${label} has an invalid severity "${sev}"`);
985
+ if (!remediation) problems.push(`${label} has no remediation`);
986
+ if (!title) problems.push(`${label} has no title/description`);
987
+ if ((sev === "critical" || sev === "high") && typeof line !== "number") {
988
+ problems.push(`${label} is ${sev} but not pinned to a line`);
989
+ }
990
+ if (sev === "critical" || sev === "high") critHigh++;
991
+ }
992
+ if (fabricated.length > 0) {
993
+ return fail(
994
+ `findings cite ${fabricated.length} file(s) that don't exist in the workspace: ${uniq2(fabricated).slice(0, 5).join(", ")} \u2014 every finding must point at a real file (no fabricated citations).`,
995
+ arr.length,
996
+ uniq2(fabricated)
997
+ );
998
+ }
999
+ if (problems.length > 0) {
1000
+ return fail(
1001
+ `${problems.length} finding(s) are incomplete: ${problems.slice(0, 4).join("; ")} \u2014 every finding needs a real file, a severity, a title, and a concrete remediation (critical/high pinned to a line).`,
1002
+ arr.length
1003
+ );
1004
+ }
1005
+ const requiredSections = opts.requiredSections ?? DEFAULT_SECTIONS2;
1006
+ const missing = requiredSections.filter((s) => !hasSection2(content, s));
1007
+ if (missing.length > 0) {
1008
+ return fail(
1009
+ `the report is missing required section(s): ${missing.map((s) => `## ${s}`).join(", ")}.`,
1010
+ arr.length
1011
+ );
1012
+ }
1013
+ const minRows = opts.minScorecardRows ?? 4;
1014
+ const rows = scorecardRows(sectionBody2(content, "Scorecard"));
1015
+ if (rows < 0) {
1016
+ return fail(
1017
+ 'the "Scorecard" section has no per-dimension table \u2014 add a markdown table with Dimension and Grade columns.',
1018
+ arr.length
1019
+ );
1020
+ }
1021
+ if (rows < minRows) {
1022
+ return fail(
1023
+ `the Scorecard table has ${rows} dimension row(s); grade at least ${minRows} dimensions (e.g. architecture, code quality, hygiene, security, tests).`,
1024
+ arr.length
1025
+ );
1026
+ }
1027
+ const themeThreshold = opts.themeThreshold ?? 3;
1028
+ const minThemes = opts.minThemes ?? 2;
1029
+ if (arr.length >= themeThreshold) {
1030
+ const themes = sectionBody2(content, "Systemic themes");
1031
+ const items = countThemeItems2(themes);
1032
+ if (items < minThemes) {
1033
+ return fail(
1034
+ `the "Systemic themes" section lists ${items} theme(s); a review with ${arr.length} findings needs \u2265 ${minThemes}, each naming a root cause and its blast radius.`,
1035
+ arr.length
1036
+ );
1037
+ }
1038
+ if (!/root[\s-]?cause/i.test(themes) || !/(blast[\s-]?radius|impact|scope|reach)/i.test(themes)) {
1039
+ return fail(
1040
+ `the "Systemic themes" section must analyze each theme's root cause and blast radius \u2014 that analysis is absent.`,
1041
+ arr.length
1042
+ );
1043
+ }
1044
+ }
1045
+ const summary = sectionBody2(content, "Executive summary").toLowerCase();
1046
+ if (critHigh > 0 && /(excellent (health|shape|condition)|no (significant |material )?(issues|problems|findings)|nothing to fix|clean bill of health)/.test(
1047
+ summary
1048
+ ) && !/(critical|high|however|but |concern|risk)/.test(summary)) {
1049
+ return fail(
1050
+ `the executive summary reads clean but there ${critHigh === 1 ? "is" : "are"} ${critHigh} critical/high finding(s) \u2014 the summary must reflect the open severity.`,
1051
+ arr.length
1052
+ );
1053
+ }
1054
+ return {
1055
+ ok: true,
1056
+ detail: `codebase review OK: ${arr.length} finding(s), all cite real files, ${rows} scorecard dimension(s), required sections present${arr.length >= themeThreshold ? ", systemic themes analyzed" : ""}.`,
1057
+ findingCount: arr.length,
1058
+ fabricated: []
1059
+ };
1060
+ }
1061
+
821
1062
  // src/checks/records.ts
822
1063
  var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
823
1064
  function isRealIsoDate(value) {
@@ -1803,14 +2044,184 @@ function findCycle(rows) {
1803
2044
  }
1804
2045
  return cycle;
1805
2046
  }
2047
+
2048
+ // src/checks/corpus-coverage.ts
2049
+ function failure(detail) {
2050
+ return {
2051
+ ok: false,
2052
+ detail,
2053
+ expectedBatches: 0,
2054
+ mergedBatches: 0,
2055
+ missingBatches: []
2056
+ };
2057
+ }
2058
+ function parseStringArray(value) {
2059
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item === "")) {
2060
+ return null;
2061
+ }
2062
+ return value;
2063
+ }
2064
+ function firstMismatch(actual, expected) {
2065
+ if (actual.length !== expected.length) return Math.min(actual.length, expected.length);
2066
+ for (let index = 0; index < expected.length; index += 1) {
2067
+ if (actual[index] !== expected[index]) return index;
2068
+ }
2069
+ return null;
2070
+ }
2071
+ function mergeCorpusCoverageShards(batchesContent, shards, opts = {}) {
2072
+ const batchesFile = opts.batchesFile ?? "batches.json";
2073
+ let batchesRaw;
2074
+ try {
2075
+ batchesRaw = JSON.parse(batchesContent);
2076
+ } catch (err) {
2077
+ return failure(
2078
+ `${batchesFile} is not valid JSON (${err instanceof Error ? err.message : String(err)}).`
2079
+ );
2080
+ }
2081
+ if (!Array.isArray(batchesRaw) || batchesRaw.length === 0) {
2082
+ return failure(`${batchesFile} must contain a non-empty batch array.`);
2083
+ }
2084
+ const batches = [];
2085
+ const allPaths = /* @__PURE__ */ new Set();
2086
+ const allRecords = /* @__PURE__ */ new Set();
2087
+ for (let index = 0; index < batchesRaw.length; index += 1) {
2088
+ const raw = batchesRaw[index];
2089
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
2090
+ return failure(`${batchesFile}[${index}] must be an object.`);
2091
+ }
2092
+ const fields = raw;
2093
+ const batchNumber = fields.batchNumber;
2094
+ const paths = parseStringArray(fields.paths);
2095
+ const records = parseStringArray(fields.records);
2096
+ if (!Number.isInteger(batchNumber) || batchNumber !== index + 1) {
2097
+ return failure(
2098
+ `${batchesFile}[${index}] must have batchNumber ${index + 1}; found ${JSON.stringify(batchNumber)}.`
2099
+ );
2100
+ }
2101
+ if (!paths || paths.length === 0) {
2102
+ return failure(`${batchesFile}[${index}].paths must be a non-empty string array.`);
2103
+ }
2104
+ if (!records || records.length !== paths.length) {
2105
+ return failure(
2106
+ `${batchesFile}[${index}].records must contain one exact record path per changed path.`
2107
+ );
2108
+ }
2109
+ for (const path of paths) {
2110
+ if (allPaths.has(path))
2111
+ return failure(`${batchesFile} assigns '${path}' to multiple batches.`);
2112
+ allPaths.add(path);
2113
+ }
2114
+ for (const record of records) {
2115
+ if (allRecords.has(record)) {
2116
+ return failure(`${batchesFile} assigns record '${record}' to multiple batches.`);
2117
+ }
2118
+ allRecords.add(record);
2119
+ }
2120
+ batches.push({ batchNumber, paths, records });
2121
+ }
2122
+ const shardByBatch = /* @__PURE__ */ new Map();
2123
+ for (const shard of shards) {
2124
+ const normalized = shard.path.replace(/\\/g, "/");
2125
+ const match = /(?:^|\/)coverage-(\d+)\.json$/.exec(normalized);
2126
+ if (!match) continue;
2127
+ const batchNumber = Number(match[1]);
2128
+ if (batchNumber < 1 || batchNumber > batches.length) {
2129
+ return failure(
2130
+ `${shard.path} claims batch ${batchNumber}, but ${batchesFile} has batches 1-${batches.length}.`
2131
+ );
2132
+ }
2133
+ if (shardByBatch.has(batchNumber)) {
2134
+ return failure(`More than one coverage shard claims batch ${batchNumber}.`);
2135
+ }
2136
+ shardByBatch.set(batchNumber, shard);
2137
+ }
2138
+ const reviewedFiles = [];
2139
+ const reviewedRecords = [];
2140
+ const sources = [];
2141
+ for (const batch of batches) {
2142
+ const shard = shardByBatch.get(batch.batchNumber);
2143
+ if (!shard) continue;
2144
+ let parsed;
2145
+ try {
2146
+ parsed = JSON.parse(shard.content);
2147
+ } catch (err) {
2148
+ return failure(
2149
+ `${shard.path} is not valid JSON (${err instanceof Error ? err.message : String(err)}).`
2150
+ );
2151
+ }
2152
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2153
+ return failure(`${shard.path} must contain a JSON object.`);
2154
+ }
2155
+ const fields = parsed;
2156
+ if (fields.batchNumber !== batch.batchNumber) {
2157
+ return failure(
2158
+ `${shard.path} must declare batchNumber ${batch.batchNumber}; found ${JSON.stringify(fields.batchNumber)}.`
2159
+ );
2160
+ }
2161
+ const files = parseStringArray(fields.reviewedFiles);
2162
+ const records = parseStringArray(fields.reviewedRecords);
2163
+ if (!files || !records) {
2164
+ return failure(`${shard.path} must contain reviewedFiles and reviewedRecords string arrays.`);
2165
+ }
2166
+ const fileMismatch = firstMismatch(files, batch.paths);
2167
+ if (fileMismatch !== null) {
2168
+ return failure(
2169
+ `${shard.path} does not exactly match batch ${batch.batchNumber}'s changed paths at position ${fileMismatch + 1}; coverage may only come from that batch's shard.`
2170
+ );
2171
+ }
2172
+ const recordMismatch = firstMismatch(records, batch.records);
2173
+ if (recordMismatch !== null) {
2174
+ return failure(
2175
+ `${shard.path} does not exactly match batch ${batch.batchNumber}'s artifact records at position ${recordMismatch + 1}.`
2176
+ );
2177
+ }
2178
+ reviewedFiles.push(...files);
2179
+ reviewedRecords.push(...records);
2180
+ sources.push({
2181
+ batchNumber: batch.batchNumber,
2182
+ shard: shard.path.replace(/\\/g, "/")
2183
+ });
2184
+ }
2185
+ const missingBatches = batches.map((batch) => batch.batchNumber).filter((batchNumber) => !shardByBatch.has(batchNumber));
2186
+ if (opts.requireComplete && missingBatches.length > 0) {
2187
+ return {
2188
+ ok: false,
2189
+ detail: `Coverage shards are still missing for batch${missingBatches.length === 1 ? "" : "es"} ${missingBatches.join(", ")}.`,
2190
+ expectedBatches: batches.length,
2191
+ mergedBatches: sources.length,
2192
+ missingBatches
2193
+ };
2194
+ }
2195
+ const ledger = {
2196
+ ...opts.pullRequest !== void 0 ? { pullRequest: opts.pullRequest } : {},
2197
+ reviewedFiles,
2198
+ reviewedRecords,
2199
+ sources,
2200
+ complete: missingBatches.length === 0
2201
+ };
2202
+ return {
2203
+ ok: true,
2204
+ detail: missingBatches.length === 0 ? `Coverage is provenance-complete across all ${batches.length} batches and ${reviewedFiles.length} changed paths.` : `Merged ${sources.length}/${batches.length} coverage shards; waiting for batches ${missingBatches.join(", ")}.`,
2205
+ ledger,
2206
+ content: `${JSON.stringify(ledger, null, 2)}
2207
+ `,
2208
+ expectedBatches: batches.length,
2209
+ mergedBatches: sources.length,
2210
+ missingBatches
2211
+ };
2212
+ }
1806
2213
  export {
1807
2214
  IMG_EXT,
1808
2215
  MIN_INLINE_JS_BYTES,
1809
2216
  MIN_JUDGE_EVIDENCE_SUBSTRING,
1810
2217
  buildJudgePrompt,
1811
2218
  citationsResolve,
2219
+ citedPathKey,
2220
+ cleanCitedPath,
2221
+ codebaseReviewReport,
1812
2222
  containsPattern,
1813
2223
  countDistinctMatches,
2224
+ createCitedPathChecker,
1814
2225
  cssMinBytes,
1815
2226
  csvShape,
1816
2227
  dataTableSniff,
@@ -1832,6 +2243,7 @@ export {
1832
2243
  jsonPathEquals,
1833
2244
  jsonValid,
1834
2245
  markdownHeadingsMatch,
2246
+ mergeCorpusCoverageShards,
1835
2247
  namedEntitiesConsistent,
1836
2248
  normalizeDigitGroups,
1837
2249
  notContainsPattern,