@claritylabs/cl-sdk 3.1.18 → 3.1.20

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
@@ -20,17 +20,19 @@ function isRetryableError(error) {
20
20
  }
21
21
  return false;
22
22
  }
23
- async function withRetry(fn, log) {
23
+ async function withRetry(fn, log, options) {
24
+ const maxRetries = options?.maxRetries ?? MAX_RETRIES;
25
+ const baseDelayMs = options?.baseDelayMs ?? BASE_DELAY_MS;
24
26
  for (let attempt = 0; ; attempt++) {
25
27
  try {
26
28
  return await fn();
27
29
  } catch (error) {
28
- if (!isRetryableError(error) || attempt >= MAX_RETRIES) {
30
+ if (!isRetryableError(error) || attempt >= maxRetries) {
29
31
  throw error;
30
32
  }
31
33
  const jitter = Math.random() * 1e3;
32
- const delay = BASE_DELAY_MS * Math.pow(2, attempt) + jitter;
33
- await log?.(`Retryable error, retrying in ${(delay / 1e3).toFixed(1)}s (attempt ${attempt + 1}/${MAX_RETRIES})...`);
34
+ const delay = baseDelayMs * Math.pow(2, attempt) + jitter;
35
+ await log?.(`Retryable error, retrying in ${(delay / 1e3).toFixed(1)}s (attempt ${attempt + 1}/${maxRetries})...`);
34
36
  await new Promise((resolve) => setTimeout(resolve, delay));
35
37
  }
36
38
  }
@@ -157,10 +159,8 @@ async function safeGenerateObject(generateObject, params, options) {
157
159
  const strictParams = { ...params, schema: toStrictSchema(params.schema) };
158
160
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
159
161
  try {
160
- const result = await withRetry(
161
- () => generateObject(strictParams),
162
- options?.log
163
- );
162
+ const generate = () => generateObject(strictParams);
163
+ const result = options?.retry === false ? await generate() : await withRetry(generate, options?.log, options?.retry);
164
164
  return {
165
165
  ...result,
166
166
  object: params.schema.parse(sanitizeNulls(result.object))
@@ -9761,6 +9761,12 @@ function endorsementDescription(title, node) {
9761
9761
  title
9762
9762
  );
9763
9763
  }
9764
+ function endorsementTitleKey(node) {
9765
+ const title = endorsementTitle(sourceNodeText(node));
9766
+ if (title) return title.toLowerCase();
9767
+ const fallback = cleanText(node.title, "");
9768
+ return fallback ? fallback.toLowerCase() : void 0;
9769
+ }
9764
9770
  function nodePageEnd2(node) {
9765
9771
  return node.pageEnd ?? node.pageStart;
9766
9772
  }
@@ -10682,7 +10688,76 @@ function applyEndorsementGrouping(sourceTree) {
10682
10688
  (node) => endorsementGroupChildIds.has(node.id) ? { ...node, parentId: groupId, order: node.order + 1e-3 } : node
10683
10689
  );
10684
10690
  }
10685
- return normalizeSemanticHierarchy(nextTree);
10691
+ return collapseNestedDuplicateEndorsements(normalizeSemanticHierarchy(nextTree));
10692
+ }
10693
+ function nearestEndorsementAncestor(node, byId) {
10694
+ let parentId = node.parentId;
10695
+ const seen = /* @__PURE__ */ new Set();
10696
+ while (parentId) {
10697
+ if (seen.has(parentId)) return void 0;
10698
+ seen.add(parentId);
10699
+ const parent = byId.get(parentId);
10700
+ if (!parent) return void 0;
10701
+ if (parent.kind === "endorsement") return parent;
10702
+ parentId = parent.parentId;
10703
+ }
10704
+ return void 0;
10705
+ }
10706
+ function collapseNestedDuplicateEndorsements(sourceTree) {
10707
+ const byId = new Map(sourceTree.map((node) => [node.id, node]));
10708
+ const replacementById = /* @__PURE__ */ new Map();
10709
+ const duplicateEvidenceByTarget = /* @__PURE__ */ new Map();
10710
+ for (const node of sourceTree) {
10711
+ if (node.kind !== "endorsement") continue;
10712
+ const ancestor = nearestEndorsementAncestor(node, byId);
10713
+ if (!ancestor) continue;
10714
+ if (endorsementTitleKey(node) !== endorsementTitleKey(ancestor)) continue;
10715
+ replacementById.set(node.id, ancestor.id);
10716
+ duplicateEvidenceByTarget.set(ancestor.id, [
10717
+ ...duplicateEvidenceByTarget.get(ancestor.id) ?? [],
10718
+ node
10719
+ ]);
10720
+ }
10721
+ if (replacementById.size === 0) return sourceTree;
10722
+ const replacementParent = (parentId) => {
10723
+ let next = parentId;
10724
+ const seen = /* @__PURE__ */ new Set();
10725
+ while (next && replacementById.has(next) && !seen.has(next)) {
10726
+ seen.add(next);
10727
+ next = replacementById.get(next);
10728
+ }
10729
+ return next;
10730
+ };
10731
+ const updated = sourceTree.filter((node) => !replacementById.has(node.id)).map((node) => {
10732
+ const evidence = duplicateEvidenceByTarget.get(node.id);
10733
+ const parentId = replacementParent(node.parentId);
10734
+ if (!evidence?.length) {
10735
+ return parentId === node.parentId ? node : {
10736
+ ...node,
10737
+ parentId,
10738
+ metadata: {
10739
+ ...node.metadata,
10740
+ organizerRepair: "collapse_duplicate_endorsement_wrapper"
10741
+ }
10742
+ };
10743
+ }
10744
+ const evidenceNodes = [node, ...evidence];
10745
+ const pageStarts = evidenceNodes.map((item) => item.pageStart).filter((page) => typeof page === "number");
10746
+ const pageEnds = evidenceNodes.map((item) => item.pageEnd ?? item.pageStart).filter((page) => typeof page === "number");
10747
+ return {
10748
+ ...node,
10749
+ parentId,
10750
+ sourceSpanIds: [...new Set(evidenceNodes.flatMap((item) => item.sourceSpanIds))],
10751
+ pageStart: pageStarts.length ? Math.min(...pageStarts) : node.pageStart,
10752
+ pageEnd: pageEnds.length ? Math.max(...pageEnds) : node.pageEnd,
10753
+ bbox: evidenceNodes.flatMap((item) => item.bbox ?? []).slice(0, 12),
10754
+ metadata: {
10755
+ ...node.metadata,
10756
+ organizerRepair: "collapse_duplicate_endorsement_wrapper"
10757
+ }
10758
+ };
10759
+ });
10760
+ return normalizeDocumentSourceTreePaths(normalizeContainerEvidenceFromChildren(updated));
10686
10761
  }
10687
10762
  function compactNode2(node, maxText = 700) {
10688
10763
  return {
@@ -10881,9 +10956,13 @@ Rules:
10881
10956
  - Every returned value must include sourceNodeIds or sourceSpanIds from the provided nodes.
10882
10957
  - If a value is not directly supported, omit it.
10883
10958
  - Prefer declarations, schedules, premium tables, and endorsement schedules over generic policy wording.
10959
+ - Keep each coverage unit tied to one evidence scope: a declaration/core schedule row, a core policy form section, or one specific endorsement schedule. Do not merge declaration facts and endorsement schedule facts into the same coverage unit, even when they use the same coverage name.
10960
+ - If the declarations schedule and an endorsement schedule both list Network Security, Social Engineering Fraud, Regulatory Proceedings, or another same-named coverage, return separate coverage units for each supported source scope.
10961
+ - Use the declaration coverage name for declaration/core schedule rows. Use the endorsement title or endorsement schedule coverage name for endorsement rows, and include formNumber and endorsementNumber when source-backed.
10884
10962
  - For life, critical illness, disability, and long-term care policies, keep named benefit units and benefit subconditions as operational facts even when they do not have dollar limits. Examples include death benefit, disability benefit, total disability, catastrophic disability, return of premium, waiver, and conversion options. Put subcondition details in coverages[].limits with kind "other" when they belong under a broader benefit.
10885
10963
  - Treat an endorsement as one coverage unit when it contains a schedule. Do not split an endorsement schedule into generic rows like "Aggregate Limit".
10886
10964
  - For coverage schedules, put each claim, aggregate, sublimit, retention, deductible, and retroactive date values in coverages[].limits with labels and source IDs. Keep the legacy coverages[].limit as the primary display value only.
10965
+ - Extract coinsurance, participation percentage, or insurer/named-insured split terms as coverages[].limits entries with kind "other" when they are part of a coverage schedule.
10887
10966
  - Use coverageOrigin: "endorsement" for endorsement units and "core" for declarations/core policy coverage units.
10888
10967
  - Do not copy entire policy wording into fields.
10889
10968
  - Extract facts directly from source nodes. There is no deterministic fact baseline.
@@ -11141,6 +11220,26 @@ function findCellByVisualPosition(sourceCell, targetCells) {
11141
11220
  );
11142
11221
  return positioned[0]?.cell;
11143
11222
  }
11223
+ function isDateLikeColumn(label) {
11224
+ return /\b(?:date|effective|expiration|retroactive)\b/i.test(cleanText(label, ""));
11225
+ }
11226
+ function containsDateValue(text) {
11227
+ return /\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b/.test(text) || /\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\.?\s+\d{1,2},?\s+\d{2,4}\b/i.test(text);
11228
+ }
11229
+ function looksLikePolicyProse(text) {
11230
+ const words = text.split(/\s+/).filter(Boolean);
11231
+ if (words.length < 8) return false;
11232
+ return /\b(?:are|is|will|shall|must|may|includes?|included|reduce|subject|payment|terms?|conditions?|provided|pursuant)\b/i.test(text);
11233
+ }
11234
+ function shouldMergeVisualContinuation(params) {
11235
+ if (startsNewVisualTableItem(params.sourceCells)) return false;
11236
+ const targetLabel = params.targetCell?.title;
11237
+ if (isDateLikeColumn(targetLabel) && !containsDateValue(params.sourceText)) return false;
11238
+ if (looksLikePolicyProse(params.sourceText) && !/\b(?:description|coverage|remarks?|notes?)\b/i.test(cleanText(targetLabel, ""))) {
11239
+ return false;
11240
+ }
11241
+ return true;
11242
+ }
11144
11243
  function columnLabelStartIndex(rows) {
11145
11244
  const headerIndex = rows.findIndex(
11146
11245
  ({ row, cells }) => isSourceTreeHeaderRow(row) && cells.length > 1
@@ -11290,6 +11389,7 @@ function applyVisualTableRepair(sourceTree, repair) {
11290
11389
  targetColumnIndex: continuation.targetColumnIndex,
11291
11390
  targetColumnLabel: continuation.targetColumnLabel
11292
11391
  });
11392
+ if (!shouldMergeVisualContinuation({ sourceText, sourceCells, targetCell })) continue;
11293
11393
  const sourceNodes = [sourceRow, ...sourceCells];
11294
11394
  const sourceSpanIds = sourceSpanIdsForNodes(sourceNodes);
11295
11395
  let mergedIntoCells = false;
@@ -11392,7 +11492,7 @@ async function runVisualTableRepair(params) {
11392
11492
  const repairResults = await Promise.all(candidates.map(async (candidate, index) => {
11393
11493
  const label = `source_tree_visual_table_repair_p${candidate.page}`;
11394
11494
  try {
11395
- const budget = params.resolveBudget("extraction_source_tree", 1800);
11495
+ const budget = params.resolveBudget("extraction_visual_table_repair", 1800);
11396
11496
  const maxTokens = Math.min(budget.maxTokens, 2400);
11397
11497
  const startedAt = Date.now();
11398
11498
  const response = await safeGenerateObject(
@@ -11401,7 +11501,7 @@ async function runVisualTableRepair(params) {
11401
11501
  prompt: buildVisualTableRepairPrompt(candidate),
11402
11502
  schema: SourceTreeVisualTableRepairSchema,
11403
11503
  maxTokens,
11404
- taskKind: "extraction_source_tree",
11504
+ taskKind: "extraction_visual_table_repair",
11405
11505
  budgetDiagnostics: { ...budget, maxTokens },
11406
11506
  trace: {
11407
11507
  label,
@@ -11414,7 +11514,9 @@ async function runVisualTableRepair(params) {
11414
11514
  },
11415
11515
  {
11416
11516
  fallback: { tables: [], warnings: [] },
11417
- log: params.log
11517
+ maxRetries: 0,
11518
+ log: params.log,
11519
+ retry: false
11418
11520
  }
11419
11521
  );
11420
11522
  return {
@@ -11440,7 +11542,7 @@ async function runVisualTableRepair(params) {
11440
11542
  continue;
11441
11543
  }
11442
11544
  params.trackUsage(result.usage, {
11443
- taskKind: "extraction_source_tree",
11545
+ taskKind: "extraction_visual_table_repair",
11444
11546
  label: result.label,
11445
11547
  maxTokens: result.maxTokens,
11446
11548
  durationMs: result.durationMs
@@ -11545,32 +11647,6 @@ var NORMALIZED_COMPATIBILITY_FIELDS = /* @__PURE__ */ new Set([
11545
11647
  "insurer",
11546
11648
  "broker"
11547
11649
  ]);
11548
- function coverageBelongsToEndorsementCleanupChunk(coverage) {
11549
- return coverage.coverageOrigin === "endorsement" || Boolean(coverage.endorsementNumber);
11550
- }
11551
- function coverageCleanupChunks(profile) {
11552
- const basePolicy = [];
11553
- const endorsements = [];
11554
- profile.coverages.forEach((coverage, coverageIndex) => {
11555
- if (coverageBelongsToEndorsementCleanupChunk(coverage)) {
11556
- endorsements.push(coverageIndex);
11557
- } else {
11558
- basePolicy.push(coverageIndex);
11559
- }
11560
- });
11561
- return [
11562
- basePolicy.length ? {
11563
- group: "base_policy",
11564
- label: "Coverage cleanup: base policy",
11565
- coverageIndexes: basePolicy
11566
- } : void 0,
11567
- endorsements.length ? {
11568
- group: "endorsements",
11569
- label: "Coverage cleanup: endorsements",
11570
- coverageIndexes: endorsements
11571
- } : void 0
11572
- ].filter((chunk) => Boolean(chunk));
11573
- }
11574
11650
  function valueOf(profile, key) {
11575
11651
  const value = profile[key];
11576
11652
  if (!value || typeof value !== "object" || Array.isArray(value) || !("value" in value)) return void 0;
@@ -11700,6 +11776,105 @@ function materializeDocument(params) {
11700
11776
  retroactiveDate: valueOf(profile, "retroactiveDate")
11701
11777
  };
11702
11778
  }
11779
+ function coverageCleanupGroups(profile) {
11780
+ const groups = [];
11781
+ const coreIndexes = [];
11782
+ const endorsementIndexes = [];
11783
+ const unclassifiedIndexes = [];
11784
+ profile.coverages.forEach((coverage, coverageIndex) => {
11785
+ if (coverage.coverageOrigin === "core") {
11786
+ coreIndexes.push(coverageIndex);
11787
+ } else if (coverage.coverageOrigin === "endorsement") {
11788
+ endorsementIndexes.push(coverageIndex);
11789
+ } else {
11790
+ unclassifiedIndexes.push(coverageIndex);
11791
+ }
11792
+ });
11793
+ if (coreIndexes.length) {
11794
+ groups.push({
11795
+ id: "policy",
11796
+ label: "Coverage schedule cleanup: policy schedules",
11797
+ coverageIndexes: coreIndexes
11798
+ });
11799
+ }
11800
+ if (endorsementIndexes.length) {
11801
+ groups.push({
11802
+ id: "endorsements",
11803
+ label: "Coverage schedule cleanup: endorsement schedules",
11804
+ coverageIndexes: endorsementIndexes
11805
+ });
11806
+ }
11807
+ if (unclassifiedIndexes.length) {
11808
+ groups.push({
11809
+ id: "source_backed",
11810
+ label: "Coverage schedule cleanup: source-backed schedules",
11811
+ coverageIndexes: unclassifiedIndexes
11812
+ });
11813
+ }
11814
+ return groups.length > 1 ? groups : [{
11815
+ id: "all",
11816
+ label: "Coverage schedule cleanup"
11817
+ }];
11818
+ }
11819
+ async function cleanupOperationalCoverageSchedules(params) {
11820
+ const groups = coverageCleanupGroups(params.operationalProfile);
11821
+ const validNodeIds = new Set(params.sourceTree.map((node) => node.id));
11822
+ const validSpanIds = new Set(params.sourceSpans.map((span) => span.id));
11823
+ const results = await Promise.all(groups.map(async (group, groupIndex) => {
11824
+ const budget = params.resolveBudget("extraction_coverage_cleanup", 4096);
11825
+ const startedAt = Date.now();
11826
+ const response = await safeGenerateObject(
11827
+ params.generateObject,
11828
+ {
11829
+ prompt: buildOperationalProfileCleanupPrompt(
11830
+ params.sourceTree,
11831
+ params.operationalProfile,
11832
+ { coverageIndexes: group.coverageIndexes, label: group.label }
11833
+ ),
11834
+ schema: OperationalProfileCleanupSchema,
11835
+ maxTokens: budget.maxTokens,
11836
+ taskKind: "extraction_coverage_cleanup",
11837
+ budgetDiagnostics: budget,
11838
+ providerOptions: params.providerOptions,
11839
+ trace: {
11840
+ phase: "coverage_cleanup",
11841
+ label: group.label,
11842
+ itemCount: group.coverageIndexes?.length ?? params.operationalProfile.coverages.length,
11843
+ coverageGroup: group.id,
11844
+ batchIndex: groups.length > 1 ? groupIndex + 1 : void 0,
11845
+ batchCount: groups.length > 1 ? groups.length : void 0,
11846
+ sourceBacked: true
11847
+ }
11848
+ },
11849
+ {
11850
+ fallback: { coverageDecisions: [], warnings: [] },
11851
+ maxRetries: 0,
11852
+ log: params.log,
11853
+ retry: false
11854
+ }
11855
+ );
11856
+ params.trackUsage(response.usage, {
11857
+ taskKind: "extraction_coverage_cleanup",
11858
+ label: group.id === "all" ? "coverage_cleanup" : `coverage_cleanup_${group.id}`,
11859
+ maxTokens: budget.maxTokens,
11860
+ durationMs: Date.now() - startedAt
11861
+ });
11862
+ return response.object;
11863
+ }));
11864
+ const cleanup = {
11865
+ coverageDecisions: results.flatMap((result) => result.coverageDecisions ?? []),
11866
+ warnings: results.flatMap((result) => result.warnings ?? [])
11867
+ };
11868
+ return {
11869
+ operationalProfile: applyOperationalProfileCleanup(
11870
+ params.operationalProfile,
11871
+ cleanup,
11872
+ validNodeIds,
11873
+ validSpanIds
11874
+ ),
11875
+ warnings: cleanup.warnings
11876
+ };
11877
+ }
11703
11878
  async function runSourceTreeExtraction(params) {
11704
11879
  const sourceSpans = normalizeSourceSpans(params.sourceSpans);
11705
11880
  const formHints = normalizeFormHints(params.formInventory?.forms, sourceSpans);
@@ -11749,7 +11924,9 @@ async function runSourceTreeExtraction(params) {
11749
11924
  },
11750
11925
  {
11751
11926
  fallback: { labels: [], groups: [] },
11752
- log: params.log
11927
+ maxRetries: 0,
11928
+ log: params.log,
11929
+ retry: false
11753
11930
  }
11754
11931
  );
11755
11932
  return {
@@ -11794,7 +11971,9 @@ async function runSourceTreeExtraction(params) {
11794
11971
  },
11795
11972
  {
11796
11973
  fallback: { labels: [], groups: [] },
11797
- log: params.log
11974
+ maxRetries: 0,
11975
+ log: params.log,
11976
+ retry: false
11798
11977
  }
11799
11978
  );
11800
11979
  localTrack(response.usage, {
@@ -11836,7 +12015,9 @@ async function runSourceTreeExtraction(params) {
11836
12015
  },
11837
12016
  {
11838
12017
  fallback: emptyProfile,
11839
- log: params.log
12018
+ maxRetries: 0,
12019
+ log: params.log,
12020
+ retry: false
11840
12021
  }
11841
12022
  );
11842
12023
  localTrack(response.usage, {
@@ -11856,69 +12037,18 @@ async function runSourceTreeExtraction(params) {
11856
12037
  }
11857
12038
  if (operationalProfile.coverages.length > 0) {
11858
12039
  try {
11859
- const validNodeIds = new Set(sourceTree.map((node) => node.id));
11860
- const validSpanIds = new Set(sourceSpans.map((span) => span.id));
11861
- const chunks = coverageCleanupChunks(operationalProfile);
11862
- if (chunks.length > 1) {
11863
- await params.log?.(`Operational profile coverage cleanup reviewing ${chunks.length} coverage groups in parallel`);
11864
- }
11865
- const cleanupResults = await Promise.all(chunks.map(async (chunk, batchIndex) => {
11866
- const budget = params.resolveBudget("extraction_coverage_cleanup", 4096);
11867
- const startedAt = Date.now();
11868
- const response = await safeGenerateObject(
11869
- params.generateObject,
11870
- {
11871
- prompt: buildOperationalProfileCleanupPrompt(sourceTree, operationalProfile, {
11872
- coverageIndexes: chunk.coverageIndexes,
11873
- label: chunk.label
11874
- }),
11875
- schema: OperationalProfileCleanupSchema,
11876
- maxTokens: budget.maxTokens,
11877
- taskKind: "extraction_coverage_cleanup",
11878
- budgetDiagnostics: budget,
11879
- providerOptions: params.providerOptions,
11880
- trace: {
11881
- phase: "coverage_cleanup",
11882
- label: chunk.label,
11883
- batchIndex: batchIndex + 1,
11884
- batchCount: chunks.length,
11885
- coverageGroup: chunk.group,
11886
- itemCount: chunk.coverageIndexes.length,
11887
- sourceBacked: true
11888
- }
11889
- },
11890
- {
11891
- fallback: { coverageDecisions: [], warnings: [] },
11892
- maxRetries: 0,
11893
- log: params.log
11894
- }
11895
- );
11896
- return {
11897
- batchIndex,
11898
- chunk,
11899
- budget,
11900
- durationMs: Date.now() - startedAt,
11901
- usage: response.usage,
11902
- cleanup: response.object
11903
- };
11904
- }));
11905
- const cleanup = { coverageDecisions: [], warnings: [] };
11906
- for (const result of cleanupResults.sort((left, right) => left.batchIndex - right.batchIndex)) {
11907
- localTrack(result.usage, {
11908
- taskKind: "extraction_coverage_cleanup",
11909
- label: result.chunk.label,
11910
- maxTokens: result.budget.maxTokens,
11911
- durationMs: result.durationMs
11912
- });
11913
- cleanup.coverageDecisions.push(...result.cleanup.coverageDecisions);
11914
- cleanup.warnings.push(...result.cleanup.warnings);
11915
- }
11916
- operationalProfile = applyOperationalProfileCleanup(
12040
+ const cleanup = await cleanupOperationalCoverageSchedules({
12041
+ sourceTree,
12042
+ sourceSpans,
11917
12043
  operationalProfile,
11918
- cleanup,
11919
- validNodeIds,
11920
- validSpanIds
11921
- );
12044
+ generateObject: params.generateObject,
12045
+ providerOptions: params.providerOptions,
12046
+ resolveBudget: params.resolveBudget,
12047
+ trackUsage: localTrack,
12048
+ log: params.log
12049
+ });
12050
+ operationalProfile = cleanup.operationalProfile;
12051
+ warnings.push(...cleanup.warnings);
11922
12052
  } catch (error) {
11923
12053
  warnings.push(`Operational profile cleanup pass failed; uncleaned profile used (${error instanceof Error ? error.message : String(error)})`);
11924
12054
  }
@@ -12313,7 +12443,9 @@ ${sourceText}`;
12313
12443
  },
12314
12444
  {
12315
12445
  fallback: { forms: [] },
12446
+ maxRetries: 0,
12316
12447
  log,
12448
+ retry: false,
12317
12449
  onError: (err, attempt) => log?.(`Form inventory attempt ${attempt + 1} failed: ${err instanceof Error ? err.message : String(err)}`)
12318
12450
  }
12319
12451
  );