@claritylabs/cl-sdk 3.0.2 → 3.0.4

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.js CHANGED
@@ -327,6 +327,7 @@ __export(index_exports, {
327
327
  normalizeDoclingDocument: () => normalizeDoclingDocument,
328
328
  normalizeDocumentSourceTreePaths: () => normalizeDocumentSourceTreePaths,
329
329
  normalizeForMatch: () => normalizeForMatch,
330
+ normalizeSourceSpans: () => normalizeSourceSpans,
330
331
  orderSourceEvidence: () => orderSourceEvidence,
331
332
  overlayTextOnPdf: () => overlayTextOnPdf,
332
333
  pLimit: () => pLimit,
@@ -2655,9 +2656,117 @@ function chunkSourceSpans(spans, options = {}) {
2655
2656
  flush();
2656
2657
  return chunks;
2657
2658
  }
2659
+ function normalizeSourceSpans(spans) {
2660
+ const droppedParentSpanIds = /* @__PURE__ */ new Set();
2661
+ const cleaned = [];
2662
+ for (const [index, span] of spans.entries()) {
2663
+ if (span.parentSpanId && droppedParentSpanIds.has(span.parentSpanId)) continue;
2664
+ const normalized = normalizeSourceSpanText(span);
2665
+ if (!normalized) {
2666
+ droppedParentSpanIds.add(span.id);
2667
+ continue;
2668
+ }
2669
+ cleaned.push({ ...normalized, __originalIndex: index });
2670
+ }
2671
+ return mergeTextRuns(cleaned).map(({ __originalIndex: _index, ...span }) => span);
2672
+ }
2658
2673
  function sourceUnit(span) {
2659
2674
  return span.sourceUnit ?? span.metadata?.sourceUnit;
2660
2675
  }
2676
+ function spanPage(span) {
2677
+ return span.pageStart ?? span.location?.page ?? span.location?.startPage;
2678
+ }
2679
+ function normalizeSourceSpanText(span) {
2680
+ const unit = sourceUnit(span);
2681
+ const text = normalizeWhitespace(span.text);
2682
+ if (!text) return void 0;
2683
+ if (isDiscardableBoilerplate(text, unit)) return void 0;
2684
+ const cleanedText = cleanBoilerplateLines(text);
2685
+ if (!cleanedText) return void 0;
2686
+ if (cleanedText === text) return span;
2687
+ return retextSpan(span, cleanedText, {
2688
+ boilerplateRemoved: "true",
2689
+ removedBoilerplateText: removedBoilerplateLines(text).join(" | ").slice(0, 500)
2690
+ });
2691
+ }
2692
+ function isDiscardableBoilerplate(text, unit) {
2693
+ const cleaned = normalizeWhitespace(text.replace(/\bColumn\s+\d+:\s*/gi, ""));
2694
+ if (/^SPECIMEN POLICY\s+[-—]\s+FOR TESTING ONLY$/i.test(cleaned)) return true;
2695
+ if (/^Page\s+\d+\s+of\s+\d+$/i.test(cleaned)) return true;
2696
+ if (/^[A-Z]{2,}(?:-[A-Z0-9]{2,})+\s+\d{2}\s+\d{2}$/i.test(cleaned)) return true;
2697
+ if (/^[A-Z]{2,}(?:-[A-Z0-9]{2,})+\s+\d{2}\s+\d{2}\s*\|\s*Page\s+\d+\s+of\s+\d+$/i.test(cleaned)) return true;
2698
+ if (unit === "table_row" && /^[^|]{0,40}\|\s*Page\s+\d+\s+of\s+\d+$/i.test(cleaned)) return true;
2699
+ return false;
2700
+ }
2701
+ function isBoilerplateLine(line) {
2702
+ const cleaned = normalizeWhitespace(line.replace(/\bColumn\s+\d+:\s*/gi, ""));
2703
+ return isDiscardableBoilerplate(cleaned) || /^IMPORTANT NOTICE\s*[-—]\s*/i.test(cleaned) || /^THIS IS A CLAIMS-MADE AND REPORTED POLICY\.? PLEASE READ IT CAREFULLY\.?$/i.test(cleaned);
2704
+ }
2705
+ function removedBoilerplateLines(text) {
2706
+ return text.split(/\s{2,}|\r?\n/).map(normalizeWhitespace).filter((line) => line && isBoilerplateLine(line));
2707
+ }
2708
+ function cleanBoilerplateLines(text) {
2709
+ const withoutInlineBoilerplate = text.replace(/\b(?:Column\s+\d+:\s*)?[A-Z]{2,}(?:-[A-Z0-9]{2,})+\s+\d{2}\s+\d{2}\s+(?:\|\s*)?(?:Column\s+\d+:\s*)?Page\s+\d+\s+of\s+\d+\b/gi, " ").replace(/\bSPECIMEN POLICY\s+[-—]\s+FOR TESTING ONLY\b/gi, " ").replace(/\bPage\s+\d+\s+of\s+\d+\b/gi, " ");
2710
+ const lines = withoutInlineBoilerplate.split(/\r?\n/);
2711
+ const filtered = lines.map(normalizeWhitespace).filter((line) => line && !isBoilerplateLine(line));
2712
+ return normalizeWhitespace(filtered.join(" "));
2713
+ }
2714
+ function shouldMergeTextSpan(left, right) {
2715
+ if (sourceUnit(left) !== "text" || sourceUnit(right) !== "text") return false;
2716
+ if (spanPage(left) !== spanPage(right)) return false;
2717
+ if (left.metadata?.elementType === "title" || right.metadata?.elementType === "title") return false;
2718
+ const leftText = normalizeWhitespace(left.text);
2719
+ const rightText = normalizeWhitespace(right.text);
2720
+ if (!leftText || !rightText) return false;
2721
+ if (/[:.;!?)]$/.test(leftText)) return false;
2722
+ if (/^(?:[A-Z][A-Z0-9 &/(),.-]{8,}|Item\s+\d+|Section\s+\d+|Part\s+[A-Z]\b)/.test(rightText)) return false;
2723
+ return /^[a-z(]/.test(rightText) || /\b(?:a|an|and|any|as|at|by|for|from|in|into|may|must|of|or|that|the|this|to|with|within|you|your)$/i.test(leftText);
2724
+ }
2725
+ function mergeTextRuns(spans) {
2726
+ const result = [];
2727
+ let current;
2728
+ for (const span of spans) {
2729
+ if (current && shouldMergeTextSpan(current, span)) {
2730
+ current = mergeTextSpanPair(current, span);
2731
+ continue;
2732
+ }
2733
+ if (current) result.push(current);
2734
+ current = span;
2735
+ }
2736
+ if (current) result.push(current);
2737
+ return result;
2738
+ }
2739
+ function mergeTextSpanPair(left, right) {
2740
+ const text = normalizeWhitespace(`${left.text} ${right.text}`);
2741
+ const merged = retextSpan(left, text, {
2742
+ mergedSourceSpanIds: [left.metadata?.mergedSourceSpanIds, left.id, right.id, right.metadata?.mergedSourceSpanIds].filter(Boolean).join(","),
2743
+ sourceSpanNormalization: "merged_text_run"
2744
+ });
2745
+ return {
2746
+ ...merged,
2747
+ bbox: [...left.bbox ?? [], ...right.bbox ?? []],
2748
+ pageEnd: right.pageEnd ?? left.pageEnd,
2749
+ location: {
2750
+ ...left.location,
2751
+ endPage: right.location?.endPage ?? right.pageEnd ?? left.location?.endPage
2752
+ },
2753
+ __originalIndex: left.__originalIndex
2754
+ };
2755
+ }
2756
+ function retextSpan(span, text, metadata) {
2757
+ const textHash = sourceSpanTextHash(text);
2758
+ return SourceSpanSchema.parse({
2759
+ ...span,
2760
+ id: `${span.id.split(":").slice(0, -1).join(":")}:${textHash.slice(0, 12)}`,
2761
+ text,
2762
+ hash: textHash,
2763
+ textHash,
2764
+ metadata: {
2765
+ ...span.metadata ?? {},
2766
+ ...metadata
2767
+ }
2768
+ });
2769
+ }
2661
2770
  function filterChunkableSourceSpans(spans) {
2662
2771
  const rowIds = new Set(
2663
2772
  spans.filter((span) => sourceUnit(span) === "table_row").map((span) => span.id)
@@ -4406,14 +4515,14 @@ function stringArray(value) {
4406
4515
  function sourceUnit4(span) {
4407
4516
  return span.sourceUnit ?? span.metadata?.sourceUnit;
4408
4517
  }
4409
- function spanPage(span) {
4518
+ function spanPage2(span) {
4410
4519
  return span.pageStart ?? span.location?.page ?? span.location?.startPage;
4411
4520
  }
4412
4521
  function spanPageEnd(span) {
4413
- return span.pageEnd ?? span.location?.endPage ?? spanPage(span);
4522
+ return span.pageEnd ?? span.location?.endPage ?? spanPage2(span);
4414
4523
  }
4415
4524
  function sourceSpansForPage(sourceSpans, page) {
4416
- return sourceSpans.filter((span) => spanPage(span) === page && sourceUnit4(span) === "page").map((span) => span.id);
4525
+ return sourceSpans.filter((span) => spanPage2(span) === page && sourceUnit4(span) === "page").map((span) => span.id);
4417
4526
  }
4418
4527
  function nodePageOverlaps(node, record) {
4419
4528
  const recordStart = numberValue(record, "pageNumber", "pageStart", "resolvedFromPage");
@@ -4514,8 +4623,8 @@ function buildNodesFromSourceSpans(sourceSpans) {
4514
4623
  const unit = sourceUnit4(span);
4515
4624
  return unit === "section" || unit === "section_candidate" || unit === "page";
4516
4625
  });
4517
- return candidates.sort((left, right) => (spanPage(left) ?? 0) - (spanPage(right) ?? 0) || left.id.localeCompare(right.id)).map((span, index) => {
4518
- const title = span.sectionId ?? span.formNumber ?? (sourceUnit4(span) === "page" && spanPage(span) ? `Page ${spanPage(span)}` : `Source unit ${index + 1}`);
4626
+ return candidates.sort((left, right) => (spanPage2(left) ?? 0) - (spanPage2(right) ?? 0) || left.id.localeCompare(right.id)).map((span, index) => {
4627
+ const title = span.sectionId ?? span.formNumber ?? (sourceUnit4(span) === "page" && spanPage2(span) ? `Page ${spanPage2(span)}` : `Source unit ${index + 1}`);
4519
4628
  return {
4520
4629
  id: `source:${index}:${slugPart(span.id)}`,
4521
4630
  title,
@@ -4523,7 +4632,7 @@ function buildNodesFromSourceSpans(sourceSpans) {
4523
4632
  type: sourceUnit4(span),
4524
4633
  label: sourceUnit4(span),
4525
4634
  level: 1,
4526
- pageStart: spanPage(span),
4635
+ pageStart: spanPage2(span),
4527
4636
  pageEnd: spanPageEnd(span),
4528
4637
  formNumber: span.formNumber,
4529
4638
  excerpt: span.text.slice(0, 500),
@@ -9236,6 +9345,8 @@ var ORGANIZABLE_KINDS = [
9236
9345
  "schedule",
9237
9346
  "clause"
9238
9347
  ];
9348
+ var ORGANIZATION_TOP_LEVEL_BATCH_SIZE = 80;
9349
+ var ORGANIZATION_CHILD_CONTEXT_LIMIT = 4;
9239
9350
  var SourceTreeOrganizationSchema = import_zod41.z.object({
9240
9351
  labels: import_zod41.z.array(import_zod41.z.object({
9241
9352
  nodeId: import_zod41.z.string(),
@@ -9300,7 +9411,7 @@ function cleanText(value, fallback) {
9300
9411
  const text = value?.replace(/\s+/g, " ").trim();
9301
9412
  return text || fallback;
9302
9413
  }
9303
- function compactNode(node) {
9414
+ function compactNode(node, maxText = 700) {
9304
9415
  return {
9305
9416
  id: node.id,
9306
9417
  kind: node.kind,
@@ -9309,20 +9420,89 @@ function compactNode(node) {
9309
9420
  pageStart: node.pageStart,
9310
9421
  pageEnd: node.pageEnd,
9311
9422
  sourceSpanIds: node.sourceSpanIds.slice(0, 8),
9312
- text: (node.textExcerpt ?? node.description).slice(0, 700)
9423
+ text: (node.textExcerpt ?? node.description).slice(0, maxText)
9313
9424
  };
9314
9425
  }
9315
- function buildOrganizationPrompt(sourceTree) {
9316
- const nodes = sourceTree.filter((node) => node.kind !== "document").slice(0, 240).map(compactNode);
9426
+ function nodesByParent(sourceTree) {
9427
+ const byParent = /* @__PURE__ */ new Map();
9428
+ for (const node of sourceTree) {
9429
+ const children = byParent.get(node.parentId) ?? [];
9430
+ children.push(node);
9431
+ byParent.set(node.parentId, children);
9432
+ }
9433
+ for (const children of byParent.values()) {
9434
+ children.sort((left, right) => left.order - right.order || left.id.localeCompare(right.id));
9435
+ }
9436
+ return byParent;
9437
+ }
9438
+ function sourceTreeRootId(sourceTree) {
9439
+ return sourceTree.find((node) => node.kind === "document")?.id;
9440
+ }
9441
+ function organizationBatches(sourceTree) {
9442
+ const byParent = nodesByParent(sourceTree);
9443
+ const rootId = sourceTreeRootId(sourceTree);
9444
+ const topLevelNodes = (byParent.get(rootId) ?? []).filter((node) => node.kind !== "document");
9445
+ if (topLevelNodes.length === 0) {
9446
+ const nodes = sourceTree.filter((node) => node.kind !== "document").slice(0, 240);
9447
+ return [{
9448
+ label: "fallback node prefix because no document root children were found",
9449
+ topLevelNodeIds: nodes.map((node) => node.id),
9450
+ nodes
9451
+ }];
9452
+ }
9453
+ const batches = [];
9454
+ for (let index = 0; index < topLevelNodes.length; index += ORGANIZATION_TOP_LEVEL_BATCH_SIZE) {
9455
+ const topLevelBatch = topLevelNodes.slice(index, index + ORGANIZATION_TOP_LEVEL_BATCH_SIZE);
9456
+ const candidates = /* @__PURE__ */ new Map();
9457
+ for (const node of topLevelBatch) {
9458
+ candidates.set(node.id, node);
9459
+ const childContext = (byParent.get(node.id) ?? []).filter((child) => child.kind !== "text" && child.kind !== "table_cell").slice(0, ORGANIZATION_CHILD_CONTEXT_LIMIT);
9460
+ for (const child of childContext) {
9461
+ candidates.set(child.id, child);
9462
+ }
9463
+ }
9464
+ batches.push({
9465
+ label: `top-level nodes ${index + 1}-${index + topLevelBatch.length} of ${topLevelNodes.length}`,
9466
+ topLevelNodeIds: topLevelBatch.map((node) => node.id),
9467
+ nodes: [...candidates.values()]
9468
+ });
9469
+ }
9470
+ return batches;
9471
+ }
9472
+ function mergeOrganizationResults(results) {
9473
+ const labels = /* @__PURE__ */ new Map();
9474
+ const groups = /* @__PURE__ */ new Map();
9475
+ for (const result of results) {
9476
+ for (const label of result.labels) {
9477
+ labels.set(label.nodeId, { ...labels.get(label.nodeId), ...label });
9478
+ }
9479
+ for (const group of result.groups) {
9480
+ const key = `${group.kind}:${group.childNodeIds.join("|")}`;
9481
+ groups.set(key, group);
9482
+ }
9483
+ }
9484
+ return {
9485
+ labels: [...labels.values()],
9486
+ groups: [...groups.values()]
9487
+ };
9488
+ }
9489
+ function buildOrganizationPrompt(batch) {
9490
+ const nodes = batch.nodes.map((node) => compactNode(node, node.kind === "page" ? 900 : 320));
9317
9491
  return `You organize an insurance document source tree.
9318
9492
 
9493
+ Scope:
9494
+ - ${batch.label}
9495
+ - The provided list is a bounded extraction-time batch. It is not necessarily the whole document.
9496
+ - Top-level page/form candidates in this batch: ${JSON.stringify(batch.topLevelNodeIds)}
9497
+
9319
9498
  Rules:
9320
9499
  - Use only node IDs from the provided list.
9321
9500
  - Do not invent text, page numbers, source spans, limits, or policy facts.
9322
- - You may relabel existing nodes and group adjacent top-level/page nodes when they are clearly one form, endorsement, declarations set, schedule, or clause family.
9501
+ - You may relabel existing nodes and group adjacent top-level/page nodes from this batch when they are clearly one form, endorsement, declarations set, schedule, or clause family.
9323
9502
  - Add concise, human-readable titles to generic text, table, row, and cell nodes when the text makes their role clear.
9324
9503
  - Groups must list existing childNodeIds only.
9325
9504
  - Keep descriptions short and useful for search.
9505
+ - Prefer the document's own form titles, endorsement titles, schedules, declarations headings, and page order over keyword-only guessing.
9326
9506
 
9327
9507
  Source nodes:
9328
9508
  ${JSON.stringify(nodes, null, 2)}
@@ -9362,8 +9542,9 @@ function groupNodeId(documentId, group) {
9362
9542
  }
9363
9543
  function applyOrganization(sourceTree, organization) {
9364
9544
  const byId = new Map(sourceTree.map((node) => [node.id, node]));
9545
+ const labels = new Map(organization.labels.map((label) => [label.nodeId, label]));
9365
9546
  let nextTree = sourceTree.map((node) => {
9366
- const label = organization.labels.find((item) => item.nodeId === node.id);
9547
+ const label = labels.get(node.id);
9367
9548
  if (!label) return node;
9368
9549
  return {
9369
9550
  ...node,
@@ -9372,7 +9553,7 @@ function applyOrganization(sourceTree, organization) {
9372
9553
  description: cleanText(label.description, node.description)
9373
9554
  };
9374
9555
  });
9375
- for (const group of organization.groups.slice(0, 40)) {
9556
+ for (const group of organization.groups) {
9376
9557
  const children = group.childNodeIds.map((id2) => byId.get(id2)).filter((node2) => Boolean(node2));
9377
9558
  if (children.length === 0) continue;
9378
9559
  const parentId = children[0].parentId;
@@ -9531,7 +9712,8 @@ function materializeDocument(params) {
9531
9712
  };
9532
9713
  }
9533
9714
  async function runSourceTreeExtraction(params) {
9534
- let sourceTree = buildDocumentSourceTree(params.sourceSpans, params.id);
9715
+ const sourceSpans = normalizeSourceSpans(params.sourceSpans);
9716
+ let sourceTree = buildDocumentSourceTree(sourceSpans, params.id);
9535
9717
  const warnings = [];
9536
9718
  let modelCalls = 0;
9537
9719
  let callsWithUsage = 0;
@@ -9554,41 +9736,46 @@ async function runSourceTreeExtraction(params) {
9554
9736
  params.trackUsage(usage, report);
9555
9737
  };
9556
9738
  try {
9557
- const budget = params.resolveBudget("extraction_source_tree", 4096);
9558
- const startedAt = Date.now();
9559
- const response = await safeGenerateObject(
9560
- params.generateObject,
9561
- {
9562
- prompt: buildOrganizationPrompt(sourceTree),
9563
- schema: SourceTreeOrganizationSchema,
9564
- maxTokens: budget.maxTokens,
9739
+ const organizations = [];
9740
+ const batches = organizationBatches(sourceTree);
9741
+ for (const [batchIndex, batch] of batches.entries()) {
9742
+ const budget = params.resolveBudget("extraction_source_tree", 4096);
9743
+ const startedAt = Date.now();
9744
+ const response = await safeGenerateObject(
9745
+ params.generateObject,
9746
+ {
9747
+ prompt: buildOrganizationPrompt(batch),
9748
+ schema: SourceTreeOrganizationSchema,
9749
+ maxTokens: budget.maxTokens,
9750
+ taskKind: "extraction_source_tree",
9751
+ budgetDiagnostics: budget,
9752
+ providerOptions: { ...params.providerOptions, sourceSpans }
9753
+ },
9754
+ {
9755
+ fallback: { labels: [], groups: [] },
9756
+ log: params.log
9757
+ }
9758
+ );
9759
+ localTrack(response.usage, {
9565
9760
  taskKind: "extraction_source_tree",
9566
- budgetDiagnostics: budget,
9567
- providerOptions: { ...params.providerOptions, sourceSpans: params.sourceSpans }
9568
- },
9569
- {
9570
- fallback: { labels: [], groups: [] },
9571
- log: params.log
9572
- }
9573
- );
9574
- localTrack(response.usage, {
9575
- taskKind: "extraction_source_tree",
9576
- label: "source_tree_organizer",
9577
- maxTokens: budget.maxTokens,
9578
- durationMs: Date.now() - startedAt
9579
- });
9580
- sourceTree = applyOrganization(sourceTree, response.object);
9761
+ label: batches.length > 1 ? `source_tree_organizer_${batchIndex + 1}` : "source_tree_organizer",
9762
+ maxTokens: budget.maxTokens,
9763
+ durationMs: Date.now() - startedAt
9764
+ });
9765
+ organizations.push(response.object);
9766
+ }
9767
+ sourceTree = applyOrganization(sourceTree, mergeOrganizationResults(organizations));
9581
9768
  } catch (error) {
9582
9769
  warnings.push(`Source-tree organizer failed; deterministic tree used (${error instanceof Error ? error.message : String(error)})`);
9583
9770
  }
9584
9771
  const deterministicProfile = buildDeterministicOperationalProfile({
9585
9772
  sourceTree,
9586
- sourceSpans: params.sourceSpans
9773
+ sourceSpans
9587
9774
  });
9588
9775
  let operationalProfile = deterministicProfile;
9589
9776
  try {
9590
9777
  const validNodeIds = new Set(sourceTree.map((node) => node.id));
9591
- const validSpanIds = new Set(params.sourceSpans.map((span) => span.id));
9778
+ const validSpanIds = new Set(sourceSpans.map((span) => span.id));
9592
9779
  const budget = params.resolveBudget("extraction_operational_profile", 8192);
9593
9780
  const startedAt = Date.now();
9594
9781
  const response = await safeGenerateObject(
@@ -9599,7 +9786,7 @@ async function runSourceTreeExtraction(params) {
9599
9786
  maxTokens: budget.maxTokens,
9600
9787
  taskKind: "extraction_operational_profile",
9601
9788
  budgetDiagnostics: budget,
9602
- providerOptions: { ...params.providerOptions, sourceSpans: params.sourceSpans, sourceTree }
9789
+ providerOptions: { ...params.providerOptions, sourceSpans, sourceTree }
9603
9790
  },
9604
9791
  {
9605
9792
  fallback: deterministicProfile,
@@ -9628,8 +9815,8 @@ async function runSourceTreeExtraction(params) {
9628
9815
  });
9629
9816
  return {
9630
9817
  sourceTree,
9631
- sourceSpans: params.sourceSpans,
9632
- sourceChunks: chunkSourceSpans(params.sourceSpans),
9818
+ sourceSpans,
9819
+ sourceChunks: chunkSourceSpans(sourceSpans),
9633
9820
  operationalProfile,
9634
9821
  document,
9635
9822
  chunks: [],
@@ -14644,6 +14831,7 @@ var AGENT_TOOLS = [
14644
14831
  normalizeDoclingDocument,
14645
14832
  normalizeDocumentSourceTreePaths,
14646
14833
  normalizeForMatch,
14834
+ normalizeSourceSpans,
14647
14835
  orderSourceEvidence,
14648
14836
  overlayTextOnPdf,
14649
14837
  pLimit,