@hraness/wordcell 0.26.3 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -21
- package/dist/capture.js +15 -7
- package/dist/cli-bin.js +2 -2
- package/dist/cli.js +21 -19
- package/dist/clip/acquire.js +4 -3
- package/dist/clip/bundle-reader.js +3 -2
- package/dist/clip/cli.js +8 -7
- package/dist/clip/defuddle-worker.js +4 -1
- package/dist/clip/doctor.js +5 -4
- package/dist/clip/jobs.js +2 -1
- package/dist/clip/persist.js +2 -1
- package/dist/clip/refresh.js +4 -3
- package/dist/evaluation-builder-bin.js +2 -2
- package/dist/evaluation-builder.js +3 -3
- package/dist/evaluation-kb.js +3 -3
- package/dist/{index-mp5y3ty3.js → index-30v9w4cy.js} +36 -20
- package/dist/{index-9paxge84.js → index-3hts6yy9.js} +1 -1
- package/dist/{index-h9vyd03z.js → index-58azscg5.js} +7 -6
- package/dist/{index-9zkba8hr.js → index-7bqwzjba.js} +7 -6
- package/dist/{index-j4zgmzjr.js → index-8djdwjjd.js} +1 -1
- package/dist/{index-5h5awwh4.js → index-b40e2dkx.js} +2 -2
- package/dist/index-e2wstc7t.js +323 -0
- package/dist/{index-hacpnaew.js → index-fd8j527z.js} +1 -1
- package/dist/{index-mxxxytys.js → index-g63d3937.js} +4 -0
- package/dist/{index-k86wepd8.js → index-h2w7syy2.js} +5 -1
- package/dist/{index-9fywek7g.js → index-ht14xne3.js} +1 -1
- package/dist/{index-82r6g4py.js → index-k9yzc4vn.js} +1 -1
- package/dist/{index-m9p2a4mj.js → index-my50xsa3.js} +221 -36
- package/dist/{index-ncwzmsge.js → index-nwdzhj1h.js} +2 -2
- package/dist/{index-9rf81m0p.js → index-pbz3qw1s.js} +35 -20
- package/dist/index-pn14a2m5.js +59 -0
- package/dist/{index-nd8v7r0z.js → index-sp43012q.js} +1 -1
- package/dist/{index-8pabwzqg.js → index-t4hjm1kk.js} +2 -1
- package/dist/{index-npg9z1a4.js → index-xn5q4kne.js} +5 -1
- package/dist/{index-j2jt49gr.js → index-yatn62qq.js} +1 -1
- package/dist/index.js +14 -4
- package/dist/pdf.js +438 -4
- package/dist/portfolio.js +3 -3
- package/dist/rerank-clef.js +16 -0
- package/dist/rerank-typesafe.js +3 -1
- package/dist/sdk.js +2 -2
- package/dist/{support-9qdr4hb8.js → support-s059eytv.js} +20 -18
- package/dist/url-intelligence.js +4 -3
- package/dist/workflows/decision-context.js +3 -3
- package/dist/workflows/index.js +3 -3
- package/package.json +23 -9
- package/skills/wordcell/SKILL.md +1 -1
- package/skills/wordcell/references/query.md +11 -6
- package/src/capture.ts +5 -0
- package/src/cli-help.ts +2 -1
- package/src/cli-program.ts +23 -8
- package/src/clip/bundle-reader.ts +3 -0
- package/src/clip/capture.ts +1 -0
- package/src/clip/extract.ts +2 -0
- package/src/clip/lib.ts +3 -0
- package/src/clip/metadata-search-tool/Cargo.lock +6 -6
- package/src/clip/metadata.ts +232 -0
- package/src/clip/persist.ts +3 -0
- package/src/index.ts +2 -1
- package/src/pdf/bundle-reader.ts +457 -0
- package/src/pdf/capture.ts +6 -2
- package/src/pdf/cli.ts +2 -1
- package/src/pdf/errors.ts +11 -0
- package/src/pdf/extract.ts +16 -13
- package/src/pdf/layout.ts +136 -3
- package/src/pdf/markdown.ts +7 -1
- package/src/pdf/model.ts +9 -0
- package/src/pdf/persist.ts +8 -2
- package/src/pdf/source.ts +5 -4
- package/src/pdf/tools.ts +8 -6
- package/src/pdf.ts +10 -0
- package/src/rerank-clef.ts +52 -0
- package/src/rerank-credentials.ts +11 -2
- package/src/rerank-typesafe.ts +44 -20
- package/src/rerank.ts +1 -1
- package/src/sdk.ts +4 -2
- /package/dist/{index-cd75vky9.js → index-jbtyp6q5.js} +0 -0
package/src/pdf/layout.ts
CHANGED
|
@@ -36,6 +36,7 @@ export type ParsedPopplerXml = {
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
export type PdfLayoutBlock =
|
|
39
|
+
| { readonly kind: "table"; readonly page: number; readonly rows: readonly (readonly string[])[] }
|
|
39
40
|
| {
|
|
40
41
|
readonly kind: "heading";
|
|
41
42
|
readonly page: number;
|
|
@@ -456,7 +457,7 @@ export function layoutBlocks(pages: readonly PdfPageLayout[]): readonly PdfLayou
|
|
|
456
457
|
|
|
457
458
|
for (const page of pages) {
|
|
458
459
|
const lines = (linesByPage.get(page.page) ?? []).filter(
|
|
459
|
-
(line) => !repeatedMargins.has(normalizedMarginText(line.text)),
|
|
460
|
+
(line) => !(line.top <= page.height * 0.06 || line.top + line.height >= page.height * 0.94) || !repeatedMargins.has(normalizedMarginText(line.text)),
|
|
460
461
|
);
|
|
461
462
|
const steps = lines.slice(1).map((line, index) =>
|
|
462
463
|
Math.max(0, line.top - (lines[index]?.top ?? line.top)));
|
|
@@ -491,7 +492,7 @@ export function layoutBlocks(pages: readonly PdfPageLayout[]): readonly PdfLayou
|
|
|
491
492
|
const { line, index } = event;
|
|
492
493
|
const previous = lines[index - 1];
|
|
493
494
|
const gapBefore = previous === undefined ? Number.POSITIVE_INFINITY : line.top - previous.top;
|
|
494
|
-
const fontHeading = line.fontSize > bodySize * 1.08;
|
|
495
|
+
const fontHeading = line.fontSize > bodySize * 1.08 && !/[=∫∑√⁰¹²³⁴⁵⁶⁷⁸⁹]/u.test(line.text);
|
|
495
496
|
const boldHeading = line.boldRatio >= 0.78
|
|
496
497
|
&& line.text.length <= 180
|
|
497
498
|
&& gapBefore >= normalStep * 1.45;
|
|
@@ -516,7 +517,7 @@ export function layoutBlocks(pages: readonly PdfPageLayout[]): readonly PdfLayou
|
|
|
516
517
|
}
|
|
517
518
|
flushParagraph();
|
|
518
519
|
}
|
|
519
|
-
return output.filter((block) => block.kind === "image" || block.text.trim() !== "");
|
|
520
|
+
return output.filter((block) => block.kind === "image" || block.kind === "table" || block.text.trim() !== "");
|
|
520
521
|
}
|
|
521
522
|
|
|
522
523
|
export function parsePdfImageInterpretations(value: unknown): readonly PdfImageInterpretation[] {
|
|
@@ -616,3 +617,135 @@ export function parsePdfImageInterpretations(value: unknown): readonly PdfImageI
|
|
|
616
617
|
}
|
|
617
618
|
return output;
|
|
618
619
|
}
|
|
620
|
+
|
|
621
|
+
/** Geometry evidence is deliberately conservative: uncertain structure remains visible in quality. */
|
|
622
|
+
export function analyzePdfLayout(pages: readonly PdfPageLayout[]): { readonly blocks: readonly PdfLayoutBlock[]; readonly issues: readonly import("./model.js").PdfQualityIssue[] } {
|
|
623
|
+
const issues: import("./model.js").PdfQualityIssue[] = [];
|
|
624
|
+
const blocks: PdfLayoutBlock[] = [];
|
|
625
|
+
const marginLines = new Map(pages.filter(page => page.text.length <= 4000).map(page => [page.page, pageLines(page)]));
|
|
626
|
+
const margins = repeatedMarginLines(pages, marginLines);
|
|
627
|
+
for (const originalInput of pages) {
|
|
628
|
+
const original = { ...originalInput, text: originalInput.text.filter(fragment => !(fragment.top <= originalInput.height * 0.06 || fragment.top + fragment.height >= originalInput.height * 0.94) || !margins.has(normalizedMarginText(fragment.text))) };
|
|
629
|
+
const warn = (code: import("./model.js").PdfQualityIssue["code"]): void => {
|
|
630
|
+
if (!issues.some(issue => issue.page === original.page && issue.code === code)) issues.push({ page: original.page, code });
|
|
631
|
+
};
|
|
632
|
+
// Bound quadratic geometry analysis independently of the parser's byte limit.
|
|
633
|
+
if (original.text.length > 4000) {
|
|
634
|
+
warn("reading-order-uncertain"); blocks.push(...layoutBlocks([original])); continue;
|
|
635
|
+
}
|
|
636
|
+
if (original.text.some(fragment => /[∫∑√]/u.test(fragment.text))) warn("math-structure-uncertain");
|
|
637
|
+
const text = [...original.text].sort((a, b) => a.top - b.top || a.left - b.left);
|
|
638
|
+
const consumed = new Set<PdfTextFragment>();
|
|
639
|
+
const replacements = new Map<PdfTextFragment, PdfTextFragment>();
|
|
640
|
+
const superscripts: Record<string, string> = { "0": "⁰", "1": "¹", "2": "²", "3": "³", "4": "⁴", "5": "⁵", "6": "⁶", "7": "⁷", "8": "⁸", "9": "⁹", "+": "⁺", "-": "⁻", "=": "⁼", "(": "⁽", ")": "⁾", "n": "ⁿ", "i": "ⁱ" };
|
|
641
|
+
for (const small of text) {
|
|
642
|
+
const bases = text.filter(base => base !== small && base.fontSize >= small.fontSize * 1.3
|
|
643
|
+
&& small.top < base.top && small.top + small.height > base.top - base.height * 0.25
|
|
644
|
+
&& Math.abs(small.left - (base.left + base.width)) < base.fontSize * 0.6);
|
|
645
|
+
if (bases.length === 0) continue;
|
|
646
|
+
const base = bases[0]!;
|
|
647
|
+
const glyphs = [...small.text.trim()];
|
|
648
|
+
if (bases.length === 1 && glyphs.length > 0 && glyphs.length <= 8 && glyphs.every(glyph => superscripts[glyph] !== undefined) && !replacements.has(base)) {
|
|
649
|
+
replacements.set(base, { ...base, text: base.text + glyphs.map(glyph => superscripts[glyph]).join(""), width: small.left + small.width - base.left });
|
|
650
|
+
consumed.add(small);
|
|
651
|
+
} else warn("math-structure-uncertain");
|
|
652
|
+
}
|
|
653
|
+
const fragments = text.filter(fragment => !consumed.has(fragment)).map(fragment => replacements.get(fragment) ?? fragment);
|
|
654
|
+
const page: PdfPageLayout = { ...original, text: fragments };
|
|
655
|
+
const rows: PdfTextFragment[][] = [];
|
|
656
|
+
for (const fragment of fragments) {
|
|
657
|
+
const row = rows.at(-1);
|
|
658
|
+
if (row === undefined || Math.abs(fragment.top - row[0]!.top) > Math.max(2, fragment.height * 0.2)) rows.push([fragment]);
|
|
659
|
+
else row.push(fragment);
|
|
660
|
+
}
|
|
661
|
+
const cellRows = rows.map(row => {
|
|
662
|
+
const groups: PdfTextFragment[][] = [];
|
|
663
|
+
for (const fragment of [...row].sort((a, b) => a.left - b.left)) {
|
|
664
|
+
const prior = groups.at(-1), last = prior?.at(-1);
|
|
665
|
+
if (last === undefined || fragment.left - (last.left + last.width) > Math.max(12, fragment.fontSize * 1.5)) groups.push([fragment]);
|
|
666
|
+
else prior!.push(fragment);
|
|
667
|
+
}
|
|
668
|
+
return groups;
|
|
669
|
+
});
|
|
670
|
+
const tables: Array<{ top: number; bottom: number; block: PdfLayoutBlock; fragments: PdfTextFragment[] }> = [];
|
|
671
|
+
for (let index = 0; index < cellRows.length; index++) {
|
|
672
|
+
const first = cellRows[index]!;
|
|
673
|
+
if (first.length < 2 || first.length > 12) continue;
|
|
674
|
+
const group = [first];
|
|
675
|
+
let next = index + 1;
|
|
676
|
+
for (; next < cellRows.length; next++) {
|
|
677
|
+
const candidate = cellRows[next]!;
|
|
678
|
+
if (candidate.length !== first.length || candidate.some((cell, column) => Math.abs(cell[0]!.left - first[column]![0]!.left) > 4)) break;
|
|
679
|
+
if (candidate[0]![0]!.top - group.at(-1)![0]![0]!.top > Math.max(40, candidate[0]![0]!.height * 3)) break;
|
|
680
|
+
group.push(candidate);
|
|
681
|
+
}
|
|
682
|
+
const cells = group.map(row => row.map(cell => joinFragments(cell)));
|
|
683
|
+
const header = first.every(cell => cell.every(fragment => fragment.bold));
|
|
684
|
+
const numeric = cells.slice(1).some(row => row.slice(1).some(cell => /^[-+\d$€£]/u.test(cell.trim())));
|
|
685
|
+
const compact = cells.every(row => row.every(cell => cell.length <= 100));
|
|
686
|
+
if (group.length >= 3 && compact && header && numeric) {
|
|
687
|
+
const members = group.flat(2);
|
|
688
|
+
tables.push({ top: Math.min(...members.map(fragment => fragment.top)), bottom: Math.max(...members.map(fragment => fragment.top + fragment.height)), block: { kind: "table", page: page.page, rows: cells }, fragments: members });
|
|
689
|
+
index = next - 1;
|
|
690
|
+
} else if (group.length >= 3 && first.length >= 3) warn("table-structure-uncertain");
|
|
691
|
+
}
|
|
692
|
+
const tableFragments = new Set(tables.flatMap(table => table.fragments));
|
|
693
|
+
const remaining = fragments.filter(fragment => !tableFragments.has(fragment));
|
|
694
|
+
const emitBand = (band: readonly PdfTextFragment[], images: readonly PdfImageCandidate[]): void => {
|
|
695
|
+
if (band.length === 0 && images.length === 0) return;
|
|
696
|
+
// Repeated gutters are detected before line grouping can join adjacent columns.
|
|
697
|
+
const candidates = new Set<number>();
|
|
698
|
+
for (const row of cellRows) {
|
|
699
|
+
for (let i = 1; i < row.length; i++) {
|
|
700
|
+
const left = row[i - 1]!.at(-1)!, right = row[i]![0]!;
|
|
701
|
+
const gap = right.left - (left.left + left.width);
|
|
702
|
+
if (gap >= Math.max(page.width * 0.025, left.fontSize * 1.5)) candidates.add(Math.round((left.left + left.width + right.left) / 2));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const valid = [...candidates].filter(gutter => {
|
|
706
|
+
const left = band.filter(fragment => fragment.left + fragment.width <= gutter);
|
|
707
|
+
const right = band.filter(fragment => fragment.left >= gutter);
|
|
708
|
+
if (left.length + right.length !== band.length || left.length < 3 || right.length < 3) return false;
|
|
709
|
+
if (left.filter(fragment => fragment.width > page.width * 0.18).length < 2 || right.filter(fragment => fragment.width > page.width * 0.18).length < 2) return false;
|
|
710
|
+
const overlap = Math.min(Math.max(...left.map(fragment => fragment.top)), Math.max(...right.map(fragment => fragment.top))) - Math.max(Math.min(...left.map(fragment => fragment.top)), Math.min(...right.map(fragment => fragment.top)));
|
|
711
|
+
return overlap > Math.max(...band.map(fragment => fragment.height)) * 1.5;
|
|
712
|
+
});
|
|
713
|
+
if (valid.length > 0) {
|
|
714
|
+
const gutter = valid[Math.floor(valid.length / 2)]!;
|
|
715
|
+
if (images.some(image => image.left < gutter && image.left + image.width > gutter)) {
|
|
716
|
+
warn("reading-order-uncertain"); blocks.push(...layoutBlocks([{ ...page, text: band, images }])); return;
|
|
717
|
+
}
|
|
718
|
+
for (const left of [true, false]) blocks.push(...layoutBlocks([{ ...page, text: band.filter(fragment => left ? fragment.left + fragment.width <= gutter : fragment.left >= gutter), images: images.filter(image => left ? image.left + image.width <= gutter : image.left >= gutter) }]));
|
|
719
|
+
} else {
|
|
720
|
+
if (band.length >= 6 && cellRows.filter(row => row.length >= 2 && row.some(cell => band.includes(cell[0]!))).length >= 3) warn("reading-order-uncertain");
|
|
721
|
+
blocks.push(...layoutBlocks([{ ...page, text: band, images }]));
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
// Spanning headings split vertical regions; tables retain their exact place.
|
|
725
|
+
const bodyFont = weightedBodyFontSize(pageLines(page));
|
|
726
|
+
const gutterVotes = new Map<number, number>();
|
|
727
|
+
for (const row of cellRows) for (let i = 1; i < row.length; i++) {
|
|
728
|
+
const left = row[i - 1]!.at(-1)!, right = row[i]![0]!;
|
|
729
|
+
if (right.left - (left.left + left.width) < Math.max(12, left.fontSize * 1.5)) continue;
|
|
730
|
+
const gutter = Math.round((left.left + left.width + right.left) / 20) * 10;
|
|
731
|
+
gutterVotes.set(gutter, (gutterVotes.get(gutter) ?? 0) + 1);
|
|
732
|
+
}
|
|
733
|
+
const repeatedGutters = [...gutterVotes].filter(([, votes]) => votes >= 3).map(([gutter]) => gutter);
|
|
734
|
+
const spans = remaining.filter(fragment => fragment.width > page.width * 0.65 || (fragment.fontSize > bodyFont * 1.08 && repeatedGutters.some(gutter => fragment.left < gutter && fragment.left + fragment.width > gutter)));
|
|
735
|
+
const boundaries = [...tables.map(table => ({ top: table.top, bottom: table.bottom, table, span: null as PdfTextFragment | null })), ...spans.map(span => ({ top: span.top, bottom: span.top + span.height, table: null, span }))].sort((a, b) => a.top - b.top);
|
|
736
|
+
const emitted = new Set<PdfTextFragment>(), emittedImages = new Set<PdfImageCandidate>();
|
|
737
|
+
for (const boundary of boundaries) {
|
|
738
|
+
const band = remaining.filter(fragment => !emitted.has(fragment) && fragment !== boundary.span && fragment.top < boundary.top);
|
|
739
|
+
const images = page.images.filter(image => !emittedImages.has(image) && image.top < boundary.top);
|
|
740
|
+
emitBand(band, images); for (const fragment of band) emitted.add(fragment); for (const image of images) emittedImages.add(image);
|
|
741
|
+
if (boundary.table !== null) blocks.push(boundary.table.block);
|
|
742
|
+
if (boundary.span !== null) {
|
|
743
|
+
const span = boundary.span;
|
|
744
|
+
blocks.push(span.fontSize > bodyFont * 1.08 ? { kind: "heading", page: page.page, level: 2, text: span.text } : { kind: "paragraph", page: page.page, text: span.text });
|
|
745
|
+
emitted.add(span);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
emitBand(remaining.filter(fragment => !emitted.has(fragment)), page.images.filter(image => !emittedImages.has(image)));
|
|
749
|
+
}
|
|
750
|
+
return { blocks, issues };
|
|
751
|
+
}
|
package/src/pdf/markdown.ts
CHANGED
|
@@ -24,7 +24,7 @@ export type ResolvedPdfImage = {
|
|
|
24
24
|
function escapeInline(value: string): string {
|
|
25
25
|
return sanitizeTerminalText(value)
|
|
26
26
|
.replace(/\\/gu, "\\\\")
|
|
27
|
-
.replace(/([`*_[\]{}
|
|
27
|
+
.replace(/([`*_[\]{}<>#|])/gu, "\\$1")
|
|
28
28
|
.replace(/\s+/gu, " ")
|
|
29
29
|
.trim();
|
|
30
30
|
}
|
|
@@ -81,6 +81,12 @@ function markdownContent(
|
|
|
81
81
|
lines.push(imageMarkdown(resolved), "");
|
|
82
82
|
continue;
|
|
83
83
|
}
|
|
84
|
+
if (block.kind === "table") {
|
|
85
|
+
textBlockCount += 1;
|
|
86
|
+
const rows = block.rows.map(row => `| ${row.map(cell => escapeInline(cell)).join(" | ")} |`);
|
|
87
|
+
if (rows.length > 0) lines.push(rows[0]!, `| ${block.rows[0]!.map(() => "---").join(" | ")} |`, ...rows.slice(1), "");
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
84
90
|
if (block.kind === "heading") {
|
|
85
91
|
headingCount += 1;
|
|
86
92
|
lines.push(`${"#".repeat(Math.max(2, Math.min(6, block.level)))} ${escapeInline(block.text)}`, "");
|
package/src/pdf/model.ts
CHANGED
|
@@ -126,7 +126,14 @@ export type PdfManifestImage = PdfBounds & {
|
|
|
126
126
|
readonly metadata: PdfImageSemanticMetadata | null;
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
+
export type PdfQualityIssue = {
|
|
130
|
+
readonly code: "reading-order-uncertain" | "table-structure-uncertain" | "math-structure-uncertain" | "ocr-needed";
|
|
131
|
+
readonly page: number;
|
|
132
|
+
};
|
|
133
|
+
|
|
129
134
|
export type PdfCaptureManifest = {
|
|
135
|
+
readonly markdown?: { readonly path: string; readonly bytes: number; readonly sha256: string };
|
|
136
|
+
readonly quality?: { readonly schemaVersion: 1; readonly issues: readonly PdfQualityIssue[] };
|
|
130
137
|
readonly schemaVersion: typeof PDF_CAPTURE_MANIFEST_SCHEMA_VERSION;
|
|
131
138
|
readonly kind: "pdf";
|
|
132
139
|
readonly capturedAt: string;
|
|
@@ -204,6 +211,8 @@ export type PdfCaptureOutcome = {
|
|
|
204
211
|
};
|
|
205
212
|
|
|
206
213
|
export type PdfToolCommand = {
|
|
214
|
+
/** An external supervisor may own the inherited group and its final cleanup. */
|
|
215
|
+
readonly processGroup?: "new" | "inherit";
|
|
207
216
|
readonly command: readonly string[];
|
|
208
217
|
readonly timeoutMs: number;
|
|
209
218
|
readonly maxOutputBytes: number;
|
package/src/pdf/persist.ts
CHANGED
|
@@ -212,9 +212,15 @@ export function persistPdfCapture(input: PersistPdfCaptureInput): string {
|
|
|
212
212
|
chmodSync(destination, 0o644);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
const markdownBytes = `${redactSensitiveText(input.markdown).trimEnd()}\n`;
|
|
216
|
+
const markdownDescriptor = { path: `${slug}.md`, bytes: Buffer.byteLength(markdownBytes), sha256: createHash("sha256").update(markdownBytes).digest("hex") };
|
|
217
|
+
if (input.manifest.markdown !== undefined && (input.manifest.markdown.path !== markdownDescriptor.path || input.manifest.markdown.bytes !== markdownDescriptor.bytes || input.manifest.markdown.sha256 !== markdownDescriptor.sha256)) {
|
|
218
|
+
throw new Error("PDF Markdown does not match its manifest provenance");
|
|
219
|
+
}
|
|
220
|
+
const manifest = { ...input.manifest, markdown: markdownDescriptor };
|
|
215
221
|
writeFileSync(
|
|
216
222
|
join(stagingDirectory, `${slug}.md`),
|
|
217
|
-
|
|
223
|
+
markdownBytes,
|
|
218
224
|
{ encoding: "utf8", flag: "wx", mode: 0o644 },
|
|
219
225
|
);
|
|
220
226
|
if (input.manifest.annotations === null) {
|
|
@@ -253,7 +259,7 @@ export function persistPdfCapture(input: PersistPdfCaptureInput): string {
|
|
|
253
259
|
}
|
|
254
260
|
writeFileSync(
|
|
255
261
|
join(stagingDirectory, PDF_CAPTURE_MANIFEST_FILENAME),
|
|
256
|
-
`${JSON.stringify(
|
|
262
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
257
263
|
{ encoding: "utf8", flag: "wx", mode: 0o644 },
|
|
258
264
|
);
|
|
259
265
|
assertStagingTreeSafe(stagingDirectory);
|
package/src/pdf/source.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PdfCaptureError } from "./errors.js";
|
|
1
2
|
import {
|
|
2
3
|
chmodSync,
|
|
3
4
|
mkdtempSync,
|
|
@@ -37,13 +38,13 @@ function parseRemoteUrl(input: string): URL | null {
|
|
|
37
38
|
try {
|
|
38
39
|
url = new URL(input);
|
|
39
40
|
} catch {
|
|
40
|
-
throw new
|
|
41
|
+
throw new PdfCaptureError("invalid-input", "PDF URL is invalid");
|
|
41
42
|
}
|
|
42
43
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
43
|
-
throw new
|
|
44
|
+
throw new PdfCaptureError("invalid-input", "PDF URL must use HTTP or HTTPS");
|
|
44
45
|
}
|
|
45
46
|
if (url.username !== "" || url.password !== "") {
|
|
46
|
-
throw new
|
|
47
|
+
throw new PdfCaptureError("invalid-input", "PDF URL must not contain embedded credentials");
|
|
47
48
|
}
|
|
48
49
|
return url;
|
|
49
50
|
}
|
|
@@ -69,7 +70,7 @@ function assertPdfSignature(bytes: Uint8Array): void {
|
|
|
69
70
|
bytes.byteLength < 5
|
|
70
71
|
|| new TextDecoder().decode(bytes.subarray(0, 5)) !== "%PDF-"
|
|
71
72
|
) {
|
|
72
|
-
throw new
|
|
73
|
+
throw new PdfCaptureError("invalid-input", "remote PDF input does not have a valid PDF signature");
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
|
package/src/pdf/tools.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PdfCaptureError } from "./errors.js";
|
|
1
2
|
import { spawn } from "node:child_process";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
@@ -26,7 +27,7 @@ async function readBoundedStream(stream: NodeJS.ReadableStream, maxBytes: number
|
|
|
26
27
|
if (typeof value === "string") chunk = new TextEncoder().encode(value);
|
|
27
28
|
else if (value instanceof Uint8Array) chunk = value;
|
|
28
29
|
else throw new Error("PDF tool returned an unsupported output chunk");
|
|
29
|
-
if (!bytes.append(chunk)) throw new
|
|
30
|
+
if (!bytes.append(chunk)) throw new PdfCaptureError("limit-exceeded", `PDF tool output exceeded ${maxBytes} bytes`);
|
|
30
31
|
}
|
|
31
32
|
return new TextDecoder().decode(bytes.toUint8Array());
|
|
32
33
|
}
|
|
@@ -37,7 +38,8 @@ export const runPdfToolCommand: PdfToolRunner = async (
|
|
|
37
38
|
): Promise<PdfToolCommandResult> => {
|
|
38
39
|
const executable = specification.command[0];
|
|
39
40
|
if (executable === undefined) throw new Error("PDF tool command is empty");
|
|
40
|
-
|
|
41
|
+
if (specification.processGroup !== undefined && !["new", "inherit"].includes(specification.processGroup)) throw new PdfCaptureError("invalid-input", "Invalid PDF process group policy");
|
|
42
|
+
const useProcessGroup = process.platform !== "win32" && specification.processGroup !== "inherit";
|
|
41
43
|
const child = spawn(executable, specification.command.slice(1), {
|
|
42
44
|
cwd: specification.cwd,
|
|
43
45
|
detached: useProcessGroup,
|
|
@@ -51,7 +53,7 @@ export const runPdfToolCommand: PdfToolRunner = async (
|
|
|
51
53
|
windowsHide: true,
|
|
52
54
|
});
|
|
53
55
|
const exited = new Promise<number>((resolveExit, rejectExit) => {
|
|
54
|
-
child.once("error", rejectExit);
|
|
56
|
+
child.once("error", (error: NodeJS.ErrnoException) => rejectExit(error.code === "ENOENT" ? new PdfCaptureError("tool-unavailable", "PDF tool executable is unavailable", { cause: error }) : error));
|
|
55
57
|
child.once("close", (code) => resolveExit(code ?? 1));
|
|
56
58
|
});
|
|
57
59
|
const signalProcessTree = (signal: NodeJS.Signals): void => {
|
|
@@ -79,7 +81,7 @@ export const runPdfToolCommand: PdfToolRunner = async (
|
|
|
79
81
|
forceKillTimer = setTimeout(() => signalProcessTree("SIGKILL"), 1_000);
|
|
80
82
|
};
|
|
81
83
|
const timer = setTimeout(() => {
|
|
82
|
-
requestStop(new
|
|
84
|
+
requestStop(new PdfCaptureError("timeout", `PDF tool timed out after ${specification.timeoutMs}ms`));
|
|
83
85
|
}, specification.timeoutMs);
|
|
84
86
|
|
|
85
87
|
try {
|
|
@@ -137,10 +139,10 @@ export function resolvePdfTools(dependencies: PdfCaptureDependencies = {}): PdfT
|
|
|
137
139
|
? discoverExecutable("tesseract", dependencies)
|
|
138
140
|
: dependencies.tools.tesseract;
|
|
139
141
|
if (pdfinfo === null) {
|
|
140
|
-
throw new
|
|
142
|
+
throw new PdfCaptureError("tool-unavailable", "pdfinfo is required for PDF ingestion; install the Poppler command-line tools");
|
|
141
143
|
}
|
|
142
144
|
if (pdftohtml === null) {
|
|
143
|
-
throw new
|
|
145
|
+
throw new PdfCaptureError("tool-unavailable", "pdftohtml is required for PDF ingestion; install the Poppler command-line tools");
|
|
144
146
|
}
|
|
145
147
|
return { pdfinfo, pdftohtml, tesseract };
|
|
146
148
|
}
|
package/src/pdf.ts
CHANGED
|
@@ -90,3 +90,13 @@ export type {
|
|
|
90
90
|
PdfToolPaths,
|
|
91
91
|
PdfToolRunner,
|
|
92
92
|
} from "./pdf/model.js";
|
|
93
|
+
|
|
94
|
+
import { inspectPdfCaptureBundle as inspectPdfCaptureBundleImplementation, verifyPdfCaptureBundle as verifyPdfCaptureBundleImplementation } from "./pdf/bundle-reader.js";
|
|
95
|
+
import { PdfCaptureError as PdfCaptureErrorImplementation } from "./pdf/errors.js";
|
|
96
|
+
export const inspectPdfCaptureBundle: typeof inspectPdfCaptureBundleImplementation = (...arguments_) => inspectPdfCaptureBundleImplementation(...arguments_);
|
|
97
|
+
export const verifyPdfCaptureBundle: typeof verifyPdfCaptureBundleImplementation = (...arguments_) => verifyPdfCaptureBundleImplementation(...arguments_);
|
|
98
|
+
export const PdfCaptureError = PdfCaptureErrorImplementation;
|
|
99
|
+
export type PdfCaptureError = InstanceType<typeof PdfCaptureErrorImplementation>;
|
|
100
|
+
export type { PdfCaptureErrorCode } from "./pdf/errors.js";
|
|
101
|
+
export type { PdfBundleInspection, PdfBundleVerification, ReadPdfCaptureBundleOptions, PdfBundleIntegrity } from "./pdf/bundle-reader.js";
|
|
102
|
+
export type { PdfQualityIssue } from "./pdf/model.js";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { SearchReranker } from "./rerank.js";
|
|
2
|
+
import { createHostedReranker, type SystemOneTransport, type TypeSafeRerankerOptions } from "./rerank-typesafe.js";
|
|
3
|
+
|
|
4
|
+
export const CLEF_MODELS = ["clef", "clef-flash"] as const;
|
|
5
|
+
export type ClefModel = (typeof CLEF_MODELS)[number];
|
|
6
|
+
export const DEFAULT_CLEF_MODEL: ClefModel = "clef";
|
|
7
|
+
export type ClefTransport = SystemOneTransport;
|
|
8
|
+
export type ClefRerankerOptions = TypeSafeRerankerOptions & { readonly model?: ClefModel };
|
|
9
|
+
|
|
10
|
+
export function clefEndpoint(accountId: string, model: ClefModel = DEFAULT_CLEF_MODEL): string {
|
|
11
|
+
if (typeof accountId !== "string" || !/^[a-f0-9]{32}$/u.test(accountId) || !CLEF_MODELS.includes(model)) {
|
|
12
|
+
throw new Error("Invalid Cloudflare Clef account or model.");
|
|
13
|
+
}
|
|
14
|
+
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run/@cf/cloudflare/${model}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function unwrapClefResponse(value: unknown): unknown {
|
|
18
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
19
|
+
const envelope = value as Record<string, unknown>;
|
|
20
|
+
if (envelope["success"] !== true || !Object.hasOwn(envelope, "result")
|
|
21
|
+
|| (envelope["errors"] !== undefined && (!Array.isArray(envelope["errors"]) || envelope["errors"].length !== 0))) return null;
|
|
22
|
+
return envelope["result"];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createClefReranker(options: ClefRerankerOptions = {}): SearchReranker {
|
|
26
|
+
const environment = options.environment ?? process.env;
|
|
27
|
+
const accountId = Object.hasOwn(environment, "CLOUDFLARE_ACCOUNT_ID") ? environment["CLOUDFLARE_ACCOUNT_ID"] : undefined;
|
|
28
|
+
const apiKey = Object.hasOwn(environment, "CLOUDFLARE_API_TOKEN")
|
|
29
|
+
? environment["CLOUDFLARE_API_TOKEN"]
|
|
30
|
+
: Object.hasOwn(environment, "CLOUDFLARE_AUTH_TOKEN") ? environment["CLOUDFLARE_AUTH_TOKEN"] : undefined;
|
|
31
|
+
const model = options.model === undefined ? DEFAULT_CLEF_MODEL : options.model;
|
|
32
|
+
let endpoint: string | undefined;
|
|
33
|
+
try { if (accountId !== undefined) endpoint = clefEndpoint(accountId, model); } catch { endpoint = undefined; }
|
|
34
|
+
const reranker = createHostedReranker(options, {
|
|
35
|
+
id: "clef", label: "Cloudflare Clef", model, endpoint, apiKey,
|
|
36
|
+
unavailableMessage: "Cloudflare Clef credentials or model are missing or invalid; set CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN.",
|
|
37
|
+
unwrap: unwrapClefResponse,
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
id: reranker.id,
|
|
41
|
+
rerank: async (request) => {
|
|
42
|
+
try {
|
|
43
|
+
if (typeof request === "object" && request !== null && "images" in request) {
|
|
44
|
+
return { status: "failed", message: "Wordcell reranking accepts text only; image evidence is not supported." };
|
|
45
|
+
}
|
|
46
|
+
return await reranker.rerank(request);
|
|
47
|
+
} catch {
|
|
48
|
+
return { status: "failed", message: "Cloudflare Clef rerank request was malformed or failed." };
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -4,9 +4,10 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { isAbsolute, join } from "node:path";
|
|
5
5
|
import type { SearchReranker } from "./rerank.js";
|
|
6
6
|
import { createTypeSafeReranker } from "./rerank-typesafe.js";
|
|
7
|
+
import { createClefReranker, type ClefModel } from "./rerank-clef.js";
|
|
7
8
|
|
|
8
|
-
function unavailable(message: string): SearchReranker {
|
|
9
|
-
return { id
|
|
9
|
+
function unavailable(message: string, id = "typesafe"): SearchReranker {
|
|
10
|
+
return { id, rerank: async () => ({ status: "unavailable", message }) };
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
/** CLI-only credential discovery, called only after explicit --rerank typesafe. */
|
|
@@ -54,3 +55,11 @@ export async function createCliTypeSafeReranker(
|
|
|
54
55
|
await handle?.close();
|
|
55
56
|
}
|
|
56
57
|
}
|
|
58
|
+
|
|
59
|
+
export async function createCliClefReranker(
|
|
60
|
+
environment: Readonly<Record<string, string | undefined>> = process.env,
|
|
61
|
+
_homeDirectory: string = homedir(),
|
|
62
|
+
model: ClefModel = "clef",
|
|
63
|
+
): Promise<SearchReranker> {
|
|
64
|
+
return createClefReranker({ environment, model });
|
|
65
|
+
}
|
package/src/rerank-typesafe.ts
CHANGED
|
@@ -154,11 +154,11 @@ function tokenCount(value: unknown): number | null {
|
|
|
154
154
|
: null;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
function acceptedResponseModel(value: unknown): value is string {
|
|
158
|
-
return value ===
|
|
157
|
+
function acceptedResponseModel(value: unknown, model: string): value is string {
|
|
158
|
+
return value === model;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
function parseSystemOneResponse(value: unknown): ParsedSystemOneAnswer | null {
|
|
161
|
+
function parseSystemOneResponse(value: unknown, model: string): ParsedSystemOneAnswer | null {
|
|
162
162
|
const root = recordValue(value);
|
|
163
163
|
if (root === null || !hasExactKeys(root, ["model", "answers", "usage"])) return null;
|
|
164
164
|
const answers = recordValue(root["answers"]);
|
|
@@ -176,7 +176,7 @@ function parseSystemOneResponse(value: unknown): ParsedSystemOneAnswer | null {
|
|
|
176
176
|
const inputTokens = tokenCount(usage["input_tokens"]);
|
|
177
177
|
const outputTokens = tokenCount(usage["output_tokens"]);
|
|
178
178
|
if (
|
|
179
|
-
!acceptedResponseModel(root["model"])
|
|
179
|
+
!acceptedResponseModel(root["model"], model)
|
|
180
180
|
|| inputTokens === null
|
|
181
181
|
|| outputTokens === null
|
|
182
182
|
) return null;
|
|
@@ -205,7 +205,7 @@ async function mapWithConcurrency<T, R>(
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
function validApiKey(value: string | undefined): value is string {
|
|
208
|
-
return value
|
|
208
|
+
return typeof value === "string" && /^[\x21-\x7e]{1,512}$/u.test(value);
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
function boundedPositiveInteger(
|
|
@@ -274,6 +274,30 @@ type ScoredCandidate =
|
|
|
274
274
|
*/
|
|
275
275
|
export function createTypeSafeReranker(
|
|
276
276
|
options: TypeSafeRerankerOptions = {},
|
|
277
|
+
): SearchReranker {
|
|
278
|
+
const environment = options.environment ?? process.env;
|
|
279
|
+
return createHostedReranker(options, {
|
|
280
|
+
id: "typesafe", label: "TypeSafe", model: DEFAULT_SYSTEMONE_MODEL,
|
|
281
|
+
endpoint: DEFAULT_SYSTEMONE_ENDPOINT,
|
|
282
|
+
apiKey: Object.hasOwn(environment, "TYPESAFE_API_KEY") ? environment["TYPESAFE_API_KEY"] : undefined,
|
|
283
|
+
unavailableMessage: "TYPESAFE_API_KEY is missing or invalid; the TypeSafe rerank lane is unavailable.",
|
|
284
|
+
unwrap: (value) => value,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export type HostedRerankProvider = {
|
|
289
|
+
readonly id: string;
|
|
290
|
+
readonly label: string;
|
|
291
|
+
readonly model: string;
|
|
292
|
+
readonly endpoint: string | undefined;
|
|
293
|
+
readonly apiKey: string | undefined;
|
|
294
|
+
readonly unavailableMessage: string;
|
|
295
|
+
readonly unwrap: (value: unknown) => unknown;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export function createHostedReranker(
|
|
299
|
+
options: TypeSafeRerankerOptions,
|
|
300
|
+
provider: HostedRerankProvider,
|
|
277
301
|
): SearchReranker {
|
|
278
302
|
const transport = options.transport ?? fetchSystemOneTransport;
|
|
279
303
|
const timeoutMs = boundedPositiveInteger(
|
|
@@ -291,13 +315,10 @@ export function createTypeSafeReranker(
|
|
|
291
315
|
DEFAULT_RERANK_CONCURRENCY,
|
|
292
316
|
MAX_RERANK_CONCURRENCY,
|
|
293
317
|
);
|
|
294
|
-
const
|
|
295
|
-
const apiKey = Object.hasOwn(environment, "TYPESAFE_API_KEY")
|
|
296
|
-
? environment["TYPESAFE_API_KEY"]
|
|
297
|
-
: undefined;
|
|
318
|
+
const apiKey = provider.apiKey;
|
|
298
319
|
|
|
299
320
|
return {
|
|
300
|
-
id:
|
|
321
|
+
id: provider.id,
|
|
301
322
|
rerank: async (request: SearchRerankRequest): Promise<SearchRerankResult> => {
|
|
302
323
|
const started = performance.now();
|
|
303
324
|
const controller = new AbortController();
|
|
@@ -330,7 +351,7 @@ export function createTypeSafeReranker(
|
|
|
330
351
|
}),
|
|
331
352
|
};
|
|
332
353
|
const failed = (message: string): SearchRerankResult => ({
|
|
333
|
-
status: "failed", message, ...details(),
|
|
354
|
+
status: "failed", message: message.replaceAll("TypeSafe", provider.label), ...details(),
|
|
334
355
|
});
|
|
335
356
|
try {
|
|
336
357
|
if (timeoutMs === null || maxResponseBytes === null || concurrency === null) {
|
|
@@ -344,8 +365,8 @@ export function createTypeSafeReranker(
|
|
|
344
365
|
if (callerSignal?.aborted === true) {
|
|
345
366
|
return failed("TypeSafe rerank request was aborted.");
|
|
346
367
|
}
|
|
347
|
-
if (!validApiKey(apiKey)) {
|
|
348
|
-
return { status: "unavailable", message:
|
|
368
|
+
if (!validApiKey(apiKey) || provider.endpoint === undefined) {
|
|
369
|
+
return { status: "unavailable", message: provider.unavailableMessage };
|
|
349
370
|
}
|
|
350
371
|
if (candidateCount === 0) return { status: "ready", ordering: [], probabilities: {} };
|
|
351
372
|
const encoder = new TextEncoder();
|
|
@@ -359,12 +380,14 @@ export function createTypeSafeReranker(
|
|
|
359
380
|
if (encoder.encode(JSON.stringify(state)).byteLength > MAX_RERANK_STATE_BYTES) {
|
|
360
381
|
return failed(`TypeSafe rerank state exceeds the ${MAX_RERANK_STATE_BYTES}-byte limit.`);
|
|
361
382
|
}
|
|
362
|
-
|
|
363
|
-
model:
|
|
383
|
+
const body = encoder.encode(JSON.stringify({
|
|
384
|
+
model: provider.model, state,
|
|
364
385
|
questions: { [RELEVANCE_QUESTION]: {
|
|
365
386
|
type: "noul", instructions: RELEVANCE_INSTRUCTIONS, criteria: RELEVANCE_CRITERIA,
|
|
366
387
|
} },
|
|
367
|
-
}))
|
|
388
|
+
}));
|
|
389
|
+
if (body.byteLength > 32 * 1_024) return failed("TypeSafe rerank body exceeds the 32768-byte limit.");
|
|
390
|
+
bodies.push(body);
|
|
368
391
|
}
|
|
369
392
|
const deadline = started + timeoutMs;
|
|
370
393
|
const timeoutMessage = "TypeSafe rerank window exceeded its deadline.";
|
|
@@ -383,7 +406,7 @@ export function createTypeSafeReranker(
|
|
|
383
406
|
let response: { readonly status: number; readonly body: Uint8Array };
|
|
384
407
|
try {
|
|
385
408
|
response = await transport({
|
|
386
|
-
url:
|
|
409
|
+
url: provider.endpoint!, body,
|
|
387
410
|
headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
|
|
388
411
|
timeoutMs: Math.max(1, Math.ceil(deadline - performance.now())),
|
|
389
412
|
maxResponseBytes, signal: controller.signal,
|
|
@@ -412,7 +435,8 @@ export function createTypeSafeReranker(
|
|
|
412
435
|
return { ok: false, message: firstFailure! };
|
|
413
436
|
}
|
|
414
437
|
// Billing evidence is independent of whether the relevance answer is usable.
|
|
415
|
-
const
|
|
438
|
+
const result = provider.unwrap(parsed);
|
|
439
|
+
const root = recordValue(result);
|
|
416
440
|
const usage = recordValue(root?.["usage"]);
|
|
417
441
|
const input = tokenCount(usage?.["input_tokens"]);
|
|
418
442
|
const output = tokenCount(usage?.["output_tokens"]);
|
|
@@ -426,12 +450,12 @@ export function createTypeSafeReranker(
|
|
|
426
450
|
outputTokens += output;
|
|
427
451
|
usageReceipts += 1;
|
|
428
452
|
}
|
|
429
|
-
if (acceptedResponseModel(root?.["model"])) observedModel =
|
|
453
|
+
if (acceptedResponseModel(root?.["model"], provider.model)) observedModel = provider.model;
|
|
430
454
|
if (response.status !== 200) {
|
|
431
455
|
stop(`TypeSafe rerank request returned HTTP ${response.status}.`);
|
|
432
456
|
return { ok: false, message: firstFailure! };
|
|
433
457
|
}
|
|
434
|
-
const answer = parseSystemOneResponse(
|
|
458
|
+
const answer = parseSystemOneResponse(result, provider.model);
|
|
435
459
|
if (answer === null) {
|
|
436
460
|
stop("TypeSafe rerank response was malformed.");
|
|
437
461
|
return { ok: false, message: firstFailure! };
|
package/src/rerank.ts
CHANGED
|
@@ -56,7 +56,7 @@ export type SearchReranker = {
|
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
export type KnowledgeBaseSearchRerankOptions = {
|
|
59
|
-
readonly engine: "typesafe";
|
|
59
|
+
readonly engine: "clef" | "typesafe";
|
|
60
60
|
/** Window size submitted to the engine, from 2 through MAX_RERANK_CANDIDATES. */
|
|
61
61
|
readonly limit?: number;
|
|
62
62
|
/** Cancels only the hosted rerank phase, not local retrieval or indexing. */
|
package/src/sdk.ts
CHANGED
|
@@ -208,6 +208,7 @@ export type KnowledgeBaseSearchHit = {
|
|
|
208
208
|
|
|
209
209
|
export type KnowledgeBaseSearchDiagnostic = {
|
|
210
210
|
readonly lane: "exact" | "git" | "graph" | "qmd" | "rerank";
|
|
211
|
+
readonly engine?: string;
|
|
211
212
|
/** Structured hosted-lane accounting, including known usage after fallback. */
|
|
212
213
|
readonly rerank?: SearchRerankDetails;
|
|
213
214
|
readonly status: "degraded" | "ready" | "unavailable";
|
|
@@ -560,8 +561,8 @@ function checkedRerankRequest(
|
|
|
560
561
|
throw new TypeError("Search rerank options must be an options object.");
|
|
561
562
|
}
|
|
562
563
|
const options = value as KnowledgeBaseSearchRerankOptions;
|
|
563
|
-
if (options.engine !== "typesafe") {
|
|
564
|
-
throw new Error('Search rerank engine must be "typesafe".');
|
|
564
|
+
if (options.engine !== "clef" && options.engine !== "typesafe") {
|
|
565
|
+
throw new Error('Search rerank engine must be "clef" or legacy "typesafe".');
|
|
565
566
|
}
|
|
566
567
|
const reranker = rerankers?.find((entry) => entry.id === options.engine);
|
|
567
568
|
if (reranker === undefined) {
|
|
@@ -1034,6 +1035,7 @@ export async function openKnowledgeBase(
|
|
|
1034
1035
|
: (applied.message ?? "Rerank engine did not return a result.");
|
|
1035
1036
|
diagnostics.push({
|
|
1036
1037
|
lane: "rerank",
|
|
1038
|
+
engine: rerankRequest.reranker.id,
|
|
1037
1039
|
...(Object.keys(resultDetails).length === 0 ? {} : { rerank: resultDetails }),
|
|
1038
1040
|
status: applied.status === "failed" ? "degraded" : applied.status,
|
|
1039
1041
|
results: applied.status === "ready" ? applied.placements.size : 0,
|
|
File without changes
|