@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.mjs CHANGED
@@ -2301,9 +2301,117 @@ function chunkSourceSpans(spans, options = {}) {
2301
2301
  flush();
2302
2302
  return chunks;
2303
2303
  }
2304
+ function normalizeSourceSpans(spans) {
2305
+ const droppedParentSpanIds = /* @__PURE__ */ new Set();
2306
+ const cleaned = [];
2307
+ for (const [index, span] of spans.entries()) {
2308
+ if (span.parentSpanId && droppedParentSpanIds.has(span.parentSpanId)) continue;
2309
+ const normalized = normalizeSourceSpanText(span);
2310
+ if (!normalized) {
2311
+ droppedParentSpanIds.add(span.id);
2312
+ continue;
2313
+ }
2314
+ cleaned.push({ ...normalized, __originalIndex: index });
2315
+ }
2316
+ return mergeTextRuns(cleaned).map(({ __originalIndex: _index, ...span }) => span);
2317
+ }
2304
2318
  function sourceUnit(span) {
2305
2319
  return span.sourceUnit ?? span.metadata?.sourceUnit;
2306
2320
  }
2321
+ function spanPage(span) {
2322
+ return span.pageStart ?? span.location?.page ?? span.location?.startPage;
2323
+ }
2324
+ function normalizeSourceSpanText(span) {
2325
+ const unit = sourceUnit(span);
2326
+ const text = normalizeWhitespace(span.text);
2327
+ if (!text) return void 0;
2328
+ if (isDiscardableBoilerplate(text, unit)) return void 0;
2329
+ const cleanedText = cleanBoilerplateLines(text);
2330
+ if (!cleanedText) return void 0;
2331
+ if (cleanedText === text) return span;
2332
+ return retextSpan(span, cleanedText, {
2333
+ boilerplateRemoved: "true",
2334
+ removedBoilerplateText: removedBoilerplateLines(text).join(" | ").slice(0, 500)
2335
+ });
2336
+ }
2337
+ function isDiscardableBoilerplate(text, unit) {
2338
+ const cleaned = normalizeWhitespace(text.replace(/\bColumn\s+\d+:\s*/gi, ""));
2339
+ if (/^SPECIMEN POLICY\s+[-—]\s+FOR TESTING ONLY$/i.test(cleaned)) return true;
2340
+ if (/^Page\s+\d+\s+of\s+\d+$/i.test(cleaned)) return true;
2341
+ if (/^[A-Z]{2,}(?:-[A-Z0-9]{2,})+\s+\d{2}\s+\d{2}$/i.test(cleaned)) return true;
2342
+ 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;
2343
+ if (unit === "table_row" && /^[^|]{0,40}\|\s*Page\s+\d+\s+of\s+\d+$/i.test(cleaned)) return true;
2344
+ return false;
2345
+ }
2346
+ function isBoilerplateLine(line) {
2347
+ const cleaned = normalizeWhitespace(line.replace(/\bColumn\s+\d+:\s*/gi, ""));
2348
+ 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);
2349
+ }
2350
+ function removedBoilerplateLines(text) {
2351
+ return text.split(/\s{2,}|\r?\n/).map(normalizeWhitespace).filter((line) => line && isBoilerplateLine(line));
2352
+ }
2353
+ function cleanBoilerplateLines(text) {
2354
+ 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, " ");
2355
+ const lines = withoutInlineBoilerplate.split(/\r?\n/);
2356
+ const filtered = lines.map(normalizeWhitespace).filter((line) => line && !isBoilerplateLine(line));
2357
+ return normalizeWhitespace(filtered.join(" "));
2358
+ }
2359
+ function shouldMergeTextSpan(left, right) {
2360
+ if (sourceUnit(left) !== "text" || sourceUnit(right) !== "text") return false;
2361
+ if (spanPage(left) !== spanPage(right)) return false;
2362
+ if (left.metadata?.elementType === "title" || right.metadata?.elementType === "title") return false;
2363
+ const leftText = normalizeWhitespace(left.text);
2364
+ const rightText = normalizeWhitespace(right.text);
2365
+ if (!leftText || !rightText) return false;
2366
+ if (/[:.;!?)]$/.test(leftText)) return false;
2367
+ if (/^(?:[A-Z][A-Z0-9 &/(),.-]{8,}|Item\s+\d+|Section\s+\d+|Part\s+[A-Z]\b)/.test(rightText)) return false;
2368
+ 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);
2369
+ }
2370
+ function mergeTextRuns(spans) {
2371
+ const result = [];
2372
+ let current;
2373
+ for (const span of spans) {
2374
+ if (current && shouldMergeTextSpan(current, span)) {
2375
+ current = mergeTextSpanPair(current, span);
2376
+ continue;
2377
+ }
2378
+ if (current) result.push(current);
2379
+ current = span;
2380
+ }
2381
+ if (current) result.push(current);
2382
+ return result;
2383
+ }
2384
+ function mergeTextSpanPair(left, right) {
2385
+ const text = normalizeWhitespace(`${left.text} ${right.text}`);
2386
+ const merged = retextSpan(left, text, {
2387
+ mergedSourceSpanIds: [left.metadata?.mergedSourceSpanIds, left.id, right.id, right.metadata?.mergedSourceSpanIds].filter(Boolean).join(","),
2388
+ sourceSpanNormalization: "merged_text_run"
2389
+ });
2390
+ return {
2391
+ ...merged,
2392
+ bbox: [...left.bbox ?? [], ...right.bbox ?? []],
2393
+ pageEnd: right.pageEnd ?? left.pageEnd,
2394
+ location: {
2395
+ ...left.location,
2396
+ endPage: right.location?.endPage ?? right.pageEnd ?? left.location?.endPage
2397
+ },
2398
+ __originalIndex: left.__originalIndex
2399
+ };
2400
+ }
2401
+ function retextSpan(span, text, metadata) {
2402
+ const textHash = sourceSpanTextHash(text);
2403
+ return SourceSpanSchema.parse({
2404
+ ...span,
2405
+ id: `${span.id.split(":").slice(0, -1).join(":")}:${textHash.slice(0, 12)}`,
2406
+ text,
2407
+ hash: textHash,
2408
+ textHash,
2409
+ metadata: {
2410
+ ...span.metadata ?? {},
2411
+ ...metadata
2412
+ }
2413
+ });
2414
+ }
2307
2415
  function filterChunkableSourceSpans(spans) {
2308
2416
  const rowIds = new Set(
2309
2417
  spans.filter((span) => sourceUnit(span) === "table_row").map((span) => span.id)
@@ -4060,14 +4168,14 @@ function stringArray(value) {
4060
4168
  function sourceUnit4(span) {
4061
4169
  return span.sourceUnit ?? span.metadata?.sourceUnit;
4062
4170
  }
4063
- function spanPage(span) {
4171
+ function spanPage2(span) {
4064
4172
  return span.pageStart ?? span.location?.page ?? span.location?.startPage;
4065
4173
  }
4066
4174
  function spanPageEnd(span) {
4067
- return span.pageEnd ?? span.location?.endPage ?? spanPage(span);
4175
+ return span.pageEnd ?? span.location?.endPage ?? spanPage2(span);
4068
4176
  }
4069
4177
  function sourceSpansForPage(sourceSpans, page) {
4070
- return sourceSpans.filter((span) => spanPage(span) === page && sourceUnit4(span) === "page").map((span) => span.id);
4178
+ return sourceSpans.filter((span) => spanPage2(span) === page && sourceUnit4(span) === "page").map((span) => span.id);
4071
4179
  }
4072
4180
  function nodePageOverlaps(node, record) {
4073
4181
  const recordStart = numberValue(record, "pageNumber", "pageStart", "resolvedFromPage");
@@ -4168,8 +4276,8 @@ function buildNodesFromSourceSpans(sourceSpans) {
4168
4276
  const unit = sourceUnit4(span);
4169
4277
  return unit === "section" || unit === "section_candidate" || unit === "page";
4170
4278
  });
4171
- return candidates.sort((left, right) => (spanPage(left) ?? 0) - (spanPage(right) ?? 0) || left.id.localeCompare(right.id)).map((span, index) => {
4172
- const title = span.sectionId ?? span.formNumber ?? (sourceUnit4(span) === "page" && spanPage(span) ? `Page ${spanPage(span)}` : `Source unit ${index + 1}`);
4279
+ return candidates.sort((left, right) => (spanPage2(left) ?? 0) - (spanPage2(right) ?? 0) || left.id.localeCompare(right.id)).map((span, index) => {
4280
+ const title = span.sectionId ?? span.formNumber ?? (sourceUnit4(span) === "page" && spanPage2(span) ? `Page ${spanPage2(span)}` : `Source unit ${index + 1}`);
4173
4281
  return {
4174
4282
  id: `source:${index}:${slugPart(span.id)}`,
4175
4283
  title,
@@ -4177,7 +4285,7 @@ function buildNodesFromSourceSpans(sourceSpans) {
4177
4285
  type: sourceUnit4(span),
4178
4286
  label: sourceUnit4(span),
4179
4287
  level: 1,
4180
- pageStart: spanPage(span),
4288
+ pageStart: spanPage2(span),
4181
4289
  pageEnd: spanPageEnd(span),
4182
4290
  formNumber: span.formNumber,
4183
4291
  excerpt: span.text.slice(0, 500),
@@ -8890,6 +8998,8 @@ var ORGANIZABLE_KINDS = [
8890
8998
  "schedule",
8891
8999
  "clause"
8892
9000
  ];
9001
+ var ORGANIZATION_TOP_LEVEL_BATCH_SIZE = 80;
9002
+ var ORGANIZATION_CHILD_CONTEXT_LIMIT = 4;
8893
9003
  var SourceTreeOrganizationSchema = z41.object({
8894
9004
  labels: z41.array(z41.object({
8895
9005
  nodeId: z41.string(),
@@ -8954,7 +9064,7 @@ function cleanText(value, fallback) {
8954
9064
  const text = value?.replace(/\s+/g, " ").trim();
8955
9065
  return text || fallback;
8956
9066
  }
8957
- function compactNode(node) {
9067
+ function compactNode(node, maxText = 700) {
8958
9068
  return {
8959
9069
  id: node.id,
8960
9070
  kind: node.kind,
@@ -8963,20 +9073,89 @@ function compactNode(node) {
8963
9073
  pageStart: node.pageStart,
8964
9074
  pageEnd: node.pageEnd,
8965
9075
  sourceSpanIds: node.sourceSpanIds.slice(0, 8),
8966
- text: (node.textExcerpt ?? node.description).slice(0, 700)
9076
+ text: (node.textExcerpt ?? node.description).slice(0, maxText)
8967
9077
  };
8968
9078
  }
8969
- function buildOrganizationPrompt(sourceTree) {
8970
- const nodes = sourceTree.filter((node) => node.kind !== "document").slice(0, 240).map(compactNode);
9079
+ function nodesByParent(sourceTree) {
9080
+ const byParent = /* @__PURE__ */ new Map();
9081
+ for (const node of sourceTree) {
9082
+ const children = byParent.get(node.parentId) ?? [];
9083
+ children.push(node);
9084
+ byParent.set(node.parentId, children);
9085
+ }
9086
+ for (const children of byParent.values()) {
9087
+ children.sort((left, right) => left.order - right.order || left.id.localeCompare(right.id));
9088
+ }
9089
+ return byParent;
9090
+ }
9091
+ function sourceTreeRootId(sourceTree) {
9092
+ return sourceTree.find((node) => node.kind === "document")?.id;
9093
+ }
9094
+ function organizationBatches(sourceTree) {
9095
+ const byParent = nodesByParent(sourceTree);
9096
+ const rootId = sourceTreeRootId(sourceTree);
9097
+ const topLevelNodes = (byParent.get(rootId) ?? []).filter((node) => node.kind !== "document");
9098
+ if (topLevelNodes.length === 0) {
9099
+ const nodes = sourceTree.filter((node) => node.kind !== "document").slice(0, 240);
9100
+ return [{
9101
+ label: "fallback node prefix because no document root children were found",
9102
+ topLevelNodeIds: nodes.map((node) => node.id),
9103
+ nodes
9104
+ }];
9105
+ }
9106
+ const batches = [];
9107
+ for (let index = 0; index < topLevelNodes.length; index += ORGANIZATION_TOP_LEVEL_BATCH_SIZE) {
9108
+ const topLevelBatch = topLevelNodes.slice(index, index + ORGANIZATION_TOP_LEVEL_BATCH_SIZE);
9109
+ const candidates = /* @__PURE__ */ new Map();
9110
+ for (const node of topLevelBatch) {
9111
+ candidates.set(node.id, node);
9112
+ const childContext = (byParent.get(node.id) ?? []).filter((child) => child.kind !== "text" && child.kind !== "table_cell").slice(0, ORGANIZATION_CHILD_CONTEXT_LIMIT);
9113
+ for (const child of childContext) {
9114
+ candidates.set(child.id, child);
9115
+ }
9116
+ }
9117
+ batches.push({
9118
+ label: `top-level nodes ${index + 1}-${index + topLevelBatch.length} of ${topLevelNodes.length}`,
9119
+ topLevelNodeIds: topLevelBatch.map((node) => node.id),
9120
+ nodes: [...candidates.values()]
9121
+ });
9122
+ }
9123
+ return batches;
9124
+ }
9125
+ function mergeOrganizationResults(results) {
9126
+ const labels = /* @__PURE__ */ new Map();
9127
+ const groups = /* @__PURE__ */ new Map();
9128
+ for (const result of results) {
9129
+ for (const label of result.labels) {
9130
+ labels.set(label.nodeId, { ...labels.get(label.nodeId), ...label });
9131
+ }
9132
+ for (const group of result.groups) {
9133
+ const key = `${group.kind}:${group.childNodeIds.join("|")}`;
9134
+ groups.set(key, group);
9135
+ }
9136
+ }
9137
+ return {
9138
+ labels: [...labels.values()],
9139
+ groups: [...groups.values()]
9140
+ };
9141
+ }
9142
+ function buildOrganizationPrompt(batch) {
9143
+ const nodes = batch.nodes.map((node) => compactNode(node, node.kind === "page" ? 900 : 320));
8971
9144
  return `You organize an insurance document source tree.
8972
9145
 
9146
+ Scope:
9147
+ - ${batch.label}
9148
+ - The provided list is a bounded extraction-time batch. It is not necessarily the whole document.
9149
+ - Top-level page/form candidates in this batch: ${JSON.stringify(batch.topLevelNodeIds)}
9150
+
8973
9151
  Rules:
8974
9152
  - Use only node IDs from the provided list.
8975
9153
  - Do not invent text, page numbers, source spans, limits, or policy facts.
8976
- - 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.
9154
+ - 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.
8977
9155
  - Add concise, human-readable titles to generic text, table, row, and cell nodes when the text makes their role clear.
8978
9156
  - Groups must list existing childNodeIds only.
8979
9157
  - Keep descriptions short and useful for search.
9158
+ - Prefer the document's own form titles, endorsement titles, schedules, declarations headings, and page order over keyword-only guessing.
8980
9159
 
8981
9160
  Source nodes:
8982
9161
  ${JSON.stringify(nodes, null, 2)}
@@ -9016,8 +9195,9 @@ function groupNodeId(documentId, group) {
9016
9195
  }
9017
9196
  function applyOrganization(sourceTree, organization) {
9018
9197
  const byId = new Map(sourceTree.map((node) => [node.id, node]));
9198
+ const labels = new Map(organization.labels.map((label) => [label.nodeId, label]));
9019
9199
  let nextTree = sourceTree.map((node) => {
9020
- const label = organization.labels.find((item) => item.nodeId === node.id);
9200
+ const label = labels.get(node.id);
9021
9201
  if (!label) return node;
9022
9202
  return {
9023
9203
  ...node,
@@ -9026,7 +9206,7 @@ function applyOrganization(sourceTree, organization) {
9026
9206
  description: cleanText(label.description, node.description)
9027
9207
  };
9028
9208
  });
9029
- for (const group of organization.groups.slice(0, 40)) {
9209
+ for (const group of organization.groups) {
9030
9210
  const children = group.childNodeIds.map((id2) => byId.get(id2)).filter((node2) => Boolean(node2));
9031
9211
  if (children.length === 0) continue;
9032
9212
  const parentId = children[0].parentId;
@@ -9185,7 +9365,8 @@ function materializeDocument(params) {
9185
9365
  };
9186
9366
  }
9187
9367
  async function runSourceTreeExtraction(params) {
9188
- let sourceTree = buildDocumentSourceTree(params.sourceSpans, params.id);
9368
+ const sourceSpans = normalizeSourceSpans(params.sourceSpans);
9369
+ let sourceTree = buildDocumentSourceTree(sourceSpans, params.id);
9189
9370
  const warnings = [];
9190
9371
  let modelCalls = 0;
9191
9372
  let callsWithUsage = 0;
@@ -9208,41 +9389,46 @@ async function runSourceTreeExtraction(params) {
9208
9389
  params.trackUsage(usage, report);
9209
9390
  };
9210
9391
  try {
9211
- const budget = params.resolveBudget("extraction_source_tree", 4096);
9212
- const startedAt = Date.now();
9213
- const response = await safeGenerateObject(
9214
- params.generateObject,
9215
- {
9216
- prompt: buildOrganizationPrompt(sourceTree),
9217
- schema: SourceTreeOrganizationSchema,
9218
- maxTokens: budget.maxTokens,
9392
+ const organizations = [];
9393
+ const batches = organizationBatches(sourceTree);
9394
+ for (const [batchIndex, batch] of batches.entries()) {
9395
+ const budget = params.resolveBudget("extraction_source_tree", 4096);
9396
+ const startedAt = Date.now();
9397
+ const response = await safeGenerateObject(
9398
+ params.generateObject,
9399
+ {
9400
+ prompt: buildOrganizationPrompt(batch),
9401
+ schema: SourceTreeOrganizationSchema,
9402
+ maxTokens: budget.maxTokens,
9403
+ taskKind: "extraction_source_tree",
9404
+ budgetDiagnostics: budget,
9405
+ providerOptions: { ...params.providerOptions, sourceSpans }
9406
+ },
9407
+ {
9408
+ fallback: { labels: [], groups: [] },
9409
+ log: params.log
9410
+ }
9411
+ );
9412
+ localTrack(response.usage, {
9219
9413
  taskKind: "extraction_source_tree",
9220
- budgetDiagnostics: budget,
9221
- providerOptions: { ...params.providerOptions, sourceSpans: params.sourceSpans }
9222
- },
9223
- {
9224
- fallback: { labels: [], groups: [] },
9225
- log: params.log
9226
- }
9227
- );
9228
- localTrack(response.usage, {
9229
- taskKind: "extraction_source_tree",
9230
- label: "source_tree_organizer",
9231
- maxTokens: budget.maxTokens,
9232
- durationMs: Date.now() - startedAt
9233
- });
9234
- sourceTree = applyOrganization(sourceTree, response.object);
9414
+ label: batches.length > 1 ? `source_tree_organizer_${batchIndex + 1}` : "source_tree_organizer",
9415
+ maxTokens: budget.maxTokens,
9416
+ durationMs: Date.now() - startedAt
9417
+ });
9418
+ organizations.push(response.object);
9419
+ }
9420
+ sourceTree = applyOrganization(sourceTree, mergeOrganizationResults(organizations));
9235
9421
  } catch (error) {
9236
9422
  warnings.push(`Source-tree organizer failed; deterministic tree used (${error instanceof Error ? error.message : String(error)})`);
9237
9423
  }
9238
9424
  const deterministicProfile = buildDeterministicOperationalProfile({
9239
9425
  sourceTree,
9240
- sourceSpans: params.sourceSpans
9426
+ sourceSpans
9241
9427
  });
9242
9428
  let operationalProfile = deterministicProfile;
9243
9429
  try {
9244
9430
  const validNodeIds = new Set(sourceTree.map((node) => node.id));
9245
- const validSpanIds = new Set(params.sourceSpans.map((span) => span.id));
9431
+ const validSpanIds = new Set(sourceSpans.map((span) => span.id));
9246
9432
  const budget = params.resolveBudget("extraction_operational_profile", 8192);
9247
9433
  const startedAt = Date.now();
9248
9434
  const response = await safeGenerateObject(
@@ -9253,7 +9439,7 @@ async function runSourceTreeExtraction(params) {
9253
9439
  maxTokens: budget.maxTokens,
9254
9440
  taskKind: "extraction_operational_profile",
9255
9441
  budgetDiagnostics: budget,
9256
- providerOptions: { ...params.providerOptions, sourceSpans: params.sourceSpans, sourceTree }
9442
+ providerOptions: { ...params.providerOptions, sourceSpans, sourceTree }
9257
9443
  },
9258
9444
  {
9259
9445
  fallback: deterministicProfile,
@@ -9282,8 +9468,8 @@ async function runSourceTreeExtraction(params) {
9282
9468
  });
9283
9469
  return {
9284
9470
  sourceTree,
9285
- sourceSpans: params.sourceSpans,
9286
- sourceChunks: chunkSourceSpans(params.sourceSpans),
9471
+ sourceSpans,
9472
+ sourceChunks: chunkSourceSpans(sourceSpans),
9287
9473
  operationalProfile,
9288
9474
  document,
9289
9475
  chunks: [],
@@ -14297,6 +14483,7 @@ export {
14297
14483
  normalizeDoclingDocument,
14298
14484
  normalizeDocumentSourceTreePaths,
14299
14485
  normalizeForMatch,
14486
+ normalizeSourceSpans,
14300
14487
  orderSourceEvidence,
14301
14488
  overlayTextOnPdf,
14302
14489
  pLimit,