@claritylabs/cl-sdk 3.0.17 → 3.0.18
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 +2 -2
- package/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +395 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +395 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ npm install @claritylabs/cl-sdk pdf-lib zod
|
|
|
12
12
|
|
|
13
13
|
## What It Does
|
|
14
14
|
|
|
15
|
-
- **Document Extraction** — Source-tree extraction that turns parser-provided PDF spans into a canonical hierarchy of document, page group, form, endorsement, section, schedule, clause, table, row, cell, and text nodes. Operational policy facts are
|
|
15
|
+
- **Document Extraction** — Source-tree extraction that turns parser-provided PDF spans into a canonical hierarchy of document, page group, form, endorsement, section, schedule, clause, table, row, cell, and text nodes. Form-inventory page ranges and parser title elements guide the hierarchy, but every node remains backed by real source spans. Operational policy facts are projections from that tree, not the canonical source of truth.
|
|
16
16
|
- **Source Grounding** — Shared source spans, source nodes, hierarchical table row/cell evidence, source stores, quoted evidence validation, and deterministic evidence ordering across extraction, query, application, PCE, and case workflows.
|
|
17
17
|
- **Query Agent** — Citation-backed question answering over stored source nodes, exact source spans, and inbound photos/PDFs/text with sub-question decomposition, bounded retrieval planning, attachment-only reasoning when retrieval is unnecessary, and grounding verification.
|
|
18
18
|
- **Application Processing** — Bounded workflows handle intake with deterministic planning — field extraction, prior-answer backfill, context auto-fill, document lookup gating, topic-based question batching, reply parsing, source-backed field provenance, and PDF mapping
|
|
@@ -81,7 +81,7 @@ const extractor = createExtractor({ generateText, generateObject, sourceStore })
|
|
|
81
81
|
const result = await extractor.extract(pdfBase64, "policy-123", { sourceSpans });
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
When source spans are available, extraction returns `sourceTree`, `sourceSpans`, `operationalProfile`, `warnings`, `tokenUsage`, and `performanceReport`. The source tree is canonical for policy wording and hierarchy. The operational profile contains policy metadata, parties, coverage lines, limits, deductibles, premiums, key dates, and endorsement-support facts, each with `sourceNodeIds` and `sourceSpanIds`.
|
|
84
|
+
When source spans are available, extraction returns `sourceTree`, `sourceSpans`, `operationalProfile`, `warnings`, `tokenUsage`, and `performanceReport`. The source tree is canonical for policy wording and hierarchy. The extractor uses form/page-range hints to group declarations, policy forms, and endorsements, then promotes title elements into section/schedule nodes that can span page breaks. The operational profile contains policy metadata, parties, coverage lines, limits, deductibles, premiums, key dates, and endorsement-support facts, each with `sourceNodeIds` and `sourceSpanIds`.
|
|
85
85
|
|
|
86
86
|
Store `result.sourceTree` in a retrievable node index and embed node `description` values for search. Keep `result.sourceSpans` as the exact PDF highlighting layer. `result.document` and its `documentOutline` remain compatibility projections for existing host screens; do not treat broad structured policy JSON as canonical extraction truth.
|
|
87
87
|
|
package/dist/index.d.mts
CHANGED
|
@@ -34613,6 +34613,8 @@ interface ExtractionResult {
|
|
|
34613
34613
|
interface ExtractOptions {
|
|
34614
34614
|
/** Resume extraction from a previously saved checkpoint. */
|
|
34615
34615
|
resumeFrom?: PipelineCheckpoint<ExtractionState>;
|
|
34616
|
+
/** Optional form inventory/page-range hints to guide v3 source-tree hierarchy when source spans are provided. */
|
|
34617
|
+
formInventory?: FormInventoryResult;
|
|
34616
34618
|
/** Caller-provided raw source spans for this document, reused for evidence grounding and optional persistence. */
|
|
34617
34619
|
sourceSpans?: SourceSpan[];
|
|
34618
34620
|
}
|
|
@@ -34621,10 +34623,19 @@ declare function createExtractor(config: ExtractorConfig): {
|
|
|
34621
34623
|
extract: (input: ExtractionInput, documentId?: string, options?: ExtractOptions) => Promise<ExtractionResult>;
|
|
34622
34624
|
};
|
|
34623
34625
|
|
|
34626
|
+
type SourceTreeFormHint = {
|
|
34627
|
+
formNumber?: string;
|
|
34628
|
+
editionDate?: string;
|
|
34629
|
+
title?: string;
|
|
34630
|
+
formType: "coverage" | "endorsement" | "declarations" | "application" | "notice" | "other";
|
|
34631
|
+
pageStart?: number;
|
|
34632
|
+
pageEnd?: number;
|
|
34633
|
+
};
|
|
34624
34634
|
type ExtractionV3Result = {
|
|
34625
34635
|
sourceTree: DocumentSourceNode[];
|
|
34626
34636
|
sourceSpans: SourceSpan[];
|
|
34627
34637
|
sourceChunks: SourceChunk[];
|
|
34638
|
+
formInventory: SourceTreeFormHint[];
|
|
34628
34639
|
operationalProfile: PolicyOperationalProfile;
|
|
34629
34640
|
document: InsuranceDocument;
|
|
34630
34641
|
chunks: [];
|
package/dist/index.d.ts
CHANGED
|
@@ -34613,6 +34613,8 @@ interface ExtractionResult {
|
|
|
34613
34613
|
interface ExtractOptions {
|
|
34614
34614
|
/** Resume extraction from a previously saved checkpoint. */
|
|
34615
34615
|
resumeFrom?: PipelineCheckpoint<ExtractionState>;
|
|
34616
|
+
/** Optional form inventory/page-range hints to guide v3 source-tree hierarchy when source spans are provided. */
|
|
34617
|
+
formInventory?: FormInventoryResult;
|
|
34616
34618
|
/** Caller-provided raw source spans for this document, reused for evidence grounding and optional persistence. */
|
|
34617
34619
|
sourceSpans?: SourceSpan[];
|
|
34618
34620
|
}
|
|
@@ -34621,10 +34623,19 @@ declare function createExtractor(config: ExtractorConfig): {
|
|
|
34621
34623
|
extract: (input: ExtractionInput, documentId?: string, options?: ExtractOptions) => Promise<ExtractionResult>;
|
|
34622
34624
|
};
|
|
34623
34625
|
|
|
34626
|
+
type SourceTreeFormHint = {
|
|
34627
|
+
formNumber?: string;
|
|
34628
|
+
editionDate?: string;
|
|
34629
|
+
title?: string;
|
|
34630
|
+
formType: "coverage" | "endorsement" | "declarations" | "application" | "notice" | "other";
|
|
34631
|
+
pageStart?: number;
|
|
34632
|
+
pageEnd?: number;
|
|
34633
|
+
};
|
|
34624
34634
|
type ExtractionV3Result = {
|
|
34625
34635
|
sourceTree: DocumentSourceNode[];
|
|
34626
34636
|
sourceSpans: SourceSpan[];
|
|
34627
34637
|
sourceChunks: SourceChunk[];
|
|
34638
|
+
formInventory: SourceTreeFormHint[];
|
|
34628
34639
|
operationalProfile: PolicyOperationalProfile;
|
|
34629
34640
|
document: InsuranceDocument;
|
|
34630
34641
|
chunks: [];
|
package/dist/index.js
CHANGED
|
@@ -9515,6 +9515,17 @@ var OperationalProfilePromptSchema = import_zod41.z.object({
|
|
|
9515
9515
|
sourceNodeIds: import_zod41.z.array(import_zod41.z.string()).optional(),
|
|
9516
9516
|
sourceSpanIds: import_zod41.z.array(import_zod41.z.string()).optional()
|
|
9517
9517
|
});
|
|
9518
|
+
function formatFormHintsForPrompt(forms) {
|
|
9519
|
+
const usable = forms.filter((form) => typeof form.pageStart === "number" && typeof form.pageEnd === "number").slice(0, 120).map((form) => ({
|
|
9520
|
+
title: form.title,
|
|
9521
|
+
formType: form.formType,
|
|
9522
|
+
formNumber: form.formNumber,
|
|
9523
|
+
editionDate: form.editionDate,
|
|
9524
|
+
pageStart: form.pageStart,
|
|
9525
|
+
pageEnd: form.pageEnd
|
|
9526
|
+
}));
|
|
9527
|
+
return usable.length ? JSON.stringify(usable, null, 2) : "[]";
|
|
9528
|
+
}
|
|
9518
9529
|
function cleanText(value, fallback) {
|
|
9519
9530
|
const text = value?.replace(/\s+/g, " ").trim();
|
|
9520
9531
|
return text || fallback;
|
|
@@ -9612,6 +9623,197 @@ function semanticGroupNodeId(documentId, kind, title, childNodeIds) {
|
|
|
9612
9623
|
childNodeIds.join("_").replace(/[^a-zA-Z0-9_.:-]/g, "_").slice(0, 80)
|
|
9613
9624
|
].join(":");
|
|
9614
9625
|
}
|
|
9626
|
+
function spanPageStart(span) {
|
|
9627
|
+
return span.pageStart ?? span.location?.page ?? span.location?.startPage;
|
|
9628
|
+
}
|
|
9629
|
+
function spanPageEnd2(span) {
|
|
9630
|
+
return span.pageEnd ?? span.location?.endPage ?? spanPageStart(span);
|
|
9631
|
+
}
|
|
9632
|
+
function spanSourceUnit(span) {
|
|
9633
|
+
return span.sourceUnit ?? span.metadata?.sourceUnit ?? span.metadata?.elementType;
|
|
9634
|
+
}
|
|
9635
|
+
function formNumberFromText(value) {
|
|
9636
|
+
return cleanText(value, "").match(/\b[A-Z]{2,}(?:-[A-Z0-9]+)+\s+\d{2}\s+\d{2}\b/)?.[0]?.replace(/\s+/g, " ");
|
|
9637
|
+
}
|
|
9638
|
+
function editionDateFromFormNumber(formNumber) {
|
|
9639
|
+
const match = formNumber?.match(/\b(\d{2})\s+(\d{2})$/);
|
|
9640
|
+
return match ? `${match[1]}/${match[2]}` : void 0;
|
|
9641
|
+
}
|
|
9642
|
+
function pageTitleFromText(text, fallback) {
|
|
9643
|
+
const normalized = cleanText(text, fallback);
|
|
9644
|
+
const patterns = [
|
|
9645
|
+
/\bIMPORTANT NOTICE\s+[—-]\s+HOW TO REPORT A CLAIM\b/i,
|
|
9646
|
+
/\bPRIVACY NOTICE TO POLICYHOLDERS\b/i,
|
|
9647
|
+
/\bOFAC ADVISORY NOTICE\b/i,
|
|
9648
|
+
/\bTERRORISM RISK INSURANCE ACT\s*\(TRIA\)\s*DISCLOSURE AND REJECTION\b/i,
|
|
9649
|
+
/\bDECLARATIONS PAGE\b/i,
|
|
9650
|
+
/\bTECHNOLOGY ERRORS?\s*&\s*OMISSIONS AND CYBER LIABILITY INSURANCE POLICY\b/i,
|
|
9651
|
+
/\bTRADE OR ECONOMIC SANCTIONS LIMITATION\b/i,
|
|
9652
|
+
/\bFORMS? AND ENDORSEMENTS\b/i
|
|
9653
|
+
];
|
|
9654
|
+
for (const pattern of patterns) {
|
|
9655
|
+
const match = normalized.match(pattern)?.[0];
|
|
9656
|
+
if (match) return cleanText(match, fallback);
|
|
9657
|
+
}
|
|
9658
|
+
const endorsement = normalized.match(/\bENDORSEMENT\s+(?:NO\.?|NUMBER|#)\s*[A-Z0-9][A-Z0-9.-]*\b/i)?.[0];
|
|
9659
|
+
if (endorsement) return cleanText(endorsement, fallback);
|
|
9660
|
+
const firstSentence = normalized.split(/(?<=\.)\s+/)[0];
|
|
9661
|
+
if (firstSentence && firstSentence.length <= 120) return firstSentence.replace(/[.]$/, "");
|
|
9662
|
+
return fallback;
|
|
9663
|
+
}
|
|
9664
|
+
function pageFormTypeFromText(text) {
|
|
9665
|
+
if (/\b(declarations?\s+page|declarations?\s+schedule)\b/i.test(text)) return "declarations";
|
|
9666
|
+
if (/\b(endorsement\s+(?:no\.?|number|#)|this endorsement changes the policy|[A-Z]{2,}-END\s+\d{2,})\b/i.test(text)) return "endorsement";
|
|
9667
|
+
if (/\b(technology errors?\s*&?\s*omissions.*liability insurance policy|policy form|coverage form|insuring agreement|definitions?|exclusions?|conditions?)\b/i.test(text)) return "coverage";
|
|
9668
|
+
if (/\b(important notice|privacy notice|ofac advisory|terrorism risk insurance act|tria|trade or economic sanctions)\b/i.test(text)) return "notice";
|
|
9669
|
+
return "other";
|
|
9670
|
+
}
|
|
9671
|
+
function inferFormHintsFromSourceSpans(sourceSpans) {
|
|
9672
|
+
const pageTexts = /* @__PURE__ */ new Map();
|
|
9673
|
+
const pageSpanTexts = /* @__PURE__ */ new Map();
|
|
9674
|
+
for (const span of sourceSpans) {
|
|
9675
|
+
const start = spanPageStart(span);
|
|
9676
|
+
if (typeof start !== "number") continue;
|
|
9677
|
+
const end = spanPageEnd2(span) ?? start;
|
|
9678
|
+
for (let page = start; page <= end; page += 1) {
|
|
9679
|
+
if (spanSourceUnit(span) === "page") {
|
|
9680
|
+
pageSpanTexts.set(page, cleanText(span.text, ""));
|
|
9681
|
+
continue;
|
|
9682
|
+
}
|
|
9683
|
+
const existing = pageTexts.get(page) ?? "";
|
|
9684
|
+
if (existing.length < 4e3) pageTexts.set(page, cleanText([existing, span.text].filter(Boolean).join(" "), ""));
|
|
9685
|
+
}
|
|
9686
|
+
}
|
|
9687
|
+
if (pageSpanTexts.size === 0) return [];
|
|
9688
|
+
const pageHints = [.../* @__PURE__ */ new Set([...pageTexts.keys(), ...pageSpanTexts.keys()])].sort((left, right) => left - right).map((page) => {
|
|
9689
|
+
const text = pageSpanTexts.get(page) ?? pageTexts.get(page) ?? "";
|
|
9690
|
+
const formNumber = formNumberFromText(text);
|
|
9691
|
+
return {
|
|
9692
|
+
formNumber,
|
|
9693
|
+
editionDate: editionDateFromFormNumber(formNumber),
|
|
9694
|
+
title: pageTitleFromText(text, `Page ${page}`),
|
|
9695
|
+
formType: pageFormTypeFromText(text),
|
|
9696
|
+
pageStart: page,
|
|
9697
|
+
pageEnd: page
|
|
9698
|
+
};
|
|
9699
|
+
});
|
|
9700
|
+
const merged = [];
|
|
9701
|
+
for (const hint of pageHints) {
|
|
9702
|
+
const previous = merged[merged.length - 1];
|
|
9703
|
+
const startsNewEndorsement = hint.formType === "endorsement" && /\bendorsement\s+(?:no\.?|number|#)\s*[A-Z0-9]|this endorsement changes the policy/i.test(hint.title ?? "");
|
|
9704
|
+
const canMerge = previous && previous.formType === hint.formType && previous.pageEnd !== void 0 && hint.pageStart === previous.pageEnd + 1 && (hint.formType === "declarations" || hint.formType === "coverage" || hint.formType === "endorsement" && !startsNewEndorsement);
|
|
9705
|
+
if (!canMerge) {
|
|
9706
|
+
merged.push(hint);
|
|
9707
|
+
continue;
|
|
9708
|
+
}
|
|
9709
|
+
previous.pageEnd = hint.pageEnd;
|
|
9710
|
+
previous.title = previous.title ?? hint.title;
|
|
9711
|
+
previous.formNumber = previous.formNumber ?? hint.formNumber;
|
|
9712
|
+
previous.editionDate = previous.editionDate ?? hint.editionDate;
|
|
9713
|
+
}
|
|
9714
|
+
return merged;
|
|
9715
|
+
}
|
|
9716
|
+
function normalizeFormHints(forms, sourceSpans) {
|
|
9717
|
+
const provided = (forms ?? []).filter(
|
|
9718
|
+
(form) => typeof form.pageStart === "number" && typeof form.pageEnd === "number" && form.pageStart > 0 && form.pageEnd >= form.pageStart
|
|
9719
|
+
).map((form) => ({
|
|
9720
|
+
...form,
|
|
9721
|
+
title: form.title ? cleanText(form.title, "") : void 0
|
|
9722
|
+
})).sort(
|
|
9723
|
+
(left, right) => (left.pageStart ?? Number.MAX_SAFE_INTEGER) - (right.pageStart ?? Number.MAX_SAFE_INTEGER) || (left.pageEnd ?? Number.MAX_SAFE_INTEGER) - (right.pageEnd ?? Number.MAX_SAFE_INTEGER)
|
|
9724
|
+
);
|
|
9725
|
+
return provided.length ? provided : inferFormHintsFromSourceSpans(sourceSpans);
|
|
9726
|
+
}
|
|
9727
|
+
function formHintForPage(forms, page) {
|
|
9728
|
+
return forms.find(
|
|
9729
|
+
(form) => typeof form.pageStart === "number" && typeof form.pageEnd === "number" && page >= form.pageStart && page <= form.pageEnd
|
|
9730
|
+
);
|
|
9731
|
+
}
|
|
9732
|
+
function titleFromFormHint(form, fallback) {
|
|
9733
|
+
if (form.formType === "declarations") return "Declarations";
|
|
9734
|
+
if (form.formType === "coverage") return "Policy Form";
|
|
9735
|
+
if (form.formType === "endorsement") return endorsementTitle([form.title, form.formNumber].filter(Boolean).join(" ")) ?? cleanText(form.title, fallback);
|
|
9736
|
+
return cleanText(form.title, fallback);
|
|
9737
|
+
}
|
|
9738
|
+
function formGroupConfig(form) {
|
|
9739
|
+
if (form.formType === "declarations") {
|
|
9740
|
+
return {
|
|
9741
|
+
kind: "page_group",
|
|
9742
|
+
title: "Declarations",
|
|
9743
|
+
description: "Declarations pages and schedules grouped from form inventory",
|
|
9744
|
+
organizer: "form_inventory_declarations_grouping"
|
|
9745
|
+
};
|
|
9746
|
+
}
|
|
9747
|
+
if (form.formType === "coverage") {
|
|
9748
|
+
return {
|
|
9749
|
+
kind: "form",
|
|
9750
|
+
title: "Policy Form",
|
|
9751
|
+
description: "Policy form pages grouped from form inventory",
|
|
9752
|
+
organizer: "form_inventory_policy_form_grouping"
|
|
9753
|
+
};
|
|
9754
|
+
}
|
|
9755
|
+
if (form.formType === "endorsement") {
|
|
9756
|
+
const title = titleFromFormHint(form, "Endorsement");
|
|
9757
|
+
return {
|
|
9758
|
+
kind: "endorsement",
|
|
9759
|
+
title,
|
|
9760
|
+
description: `${title} grouped from form inventory`,
|
|
9761
|
+
organizer: "form_inventory_endorsement_grouping"
|
|
9762
|
+
};
|
|
9763
|
+
}
|
|
9764
|
+
return void 0;
|
|
9765
|
+
}
|
|
9766
|
+
function applyFormInventoryHints(sourceTree, forms) {
|
|
9767
|
+
if (forms.length === 0) return sourceTree;
|
|
9768
|
+
const rootId = sourceTreeRootId(sourceTree);
|
|
9769
|
+
if (!rootId) return sourceTree;
|
|
9770
|
+
const byParent = nodesByParent(sourceTree);
|
|
9771
|
+
const children = (byParent.get(rootId) ?? []).filter((node) => node.kind !== "document").sort((left, right) => left.order - right.order);
|
|
9772
|
+
const rootPages = children.filter((node) => node.kind === "page" && typeof node.pageStart === "number");
|
|
9773
|
+
let nextTree = sourceTree.map((node) => {
|
|
9774
|
+
if (node.kind !== "page" || typeof node.pageStart !== "number") return node;
|
|
9775
|
+
const form = formHintForPage(forms, node.pageStart);
|
|
9776
|
+
if (!form) return node;
|
|
9777
|
+
const isStartPage = form.pageStart === node.pageStart;
|
|
9778
|
+
const shouldRetitle = isStartPage && form.formType !== "other";
|
|
9779
|
+
return {
|
|
9780
|
+
...node,
|
|
9781
|
+
title: shouldRetitle ? titleFromFormHint(form, node.title) : node.title,
|
|
9782
|
+
metadata: {
|
|
9783
|
+
...node.metadata,
|
|
9784
|
+
formInventoryHint: {
|
|
9785
|
+
formType: form.formType,
|
|
9786
|
+
formNumber: form.formNumber,
|
|
9787
|
+
title: form.title,
|
|
9788
|
+
pageStart: form.pageStart,
|
|
9789
|
+
pageEnd: form.pageEnd
|
|
9790
|
+
}
|
|
9791
|
+
}
|
|
9792
|
+
};
|
|
9793
|
+
});
|
|
9794
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
9795
|
+
for (const form of forms) {
|
|
9796
|
+
const config = formGroupConfig(form);
|
|
9797
|
+
const pageStart2 = form.pageStart;
|
|
9798
|
+
const pageEnd2 = form.pageEnd;
|
|
9799
|
+
if (!config || typeof pageStart2 !== "number" || typeof pageEnd2 !== "number") continue;
|
|
9800
|
+
const childIds = rootPages.filter(
|
|
9801
|
+
(page) => !claimed.has(page.id) && typeof page.pageStart === "number" && page.pageStart >= pageStart2 && page.pageStart <= pageEnd2
|
|
9802
|
+
).map((page) => page.id);
|
|
9803
|
+
if (childIds.length === 0) continue;
|
|
9804
|
+
nextTree = groupAdjacentChildren({
|
|
9805
|
+
sourceTree: nextTree,
|
|
9806
|
+
children,
|
|
9807
|
+
childIds,
|
|
9808
|
+
kind: config.kind,
|
|
9809
|
+
title: config.title,
|
|
9810
|
+
description: config.description,
|
|
9811
|
+
organizer: config.organizer
|
|
9812
|
+
});
|
|
9813
|
+
childIds.forEach((id) => claimed.add(id));
|
|
9814
|
+
}
|
|
9815
|
+
return normalizeDocumentSourceTreePaths(nextTree);
|
|
9816
|
+
}
|
|
9615
9817
|
function looksLikeDeclarationsStart(node) {
|
|
9616
9818
|
const title = cleanText(node.title, "");
|
|
9617
9819
|
const text = sourceNodeText(node);
|
|
@@ -9672,6 +9874,21 @@ function groupAdjacentChildren(params) {
|
|
|
9672
9874
|
groupNode
|
|
9673
9875
|
];
|
|
9674
9876
|
}
|
|
9877
|
+
function reparentNodes(sourceTree, childIds, parentId, organizerRepair) {
|
|
9878
|
+
const wanted = new Set(childIds);
|
|
9879
|
+
if (wanted.size === 0) return sourceTree;
|
|
9880
|
+
return sourceTree.map(
|
|
9881
|
+
(node) => wanted.has(node.id) ? {
|
|
9882
|
+
...node,
|
|
9883
|
+
parentId,
|
|
9884
|
+
order: node.order + 1e-3,
|
|
9885
|
+
metadata: {
|
|
9886
|
+
...node.metadata,
|
|
9887
|
+
organizerRepair
|
|
9888
|
+
}
|
|
9889
|
+
} : node
|
|
9890
|
+
);
|
|
9891
|
+
}
|
|
9675
9892
|
function applySemanticPageGrouping(sourceTree) {
|
|
9676
9893
|
const relabeled = sourceTree.map((node) => {
|
|
9677
9894
|
if (node.kind === "document" || node.kind === "page_group") return node;
|
|
@@ -9731,7 +9948,13 @@ function applySemanticPageGrouping(sourceTree) {
|
|
|
9731
9948
|
if (!looksLikeDeclarationsContinuation(child)) break;
|
|
9732
9949
|
declarationIds.push(child.id);
|
|
9733
9950
|
}
|
|
9734
|
-
|
|
9951
|
+
const existingDeclarations = children[declarationsStartIndex];
|
|
9952
|
+
nextTree = isDeclarationsNode(existingDeclarations) ? reparentNodes(
|
|
9953
|
+
nextTree,
|
|
9954
|
+
declarationIds.filter((id) => id !== existingDeclarations.id),
|
|
9955
|
+
existingDeclarations.id,
|
|
9956
|
+
"semantic_declarations_continuation"
|
|
9957
|
+
) : groupAdjacentChildren({
|
|
9735
9958
|
sourceTree: nextTree,
|
|
9736
9959
|
children,
|
|
9737
9960
|
childIds: declarationIds,
|
|
@@ -9755,7 +9978,13 @@ function applySemanticPageGrouping(sourceTree) {
|
|
|
9755
9978
|
if (!looksLikePolicyFormContinuation(child)) break;
|
|
9756
9979
|
policyIds.push(child.id);
|
|
9757
9980
|
}
|
|
9758
|
-
|
|
9981
|
+
const existingPolicyForm = children[policyStartIndex];
|
|
9982
|
+
nextTree = isPolicyFormNode(existingPolicyForm) ? reparentNodes(
|
|
9983
|
+
nextTree,
|
|
9984
|
+
policyIds.filter((id) => id !== existingPolicyForm.id),
|
|
9985
|
+
existingPolicyForm.id,
|
|
9986
|
+
"semantic_policy_form_continuation"
|
|
9987
|
+
) : groupAdjacentChildren({
|
|
9759
9988
|
sourceTree: nextTree,
|
|
9760
9989
|
children,
|
|
9761
9990
|
childIds: policyIds,
|
|
@@ -9952,6 +10181,130 @@ function normalizeContainerEvidenceFromChildren(sourceTree) {
|
|
|
9952
10181
|
}
|
|
9953
10182
|
return sourceTree.map((node) => byId.get(node.id) ?? node);
|
|
9954
10183
|
}
|
|
10184
|
+
function metadataText(node, key) {
|
|
10185
|
+
const value = node.metadata?.[key];
|
|
10186
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
10187
|
+
}
|
|
10188
|
+
function isTitleBlockNode(node) {
|
|
10189
|
+
if (node.kind !== "text") return false;
|
|
10190
|
+
return metadataText(node, "organizer") === "title_block" || metadataText(node, "elementType") === "title" || metadataText(node, "sourceUnit") === "title";
|
|
10191
|
+
}
|
|
10192
|
+
function isRejectableSectionHeading(text, container) {
|
|
10193
|
+
const normalized = cleanText(text, "");
|
|
10194
|
+
if (!normalized) return true;
|
|
10195
|
+
if (normalized.length > 160) return true;
|
|
10196
|
+
if (/^page\s+\d+$/i.test(normalized)) return true;
|
|
10197
|
+
if (/^table\s+\d+$/i.test(normalized)) return true;
|
|
10198
|
+
if (/^(document|text|header row|row\s+\d+)$/i.test(normalized)) return true;
|
|
10199
|
+
if (/^(northwoods continental insurance company|specimen policy|for testing only)$/i.test(normalized)) return true;
|
|
10200
|
+
if (/^technology errors?\s*&\s*omissions and cyber liability insurance policy$/i.test(normalized)) return true;
|
|
10201
|
+
if (/^declarations(?:\s+page)?$/i.test(normalized) && /^declarations$/i.test(container.title)) return true;
|
|
10202
|
+
if (/^policy\s+form$/i.test(normalized) && /^policy\s+form$/i.test(container.title)) return true;
|
|
10203
|
+
if (/^endorsement\s+(?:no\.?|number|#)/i.test(normalized) && container.kind === "endorsement") return true;
|
|
10204
|
+
if (/\b(policyholder|policyholders)\b/i.test(normalized) && normalized.length < 40) return true;
|
|
10205
|
+
return false;
|
|
10206
|
+
}
|
|
10207
|
+
function sectionHeadingTitle(node, container) {
|
|
10208
|
+
if (!isTitleBlockNode(node)) return void 0;
|
|
10209
|
+
const text = cleanText(node.title || node.textExcerpt, "");
|
|
10210
|
+
if (isRejectableSectionHeading(text, container)) return void 0;
|
|
10211
|
+
const words = text.split(/\s+/);
|
|
10212
|
+
if (words.length > 18) return void 0;
|
|
10213
|
+
const structured = /^(SECTION|PART|ARTICLE|SCHEDULE)\b/i.test(text) || /^Item\s+\d+[\.:]/i.test(text) || /^Coverage\s+Part\b/i.test(text) || /^Endorsement\s+(?:No\.?|Number|#)\s+/i.test(text);
|
|
10214
|
+
const uppercaseLetters = [...text].filter((char) => /[A-Z]/.test(char)).length;
|
|
10215
|
+
const lowercaseLetters = [...text].filter((char) => /[a-z]/.test(char)).length;
|
|
10216
|
+
const mostlyUppercase = uppercaseLetters > 0 && uppercaseLetters >= lowercaseLetters * 1.5;
|
|
10217
|
+
const hasSentencePunctuation = /[.;:]\s+\S/.test(text) || /[.;:]$/.test(text);
|
|
10218
|
+
const sentenceLike = /\b(is|are|was|were|will|shall|may|must|means|includes|provided|subject|available|attached|remain|constitutes)\b/i.test(text) && /[a-z]/.test(text);
|
|
10219
|
+
if (!structured && (!mostlyUppercase || hasSentencePunctuation || sentenceLike)) return void 0;
|
|
10220
|
+
return simplifyOrganizerTitle(text, text, node.kind);
|
|
10221
|
+
}
|
|
10222
|
+
function sectionKindForTitle(title) {
|
|
10223
|
+
if (/^schedule\b/i.test(title) || /\b(forms? and endorsements?|coverage parts?|limits?|premium|declarations?)\b/i.test(title)) return "schedule";
|
|
10224
|
+
if (/^(section|part|article|item)\b/i.test(title)) return "section";
|
|
10225
|
+
return "section";
|
|
10226
|
+
}
|
|
10227
|
+
function hasAncestor(node, ancestorId, byId) {
|
|
10228
|
+
let parentId = node.parentId;
|
|
10229
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10230
|
+
while (parentId) {
|
|
10231
|
+
if (parentId === ancestorId) return true;
|
|
10232
|
+
if (seen.has(parentId)) return false;
|
|
10233
|
+
seen.add(parentId);
|
|
10234
|
+
parentId = byId.get(parentId)?.parentId;
|
|
10235
|
+
}
|
|
10236
|
+
return false;
|
|
10237
|
+
}
|
|
10238
|
+
function shouldBuildSectionsForContainer(node) {
|
|
10239
|
+
if (isNoticesGroup(node)) return false;
|
|
10240
|
+
if (isEndorsementGroup(node)) return false;
|
|
10241
|
+
return node.kind === "form" || node.kind === "page_group" || node.kind === "endorsement";
|
|
10242
|
+
}
|
|
10243
|
+
function applyTitleSectionHierarchy(sourceTree) {
|
|
10244
|
+
const byId = new Map(sourceTree.map((node) => [node.id, node]));
|
|
10245
|
+
const byParent = nodesByParent(sourceTree);
|
|
10246
|
+
const updates = /* @__PURE__ */ new Map();
|
|
10247
|
+
for (const container of sourceTree.filter(shouldBuildSectionsForContainer)) {
|
|
10248
|
+
const pageIds = new Set(
|
|
10249
|
+
sourceTree.filter((node) => node.kind === "page" && hasAncestor(node, container.id, byId)).map((node) => node.id)
|
|
10250
|
+
);
|
|
10251
|
+
if (pageIds.size === 0) continue;
|
|
10252
|
+
const directPageChildren = sourceTree.filter(
|
|
10253
|
+
(node) => node.parentId !== void 0 && pageIds.has(node.parentId) && node.kind !== "table_row" && node.kind !== "table_cell"
|
|
10254
|
+
).sort(
|
|
10255
|
+
(left, right) => (left.pageStart ?? Number.MAX_SAFE_INTEGER) - (right.pageStart ?? Number.MAX_SAFE_INTEGER) || left.order - right.order || left.id.localeCompare(right.id)
|
|
10256
|
+
);
|
|
10257
|
+
if (directPageChildren.length === 0) continue;
|
|
10258
|
+
let currentSectionId;
|
|
10259
|
+
for (let index = 0; index < directPageChildren.length; index += 1) {
|
|
10260
|
+
const child = directPageChildren[index];
|
|
10261
|
+
const current = updates.get(child.id) ?? child;
|
|
10262
|
+
const heading = sectionHeadingTitle(current, container);
|
|
10263
|
+
if (heading) {
|
|
10264
|
+
const descendants = byParent.get(child.id) ?? [];
|
|
10265
|
+
const hasOwnContent = descendants.some((descendant) => descendant.kind !== "table_row" && descendant.kind !== "table_cell");
|
|
10266
|
+
let hasFollowingContent = false;
|
|
10267
|
+
for (const nextChild of directPageChildren.slice(index + 1)) {
|
|
10268
|
+
const next = updates.get(nextChild.id) ?? nextChild;
|
|
10269
|
+
if (sectionHeadingTitle(next, container)) break;
|
|
10270
|
+
hasFollowingContent = true;
|
|
10271
|
+
break;
|
|
10272
|
+
}
|
|
10273
|
+
if (!hasOwnContent && !hasFollowingContent) {
|
|
10274
|
+
currentSectionId = void 0;
|
|
10275
|
+
continue;
|
|
10276
|
+
}
|
|
10277
|
+
currentSectionId = child.id;
|
|
10278
|
+
updates.set(child.id, {
|
|
10279
|
+
...current,
|
|
10280
|
+
parentId: container.id,
|
|
10281
|
+
kind: sectionKindForTitle(heading),
|
|
10282
|
+
title: heading,
|
|
10283
|
+
description: descriptionWithPages(cleanText([heading, "section"].join(" "), heading), [current, ...descendants]),
|
|
10284
|
+
metadata: {
|
|
10285
|
+
...current.metadata,
|
|
10286
|
+
organizer: "title_section",
|
|
10287
|
+
sourceTreeVersion: "v3"
|
|
10288
|
+
}
|
|
10289
|
+
});
|
|
10290
|
+
continue;
|
|
10291
|
+
}
|
|
10292
|
+
if (!currentSectionId) continue;
|
|
10293
|
+
const parent = current.parentId ? byId.get(current.parentId) : void 0;
|
|
10294
|
+
if (!parent || parent.kind !== "page") continue;
|
|
10295
|
+
updates.set(child.id, {
|
|
10296
|
+
...current,
|
|
10297
|
+
parentId: currentSectionId,
|
|
10298
|
+
metadata: {
|
|
10299
|
+
...current.metadata,
|
|
10300
|
+
organizerRepair: "title_section_continuation"
|
|
10301
|
+
}
|
|
10302
|
+
});
|
|
10303
|
+
}
|
|
10304
|
+
}
|
|
10305
|
+
if (updates.size === 0) return sourceTree;
|
|
10306
|
+
return normalizeDocumentSourceTreePaths(sourceTree.map((node) => updates.get(node.id) ?? node));
|
|
10307
|
+
}
|
|
9955
10308
|
function normalizeSemanticHierarchy(sourceTree) {
|
|
9956
10309
|
const normalized = normalizeDocumentSourceTreePaths(
|
|
9957
10310
|
normalizePolicyFormStructure(
|
|
@@ -9960,7 +10313,8 @@ function normalizeSemanticHierarchy(sourceTree) {
|
|
|
9960
10313
|
);
|
|
9961
10314
|
const nested = normalizeDocumentSourceTreePaths(nestEndorsementContinuationPages(normalized));
|
|
9962
10315
|
const mergedNotices = normalizeDocumentSourceTreePaths(mergeAdministrativeNoticesIntoFrontMatter(nested));
|
|
9963
|
-
const
|
|
10316
|
+
const sectioned = normalizeDocumentSourceTreePaths(applyTitleSectionHierarchy(mergedNotices));
|
|
10317
|
+
const withEvidence = normalizeContainerEvidenceFromChildren(sectioned);
|
|
9964
10318
|
return normalizeDocumentSourceTreePaths(
|
|
9965
10319
|
normalizeRootSemanticOrder(normalizeContainerEvidenceFromChildren(withEvidence))
|
|
9966
10320
|
);
|
|
@@ -10179,7 +10533,7 @@ function mergeOrganizationResults(results) {
|
|
|
10179
10533
|
groups: [...groups.values()]
|
|
10180
10534
|
};
|
|
10181
10535
|
}
|
|
10182
|
-
function buildOrganizationPrompt(batch) {
|
|
10536
|
+
function buildOrganizationPrompt(batch, formHints) {
|
|
10183
10537
|
const nodes = batch.nodes.map((node) => compactNode(node, node.kind === "page" ? 900 : 320));
|
|
10184
10538
|
return `You organize an insurance document source tree.
|
|
10185
10539
|
|
|
@@ -10188,10 +10542,15 @@ Scope:
|
|
|
10188
10542
|
- The provided list is a bounded extraction-time batch. It is not necessarily the whole document.
|
|
10189
10543
|
- Top-level page/form candidates in this batch: ${JSON.stringify(batch.topLevelNodeIds)}
|
|
10190
10544
|
|
|
10545
|
+
Expected form inventory / page ranges:
|
|
10546
|
+
${formatFormHintsForPrompt(formHints)}
|
|
10547
|
+
|
|
10191
10548
|
Rules:
|
|
10192
10549
|
- Use only node IDs from the provided list.
|
|
10193
10550
|
- Do not invent text, page numbers, source spans, limits, or policy facts.
|
|
10194
10551
|
- You may relabel existing nodes and group adjacent top-level/page nodes from this batch only when they are clearly one continuous form, one declarations set, one schedule, or one clause family.
|
|
10552
|
+
- Treat the form inventory as a page-range hint for the expected order: front matter/notices, declarations, policy form, then endorsements.
|
|
10553
|
+
- Prefer section hierarchy from printed title elements inside a form over page-by-page grouping.
|
|
10195
10554
|
- Group adjacent separately numbered endorsements under a single generic "Endorsements" page_group parent, with each individual endorsement preserved as its own child node.
|
|
10196
10555
|
- Never create rollup titles such as "Endorsements 1-3 (...)" or merge multiple endorsements into one endorsement node.
|
|
10197
10556
|
- Add concise, human-readable titles to generic text, table, row, and cell nodes when the text makes their role clear.
|
|
@@ -10215,7 +10574,7 @@ function shouldRunOutlineCleanup(sourceTree) {
|
|
|
10215
10574
|
if (hasDeclarations && hasPolicyForm && hasEndorsements) return false;
|
|
10216
10575
|
return genericPages.length > 0 || topLevel.length > 6 || !hasDeclarations || !hasPolicyForm || !hasEndorsements;
|
|
10217
10576
|
}
|
|
10218
|
-
function buildOutlineCleanupPrompt(sourceTree) {
|
|
10577
|
+
function buildOutlineCleanupPrompt(sourceTree, formHints) {
|
|
10219
10578
|
const topLevel = rootChildren(sourceTree).slice(0, OUTLINE_CLEANUP_MAX_TOP_LEVEL_NODES);
|
|
10220
10579
|
const nodes = topLevel.map((node) => compactNode(node, 900));
|
|
10221
10580
|
return `You clean a top-level source outline for an insurance policy.
|
|
@@ -10226,6 +10585,9 @@ Expected product-facing order:
|
|
|
10226
10585
|
3. Policy Form: the main policy wording, insuring agreements, definitions, exclusions, conditions, claim provisions, and general policy terms.
|
|
10227
10586
|
4. Endorsements: one generic "Endorsements" page_group containing each separately numbered endorsement as its own child.
|
|
10228
10587
|
|
|
10588
|
+
Expected form inventory / page ranges:
|
|
10589
|
+
${formatFormHintsForPrompt(formHints)}
|
|
10590
|
+
|
|
10229
10591
|
Rules:
|
|
10230
10592
|
- Use only node IDs from this top-level list: ${JSON.stringify(topLevel.map((node) => node.id))}
|
|
10231
10593
|
- Group only adjacent top-level nodes.
|
|
@@ -10234,6 +10596,7 @@ Rules:
|
|
|
10234
10596
|
- Use canonical terse titles: "Notices and Jacket", "Declarations", "Policy Form", "Endorsements", or the printed endorsement number.
|
|
10235
10597
|
- Keep page_group descriptions short and include the page range when pages are known, for example "Declarations pages 6-8".
|
|
10236
10598
|
- If a page is an OFAC, privacy, terrorism/TRIA, claim-reporting notice, signature page, or jacket, do not label it as declarations or policy form.
|
|
10599
|
+
- If the form inventory provides page ranges, keep groups aligned to those ranges unless the source node text clearly contradicts them.
|
|
10237
10600
|
- If the existing deterministic outline is already correct, return empty labels and groups.
|
|
10238
10601
|
|
|
10239
10602
|
Top-level source nodes:
|
|
@@ -10408,6 +10771,14 @@ function materializeDocument(params) {
|
|
|
10408
10771
|
insuredName,
|
|
10409
10772
|
premium,
|
|
10410
10773
|
policyTypes: profile.policyTypes,
|
|
10774
|
+
formInventory: params.formInventory.filter((form) => typeof form.formNumber === "string" && form.formNumber.trim().length > 0).map((form) => ({
|
|
10775
|
+
formNumber: form.formNumber,
|
|
10776
|
+
editionDate: form.editionDate,
|
|
10777
|
+
title: form.title,
|
|
10778
|
+
formType: form.formType,
|
|
10779
|
+
pageStart: form.pageStart,
|
|
10780
|
+
pageEnd: form.pageEnd
|
|
10781
|
+
})),
|
|
10411
10782
|
coverages,
|
|
10412
10783
|
documentMetadata,
|
|
10413
10784
|
documentOutline,
|
|
@@ -10448,7 +10819,8 @@ function materializeDocument(params) {
|
|
|
10448
10819
|
}
|
|
10449
10820
|
async function runSourceTreeExtraction(params) {
|
|
10450
10821
|
const sourceSpans = normalizeSourceSpans(params.sourceSpans);
|
|
10451
|
-
|
|
10822
|
+
const formHints = normalizeFormHints(params.formInventory?.forms, sourceSpans);
|
|
10823
|
+
let sourceTree = applySemanticPageGrouping(applyFormInventoryHints(buildDocumentSourceTree(sourceSpans, params.id), formHints));
|
|
10452
10824
|
const warnings = [];
|
|
10453
10825
|
let modelCalls = 0;
|
|
10454
10826
|
let callsWithUsage = 0;
|
|
@@ -10480,7 +10852,7 @@ async function runSourceTreeExtraction(params) {
|
|
|
10480
10852
|
const response = await safeGenerateObject(
|
|
10481
10853
|
params.generateObject,
|
|
10482
10854
|
{
|
|
10483
|
-
prompt: buildOrganizationPrompt(batch),
|
|
10855
|
+
prompt: buildOrganizationPrompt(batch, formHints),
|
|
10484
10856
|
schema: SourceTreeOrganizationSchema,
|
|
10485
10857
|
maxTokens: budget.maxTokens,
|
|
10486
10858
|
taskKind: "extraction_source_tree",
|
|
@@ -10514,7 +10886,7 @@ async function runSourceTreeExtraction(params) {
|
|
|
10514
10886
|
const response = await safeGenerateObject(
|
|
10515
10887
|
params.generateObject,
|
|
10516
10888
|
{
|
|
10517
|
-
prompt: buildOutlineCleanupPrompt(sourceTree),
|
|
10889
|
+
prompt: buildOutlineCleanupPrompt(sourceTree, formHints),
|
|
10518
10890
|
schema: SourceTreeOrganizationSchema,
|
|
10519
10891
|
maxTokens,
|
|
10520
10892
|
taskKind: "extraction_source_tree",
|
|
@@ -10579,12 +10951,14 @@ async function runSourceTreeExtraction(params) {
|
|
|
10579
10951
|
const document = materializeDocument({
|
|
10580
10952
|
id: params.id,
|
|
10581
10953
|
sourceTree,
|
|
10954
|
+
formInventory: formHints,
|
|
10582
10955
|
operationalProfile
|
|
10583
10956
|
});
|
|
10584
10957
|
return {
|
|
10585
10958
|
sourceTree,
|
|
10586
10959
|
sourceSpans,
|
|
10587
10960
|
sourceChunks: chunkSourceSpans(sourceSpans),
|
|
10961
|
+
formInventory: formHints,
|
|
10588
10962
|
operationalProfile,
|
|
10589
10963
|
document,
|
|
10590
10964
|
chunks: [],
|
|
@@ -10938,12 +11312,24 @@ ${span.text}` : span.text;
|
|
|
10938
11312
|
const v3 = await runSourceTreeExtraction({
|
|
10939
11313
|
id,
|
|
10940
11314
|
sourceSpans,
|
|
11315
|
+
formInventory: options?.formInventory,
|
|
10941
11316
|
generateObject,
|
|
10942
11317
|
providerOptions: activeProviderOptions,
|
|
10943
11318
|
resolveBudget,
|
|
10944
11319
|
trackUsage,
|
|
10945
11320
|
log
|
|
10946
11321
|
});
|
|
11322
|
+
const sourceTreeFormInventory = v3.formInventory.flatMap((form) => {
|
|
11323
|
+
const formNumber = typeof form.formNumber === "string" ? form.formNumber.trim() : "";
|
|
11324
|
+
if (!formNumber) return [];
|
|
11325
|
+
return [{
|
|
11326
|
+
formNumber,
|
|
11327
|
+
title: form.title,
|
|
11328
|
+
pageStart: form.pageStart,
|
|
11329
|
+
pageEnd: form.pageEnd,
|
|
11330
|
+
sources: ["source_tree"]
|
|
11331
|
+
}];
|
|
11332
|
+
});
|
|
10947
11333
|
const reviewReport2 = {
|
|
10948
11334
|
issues: v3.warnings.map((warning) => ({
|
|
10949
11335
|
code: "source_tree_warning",
|
|
@@ -10957,7 +11343,7 @@ ${span.text}` : span.text;
|
|
|
10957
11343
|
{ kind: "operational_profile", label: "Operational Profile", itemCount: v3.operationalProfile.coverages.length }
|
|
10958
11344
|
],
|
|
10959
11345
|
reviewRoundRecords: [],
|
|
10960
|
-
formInventory:
|
|
11346
|
+
formInventory: sourceTreeFormInventory,
|
|
10961
11347
|
qualityGateStatus: v3.warnings.length > 0 ? "warning" : "passed"
|
|
10962
11348
|
};
|
|
10963
11349
|
if (shouldFailQualityGate(qualityGate, reviewReport2.qualityGateStatus)) {
|