@bendyline/gezel-sdk 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.
- package/dist/checks.d.ts +102 -3
- package/dist/checks.js +433 -21
- package/dist/index.d.ts +121 -3
- package/dist/index.js +32 -1
- package/dist/page.d.ts +6 -1
- package/dist/{types-cYfcp6_8.d.ts → types-DCBb1goX.d.ts} +1 -1
- package/package.json +2 -2
package/dist/checks.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { G as GateScriptResult } from './types-
|
|
1
|
+
import { G as GateScriptResult } from './types-DCBb1goX.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* ─ Shared deliverable checks ─────────────────────────────────────────
|
|
@@ -42,6 +42,33 @@ interface CheckResult {
|
|
|
42
42
|
detail: string;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Existence oracle for cited workspace paths — the shared spine of the
|
|
47
|
+
* anti-fabrication checks (`citationsResolve`, `securityReport`,
|
|
48
|
+
* `codebaseReviewReport`).
|
|
49
|
+
*
|
|
50
|
+
* `list()` is only a FAST PATH here, never ground truth. The real surface
|
|
51
|
+
* behind it walks the workspace breadth-first under an entry cap (500 on
|
|
52
|
+
* the service side) and skips dotfiles, so in any repo bigger than the cap
|
|
53
|
+
* a truthful citation (`packages/core/src/paths.ts`, `.gitignore`) is
|
|
54
|
+
* simply absent from the listing. Treating that absence as fabrication
|
|
55
|
+
* made the gates unsatisfiable by honest citation and pressured models
|
|
56
|
+
* toward exactly the shallow or invented paths the gates exist to catch
|
|
57
|
+
* (task gezel/8, 2026-08-23: a 3,843-file repo where only the first 500
|
|
58
|
+
* entries counted as "real"). A listing miss therefore falls back to a
|
|
59
|
+
* per-path `read()` probe before any path may be called fabricated.
|
|
60
|
+
*/
|
|
61
|
+
declare function createCitedPathChecker(ws: WorkspaceLike): (cited: string) => Promise<boolean>;
|
|
62
|
+
/**
|
|
63
|
+
* Strip citation decoration down to a workspace-relative path: backticks,
|
|
64
|
+
* a `:line` / `#anchor` suffix, leading `./` or `/`, and a `workspace/`
|
|
65
|
+
* prefix. Case is preserved — the probe read needs the real path on
|
|
66
|
+
* case-sensitive filesystems; only the listing key lowercases.
|
|
67
|
+
*/
|
|
68
|
+
declare function cleanCitedPath(p: string): string;
|
|
69
|
+
/** Case-insensitive membership key for listing entries and citations alike. */
|
|
70
|
+
declare function citedPathKey(p: string): string;
|
|
71
|
+
|
|
45
72
|
/**
|
|
46
73
|
* HTML deliverable checks: truncation detection, inline-script
|
|
47
74
|
* extraction, V8 syntax validation, and the two content sniffs the
|
|
@@ -327,19 +354,36 @@ interface CitationsResult extends CheckResult {
|
|
|
327
354
|
unresolved: string[];
|
|
328
355
|
/** Cited URLs (not checked offline unless a corpus allowlist is given). */
|
|
329
356
|
urls: string[];
|
|
357
|
+
/** Unresolvable cited paths forgiven as task metadata (see `knownPaths`). */
|
|
358
|
+
forgiven?: string[];
|
|
330
359
|
}
|
|
331
360
|
/**
|
|
332
361
|
* Every source `file` cites must exist. File-path citations are resolved
|
|
333
362
|
* against the workspace listing (tolerant of leading `./`, `/`, and
|
|
334
|
-
* `workspace/`, case-insensitive)
|
|
363
|
+
* `workspace/`, case-insensitive), with a per-path read probe for listing
|
|
364
|
+
* misses — the listing is capped and dotfile-blind, see
|
|
365
|
+
* `createCitedPathChecker`. URLs cannot be fetched offline, so
|
|
335
366
|
* they pass unless `corpus` is supplied, in which case every cited path
|
|
336
367
|
* AND URL must be a member of the allowlist. The anti-fabrication gate.
|
|
368
|
+
*
|
|
369
|
+
* `knownPaths` are paths the surrounding task itself supplied — invocation
|
|
370
|
+
* parameters, the step prompt's own path tokens, the artifact working
|
|
371
|
+
* folder. A cited path matching one of these that does NOT resolve is
|
|
372
|
+
* FORGIVEN (dropped from the citation set — it counts toward neither the
|
|
373
|
+
* minimum nor the fabrication verdict): transcribing the run's own
|
|
374
|
+
* metadata into a packet is bookkeeping, not sourcing. A knownPath that
|
|
375
|
+
* DOES resolve stays an ordinary resolved citation. Wild-caught: the
|
|
376
|
+
* powerpoint-deck research step requires sources.md to record the
|
|
377
|
+
* invocation inputs, and the backticked `tasks/8` / `powerpoint/task-8/
|
|
378
|
+
* deck.pptx` tokens — directory handles and a future output — read as six
|
|
379
|
+
* fabricated citations, failing the gate's honest no-research path.
|
|
337
380
|
*/
|
|
338
381
|
declare function citationsResolve(ws: WorkspaceLike, file: string, opts?: {
|
|
339
382
|
pattern?: string;
|
|
340
383
|
flags?: string;
|
|
341
384
|
minCitations?: number;
|
|
342
385
|
corpus?: string[];
|
|
386
|
+
knownPaths?: readonly string[];
|
|
343
387
|
}): Promise<CitationsResult>;
|
|
344
388
|
/** Spec for {@link valuesSubsetOf}. */
|
|
345
389
|
interface ValuesSubsetSpec {
|
|
@@ -394,6 +438,25 @@ interface SecurityReportResult extends CheckResult {
|
|
|
394
438
|
}
|
|
395
439
|
declare function securityReport(ws: WorkspaceLike, reportFile: string, opts?: SecurityReportOptions): Promise<SecurityReportResult>;
|
|
396
440
|
|
|
441
|
+
interface CodebaseReviewReportOptions {
|
|
442
|
+
/** Path (in `reports`) to the machine-readable findings JSON. */
|
|
443
|
+
findings?: string;
|
|
444
|
+
/** Section headings the report must contain. */
|
|
445
|
+
requiredSections?: string[];
|
|
446
|
+
/** Minimum systemic themes required once findings ≥ themeThreshold. */
|
|
447
|
+
minThemes?: number;
|
|
448
|
+
/** Findings count at/above which systemic-theme synthesis is required. */
|
|
449
|
+
themeThreshold?: number;
|
|
450
|
+
/** Minimum data rows the Scorecard table must carry. */
|
|
451
|
+
minScorecardRows?: number;
|
|
452
|
+
}
|
|
453
|
+
interface CodebaseReviewReportResult extends CheckResult {
|
|
454
|
+
findingCount: number;
|
|
455
|
+
/** Cited files that don't exist in the workspace, for logs/facts. */
|
|
456
|
+
fabricated: string[];
|
|
457
|
+
}
|
|
458
|
+
declare function codebaseReviewReport(reports: WorkspaceLike, workspace: WorkspaceLike, reportFile: string, opts?: CodebaseReviewReportOptions): Promise<CodebaseReviewReportResult>;
|
|
459
|
+
|
|
397
460
|
/** ISO yyyy-mm-dd AND a real calendar date (ported from data-wrangle). */
|
|
398
461
|
declare function isRealIsoDate(value: string): boolean;
|
|
399
462
|
/**
|
|
@@ -692,6 +755,42 @@ interface PlanStructureResult {
|
|
|
692
755
|
}
|
|
693
756
|
declare function planStructure(text: string, spec?: PlanStructureSpec): PlanStructureResult;
|
|
694
757
|
|
|
758
|
+
interface CorpusCoverageShardInput {
|
|
759
|
+
path: string;
|
|
760
|
+
content: string;
|
|
761
|
+
}
|
|
762
|
+
interface CorpusCoverageLedger {
|
|
763
|
+
pullRequest?: number;
|
|
764
|
+
reviewedFiles: string[];
|
|
765
|
+
reviewedRecords: string[];
|
|
766
|
+
sources: Array<{
|
|
767
|
+
batchNumber: number;
|
|
768
|
+
shard: string;
|
|
769
|
+
}>;
|
|
770
|
+
complete: boolean;
|
|
771
|
+
}
|
|
772
|
+
interface CorpusCoverageMergeResult extends CheckResult {
|
|
773
|
+
ledger?: CorpusCoverageLedger;
|
|
774
|
+
content?: string;
|
|
775
|
+
expectedBatches: number;
|
|
776
|
+
mergedBatches: number;
|
|
777
|
+
missingBatches: number[];
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Deterministically merge bounded corpus-coverage shards.
|
|
781
|
+
*
|
|
782
|
+
* The published batch manifest is the authority. Every accepted shard must
|
|
783
|
+
* name exactly one batch and reproduce that batch's path and record arrays in
|
|
784
|
+
* order; a parent cannot manufacture full-run coverage by copying paths that
|
|
785
|
+
* no child shard reported. Missing shards may be merged for an in-progress
|
|
786
|
+
* ledger, but malformed or out-of-scope shards always fail closed.
|
|
787
|
+
*/
|
|
788
|
+
declare function mergeCorpusCoverageShards(batchesContent: string, shards: readonly CorpusCoverageShardInput[], opts?: {
|
|
789
|
+
pullRequest?: number;
|
|
790
|
+
requireComplete?: boolean;
|
|
791
|
+
batchesFile?: string;
|
|
792
|
+
}): CorpusCoverageMergeResult;
|
|
793
|
+
|
|
695
794
|
/**
|
|
696
795
|
* `@bendyline/gezel-sdk/checks` — the shared deliverable-check predicates,
|
|
697
796
|
* re-exported for use INSIDE sandboxed scripts.
|
|
@@ -730,4 +829,4 @@ declare function workspaceFromGezel(g: {
|
|
|
730
829
|
};
|
|
731
830
|
}): WorkspaceLike;
|
|
732
831
|
|
|
733
|
-
export { type CellType, type CheckResult, type CitationsResult, type CsvShapeResult, type CsvShapeSpec, type EntitiesResult, type EntitySpec, type ExplainableSniff, GateScriptResult, type GroundingFact, type GroundingResult, IMG_EXT, type ImageRefsReport, type InlineScript, type JsonPathEqualsResult, type JsonScalar, type JudgeVerdict, MIN_INLINE_JS_BYTES, MIN_JUDGE_EVIDENCE_SUBSTRING, type MarkdownHeadingsMatchResult, type ParsedTable, type PlanRow, type PlanStructureResult, type PlanStructureSpec, type ReadingLevelResult, type RecordFieldSpec, type RecordSchemaResult, type RecordSchemaSpec, type ScriptValidation, type SecurityReportOptions, type SecurityReportResult, type TableShapeResult, type TableShapeSpec, type UnsupportedClaimPattern, type UnsupportedClaimViolation, type UnsupportedClaimsResult, type ValuesSubsetResult, type ValuesSubsetSpec, type WordBandResult, type WorkspaceLike, buildJudgePrompt, citationsResolve, containsPattern, countDistinctMatches, cssMinBytes, csvShape, dataTableSniff, detectTypeScriptOnlySyntax, detectUnclosedScript, esmImports, explainSniff, extractInlineScripts, fileCountByExt, fileMinBytes, fileMinLines, findImageRefs, gateResult, grepMatches, htmlCompleteSniff, htmlGameSniff, imageRefsResolve, inlineJsBytes, isRealIsoDate, jsonPathEquals, jsonValid, markdownHeadingsMatch, namedEntitiesConsistent, normalizeDigitGroups, notContainsPattern, parseCsv, parseJudgeVerdict, parseMarkdownTable, planStructure, readingLevel, recordSchema, requireOrderedSections, resolveRelative, securityReport, standaloneJsParses, tableShape, totalMinBytes, unsupportedClaims, validateJudgeEvidence, validateScriptSyntax, valueGrounding, valuesSubsetOf, wordBand, workspaceFromGezel, wrapperReturnHint };
|
|
832
|
+
export { type CellType, type CheckResult, type CitationsResult, type CodebaseReviewReportOptions, type CodebaseReviewReportResult, type CorpusCoverageLedger, type CorpusCoverageMergeResult, type CorpusCoverageShardInput, type CsvShapeResult, type CsvShapeSpec, type EntitiesResult, type EntitySpec, type ExplainableSniff, GateScriptResult, type GroundingFact, type GroundingResult, IMG_EXT, type ImageRefsReport, type InlineScript, type JsonPathEqualsResult, type JsonScalar, type JudgeVerdict, MIN_INLINE_JS_BYTES, MIN_JUDGE_EVIDENCE_SUBSTRING, type MarkdownHeadingsMatchResult, type ParsedTable, type PlanRow, type PlanStructureResult, type PlanStructureSpec, type ReadingLevelResult, type RecordFieldSpec, type RecordSchemaResult, type RecordSchemaSpec, type ScriptValidation, type SecurityReportOptions, type SecurityReportResult, type TableShapeResult, type TableShapeSpec, type UnsupportedClaimPattern, type UnsupportedClaimViolation, type UnsupportedClaimsResult, type ValuesSubsetResult, type ValuesSubsetSpec, type WordBandResult, type WorkspaceLike, buildJudgePrompt, citationsResolve, citedPathKey, cleanCitedPath, codebaseReviewReport, containsPattern, countDistinctMatches, createCitedPathChecker, cssMinBytes, csvShape, dataTableSniff, detectTypeScriptOnlySyntax, detectUnclosedScript, esmImports, explainSniff, extractInlineScripts, fileCountByExt, fileMinBytes, fileMinLines, findImageRefs, gateResult, grepMatches, htmlCompleteSniff, htmlGameSniff, imageRefsResolve, inlineJsBytes, isRealIsoDate, jsonPathEquals, jsonValid, markdownHeadingsMatch, mergeCorpusCoverageShards, namedEntitiesConsistent, normalizeDigitGroups, notContainsPattern, parseCsv, parseJudgeVerdict, parseMarkdownTable, planStructure, readingLevel, recordSchema, requireOrderedSections, resolveRelative, securityReport, standaloneJsParses, tableShape, totalMinBytes, unsupportedClaims, validateJudgeEvidence, validateScriptSyntax, valueGrounding, valuesSubsetOf, wordBand, workspaceFromGezel, wrapperReturnHint };
|
package/dist/checks.js
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
// ../core/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
|
// ../core/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
|
+
// ../core/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
|
// ../core/src/checks/records.ts
|
|
822
1063
|
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
823
1064
|
function isRealIsoDate(value) {
|
|
@@ -1804,6 +2045,172 @@ function findCycle(rows) {
|
|
|
1804
2045
|
return cycle;
|
|
1805
2046
|
}
|
|
1806
2047
|
|
|
2048
|
+
// ../core/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
|
+
}
|
|
2213
|
+
|
|
1807
2214
|
// src/checks.ts
|
|
1808
2215
|
function gateResult(ok, detail) {
|
|
1809
2216
|
return ok ? { decision: "approve", message: detail } : { decision: "reject", message: detail };
|
|
@@ -1826,8 +2233,12 @@ export {
|
|
|
1826
2233
|
MIN_JUDGE_EVIDENCE_SUBSTRING,
|
|
1827
2234
|
buildJudgePrompt,
|
|
1828
2235
|
citationsResolve,
|
|
2236
|
+
citedPathKey,
|
|
2237
|
+
cleanCitedPath,
|
|
2238
|
+
codebaseReviewReport,
|
|
1829
2239
|
containsPattern,
|
|
1830
2240
|
countDistinctMatches,
|
|
2241
|
+
createCitedPathChecker,
|
|
1831
2242
|
cssMinBytes,
|
|
1832
2243
|
csvShape,
|
|
1833
2244
|
dataTableSniff,
|
|
@@ -1850,6 +2261,7 @@ export {
|
|
|
1850
2261
|
jsonPathEquals,
|
|
1851
2262
|
jsonValid,
|
|
1852
2263
|
markdownHeadingsMatch,
|
|
2264
|
+
mergeCorpusCoverageShards,
|
|
1853
2265
|
namedEntitiesConsistent,
|
|
1854
2266
|
normalizeDigitGroups,
|
|
1855
2267
|
notContainsPattern,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,96 @@
|
|
|
1
|
-
import { S as ScriptMeta, I as InferInput, a as InferOutput, b as ScriptInputs, c as ScriptOutputs } from './types-
|
|
2
|
-
export { G as GateScriptResult, d as ScriptArrayOutput, e as ScriptBooleanInput, f as ScriptBooleanOutput, g as ScriptCapability, h as ScriptChoiceInput, i as ScriptChoiceOption, j as ScriptInputField, k as ScriptJsonInput, l as ScriptJsonOutput, m as ScriptNumberInput, n as ScriptNumberOutput, o as ScriptObjectOutput, p as ScriptOutputField, q as ScriptRefInput, r as ScriptStringInput, s as ScriptStringOutput } from './types-
|
|
1
|
+
import { S as ScriptMeta, I as InferInput, a as InferOutput, b as ScriptInputs, c as ScriptOutputs } from './types-DCBb1goX.js';
|
|
2
|
+
export { G as GateScriptResult, d as ScriptArrayOutput, e as ScriptBooleanInput, f as ScriptBooleanOutput, g as ScriptCapability, h as ScriptChoiceInput, i as ScriptChoiceOption, j as ScriptInputField, k as ScriptJsonInput, l as ScriptJsonOutput, m as ScriptNumberInput, n as ScriptNumberOutput, o as ScriptObjectOutput, p as ScriptOutputField, q as ScriptRefInput, r as ScriptStringInput, s as ScriptStringOutput } from './types-DCBb1goX.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Script-SDK wire types for workspace-index status and readiness.
|
|
6
|
+
*
|
|
7
|
+
* These deliberately live in the self-contained SDK instead of importing
|
|
8
|
+
* the Zod-inferred core types. Script sandboxes and Monaco receive only the
|
|
9
|
+
* SDK declaration files, so an external type import would make that surface
|
|
10
|
+
* depend on another package. The parity test beside this file keeps these
|
|
11
|
+
* structural copies aligned with the canonical core schemas.
|
|
12
|
+
*/
|
|
13
|
+
interface WorkspaceIndexStatus {
|
|
14
|
+
state: 'fresh' | 'stale' | 'indexing' | 'never' | 'disabled';
|
|
15
|
+
embeddings?: {
|
|
16
|
+
status: 'cold' | 'warming' | 'ready' | 'disabled' | 'unavailable';
|
|
17
|
+
reason?: string;
|
|
18
|
+
};
|
|
19
|
+
meta?: {
|
|
20
|
+
version: number;
|
|
21
|
+
scannedAt: string;
|
|
22
|
+
root: string;
|
|
23
|
+
durationMs: number;
|
|
24
|
+
fileCount: number;
|
|
25
|
+
commandCount: number;
|
|
26
|
+
shapeCount?: number;
|
|
27
|
+
};
|
|
28
|
+
aiScanPending?: boolean;
|
|
29
|
+
aiDrive?: 'background' | 'full';
|
|
30
|
+
enrichment?: {
|
|
31
|
+
eligible: number;
|
|
32
|
+
summarized: number;
|
|
33
|
+
embedded: number;
|
|
34
|
+
searchReady?: number;
|
|
35
|
+
pending: number;
|
|
36
|
+
skipped?: number;
|
|
37
|
+
skippedFiles?: Array<{
|
|
38
|
+
path: string;
|
|
39
|
+
attempts: number;
|
|
40
|
+
reason?: string;
|
|
41
|
+
}>;
|
|
42
|
+
shadowsPending?: number;
|
|
43
|
+
embedOnlyPending?: number;
|
|
44
|
+
embedModel?: string;
|
|
45
|
+
vectorsAvailable?: boolean;
|
|
46
|
+
reviews?: {
|
|
47
|
+
eligible: number;
|
|
48
|
+
reviewed: number;
|
|
49
|
+
stale: number;
|
|
50
|
+
pending: number;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
interface IndexReadinessReport {
|
|
55
|
+
version: 1;
|
|
56
|
+
projectId: string;
|
|
57
|
+
generatedAt: string;
|
|
58
|
+
indexingEnabled: boolean;
|
|
59
|
+
staticState: 'fresh' | 'stale' | 'indexing' | 'never' | 'disabled';
|
|
60
|
+
fileCount?: number;
|
|
61
|
+
scannedAt?: string;
|
|
62
|
+
search: {
|
|
63
|
+
ready: boolean;
|
|
64
|
+
eligible?: number;
|
|
65
|
+
embedded?: number;
|
|
66
|
+
pendingEmbedOnly?: number;
|
|
67
|
+
embedModel?: string;
|
|
68
|
+
vectorsAvailable?: boolean;
|
|
69
|
+
};
|
|
70
|
+
aiTier: {
|
|
71
|
+
staffed: boolean;
|
|
72
|
+
paused: boolean;
|
|
73
|
+
achievable: boolean;
|
|
74
|
+
summariesEligible?: number;
|
|
75
|
+
summarized?: number;
|
|
76
|
+
summariesPending?: number;
|
|
77
|
+
shadowsPending?: number;
|
|
78
|
+
skipped?: number;
|
|
79
|
+
reviews?: {
|
|
80
|
+
eligible: number;
|
|
81
|
+
reviewed: number;
|
|
82
|
+
stale: number;
|
|
83
|
+
pending: number;
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
wait: {
|
|
87
|
+
budgetMs: number;
|
|
88
|
+
waitedMs: number;
|
|
89
|
+
drained: boolean;
|
|
90
|
+
driveStillRunning: boolean;
|
|
91
|
+
};
|
|
92
|
+
notes: string[];
|
|
93
|
+
}
|
|
3
94
|
|
|
4
95
|
/**
|
|
5
96
|
* `@bendyline/gezel-sdk` — imported by TypeScript scripts running in the
|
|
@@ -556,6 +647,33 @@ interface GezelSDK<TInput = Record<string, unknown>> {
|
|
|
556
647
|
*/
|
|
557
648
|
authed(url: string, opts: AuthedHttpOpts): Promise<AuthedHttpResponse>;
|
|
558
649
|
};
|
|
650
|
+
/**
|
|
651
|
+
* The project's **workspace index** — status of the static scan and the
|
|
652
|
+
* AI tiers, plus a bounded "make it fresh" ensure. Requires `index.read`
|
|
653
|
+
* for {@link GezelSDK.index.status | status} and `index.refresh` for
|
|
654
|
+
* {@link GezelSDK.index.ensureFresh | ensureFresh}.
|
|
655
|
+
*/
|
|
656
|
+
index: {
|
|
657
|
+
/** Current index status (static state + enrichment/review coverage). */
|
|
658
|
+
status(): Promise<WorkspaceIndexStatus>;
|
|
659
|
+
/**
|
|
660
|
+
* Make the index as fresh as it can get within an awake-time budget:
|
|
661
|
+
* awaited static re-scan, then an AI-tier catch-up drive raced against
|
|
662
|
+
* the budget. Always resolves with an honest readiness report — on a
|
|
663
|
+
* crew with no Boekwachter the AI tiers are reported unachievable
|
|
664
|
+
* rather than awaited, and an expired budget leaves the drive running
|
|
665
|
+
* in the background.
|
|
666
|
+
*
|
|
667
|
+
* @param opts.waitBudgetMs - Awake-time wait budget (default 180s,
|
|
668
|
+
* capped at 240s to stay inside the script run timeout).
|
|
669
|
+
* @param opts.reviews - Also wait for the per-file AI review tier
|
|
670
|
+
* (default true).
|
|
671
|
+
*/
|
|
672
|
+
ensureFresh(opts?: {
|
|
673
|
+
waitBudgetMs?: number;
|
|
674
|
+
reviews?: boolean;
|
|
675
|
+
}): Promise<IndexReadinessReport>;
|
|
676
|
+
};
|
|
559
677
|
/**
|
|
560
678
|
* **Nested scripts** — run another script in the same project and get
|
|
561
679
|
* its stamped output back. No capability is required to call, but the
|
|
@@ -592,4 +710,4 @@ declare const gezel: GezelSDK;
|
|
|
592
710
|
type InferredInput<M> = M extends ScriptMeta<infer I, infer _O> ? InferInput<I> : never;
|
|
593
711
|
type InferredOutput<M> = M extends ScriptMeta<infer _I, infer O> ? InferOutput<O> : never;
|
|
594
712
|
|
|
595
|
-
export { type ArtifactEntry, type AuthedHttpOpts, type AuthedHttpResponse, type DocumentEntry, type FsEntry, type FsStat, type GezelSDK, type HttpRequestOpts, type HttpResponse, InferInput, InferOutput, type InferredInput, type InferredOutput, type OneShotOpts, ScriptInputs, ScriptMeta, ScriptOutputs, type ScriptResult, type TaskNote, type TaskNoteAuthor, type TaskStep, defineScript, gezel };
|
|
713
|
+
export { type ArtifactEntry, type AuthedHttpOpts, type AuthedHttpResponse, type DocumentEntry, type FsEntry, type FsStat, type GezelSDK, type HttpRequestOpts, type HttpResponse, type IndexReadinessReport, InferInput, InferOutput, type InferredInput, type InferredOutput, type OneShotOpts, ScriptInputs, ScriptMeta, ScriptOutputs, type ScriptResult, type TaskNote, type TaskNoteAuthor, type TaskStep, type WorkspaceIndexStatus, defineScript, gezel };
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,11 @@ var DEFAULT_INIT = {
|
|
|
8
8
|
engagementMode: "off",
|
|
9
9
|
engagementFlags: { llmAllowed: false }
|
|
10
10
|
};
|
|
11
|
+
var WRITE_BACKPRESSURE_RETRY_MS = 1;
|
|
12
|
+
var WRITE_BACKPRESSURE_TIMEOUT_MS = 3e4;
|
|
13
|
+
var WRITE_BACKPRESSURE_SIGNAL = new Int32Array(
|
|
14
|
+
new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)
|
|
15
|
+
);
|
|
11
16
|
function readInitSync() {
|
|
12
17
|
if (process.env.GEZEL_SCRIPT_RUNTIME !== "1") {
|
|
13
18
|
return DEFAULT_INIT;
|
|
@@ -132,8 +137,27 @@ var RpcClient = class {
|
|
|
132
137
|
function writeFrame(payload) {
|
|
133
138
|
const buf = Buffer.from(payload, "utf8");
|
|
134
139
|
let offset = 0;
|
|
140
|
+
const startedAt = Date.now();
|
|
135
141
|
while (offset < buf.length) {
|
|
136
|
-
|
|
142
|
+
try {
|
|
143
|
+
const written = writeSync(3, buf, offset, buf.length - offset);
|
|
144
|
+
if (written > 0) {
|
|
145
|
+
offset += written;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const code = error.code;
|
|
150
|
+
if (code === "EINTR") continue;
|
|
151
|
+
if (code !== "EAGAIN" && code !== "EWOULDBLOCK") throw error;
|
|
152
|
+
}
|
|
153
|
+
if (Date.now() - startedAt >= WRITE_BACKPRESSURE_TIMEOUT_MS) {
|
|
154
|
+
const error = new Error(
|
|
155
|
+
`script RPC channel remained backpressured after writing ${offset} of ${buf.length} bytes`
|
|
156
|
+
);
|
|
157
|
+
error.code = "ETIMEDOUT";
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
Atomics.wait(WRITE_BACKPRESSURE_SIGNAL, 0, 0, WRITE_BACKPRESSURE_RETRY_MS);
|
|
137
161
|
}
|
|
138
162
|
}
|
|
139
163
|
|
|
@@ -208,6 +232,13 @@ var gezel = {
|
|
|
208
232
|
mcp: {
|
|
209
233
|
call: (tool, args) => rpc.call("mcp.call", { tool, args })
|
|
210
234
|
},
|
|
235
|
+
index: {
|
|
236
|
+
status: () => rpc.call("index.status"),
|
|
237
|
+
ensureFresh: (opts) => rpc.call("index.ensureFresh", {
|
|
238
|
+
...opts?.waitBudgetMs !== void 0 ? { waitBudgetMs: opts.waitBudgetMs } : {},
|
|
239
|
+
...opts?.reviews !== void 0 ? { reviews: opts.reviews } : {}
|
|
240
|
+
})
|
|
241
|
+
},
|
|
211
242
|
http: {
|
|
212
243
|
request: (url, opts) => rpc.call("http.request", {
|
|
213
244
|
url,
|
package/dist/page.d.ts
CHANGED
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
* - `browser` — an "Open in browser" tab with no embedding parent. Data
|
|
20
20
|
* reads fall back to same-origin capability fetches; `tools.invoke`
|
|
21
21
|
* rejects with code `'unavailable'`; `refresh()` reloads the page.
|
|
22
|
+
* - `serve` — the page is a shareable mini-site served by the daemon's
|
|
23
|
+
* app-serve head (`gezel app serve`). Nothing degrades: `tools.invoke`
|
|
24
|
+
* and `data.*` travel as same-origin fetches against the head API under
|
|
25
|
+
* the visitor's cookie; `data.watch` polls; `refresh()` reloads. The
|
|
26
|
+
* page still never sees a credential.
|
|
22
27
|
* - `demo` — a raw file opened outside gezel entirely. The real shim is
|
|
23
28
|
* never present there; gilde ships a paste-in stub that defines
|
|
24
29
|
* `window.gezel` only when the real one is absent.
|
|
@@ -35,7 +40,7 @@ declare const GEZEL_PAGE_API_VERSION: 1;
|
|
|
35
40
|
*/
|
|
36
41
|
type GezelPageReadSource = 'workspace' | 'artifacts';
|
|
37
42
|
/** How the page is being viewed; see the module doc for what each mode degrades. */
|
|
38
|
-
type GezelPageMode = 'embedded' | 'browser' | 'demo';
|
|
43
|
+
type GezelPageMode = 'embedded' | 'browser' | 'serve' | 'demo';
|
|
39
44
|
/** Error codes a rejected page-API call carries (`GezelPageError.code`). */
|
|
40
45
|
type GezelPageErrorCode = 'not-allowed' | 'invalid-input' | 'script-error' | 'timeout' | 'unavailable' | 'rate-limited';
|
|
41
46
|
/**
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* `gezel.http.authed` additionally needs a `credential:<name>` entry
|
|
22
22
|
* (declared as a raw string in `requires`) naming each credential it uses.
|
|
23
23
|
*/
|
|
24
|
-
type ScriptCapability = 'llm' | 'network' | 'workspace.read' | 'workspace.write' | 'artifacts.read' | 'artifacts.write' | 'documents.read' | 'documents.write' | 'tasks.read' | 'tasks.write' | 'memory.read' | 'memory.write';
|
|
24
|
+
type ScriptCapability = 'llm' | 'network' | 'workspace.read' | 'workspace.write' | 'artifacts.read' | 'artifacts.write' | 'documents.read' | 'documents.write' | 'tasks.read' | 'tasks.write' | 'memory.read' | 'memory.write' | 'index.read' | 'index.refresh';
|
|
25
25
|
/** A free-text string input field. */
|
|
26
26
|
interface ScriptStringInput {
|
|
27
27
|
type: 'string';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/gezel-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "SDK imported by scripts running in the Gezel sandbox. Exposes the `gezel` object and `defineScript` helper.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"!dist/**/*.map"
|
|
60
60
|
],
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@bendyline/gezel": "1.0.
|
|
62
|
+
"@bendyline/gezel": "1.0.7",
|
|
63
63
|
"tsup": "^8.5.1",
|
|
64
64
|
"typescript": "^6.0.3",
|
|
65
65
|
"vitest": "^4.1.10"
|