@claritylabs/cl-sdk 3.1.4 → 3.1.6

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
@@ -80,53 +80,70 @@ function sanitizeNulls(obj) {
80
80
 
81
81
  // src/core/strict-schema.ts
82
82
  import { z } from "zod";
83
+ function schemaDef(schema) {
84
+ return schema._zod?.def ?? schema._def ?? {};
85
+ }
86
+ function schemaKind(schema) {
87
+ const def = schemaDef(schema);
88
+ const raw = typeof def.type === "string" ? def.type : typeof def.typeName === "string" ? def.typeName : typeof schema.type === "string" ? schema.type : void 0;
89
+ return raw?.replace(/^Zod/, "").toLowerCase();
90
+ }
91
+ function schemaDescription(schema) {
92
+ const def = schemaDef(schema);
93
+ return schema.description ?? def.description;
94
+ }
95
+ function objectShape(schema) {
96
+ const def = schemaDef(schema);
97
+ const shape = schema.shape ?? def.shape;
98
+ return typeof shape === "function" ? shape() : shape;
99
+ }
100
+ function withDescription(schema, description) {
101
+ return description ? schema.describe(description) : schema;
102
+ }
83
103
  function toStrictSchema(schema) {
84
- const def = schema._zod?.def;
85
- const typeName = def?.type ?? schema.type;
86
- if (typeName === "object") {
87
- const shape = schema.shape;
104
+ const kind = schemaKind(schema);
105
+ const def = schemaDef(schema);
106
+ if (kind === "object") {
107
+ const shape = objectShape(schema);
88
108
  if (!shape) return schema;
89
109
  const newShape = {};
90
110
  for (const [key, value] of Object.entries(shape)) {
91
111
  const field = value;
92
- const fieldDef = field._zod?.def;
93
- const fieldType = fieldDef?.type ?? field.type;
94
- if (fieldType === "optional") {
112
+ const fieldDef = schemaDef(field);
113
+ const fieldKind = schemaKind(field);
114
+ if (fieldKind === "optional") {
95
115
  const innerType = fieldDef?.innerType;
96
- const description = field.description ?? fieldDef?.description ?? field._zod?.def?.description;
116
+ const description = schemaDescription(field);
97
117
  if (innerType) {
98
118
  const transformed = toStrictSchema(innerType);
99
- let nullable = z.nullable(transformed);
100
- if (description) nullable = nullable.describe(description);
101
- newShape[key] = nullable;
119
+ newShape[key] = withDescription(z.nullable(transformed), description);
102
120
  } else {
103
- let nullable = z.nullable(field);
104
- if (description) nullable = nullable.describe(description);
105
- newShape[key] = nullable;
121
+ newShape[key] = withDescription(z.nullable(field), description);
106
122
  }
107
123
  } else {
108
124
  newShape[key] = toStrictSchema(field);
109
125
  }
110
126
  }
111
- const objDesc = schema.description ?? def?.description ?? schema._zod?.def?.description;
112
- const result = z.object(newShape);
113
- return objDesc ? result.describe(objDesc) : result;
127
+ return withDescription(z.object(newShape), schemaDescription(schema));
114
128
  }
115
- if (typeName === "array") {
116
- const element = def?.element ?? schema.element;
129
+ if (kind === "array") {
130
+ const element = def.element ?? def.type ?? schema.element;
117
131
  if (element) {
118
- const arrDesc = schema.description ?? def?.description ?? schema._zod?.def?.description;
119
- const result = z.array(toStrictSchema(element));
120
- return arrDesc ? result.describe(arrDesc) : result;
132
+ return withDescription(z.array(toStrictSchema(element)), schemaDescription(schema));
133
+ }
134
+ return schema;
135
+ }
136
+ if (kind === "nullable") {
137
+ const innerType = def?.innerType;
138
+ if (innerType) {
139
+ return withDescription(z.nullable(toStrictSchema(innerType)), schemaDescription(schema));
121
140
  }
122
141
  return schema;
123
142
  }
124
- if (typeName === "nullable") {
143
+ if (kind === "default") {
125
144
  const innerType = def?.innerType;
126
145
  if (innerType) {
127
- const nullDesc = schema.description ?? def?.description ?? schema._zod?.def?.description;
128
- const result = z.nullable(toStrictSchema(innerType));
129
- return nullDesc ? result.describe(nullDesc) : result;
146
+ return withDescription(z.nullable(toStrictSchema(innerType)), schemaDescription(schema));
130
147
  }
131
148
  return schema;
132
149
  }
@@ -2779,8 +2796,30 @@ function sortSpans(left, right) {
2779
2796
  const leftCol = left.table?.columnIndex ?? Number(left.metadata?.columnIndex ?? 0);
2780
2797
  const rightCol = right.table?.columnIndex ?? Number(right.metadata?.columnIndex ?? 0);
2781
2798
  if (leftCol !== rightCol) return leftCol - rightCol;
2799
+ if (tableId(left) && tableId(left) === tableId(right)) {
2800
+ const leftUnitRank = sourceUnitSortRank(left);
2801
+ const rightUnitRank = sourceUnitSortRank(right);
2802
+ if (leftUnitRank !== rightUnitRank) return leftUnitRank - rightUnitRank;
2803
+ }
2782
2804
  return left.id.localeCompare(right.id);
2783
2805
  }
2806
+ function sourceUnitSortRank(span) {
2807
+ switch (sourceUnit2(span)) {
2808
+ case "page":
2809
+ return 0;
2810
+ case "table":
2811
+ return 1;
2812
+ case "table_row":
2813
+ return 2;
2814
+ case "table_cell":
2815
+ return 3;
2816
+ case "section":
2817
+ case "key_value":
2818
+ case "text":
2819
+ default:
2820
+ return 4;
2821
+ }
2822
+ }
2784
2823
  function normalizeDocumentSourceTreePaths(nodes) {
2785
2824
  const byParent = /* @__PURE__ */ new Map();
2786
2825
  for (const node of nodes) {
@@ -9849,6 +9888,8 @@ Rules:
9849
9888
  - Keep a coverage when it is a real operational coverage/benefit even if only one term needs cleanup.
9850
9889
  - When changing a term's semantic meaning, set kind to the corrected normalized term kind.
9851
9890
  - Do not add new coverage rows or new terms; this pass cleans the existing projection.
9891
+ - Include every JSON key in each decision. Use null for scalar fields you are not changing and [] for source ID lists you are not changing.
9892
+ - For each coverage decision, always include termDecisions. Use [] when no terms need cleanup.
9852
9893
  - Keep reasons concise and factual.
9853
9894
 
9854
9895
  Candidate projection:
@@ -9859,9 +9900,6 @@ ${JSON.stringify(nodes, null, 2)}
9859
9900
 
9860
9901
  Return JSON with coverageDecisions and warnings only.`;
9861
9902
  }
9862
- function hasOwn(object, key) {
9863
- return Object.prototype.hasOwnProperty.call(object, key);
9864
- }
9865
9903
  function uniqueStrings(values) {
9866
9904
  return [...new Set(values.filter((value) => value.length > 0))];
9867
9905
  }
@@ -9936,18 +9974,16 @@ function applyTermCleanupDecision(term, decision, validNodeIds, validSpanIds) {
9936
9974
  sourceSpanIds: cleanupIds(decision.sourceSpanIds, validSpanIds, term.sourceSpanIds)
9937
9975
  };
9938
9976
  if (next.sourceNodeIds.length === 0 && next.sourceSpanIds.length === 0) return term;
9939
- if (hasOwn(decision, "amount")) {
9940
- if (typeof decision.amount === "number" && Number.isFinite(decision.amount)) next.amount = decision.amount;
9941
- else delete next.amount;
9977
+ if (typeof decision.amount === "number" && Number.isFinite(decision.amount)) {
9978
+ next.amount = decision.amount;
9942
9979
  } else if (decision.value || decision.kind) {
9943
9980
  const amount = next.kind === "retroactive_date" ? void 0 : amountFromOperationalValue(next.value);
9944
9981
  if (amount === void 0) delete next.amount;
9945
9982
  else next.amount = amount;
9946
9983
  }
9947
- if (hasOwn(decision, "appliesTo")) {
9984
+ if (decision.appliesTo != null) {
9948
9985
  const appliesTo = cleanProfileValue(decision.appliesTo);
9949
9986
  if (appliesTo) next.appliesTo = appliesTo;
9950
- else delete next.appliesTo;
9951
9987
  }
9952
9988
  return next;
9953
9989
  }
@@ -9966,46 +10002,42 @@ function applyCoverageCleanupDecision(coverage, decision, validNodeIds, validSpa
9966
10002
  const name = cleanProfileValue(decision.name);
9967
10003
  if (name) next.name = name;
9968
10004
  if (decision.coverageOrigin) next.coverageOrigin = decision.coverageOrigin;
9969
- if (hasOwn(decision, "limit")) {
10005
+ if (decision.limit != null) {
9970
10006
  const value = cleanProfileValue(decision.limit);
9971
10007
  if (value) next.limit = value;
9972
- else delete next.limit;
9973
10008
  }
9974
- if (hasOwn(decision, "deductible")) {
10009
+ if (decision.deductible != null) {
9975
10010
  const value = cleanProfileValue(decision.deductible);
9976
10011
  if (value) next.deductible = value;
9977
- else delete next.deductible;
9978
10012
  }
9979
- if (hasOwn(decision, "premium")) {
10013
+ if (decision.premium != null) {
9980
10014
  const value = cleanProfileValue(decision.premium);
9981
10015
  if (value) next.premium = value;
9982
- else delete next.premium;
9983
10016
  }
9984
- if (hasOwn(decision, "retroactiveDate")) {
10017
+ if (decision.retroactiveDate != null) {
9985
10018
  const value = cleanProfileValue(decision.retroactiveDate);
9986
10019
  if (value) next.retroactiveDate = value;
9987
- else delete next.retroactiveDate;
9988
10020
  }
9989
10021
  const termDecisions = (decision.termDecisions ?? []).filter((termDecision) => termDecision.termIndex < coverage.limits.length);
9990
10022
  const termDecisionByIndex = new Map(termDecisions.map((termDecision) => [termDecision.termIndex, termDecision]));
9991
10023
  next.limits = coverage.limits.map((term, index) => applyTermCleanupDecision(term, termDecisionByIndex.get(index), validNodeIds, validSpanIds)).filter((term) => Boolean(term));
9992
10024
  if (termDecisions.length > 0) {
9993
- if (!hasOwn(decision, "limit") && termDecisionsTouch(coverage, termDecisions, isLimitTerm)) {
10025
+ if (decision.limit == null && termDecisionsTouch(coverage, termDecisions, isLimitTerm)) {
9994
10026
  const value = primaryLimitFromTerms(next.limits);
9995
10027
  if (value) next.limit = value;
9996
10028
  else delete next.limit;
9997
10029
  }
9998
- if (!hasOwn(decision, "deductible") && termDecisionsTouch(coverage, termDecisions, isDeductibleTerm)) {
10030
+ if (decision.deductible == null && termDecisionsTouch(coverage, termDecisions, isDeductibleTerm)) {
9999
10031
  const value = deductibleFromTerms(next.limits);
10000
10032
  if (value) next.deductible = value;
10001
10033
  else delete next.deductible;
10002
10034
  }
10003
- if (!hasOwn(decision, "premium") && termDecisionsTouch(coverage, termDecisions, isPremiumTerm)) {
10035
+ if (decision.premium == null && termDecisionsTouch(coverage, termDecisions, isPremiumTerm)) {
10004
10036
  const value = premiumFromTerms(next.limits);
10005
10037
  if (value) next.premium = value;
10006
10038
  else delete next.premium;
10007
10039
  }
10008
- if (!hasOwn(decision, "retroactiveDate") && termDecisionsTouch(coverage, termDecisions, isRetroactiveDateTerm)) {
10040
+ if (decision.retroactiveDate == null && termDecisionsTouch(coverage, termDecisions, isRetroactiveDateTerm)) {
10009
10041
  const value = retroactiveDateFromTerms(next.limits);
10010
10042
  if (value) next.retroactiveDate = value;
10011
10043
  else delete next.retroactiveDate;
@@ -10113,6 +10145,23 @@ var SourceTreeOrganizationSchema = z42.object({
10113
10145
  childNodeIds: z42.array(z42.string()).min(1)
10114
10146
  }))
10115
10147
  });
10148
+ var SourceTreeVisualTableRepairSchema = z42.object({
10149
+ tables: z42.array(z42.object({
10150
+ tableNodeId: z42.string(),
10151
+ columnLabels: z42.array(z42.object({
10152
+ columnIndex: z42.number().int().nonnegative(),
10153
+ label: z42.string()
10154
+ })).default([]),
10155
+ continuationRows: z42.array(z42.object({
10156
+ sourceRowNodeId: z42.string(),
10157
+ targetRowNodeId: z42.string(),
10158
+ targetColumnIndex: z42.number().int().nonnegative().optional(),
10159
+ targetColumnLabel: z42.string().optional(),
10160
+ reason: z42.string().optional()
10161
+ })).default([])
10162
+ })).default([]),
10163
+ warnings: z42.array(z42.string()).default([])
10164
+ });
10116
10165
  var SourceBackedValueForPromptSchema = z42.object({
10117
10166
  value: z42.string(),
10118
10167
  normalizedValue: z42.string().optional(),
@@ -11335,6 +11384,319 @@ ${JSON.stringify(nodes, null, 2)}
11335
11384
 
11336
11385
  Return JSON for the operational profile.`;
11337
11386
  }
11387
+ var VISUAL_TABLE_REPAIR_MAX_TABLES = 4;
11388
+ var VISUAL_TABLE_REPAIR_MAX_ROWS = 28;
11389
+ var VISUAL_TABLE_REPAIR_MAX_CELLS = 140;
11390
+ var VISUAL_TABLE_REPAIR_KEYWORDS = /\b(coverage|limit|limits?|deductible|retroactive|premium|tax|fee|sublimit|sub-limit|aggregate|claim|occurrence|retention|declarations?)\b/i;
11391
+ function isSourceTreeHeaderRow(row) {
11392
+ return row.metadata?.isHeader === true || row.metadata?.isHeader === "true";
11393
+ }
11394
+ function tableCellText(cell) {
11395
+ return cleanText(cell.textExcerpt ?? cell.description ?? cell.title, "");
11396
+ }
11397
+ function tableRowTextForPrompt(row, cells) {
11398
+ return cleanText(
11399
+ cells.length ? cells.map(tableCellText).filter(Boolean).join(" | ") : row.textExcerpt ?? row.description ?? row.title,
11400
+ row.title
11401
+ );
11402
+ }
11403
+ function tableCellColumnIndex(cell, fallbackIndex) {
11404
+ const metadataIndex = cell.metadata?.columnIndex;
11405
+ return typeof metadataIndex === "number" && Number.isInteger(metadataIndex) ? metadataIndex : fallbackIndex;
11406
+ }
11407
+ function isGenericColumnTitle(value) {
11408
+ const title = cleanText(value, "");
11409
+ return !title || /^(?:column\s+\d+|table cell|value)$/i.test(title);
11410
+ }
11411
+ function bboxSummary(node) {
11412
+ const box = node.bbox?.[0];
11413
+ if (!box) return void 0;
11414
+ const round = (value) => Math.round(value * 10) / 10;
11415
+ return {
11416
+ page: box.page,
11417
+ x: round(box.x),
11418
+ y: round(box.y),
11419
+ width: round(box.width),
11420
+ height: round(box.height)
11421
+ };
11422
+ }
11423
+ function tableRowsWithCells(table, byParent) {
11424
+ return (byParent.get(table.id) ?? []).filter((node) => node.kind === "table_row").map((row) => ({
11425
+ row,
11426
+ cells: (byParent.get(row.id) ?? []).filter((child) => child.kind === "table_cell").sort(
11427
+ (left, right) => tableCellColumnIndex(left, 0) - tableCellColumnIndex(right, 0) || left.order - right.order || left.id.localeCompare(right.id)
11428
+ )
11429
+ })).sort((left, right) => left.row.order - right.row.order || left.row.id.localeCompare(right.row.id));
11430
+ }
11431
+ function primaryHeaderColumnCount(rows) {
11432
+ const header = rows.find(({ row, cells }) => isSourceTreeHeaderRow(row) && cells.length > 1);
11433
+ if (header) return header.cells.length;
11434
+ return Math.max(0, ...rows.map(({ cells }) => cells.length));
11435
+ }
11436
+ function shouldRepairVisualTable(candidate) {
11437
+ const rows = candidate.rows;
11438
+ if (rows.length < 3) return false;
11439
+ const tableText = rows.map(({ row, cells }) => tableRowTextForPrompt(row, cells)).join(" ");
11440
+ if (!VISUAL_TABLE_REPAIR_KEYWORDS.test(tableText)) return false;
11441
+ const headerCount = primaryHeaderColumnCount(rows);
11442
+ if (headerCount < 2) return false;
11443
+ const firstHeaderIndex = rows.findIndex(({ row, cells }) => isSourceTreeHeaderRow(row) && cells.length > 1);
11444
+ const repeatedHeader = firstHeaderIndex >= 0 && rows.some(({ row }, index) => index > firstHeaderIndex && isSourceTreeHeaderRow(row));
11445
+ const shortContinuation = rows.some(
11446
+ ({ row, cells }, index) => index > 0 && !isSourceTreeHeaderRow(row) && cells.length > 0 && cells.length < headerCount && !/^(?:item\s+\d+|[A-Z]\.)\b/i.test(tableCellText(cells[0]))
11447
+ );
11448
+ const genericDataLabels = rows.some(
11449
+ ({ row, cells }) => !isSourceTreeHeaderRow(row) && cells.length >= 2 && cells.some((cell) => isGenericColumnTitle(cell.title))
11450
+ );
11451
+ const danglingSlash = rows.some(
11452
+ ({ row, cells }) => !isSourceTreeHeaderRow(row) && /\/\s*$/.test(tableRowTextForPrompt(row, cells))
11453
+ );
11454
+ return repeatedHeader || shortContinuation || genericDataLabels || danglingSlash;
11455
+ }
11456
+ function visualTableCandidates(sourceTree) {
11457
+ const byParent = nodesByParent(sourceTree);
11458
+ return sourceTree.filter((node) => node.kind === "table" && typeof node.pageStart === "number").map((table) => ({
11459
+ table,
11460
+ page: table.pageStart,
11461
+ rows: tableRowsWithCells(table, byParent)
11462
+ })).filter(shouldRepairVisualTable).sort(
11463
+ (left, right) => left.page - right.page || left.table.order - right.table.order || left.table.id.localeCompare(right.table.id)
11464
+ ).slice(0, VISUAL_TABLE_REPAIR_MAX_TABLES);
11465
+ }
11466
+ function compactVisualTableCandidate(candidate) {
11467
+ let cellCount = 0;
11468
+ const rows = candidate.rows.slice(0, VISUAL_TABLE_REPAIR_MAX_ROWS).map(({ row, cells }) => {
11469
+ const compactCells = cells.slice(0, Math.max(0, VISUAL_TABLE_REPAIR_MAX_CELLS - cellCount)).map((cell, index) => ({
11470
+ cellNodeId: cell.id,
11471
+ columnIndex: tableCellColumnIndex(cell, index),
11472
+ currentColumnName: cell.title,
11473
+ text: tableCellText(cell),
11474
+ bbox: bboxSummary(cell)
11475
+ }));
11476
+ cellCount += compactCells.length;
11477
+ return {
11478
+ rowNodeId: row.id,
11479
+ order: row.order,
11480
+ isHeader: isSourceTreeHeaderRow(row),
11481
+ text: tableRowTextForPrompt(row, cells),
11482
+ bbox: bboxSummary(row),
11483
+ cells: compactCells
11484
+ };
11485
+ });
11486
+ return {
11487
+ tableNodeId: candidate.table.id,
11488
+ page: candidate.page,
11489
+ title: candidate.table.title,
11490
+ bbox: bboxSummary(candidate.table),
11491
+ rows
11492
+ };
11493
+ }
11494
+ function buildVisualTableRepairPrompt(candidate) {
11495
+ return `Compare a parsed insurance source table against the original page visual layout.
11496
+
11497
+ If a page image is attached, use it as the primary reference. If no image is available, use the bbox coordinates below as the visual layout reference.
11498
+
11499
+ Task:
11500
+ - Identify rows that are not real standalone rows because they are visually wrapped continuation text for a nearby row.
11501
+ - Identify the primary printed column labels for the table.
11502
+
11503
+ Rules:
11504
+ - Return only high-confidence repairs.
11505
+ - Use only rowNodeId/tableNodeId/cellNodeId values from the provided JSON.
11506
+ - Do not invent policy facts, values, row text, source spans, or page numbers.
11507
+ - continuationRows.sourceRowNodeId must be a parsed row that should be removed as a standalone row.
11508
+ - continuationRows.targetRowNodeId must be the row that visually owns that wrapped text, usually the immediately previous non-header row.
11509
+ - targetColumnIndex/targetColumnLabel should point to the visual column that owns the wrapped text, usually the limit/amount/value column.
11510
+ - Do not mark actual data rows as continuations when they begin a new item, coverage part, endorsement, form, location, person, or premium/tax row.
11511
+ - columnLabels should be the primary visual header labels, not later wrapped cell text that the parser misread as a header.
11512
+
11513
+ Parsed table with visual coordinates:
11514
+ ${JSON.stringify(compactVisualTableCandidate(candidate), null, 2)}
11515
+
11516
+ Return JSON with tables[].columnLabels and tables[].continuationRows. Return empty arrays if no repair is needed.`;
11517
+ }
11518
+ function sourceSpanIdsForNodes(nodes) {
11519
+ return [...new Set(nodes.flatMap((node) => node.sourceSpanIds))];
11520
+ }
11521
+ function appendDistinctText(base, addition) {
11522
+ const current = cleanText(base, "");
11523
+ const next = cleanText(addition, "");
11524
+ if (!current) return next || void 0;
11525
+ if (!next) return current;
11526
+ if (current.toLowerCase().includes(next.toLowerCase())) return current;
11527
+ const delimiter = /(?:[/(:;-]|,\s*)$/.test(current) ? " " : " / ";
11528
+ return cleanText(`${current}${delimiter}${next}`, current);
11529
+ }
11530
+ function mergedBbox(nodes) {
11531
+ const boxes = nodes.flatMap((node) => node.bbox ?? []);
11532
+ return boxes.length ? boxes.slice(0, 12) : void 0;
11533
+ }
11534
+ function normalizedRepairLabel(value) {
11535
+ const label = cleanText(value, "");
11536
+ if (!label || label.length > 80) return void 0;
11537
+ if (/^(?:source|page|row|table)$/i.test(label)) return void 0;
11538
+ return label;
11539
+ }
11540
+ function findCellForContinuation(params) {
11541
+ if (typeof params.targetColumnIndex === "number") {
11542
+ const byIndex = params.cells.find(
11543
+ (cell, index) => tableCellColumnIndex(cell, index) === params.targetColumnIndex
11544
+ );
11545
+ if (byIndex) return byIndex;
11546
+ }
11547
+ const label = normalizedRepairLabel(params.targetColumnLabel);
11548
+ if (label) {
11549
+ const byLabel = params.cells.find(
11550
+ (cell) => cleanText(cell.title, "").toLowerCase() === label.toLowerCase()
11551
+ );
11552
+ if (byLabel) return byLabel;
11553
+ }
11554
+ return params.cells[1] ?? params.cells[params.cells.length - 1];
11555
+ }
11556
+ function applyVisualTableRepair(sourceTree, repair) {
11557
+ if (repair.tables.length === 0) return sourceTree;
11558
+ const byId = new Map(sourceTree.map((node) => [node.id, node]));
11559
+ const byParent = nodesByParent(sourceTree);
11560
+ const updates = /* @__PURE__ */ new Map();
11561
+ const removeIds = /* @__PURE__ */ new Set();
11562
+ const currentNode = (id) => updates.get(id) ?? byId.get(id);
11563
+ for (const tableRepair of repair.tables) {
11564
+ const table = byId.get(tableRepair.tableNodeId);
11565
+ if (!table || table.kind !== "table") continue;
11566
+ const rows = tableRowsWithCells(table, byParent);
11567
+ const rowIds = new Set(rows.map(({ row }) => row.id));
11568
+ const rowOrder = new Map(rows.map(({ row }, index) => [row.id, index]));
11569
+ const columnLabels = /* @__PURE__ */ new Map();
11570
+ for (const label of tableRepair.columnLabels) {
11571
+ const normalized = normalizedRepairLabel(label.label);
11572
+ if (normalized) columnLabels.set(label.columnIndex, normalized);
11573
+ }
11574
+ if (columnLabels.size > 0) {
11575
+ for (const { row, cells } of rows) {
11576
+ if (removeIds.has(row.id)) continue;
11577
+ for (const [fallbackIndex, cell] of cells.entries()) {
11578
+ const columnIndex = tableCellColumnIndex(cell, fallbackIndex);
11579
+ const label = columnLabels.get(columnIndex);
11580
+ if (!label || cell.title === label) continue;
11581
+ updates.set(cell.id, {
11582
+ ...currentNode(cell.id) ?? cell,
11583
+ title: label,
11584
+ metadata: {
11585
+ ...cell.metadata ?? {},
11586
+ visualTableRepairColumnLabel: label
11587
+ }
11588
+ });
11589
+ }
11590
+ }
11591
+ }
11592
+ for (const continuation of tableRepair.continuationRows) {
11593
+ if (!rowIds.has(continuation.sourceRowNodeId) || !rowIds.has(continuation.targetRowNodeId)) continue;
11594
+ if (continuation.sourceRowNodeId === continuation.targetRowNodeId) continue;
11595
+ const sourceIndex = rowOrder.get(continuation.sourceRowNodeId);
11596
+ const targetIndex = rowOrder.get(continuation.targetRowNodeId);
11597
+ if (sourceIndex === void 0 || targetIndex === void 0) continue;
11598
+ if (Math.abs(sourceIndex - targetIndex) > 3) continue;
11599
+ const sourceRow = currentNode(continuation.sourceRowNodeId);
11600
+ const targetRow = currentNode(continuation.targetRowNodeId);
11601
+ if (!sourceRow || !targetRow || sourceRow.kind !== "table_row" || targetRow.kind !== "table_row") continue;
11602
+ if (isSourceTreeHeaderRow(targetRow)) continue;
11603
+ const sourceCells = (byParent.get(sourceRow.id) ?? []).filter((node) => node.kind === "table_cell").map((node) => currentNode(node.id) ?? node);
11604
+ const targetCells = (byParent.get(targetRow.id) ?? []).filter((node) => node.kind === "table_cell").map((node) => currentNode(node.id) ?? node);
11605
+ const sourceText = tableRowTextForPrompt(sourceRow, sourceCells);
11606
+ if (!sourceText) continue;
11607
+ const targetCell = findCellForContinuation({
11608
+ cells: targetCells,
11609
+ targetColumnIndex: continuation.targetColumnIndex,
11610
+ targetColumnLabel: continuation.targetColumnLabel
11611
+ });
11612
+ const sourceNodes = [sourceRow, ...sourceCells];
11613
+ const sourceSpanIds = sourceSpanIdsForNodes(sourceNodes);
11614
+ if (targetCell) {
11615
+ const nextCellText = appendDistinctText(tableCellText(targetCell), sourceText);
11616
+ if (nextCellText) {
11617
+ const mergedNodes = [targetCell, ...sourceNodes];
11618
+ updates.set(targetCell.id, {
11619
+ ...currentNode(targetCell.id) ?? targetCell,
11620
+ textExcerpt: nextCellText,
11621
+ description: cleanText([targetCell.title, nextCellText].filter(Boolean).join(" | "), targetCell.description),
11622
+ sourceSpanIds: [.../* @__PURE__ */ new Set([...targetCell.sourceSpanIds, ...sourceSpanIds])],
11623
+ bbox: mergedBbox(mergedNodes),
11624
+ metadata: {
11625
+ ...targetCell.metadata ?? {},
11626
+ visualTableRepair: "merged_continuation"
11627
+ }
11628
+ });
11629
+ }
11630
+ }
11631
+ const nextRowText = appendDistinctText(targetRow.textExcerpt ?? targetRow.description, sourceText);
11632
+ updates.set(targetRow.id, {
11633
+ ...currentNode(targetRow.id) ?? targetRow,
11634
+ textExcerpt: nextRowText,
11635
+ description: cleanText([targetRow.title, nextRowText].filter(Boolean).join(" | "), targetRow.description),
11636
+ sourceSpanIds: [.../* @__PURE__ */ new Set([...targetRow.sourceSpanIds, ...sourceSpanIds])],
11637
+ bbox: mergedBbox([targetRow, ...sourceNodes]),
11638
+ metadata: {
11639
+ ...targetRow.metadata ?? {},
11640
+ visualTableRepair: "merged_continuation"
11641
+ }
11642
+ });
11643
+ removeIds.add(sourceRow.id);
11644
+ for (const sourceCell of sourceCells) removeIds.add(sourceCell.id);
11645
+ }
11646
+ }
11647
+ if (updates.size === 0 && removeIds.size === 0) return sourceTree;
11648
+ const repaired = sourceTree.filter((node) => !removeIds.has(node.id)).map((node) => updates.get(node.id) ?? node);
11649
+ return normalizeDocumentSourceTreePaths(normalizeContainerEvidenceFromChildren(repaired));
11650
+ }
11651
+ async function runVisualTableRepair(params) {
11652
+ const candidates = visualTableCandidates(params.sourceTree);
11653
+ if (candidates.length === 0) return { sourceTree: params.sourceTree, warnings: [] };
11654
+ let sourceTree = params.sourceTree;
11655
+ const warnings = [];
11656
+ for (const [index, candidate] of candidates.entries()) {
11657
+ try {
11658
+ const budget = params.resolveBudget("extraction_source_tree", 1800);
11659
+ const maxTokens = Math.min(budget.maxTokens, 2400);
11660
+ const startedAt = Date.now();
11661
+ const response = await safeGenerateObject(
11662
+ params.generateObject,
11663
+ {
11664
+ prompt: buildVisualTableRepairPrompt(candidate),
11665
+ schema: SourceTreeVisualTableRepairSchema,
11666
+ maxTokens,
11667
+ taskKind: "extraction_source_tree",
11668
+ budgetDiagnostics: { ...budget, maxTokens },
11669
+ trace: {
11670
+ label: `source_tree_visual_table_repair_p${candidate.page}`,
11671
+ startPage: candidate.page,
11672
+ endPage: candidate.page,
11673
+ batchIndex: index + 1,
11674
+ batchCount: candidates.length,
11675
+ sourceBacked: true
11676
+ }
11677
+ },
11678
+ {
11679
+ fallback: { tables: [], warnings: [] },
11680
+ log: params.log
11681
+ }
11682
+ );
11683
+ params.trackUsage(response.usage, {
11684
+ taskKind: "extraction_source_tree",
11685
+ label: `source_tree_visual_table_repair_p${candidate.page}`,
11686
+ maxTokens,
11687
+ durationMs: Date.now() - startedAt
11688
+ });
11689
+ const repair = response.object;
11690
+ sourceTree = applyVisualTableRepair(sourceTree, repair);
11691
+ warnings.push(...repair.warnings.map(
11692
+ (warning) => `Visual table repair warning on page ${candidate.page}: ${warning}`
11693
+ ));
11694
+ } catch (error) {
11695
+ warnings.push(`Visual table repair skipped on page ${candidate.page}; parsed table kept (${error instanceof Error ? error.message : String(error)})`);
11696
+ }
11697
+ }
11698
+ return { sourceTree, warnings };
11699
+ }
11338
11700
  function groupNodeId(documentId, group) {
11339
11701
  return [
11340
11702
  documentId.replace(/[^a-zA-Z0-9_.:-]/g, "_"),
@@ -11648,6 +12010,15 @@ async function runSourceTreeExtraction(params) {
11648
12010
  warnings.push(`Source-tree outline cleanup failed; deterministic tree used (${error instanceof Error ? error.message : String(error)})`);
11649
12011
  }
11650
12012
  }
12013
+ const visualTableRepair = await runVisualTableRepair({
12014
+ sourceTree,
12015
+ generateObject: params.generateObject,
12016
+ resolveBudget: params.resolveBudget,
12017
+ trackUsage: localTrack,
12018
+ log: params.log
12019
+ });
12020
+ sourceTree = visualTableRepair.sourceTree;
12021
+ warnings.push(...visualTableRepair.warnings);
11651
12022
  const deterministicProfile = buildDeterministicOperationalProfile({
11652
12023
  sourceTree,
11653
12024
  sourceSpans