@claritylabs/cl-sdk 3.0.16 → 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 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 source-backed projections from that tree, not the canonical source of truth.
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
@@ -3005,7 +3005,7 @@ function isTitleContentNode(node) {
3005
3005
  if (!text || text.length > 140) return false;
3006
3006
  const words = text.split(/\s+/);
3007
3007
  if (words.length > 14) return false;
3008
- const startsWithStructuredHeading = /^(section|item|part|endorsement|schedule|article)\b|^[A-Z]\.\s|\b[IVX]+\.\s/i.test(text);
3008
+ const startsWithStructuredHeading = /^(SECTION|PART|SCHEDULE|ARTICLE)\b/.test(text) || /^Section\s+[IVX0-9]+(?:\.[A-Z])?\s*[—:-]/.test(text) || /^Item\s+\d+[\.:]/i.test(text) || /^Endorsement\s+(?:No\.?|Number|#)\s+/i.test(text) || /^[A-Z]\.\s/.test(text) || /\b[IVX]+\.\s/.test(text);
3009
3009
  const uppercaseLetters = [...text].filter((char) => /[A-Z]/.test(char)).length;
3010
3010
  const lowercaseLetters = [...text].filter((char) => /[a-z]/.test(char)).length;
3011
3011
  const mostlyUppercase = uppercaseLetters > 0 && uppercaseLetters >= lowercaseLetters * 1.6;
@@ -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
- nextTree = groupAdjacentChildren({
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
- nextTree = groupAdjacentChildren({
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 withEvidence = normalizeContainerEvidenceFromChildren(mergedNotices);
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
- let sourceTree = applySemanticPageGrouping(buildDocumentSourceTree(sourceSpans, params.id));
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)) {