@claritylabs/cl-sdk 3.0.3 → 3.0.5

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),
@@ -8956,6 +9064,29 @@ function cleanText(value, fallback) {
8956
9064
  const text = value?.replace(/\s+/g, " ").trim();
8957
9065
  return text || fallback;
8958
9066
  }
9067
+ function simplifyOrganizerTitle(value, fallback, kind) {
9068
+ const title = cleanText(value, fallback);
9069
+ if (/^declarations\b/i.test(title)) return "Declarations";
9070
+ if (/^policy\s+form\b/i.test(title)) return "Policy Form";
9071
+ if (/^definitions\b/i.test(title)) return "Definitions";
9072
+ const endorsementNumber = title.match(/^endorsement\s+(?:no\.?|number|#)?\s*([A-Z0-9][A-Z0-9.-]*)\b/i)?.[1];
9073
+ if (endorsementNumber) return `Endorsement No. ${endorsementNumber}`;
9074
+ if (kind === "endorsement" && /^endorsements?\s+\d+\s*[–-]\s*\d+\b/i.test(title)) {
9075
+ return title.replace(/[–—]/g, "-").replace(/\s*\(.*/, "").trim();
9076
+ }
9077
+ return title;
9078
+ }
9079
+ function endorsementReference(value) {
9080
+ return value?.match(/\bendorsement\s+(?:no\.?|number|#)?\s*([A-Z0-9][A-Z0-9.-]*)\b/i)?.[1]?.toUpperCase();
9081
+ }
9082
+ function rejectsOrganizerGroup(group, children) {
9083
+ if (group.kind !== "endorsement") return false;
9084
+ if (/^endorsements?\s+\d+\s*[–-]\s*\d+\b/i.test(group.title)) return true;
9085
+ const childNumbers = new Set(
9086
+ children.map((child) => endorsementReference([child.title, child.description, child.textExcerpt].filter(Boolean).join(" "))).filter((value) => Boolean(value))
9087
+ );
9088
+ return childNumbers.size > 1;
9089
+ }
8959
9090
  function compactNode(node, maxText = 700) {
8960
9091
  return {
8961
9092
  id: node.id,
@@ -9043,8 +9174,10 @@ Scope:
9043
9174
  Rules:
9044
9175
  - Use only node IDs from the provided list.
9045
9176
  - Do not invent text, page numbers, source spans, limits, or policy facts.
9046
- - 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.
9177
+ - 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.
9178
+ - Do not group separately numbered endorsements into one parent. Never create rollup titles such as "Endorsements 1-3 (...)".
9047
9179
  - Add concise, human-readable titles to generic text, table, row, and cell nodes when the text makes their role clear.
9180
+ - Keep organizer titles terse. Use the printed heading or a compact canonical title such as "Declarations", "Policy Form", "Definitions", or "Endorsement No. 3"; do not add parenthetical summaries.
9048
9181
  - Groups must list existing childNodeIds only.
9049
9182
  - Keep descriptions short and useful for search.
9050
9183
  - Prefer the document's own form titles, endorsement titles, schedules, declarations headings, and page order over keyword-only guessing.
@@ -9094,17 +9227,20 @@ function applyOrganization(sourceTree, organization) {
9094
9227
  return {
9095
9228
  ...node,
9096
9229
  kind: label.kind ?? node.kind,
9097
- title: cleanText(label.title, node.title),
9230
+ title: simplifyOrganizerTitle(label.title, node.title, label.kind ?? node.kind),
9098
9231
  description: cleanText(label.description, node.description)
9099
9232
  };
9100
9233
  });
9101
9234
  for (const group of organization.groups) {
9102
9235
  const children = group.childNodeIds.map((id2) => byId.get(id2)).filter((node2) => Boolean(node2));
9103
9236
  if (children.length === 0) continue;
9237
+ if (rejectsOrganizerGroup(group, children)) continue;
9104
9238
  const parentId = children[0].parentId;
9105
9239
  if (!children.every((child) => child.parentId === parentId)) continue;
9106
9240
  const documentId = children[0].documentId;
9107
- const id = groupNodeId(documentId, group);
9241
+ const title = simplifyOrganizerTitle(group.title, group.title, group.kind);
9242
+ const description = cleanText(group.description, title);
9243
+ const id = groupNodeId(documentId, { ...group, title });
9108
9244
  if (byId.has(id)) continue;
9109
9245
  const sourceSpanIds = [...new Set(children.flatMap((child) => child.sourceSpanIds))];
9110
9246
  const pageStarts = children.map((child) => child.pageStart).filter((page) => typeof page === "number");
@@ -9115,8 +9251,8 @@ function applyOrganization(sourceTree, organization) {
9115
9251
  documentId,
9116
9252
  parentId,
9117
9253
  kind: group.kind,
9118
- title: group.title,
9119
- description: group.description ?? group.title,
9254
+ title,
9255
+ description,
9120
9256
  textExcerpt: children.map((child) => child.textExcerpt ?? child.description).filter(Boolean).join("\n\n").slice(0, 1600),
9121
9257
  sourceSpanIds,
9122
9258
  pageStart: pageStarts.length ? Math.min(...pageStarts) : void 0,
@@ -9257,7 +9393,8 @@ function materializeDocument(params) {
9257
9393
  };
9258
9394
  }
9259
9395
  async function runSourceTreeExtraction(params) {
9260
- let sourceTree = buildDocumentSourceTree(params.sourceSpans, params.id);
9396
+ const sourceSpans = normalizeSourceSpans(params.sourceSpans);
9397
+ let sourceTree = buildDocumentSourceTree(sourceSpans, params.id);
9261
9398
  const warnings = [];
9262
9399
  let modelCalls = 0;
9263
9400
  let callsWithUsage = 0;
@@ -9293,7 +9430,7 @@ async function runSourceTreeExtraction(params) {
9293
9430
  maxTokens: budget.maxTokens,
9294
9431
  taskKind: "extraction_source_tree",
9295
9432
  budgetDiagnostics: budget,
9296
- providerOptions: { ...params.providerOptions, sourceSpans: params.sourceSpans }
9433
+ providerOptions: { ...params.providerOptions, sourceSpans }
9297
9434
  },
9298
9435
  {
9299
9436
  fallback: { labels: [], groups: [] },
@@ -9314,12 +9451,12 @@ async function runSourceTreeExtraction(params) {
9314
9451
  }
9315
9452
  const deterministicProfile = buildDeterministicOperationalProfile({
9316
9453
  sourceTree,
9317
- sourceSpans: params.sourceSpans
9454
+ sourceSpans
9318
9455
  });
9319
9456
  let operationalProfile = deterministicProfile;
9320
9457
  try {
9321
9458
  const validNodeIds = new Set(sourceTree.map((node) => node.id));
9322
- const validSpanIds = new Set(params.sourceSpans.map((span) => span.id));
9459
+ const validSpanIds = new Set(sourceSpans.map((span) => span.id));
9323
9460
  const budget = params.resolveBudget("extraction_operational_profile", 8192);
9324
9461
  const startedAt = Date.now();
9325
9462
  const response = await safeGenerateObject(
@@ -9330,7 +9467,7 @@ async function runSourceTreeExtraction(params) {
9330
9467
  maxTokens: budget.maxTokens,
9331
9468
  taskKind: "extraction_operational_profile",
9332
9469
  budgetDiagnostics: budget,
9333
- providerOptions: { ...params.providerOptions, sourceSpans: params.sourceSpans, sourceTree }
9470
+ providerOptions: { ...params.providerOptions, sourceSpans, sourceTree }
9334
9471
  },
9335
9472
  {
9336
9473
  fallback: deterministicProfile,
@@ -9359,8 +9496,8 @@ async function runSourceTreeExtraction(params) {
9359
9496
  });
9360
9497
  return {
9361
9498
  sourceTree,
9362
- sourceSpans: params.sourceSpans,
9363
- sourceChunks: chunkSourceSpans(params.sourceSpans),
9499
+ sourceSpans,
9500
+ sourceChunks: chunkSourceSpans(sourceSpans),
9364
9501
  operationalProfile,
9365
9502
  document,
9366
9503
  chunks: [],
@@ -14374,6 +14511,7 @@ export {
14374
14511
  normalizeDoclingDocument,
14375
14512
  normalizeDocumentSourceTreePaths,
14376
14513
  normalizeForMatch,
14514
+ normalizeSourceSpans,
14377
14515
  orderSourceEvidence,
14378
14516
  overlayTextOnPdf,
14379
14517
  pLimit,