@claritylabs/cl-sdk 3.1.19 → 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
@@ -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,
@@ -11442,7 +11542,7 @@ async function runVisualTableRepair(params) {
11442
11542
  continue;
11443
11543
  }
11444
11544
  params.trackUsage(result.usage, {
11445
- taskKind: "extraction_source_tree",
11545
+ taskKind: "extraction_visual_table_repair",
11446
11546
  label: result.label,
11447
11547
  maxTokens: result.maxTokens,
11448
11548
  durationMs: result.durationMs
@@ -11676,6 +11776,105 @@ function materializeDocument(params) {
11676
11776
  retroactiveDate: valueOf(profile, "retroactiveDate")
11677
11777
  };
11678
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
+ }
11679
11878
  async function runSourceTreeExtraction(params) {
11680
11879
  const sourceSpans = normalizeSourceSpans(params.sourceSpans);
11681
11880
  const formHints = normalizeFormHints(params.formInventory?.forms, sourceSpans);
@@ -11838,45 +12037,18 @@ async function runSourceTreeExtraction(params) {
11838
12037
  }
11839
12038
  if (operationalProfile.coverages.length > 0) {
11840
12039
  try {
11841
- const validNodeIds = new Set(sourceTree.map((node) => node.id));
11842
- const validSpanIds = new Set(sourceSpans.map((span) => span.id));
11843
- const budget = params.resolveBudget("extraction_coverage_cleanup", 4096);
11844
- const startedAt = Date.now();
11845
- const response = await safeGenerateObject(
11846
- params.generateObject,
11847
- {
11848
- prompt: buildOperationalProfileCleanupPrompt(sourceTree, operationalProfile),
11849
- schema: OperationalProfileCleanupSchema,
11850
- maxTokens: budget.maxTokens,
11851
- taskKind: "extraction_coverage_cleanup",
11852
- budgetDiagnostics: budget,
11853
- providerOptions: params.providerOptions,
11854
- trace: {
11855
- phase: "coverage_cleanup",
11856
- label: "Coverage cleanup",
11857
- itemCount: operationalProfile.coverages.length,
11858
- sourceBacked: true
11859
- }
11860
- },
11861
- {
11862
- fallback: { coverageDecisions: [], warnings: [] },
11863
- maxRetries: 0,
11864
- log: params.log,
11865
- retry: false
11866
- }
11867
- );
11868
- localTrack(response.usage, {
11869
- taskKind: "extraction_coverage_cleanup",
11870
- label: "coverage_cleanup",
11871
- maxTokens: budget.maxTokens,
11872
- durationMs: Date.now() - startedAt
11873
- });
11874
- operationalProfile = applyOperationalProfileCleanup(
12040
+ const cleanup = await cleanupOperationalCoverageSchedules({
12041
+ sourceTree,
12042
+ sourceSpans,
11875
12043
  operationalProfile,
11876
- response.object,
11877
- validNodeIds,
11878
- validSpanIds
11879
- );
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);
11880
12052
  } catch (error) {
11881
12053
  warnings.push(`Operational profile cleanup pass failed; uncleaned profile used (${error instanceof Error ? error.message : String(error)})`);
11882
12054
  }