@bendyline/gezel 1.0.5 → 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.
- package/dist/checks/index.d.ts +101 -2
- package/dist/checks/index.js +433 -21
- package/dist/{index-CiHSDsBv.d.ts → index-BgRfJ_Pt.d.ts} +13833 -905
- package/dist/index.d.ts +1131 -15
- package/dist/index.js +19787 -5857
- package/dist/markdown/index.d.ts +1 -1
- package/dist/markdown/index.js +1301 -868
- package/dist/native/index.d.ts +121 -11
- package/dist/native/index.js +80 -7
- package/dist/paths.d.ts +133 -5
- package/dist/paths.js +396 -2
- package/dist/{report-action-BUinEFPt.d.ts → report-action-Kt6W23xy.d.ts} +1021 -4
- package/dist/schemas/index.d.ts +2 -2
- package/dist/schemas/index.js +9855 -5599
- package/package.json +1 -1
package/dist/checks/index.js
CHANGED
|
@@ -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 =
|
|
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
|
|
554
|
-
return p.trim().
|
|
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 = [
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
799
|
+
const citedPathExists = createCitedPathChecker(ws);
|
|
744
800
|
const problems = [];
|
|
745
801
|
const fabricated = [];
|
|
746
802
|
let critHigh = 0;
|
|
747
|
-
arr.
|
|
748
|
-
const f =
|
|
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 (!
|
|
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,
|