@claritylabs/cl-sdk 3.0.17 → 3.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -9168,6 +9168,17 @@ var OperationalProfilePromptSchema = z41.object({
9168
9168
  sourceNodeIds: z41.array(z41.string()).optional(),
9169
9169
  sourceSpanIds: z41.array(z41.string()).optional()
9170
9170
  });
9171
+ function formatFormHintsForPrompt(forms) {
9172
+ const usable = forms.filter((form) => typeof form.pageStart === "number" && typeof form.pageEnd === "number").slice(0, 120).map((form) => ({
9173
+ title: form.title,
9174
+ formType: form.formType,
9175
+ formNumber: form.formNumber,
9176
+ editionDate: form.editionDate,
9177
+ pageStart: form.pageStart,
9178
+ pageEnd: form.pageEnd
9179
+ }));
9180
+ return usable.length ? JSON.stringify(usable, null, 2) : "[]";
9181
+ }
9171
9182
  function cleanText(value, fallback) {
9172
9183
  const text = value?.replace(/\s+/g, " ").trim();
9173
9184
  return text || fallback;
@@ -9265,6 +9276,197 @@ function semanticGroupNodeId(documentId, kind, title, childNodeIds) {
9265
9276
  childNodeIds.join("_").replace(/[^a-zA-Z0-9_.:-]/g, "_").slice(0, 80)
9266
9277
  ].join(":");
9267
9278
  }
9279
+ function spanPageStart(span) {
9280
+ return span.pageStart ?? span.location?.page ?? span.location?.startPage;
9281
+ }
9282
+ function spanPageEnd2(span) {
9283
+ return span.pageEnd ?? span.location?.endPage ?? spanPageStart(span);
9284
+ }
9285
+ function spanSourceUnit(span) {
9286
+ return span.sourceUnit ?? span.metadata?.sourceUnit ?? span.metadata?.elementType;
9287
+ }
9288
+ function formNumberFromText(value) {
9289
+ return cleanText(value, "").match(/\b[A-Z]{2,}(?:-[A-Z0-9]+)+\s+\d{2}\s+\d{2}\b/)?.[0]?.replace(/\s+/g, " ");
9290
+ }
9291
+ function editionDateFromFormNumber(formNumber) {
9292
+ const match = formNumber?.match(/\b(\d{2})\s+(\d{2})$/);
9293
+ return match ? `${match[1]}/${match[2]}` : void 0;
9294
+ }
9295
+ function pageTitleFromText(text, fallback) {
9296
+ const normalized = cleanText(text, fallback);
9297
+ const patterns = [
9298
+ /\bIMPORTANT NOTICE\s+[—-]\s+HOW TO REPORT A CLAIM\b/i,
9299
+ /\bPRIVACY NOTICE TO POLICYHOLDERS\b/i,
9300
+ /\bOFAC ADVISORY NOTICE\b/i,
9301
+ /\bTERRORISM RISK INSURANCE ACT\s*\(TRIA\)\s*DISCLOSURE AND REJECTION\b/i,
9302
+ /\bDECLARATIONS PAGE\b/i,
9303
+ /\bTECHNOLOGY ERRORS?\s*&\s*OMISSIONS AND CYBER LIABILITY INSURANCE POLICY\b/i,
9304
+ /\bTRADE OR ECONOMIC SANCTIONS LIMITATION\b/i,
9305
+ /\bFORMS? AND ENDORSEMENTS\b/i
9306
+ ];
9307
+ for (const pattern of patterns) {
9308
+ const match = normalized.match(pattern)?.[0];
9309
+ if (match) return cleanText(match, fallback);
9310
+ }
9311
+ const endorsement = normalized.match(/\bENDORSEMENT\s+(?:NO\.?|NUMBER|#)\s*[A-Z0-9][A-Z0-9.-]*\b/i)?.[0];
9312
+ if (endorsement) return cleanText(endorsement, fallback);
9313
+ const firstSentence = normalized.split(/(?<=\.)\s+/)[0];
9314
+ if (firstSentence && firstSentence.length <= 120) return firstSentence.replace(/[.]$/, "");
9315
+ return fallback;
9316
+ }
9317
+ function pageFormTypeFromText(text) {
9318
+ if (/\b(declarations?\s+page|declarations?\s+schedule)\b/i.test(text)) return "declarations";
9319
+ if (/\b(endorsement\s+(?:no\.?|number|#)|this endorsement changes the policy|[A-Z]{2,}-END\s+\d{2,})\b/i.test(text)) return "endorsement";
9320
+ 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";
9321
+ if (/\b(important notice|privacy notice|ofac advisory|terrorism risk insurance act|tria|trade or economic sanctions)\b/i.test(text)) return "notice";
9322
+ return "other";
9323
+ }
9324
+ function inferFormHintsFromSourceSpans(sourceSpans) {
9325
+ const pageTexts = /* @__PURE__ */ new Map();
9326
+ const pageSpanTexts = /* @__PURE__ */ new Map();
9327
+ for (const span of sourceSpans) {
9328
+ const start = spanPageStart(span);
9329
+ if (typeof start !== "number") continue;
9330
+ const end = spanPageEnd2(span) ?? start;
9331
+ for (let page = start; page <= end; page += 1) {
9332
+ if (spanSourceUnit(span) === "page") {
9333
+ pageSpanTexts.set(page, cleanText(span.text, ""));
9334
+ continue;
9335
+ }
9336
+ const existing = pageTexts.get(page) ?? "";
9337
+ if (existing.length < 4e3) pageTexts.set(page, cleanText([existing, span.text].filter(Boolean).join(" "), ""));
9338
+ }
9339
+ }
9340
+ if (pageSpanTexts.size === 0) return [];
9341
+ const pageHints = [.../* @__PURE__ */ new Set([...pageTexts.keys(), ...pageSpanTexts.keys()])].sort((left, right) => left - right).map((page) => {
9342
+ const text = pageSpanTexts.get(page) ?? pageTexts.get(page) ?? "";
9343
+ const formNumber = formNumberFromText(text);
9344
+ return {
9345
+ formNumber,
9346
+ editionDate: editionDateFromFormNumber(formNumber),
9347
+ title: pageTitleFromText(text, `Page ${page}`),
9348
+ formType: pageFormTypeFromText(text),
9349
+ pageStart: page,
9350
+ pageEnd: page
9351
+ };
9352
+ });
9353
+ const merged = [];
9354
+ for (const hint of pageHints) {
9355
+ const previous = merged[merged.length - 1];
9356
+ const startsNewEndorsement = hint.formType === "endorsement" && /\bendorsement\s+(?:no\.?|number|#)\s*[A-Z0-9]|this endorsement changes the policy/i.test(hint.title ?? "");
9357
+ 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);
9358
+ if (!canMerge) {
9359
+ merged.push(hint);
9360
+ continue;
9361
+ }
9362
+ previous.pageEnd = hint.pageEnd;
9363
+ previous.title = previous.title ?? hint.title;
9364
+ previous.formNumber = previous.formNumber ?? hint.formNumber;
9365
+ previous.editionDate = previous.editionDate ?? hint.editionDate;
9366
+ }
9367
+ return merged;
9368
+ }
9369
+ function normalizeFormHints(forms, sourceSpans) {
9370
+ const provided = (forms ?? []).filter(
9371
+ (form) => typeof form.pageStart === "number" && typeof form.pageEnd === "number" && form.pageStart > 0 && form.pageEnd >= form.pageStart
9372
+ ).map((form) => ({
9373
+ ...form,
9374
+ title: form.title ? cleanText(form.title, "") : void 0
9375
+ })).sort(
9376
+ (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)
9377
+ );
9378
+ return provided.length ? provided : inferFormHintsFromSourceSpans(sourceSpans);
9379
+ }
9380
+ function formHintForPage(forms, page) {
9381
+ return forms.find(
9382
+ (form) => typeof form.pageStart === "number" && typeof form.pageEnd === "number" && page >= form.pageStart && page <= form.pageEnd
9383
+ );
9384
+ }
9385
+ function titleFromFormHint(form, fallback) {
9386
+ if (form.formType === "declarations") return "Declarations";
9387
+ if (form.formType === "coverage") return "Policy Form";
9388
+ if (form.formType === "endorsement") return endorsementTitle([form.title, form.formNumber].filter(Boolean).join(" ")) ?? cleanText(form.title, fallback);
9389
+ return cleanText(form.title, fallback);
9390
+ }
9391
+ function formGroupConfig(form) {
9392
+ if (form.formType === "declarations") {
9393
+ return {
9394
+ kind: "page_group",
9395
+ title: "Declarations",
9396
+ description: "Declarations pages and schedules grouped from form inventory",
9397
+ organizer: "form_inventory_declarations_grouping"
9398
+ };
9399
+ }
9400
+ if (form.formType === "coverage") {
9401
+ return {
9402
+ kind: "form",
9403
+ title: "Policy Form",
9404
+ description: "Policy form pages grouped from form inventory",
9405
+ organizer: "form_inventory_policy_form_grouping"
9406
+ };
9407
+ }
9408
+ if (form.formType === "endorsement") {
9409
+ const title = titleFromFormHint(form, "Endorsement");
9410
+ return {
9411
+ kind: "endorsement",
9412
+ title,
9413
+ description: `${title} grouped from form inventory`,
9414
+ organizer: "form_inventory_endorsement_grouping"
9415
+ };
9416
+ }
9417
+ return void 0;
9418
+ }
9419
+ function applyFormInventoryHints(sourceTree, forms) {
9420
+ if (forms.length === 0) return sourceTree;
9421
+ const rootId = sourceTreeRootId(sourceTree);
9422
+ if (!rootId) return sourceTree;
9423
+ const byParent = nodesByParent(sourceTree);
9424
+ const children = (byParent.get(rootId) ?? []).filter((node) => node.kind !== "document").sort((left, right) => left.order - right.order);
9425
+ const rootPages = children.filter((node) => node.kind === "page" && typeof node.pageStart === "number");
9426
+ let nextTree = sourceTree.map((node) => {
9427
+ if (node.kind !== "page" || typeof node.pageStart !== "number") return node;
9428
+ const form = formHintForPage(forms, node.pageStart);
9429
+ if (!form) return node;
9430
+ const isStartPage = form.pageStart === node.pageStart;
9431
+ const shouldRetitle = isStartPage && form.formType !== "other";
9432
+ return {
9433
+ ...node,
9434
+ title: shouldRetitle ? titleFromFormHint(form, node.title) : node.title,
9435
+ metadata: {
9436
+ ...node.metadata,
9437
+ formInventoryHint: {
9438
+ formType: form.formType,
9439
+ formNumber: form.formNumber,
9440
+ title: form.title,
9441
+ pageStart: form.pageStart,
9442
+ pageEnd: form.pageEnd
9443
+ }
9444
+ }
9445
+ };
9446
+ });
9447
+ const claimed = /* @__PURE__ */ new Set();
9448
+ for (const form of forms) {
9449
+ const config = formGroupConfig(form);
9450
+ const pageStart2 = form.pageStart;
9451
+ const pageEnd2 = form.pageEnd;
9452
+ if (!config || typeof pageStart2 !== "number" || typeof pageEnd2 !== "number") continue;
9453
+ const childIds = rootPages.filter(
9454
+ (page) => !claimed.has(page.id) && typeof page.pageStart === "number" && page.pageStart >= pageStart2 && page.pageStart <= pageEnd2
9455
+ ).map((page) => page.id);
9456
+ if (childIds.length === 0) continue;
9457
+ nextTree = groupAdjacentChildren({
9458
+ sourceTree: nextTree,
9459
+ children,
9460
+ childIds,
9461
+ kind: config.kind,
9462
+ title: config.title,
9463
+ description: config.description,
9464
+ organizer: config.organizer
9465
+ });
9466
+ childIds.forEach((id) => claimed.add(id));
9467
+ }
9468
+ return normalizeDocumentSourceTreePaths(nextTree);
9469
+ }
9268
9470
  function looksLikeDeclarationsStart(node) {
9269
9471
  const title = cleanText(node.title, "");
9270
9472
  const text = sourceNodeText(node);
@@ -9325,6 +9527,21 @@ function groupAdjacentChildren(params) {
9325
9527
  groupNode
9326
9528
  ];
9327
9529
  }
9530
+ function reparentNodes(sourceTree, childIds, parentId, organizerRepair) {
9531
+ const wanted = new Set(childIds);
9532
+ if (wanted.size === 0) return sourceTree;
9533
+ return sourceTree.map(
9534
+ (node) => wanted.has(node.id) ? {
9535
+ ...node,
9536
+ parentId,
9537
+ order: node.order + 1e-3,
9538
+ metadata: {
9539
+ ...node.metadata,
9540
+ organizerRepair
9541
+ }
9542
+ } : node
9543
+ );
9544
+ }
9328
9545
  function applySemanticPageGrouping(sourceTree) {
9329
9546
  const relabeled = sourceTree.map((node) => {
9330
9547
  if (node.kind === "document" || node.kind === "page_group") return node;
@@ -9384,7 +9601,13 @@ function applySemanticPageGrouping(sourceTree) {
9384
9601
  if (!looksLikeDeclarationsContinuation(child)) break;
9385
9602
  declarationIds.push(child.id);
9386
9603
  }
9387
- nextTree = groupAdjacentChildren({
9604
+ const existingDeclarations = children[declarationsStartIndex];
9605
+ nextTree = isDeclarationsNode(existingDeclarations) ? reparentNodes(
9606
+ nextTree,
9607
+ declarationIds.filter((id) => id !== existingDeclarations.id),
9608
+ existingDeclarations.id,
9609
+ "semantic_declarations_continuation"
9610
+ ) : groupAdjacentChildren({
9388
9611
  sourceTree: nextTree,
9389
9612
  children,
9390
9613
  childIds: declarationIds,
@@ -9408,7 +9631,13 @@ function applySemanticPageGrouping(sourceTree) {
9408
9631
  if (!looksLikePolicyFormContinuation(child)) break;
9409
9632
  policyIds.push(child.id);
9410
9633
  }
9411
- nextTree = groupAdjacentChildren({
9634
+ const existingPolicyForm = children[policyStartIndex];
9635
+ nextTree = isPolicyFormNode(existingPolicyForm) ? reparentNodes(
9636
+ nextTree,
9637
+ policyIds.filter((id) => id !== existingPolicyForm.id),
9638
+ existingPolicyForm.id,
9639
+ "semantic_policy_form_continuation"
9640
+ ) : groupAdjacentChildren({
9412
9641
  sourceTree: nextTree,
9413
9642
  children,
9414
9643
  childIds: policyIds,
@@ -9605,6 +9834,130 @@ function normalizeContainerEvidenceFromChildren(sourceTree) {
9605
9834
  }
9606
9835
  return sourceTree.map((node) => byId.get(node.id) ?? node);
9607
9836
  }
9837
+ function metadataText(node, key) {
9838
+ const value = node.metadata?.[key];
9839
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
9840
+ }
9841
+ function isTitleBlockNode(node) {
9842
+ if (node.kind !== "text") return false;
9843
+ return metadataText(node, "organizer") === "title_block" || metadataText(node, "elementType") === "title" || metadataText(node, "sourceUnit") === "title";
9844
+ }
9845
+ function isRejectableSectionHeading(text, container) {
9846
+ const normalized = cleanText(text, "");
9847
+ if (!normalized) return true;
9848
+ if (normalized.length > 160) return true;
9849
+ if (/^page\s+\d+$/i.test(normalized)) return true;
9850
+ if (/^table\s+\d+$/i.test(normalized)) return true;
9851
+ if (/^(document|text|header row|row\s+\d+)$/i.test(normalized)) return true;
9852
+ if (/^(northwoods continental insurance company|specimen policy|for testing only)$/i.test(normalized)) return true;
9853
+ if (/^technology errors?\s*&\s*omissions and cyber liability insurance policy$/i.test(normalized)) return true;
9854
+ if (/^declarations(?:\s+page)?$/i.test(normalized) && /^declarations$/i.test(container.title)) return true;
9855
+ if (/^policy\s+form$/i.test(normalized) && /^policy\s+form$/i.test(container.title)) return true;
9856
+ if (/^endorsement\s+(?:no\.?|number|#)/i.test(normalized) && container.kind === "endorsement") return true;
9857
+ if (/\b(policyholder|policyholders)\b/i.test(normalized) && normalized.length < 40) return true;
9858
+ return false;
9859
+ }
9860
+ function sectionHeadingTitle(node, container) {
9861
+ if (!isTitleBlockNode(node)) return void 0;
9862
+ const text = cleanText(node.title || node.textExcerpt, "");
9863
+ if (isRejectableSectionHeading(text, container)) return void 0;
9864
+ const words = text.split(/\s+/);
9865
+ if (words.length > 18) return void 0;
9866
+ 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);
9867
+ const uppercaseLetters = [...text].filter((char) => /[A-Z]/.test(char)).length;
9868
+ const lowercaseLetters = [...text].filter((char) => /[a-z]/.test(char)).length;
9869
+ const mostlyUppercase = uppercaseLetters > 0 && uppercaseLetters >= lowercaseLetters * 1.5;
9870
+ const hasSentencePunctuation = /[.;:]\s+\S/.test(text) || /[.;:]$/.test(text);
9871
+ 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);
9872
+ if (!structured && (!mostlyUppercase || hasSentencePunctuation || sentenceLike)) return void 0;
9873
+ return simplifyOrganizerTitle(text, text, node.kind);
9874
+ }
9875
+ function sectionKindForTitle(title) {
9876
+ if (/^schedule\b/i.test(title) || /\b(forms? and endorsements?|coverage parts?|limits?|premium|declarations?)\b/i.test(title)) return "schedule";
9877
+ if (/^(section|part|article|item)\b/i.test(title)) return "section";
9878
+ return "section";
9879
+ }
9880
+ function hasAncestor(node, ancestorId, byId) {
9881
+ let parentId = node.parentId;
9882
+ const seen = /* @__PURE__ */ new Set();
9883
+ while (parentId) {
9884
+ if (parentId === ancestorId) return true;
9885
+ if (seen.has(parentId)) return false;
9886
+ seen.add(parentId);
9887
+ parentId = byId.get(parentId)?.parentId;
9888
+ }
9889
+ return false;
9890
+ }
9891
+ function shouldBuildSectionsForContainer(node) {
9892
+ if (isNoticesGroup(node)) return false;
9893
+ if (isEndorsementGroup(node)) return false;
9894
+ return node.kind === "form" || node.kind === "page_group" || node.kind === "endorsement";
9895
+ }
9896
+ function applyTitleSectionHierarchy(sourceTree) {
9897
+ const byId = new Map(sourceTree.map((node) => [node.id, node]));
9898
+ const byParent = nodesByParent(sourceTree);
9899
+ const updates = /* @__PURE__ */ new Map();
9900
+ for (const container of sourceTree.filter(shouldBuildSectionsForContainer)) {
9901
+ const pageIds = new Set(
9902
+ sourceTree.filter((node) => node.kind === "page" && hasAncestor(node, container.id, byId)).map((node) => node.id)
9903
+ );
9904
+ if (pageIds.size === 0) continue;
9905
+ const directPageChildren = sourceTree.filter(
9906
+ (node) => node.parentId !== void 0 && pageIds.has(node.parentId) && node.kind !== "table_row" && node.kind !== "table_cell"
9907
+ ).sort(
9908
+ (left, right) => (left.pageStart ?? Number.MAX_SAFE_INTEGER) - (right.pageStart ?? Number.MAX_SAFE_INTEGER) || left.order - right.order || left.id.localeCompare(right.id)
9909
+ );
9910
+ if (directPageChildren.length === 0) continue;
9911
+ let currentSectionId;
9912
+ for (let index = 0; index < directPageChildren.length; index += 1) {
9913
+ const child = directPageChildren[index];
9914
+ const current = updates.get(child.id) ?? child;
9915
+ const heading = sectionHeadingTitle(current, container);
9916
+ if (heading) {
9917
+ const descendants = byParent.get(child.id) ?? [];
9918
+ const hasOwnContent = descendants.some((descendant) => descendant.kind !== "table_row" && descendant.kind !== "table_cell");
9919
+ let hasFollowingContent = false;
9920
+ for (const nextChild of directPageChildren.slice(index + 1)) {
9921
+ const next = updates.get(nextChild.id) ?? nextChild;
9922
+ if (sectionHeadingTitle(next, container)) break;
9923
+ hasFollowingContent = true;
9924
+ break;
9925
+ }
9926
+ if (!hasOwnContent && !hasFollowingContent) {
9927
+ currentSectionId = void 0;
9928
+ continue;
9929
+ }
9930
+ currentSectionId = child.id;
9931
+ updates.set(child.id, {
9932
+ ...current,
9933
+ parentId: container.id,
9934
+ kind: sectionKindForTitle(heading),
9935
+ title: heading,
9936
+ description: descriptionWithPages(cleanText([heading, "section"].join(" "), heading), [current, ...descendants]),
9937
+ metadata: {
9938
+ ...current.metadata,
9939
+ organizer: "title_section",
9940
+ sourceTreeVersion: "v3"
9941
+ }
9942
+ });
9943
+ continue;
9944
+ }
9945
+ if (!currentSectionId) continue;
9946
+ const parent = current.parentId ? byId.get(current.parentId) : void 0;
9947
+ if (!parent || parent.kind !== "page") continue;
9948
+ updates.set(child.id, {
9949
+ ...current,
9950
+ parentId: currentSectionId,
9951
+ metadata: {
9952
+ ...current.metadata,
9953
+ organizerRepair: "title_section_continuation"
9954
+ }
9955
+ });
9956
+ }
9957
+ }
9958
+ if (updates.size === 0) return sourceTree;
9959
+ return normalizeDocumentSourceTreePaths(sourceTree.map((node) => updates.get(node.id) ?? node));
9960
+ }
9608
9961
  function normalizeSemanticHierarchy(sourceTree) {
9609
9962
  const normalized = normalizeDocumentSourceTreePaths(
9610
9963
  normalizePolicyFormStructure(
@@ -9613,7 +9966,8 @@ function normalizeSemanticHierarchy(sourceTree) {
9613
9966
  );
9614
9967
  const nested = normalizeDocumentSourceTreePaths(nestEndorsementContinuationPages(normalized));
9615
9968
  const mergedNotices = normalizeDocumentSourceTreePaths(mergeAdministrativeNoticesIntoFrontMatter(nested));
9616
- const withEvidence = normalizeContainerEvidenceFromChildren(mergedNotices);
9969
+ const sectioned = normalizeDocumentSourceTreePaths(applyTitleSectionHierarchy(mergedNotices));
9970
+ const withEvidence = normalizeContainerEvidenceFromChildren(sectioned);
9617
9971
  return normalizeDocumentSourceTreePaths(
9618
9972
  normalizeRootSemanticOrder(normalizeContainerEvidenceFromChildren(withEvidence))
9619
9973
  );
@@ -9832,7 +10186,7 @@ function mergeOrganizationResults(results) {
9832
10186
  groups: [...groups.values()]
9833
10187
  };
9834
10188
  }
9835
- function buildOrganizationPrompt(batch) {
10189
+ function buildOrganizationPrompt(batch, formHints) {
9836
10190
  const nodes = batch.nodes.map((node) => compactNode(node, node.kind === "page" ? 900 : 320));
9837
10191
  return `You organize an insurance document source tree.
9838
10192
 
@@ -9841,10 +10195,15 @@ Scope:
9841
10195
  - The provided list is a bounded extraction-time batch. It is not necessarily the whole document.
9842
10196
  - Top-level page/form candidates in this batch: ${JSON.stringify(batch.topLevelNodeIds)}
9843
10197
 
10198
+ Expected form inventory / page ranges:
10199
+ ${formatFormHintsForPrompt(formHints)}
10200
+
9844
10201
  Rules:
9845
10202
  - Use only node IDs from the provided list.
9846
10203
  - Do not invent text, page numbers, source spans, limits, or policy facts.
9847
10204
  - 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.
10205
+ - Treat the form inventory as a page-range hint for the expected order: front matter/notices, declarations, policy form, then endorsements.
10206
+ - Prefer section hierarchy from printed title elements inside a form over page-by-page grouping.
9848
10207
  - Group adjacent separately numbered endorsements under a single generic "Endorsements" page_group parent, with each individual endorsement preserved as its own child node.
9849
10208
  - Never create rollup titles such as "Endorsements 1-3 (...)" or merge multiple endorsements into one endorsement node.
9850
10209
  - Add concise, human-readable titles to generic text, table, row, and cell nodes when the text makes their role clear.
@@ -9868,7 +10227,7 @@ function shouldRunOutlineCleanup(sourceTree) {
9868
10227
  if (hasDeclarations && hasPolicyForm && hasEndorsements) return false;
9869
10228
  return genericPages.length > 0 || topLevel.length > 6 || !hasDeclarations || !hasPolicyForm || !hasEndorsements;
9870
10229
  }
9871
- function buildOutlineCleanupPrompt(sourceTree) {
10230
+ function buildOutlineCleanupPrompt(sourceTree, formHints) {
9872
10231
  const topLevel = rootChildren(sourceTree).slice(0, OUTLINE_CLEANUP_MAX_TOP_LEVEL_NODES);
9873
10232
  const nodes = topLevel.map((node) => compactNode(node, 900));
9874
10233
  return `You clean a top-level source outline for an insurance policy.
@@ -9879,6 +10238,9 @@ Expected product-facing order:
9879
10238
  3. Policy Form: the main policy wording, insuring agreements, definitions, exclusions, conditions, claim provisions, and general policy terms.
9880
10239
  4. Endorsements: one generic "Endorsements" page_group containing each separately numbered endorsement as its own child.
9881
10240
 
10241
+ Expected form inventory / page ranges:
10242
+ ${formatFormHintsForPrompt(formHints)}
10243
+
9882
10244
  Rules:
9883
10245
  - Use only node IDs from this top-level list: ${JSON.stringify(topLevel.map((node) => node.id))}
9884
10246
  - Group only adjacent top-level nodes.
@@ -9887,6 +10249,7 @@ Rules:
9887
10249
  - Use canonical terse titles: "Notices and Jacket", "Declarations", "Policy Form", "Endorsements", or the printed endorsement number.
9888
10250
  - Keep page_group descriptions short and include the page range when pages are known, for example "Declarations pages 6-8".
9889
10251
  - 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.
10252
+ - If the form inventory provides page ranges, keep groups aligned to those ranges unless the source node text clearly contradicts them.
9890
10253
  - If the existing deterministic outline is already correct, return empty labels and groups.
9891
10254
 
9892
10255
  Top-level source nodes:
@@ -10061,6 +10424,14 @@ function materializeDocument(params) {
10061
10424
  insuredName,
10062
10425
  premium,
10063
10426
  policyTypes: profile.policyTypes,
10427
+ formInventory: params.formInventory.filter((form) => typeof form.formNumber === "string" && form.formNumber.trim().length > 0).map((form) => ({
10428
+ formNumber: form.formNumber,
10429
+ editionDate: form.editionDate,
10430
+ title: form.title,
10431
+ formType: form.formType,
10432
+ pageStart: form.pageStart,
10433
+ pageEnd: form.pageEnd
10434
+ })),
10064
10435
  coverages,
10065
10436
  documentMetadata,
10066
10437
  documentOutline,
@@ -10101,7 +10472,8 @@ function materializeDocument(params) {
10101
10472
  }
10102
10473
  async function runSourceTreeExtraction(params) {
10103
10474
  const sourceSpans = normalizeSourceSpans(params.sourceSpans);
10104
- let sourceTree = applySemanticPageGrouping(buildDocumentSourceTree(sourceSpans, params.id));
10475
+ const formHints = normalizeFormHints(params.formInventory?.forms, sourceSpans);
10476
+ let sourceTree = applySemanticPageGrouping(applyFormInventoryHints(buildDocumentSourceTree(sourceSpans, params.id), formHints));
10105
10477
  const warnings = [];
10106
10478
  let modelCalls = 0;
10107
10479
  let callsWithUsage = 0;
@@ -10133,7 +10505,7 @@ async function runSourceTreeExtraction(params) {
10133
10505
  const response = await safeGenerateObject(
10134
10506
  params.generateObject,
10135
10507
  {
10136
- prompt: buildOrganizationPrompt(batch),
10508
+ prompt: buildOrganizationPrompt(batch, formHints),
10137
10509
  schema: SourceTreeOrganizationSchema,
10138
10510
  maxTokens: budget.maxTokens,
10139
10511
  taskKind: "extraction_source_tree",
@@ -10167,7 +10539,7 @@ async function runSourceTreeExtraction(params) {
10167
10539
  const response = await safeGenerateObject(
10168
10540
  params.generateObject,
10169
10541
  {
10170
- prompt: buildOutlineCleanupPrompt(sourceTree),
10542
+ prompt: buildOutlineCleanupPrompt(sourceTree, formHints),
10171
10543
  schema: SourceTreeOrganizationSchema,
10172
10544
  maxTokens,
10173
10545
  taskKind: "extraction_source_tree",
@@ -10232,12 +10604,14 @@ async function runSourceTreeExtraction(params) {
10232
10604
  const document = materializeDocument({
10233
10605
  id: params.id,
10234
10606
  sourceTree,
10607
+ formInventory: formHints,
10235
10608
  operationalProfile
10236
10609
  });
10237
10610
  return {
10238
10611
  sourceTree,
10239
10612
  sourceSpans,
10240
10613
  sourceChunks: chunkSourceSpans(sourceSpans),
10614
+ formInventory: formHints,
10241
10615
  operationalProfile,
10242
10616
  document,
10243
10617
  chunks: [],
@@ -10587,16 +10961,66 @@ ${span.text}` : span.text;
10587
10961
  }
10588
10962
  }
10589
10963
  if (sourceSpans.length > 0) {
10964
+ const pageCount2 = Math.max(
10965
+ 1,
10966
+ ...sourceSpans.map((span) => span.pageEnd ?? span.pageStart ?? span.location?.endPage ?? span.location?.page ?? 1)
10967
+ );
10968
+ let formInventory2 = options?.formInventory;
10969
+ if (!formInventory2) {
10970
+ onProgress?.("Building form inventory from source spans...");
10971
+ const budget = resolveBudget("extraction_form_inventory", 2048);
10972
+ const startedAt = Date.now();
10973
+ const templateHints2 = buildTemplateHints("other", "policy", pageCount2, getTemplate("other"));
10974
+ const sourceText = formatSourceSpanText(sourceSpans);
10975
+ const prompt = `${buildFormInventoryPrompt(templateHints2)}
10976
+
10977
+ SOURCE SPAN DOCUMENT TEXT:
10978
+ ${sourceText}`;
10979
+ const response = await safeGenerateObject(
10980
+ generateObject,
10981
+ {
10982
+ prompt,
10983
+ schema: FormInventorySchema,
10984
+ maxTokens: budget.maxTokens,
10985
+ taskKind: "extraction_form_inventory",
10986
+ budgetDiagnostics: budget
10987
+ },
10988
+ {
10989
+ fallback: { forms: [] },
10990
+ log,
10991
+ onError: (err, attempt) => log?.(`Form inventory attempt ${attempt + 1} failed: ${err instanceof Error ? err.message : String(err)}`)
10992
+ }
10993
+ );
10994
+ trackUsage(response.usage, {
10995
+ taskKind: "extraction_form_inventory",
10996
+ label: "form_inventory",
10997
+ maxTokens: budget.maxTokens,
10998
+ durationMs: Date.now() - startedAt
10999
+ });
11000
+ formInventory2 = response.object;
11001
+ }
10590
11002
  onProgress?.("Building source-native document tree...");
10591
11003
  const v3 = await runSourceTreeExtraction({
10592
11004
  id,
10593
11005
  sourceSpans,
11006
+ formInventory: formInventory2,
10594
11007
  generateObject,
10595
11008
  providerOptions: activeProviderOptions,
10596
11009
  resolveBudget,
10597
11010
  trackUsage,
10598
11011
  log
10599
11012
  });
11013
+ const sourceTreeFormInventory = v3.formInventory.flatMap((form) => {
11014
+ const formNumber = typeof form.formNumber === "string" ? form.formNumber.trim() : "";
11015
+ if (!formNumber) return [];
11016
+ return [{
11017
+ formNumber,
11018
+ title: form.title,
11019
+ pageStart: form.pageStart,
11020
+ pageEnd: form.pageEnd,
11021
+ sources: ["source_tree"]
11022
+ }];
11023
+ });
10600
11024
  const reviewReport2 = {
10601
11025
  issues: v3.warnings.map((warning) => ({
10602
11026
  code: "source_tree_warning",
@@ -10610,7 +11034,7 @@ ${span.text}` : span.text;
10610
11034
  { kind: "operational_profile", label: "Operational Profile", itemCount: v3.operationalProfile.coverages.length }
10611
11035
  ],
10612
11036
  reviewRoundRecords: [],
10613
- formInventory: [],
11037
+ formInventory: sourceTreeFormInventory,
10614
11038
  qualityGateStatus: v3.warnings.length > 0 ? "warning" : "passed"
10615
11039
  };
10616
11040
  if (shouldFailQualityGate(qualityGate, reviewReport2.qualityGateStatus)) {