@claritylabs/cl-sdk 3.0.10 → 3.0.12

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
@@ -2602,7 +2602,7 @@ function normalizeNodeKind(span) {
2602
2602
  if (unit === "table_cell") return "table_cell";
2603
2603
  if (unit === "key_value") return "schedule";
2604
2604
  if (unit === "section") return "section";
2605
- if (element === "title" || element === "section_candidate") {
2605
+ if (element === "section_candidate") {
2606
2606
  const text = span.text.toLowerCase();
2607
2607
  if (/endorsement/.test(text)) return "endorsement";
2608
2608
  if (/schedule|declarations?/.test(text)) return "schedule";
@@ -2638,6 +2638,90 @@ function makeNode(params) {
2638
2638
  metadata: params.metadata
2639
2639
  };
2640
2640
  }
2641
+ function metadataString(metadata, key) {
2642
+ const value = metadata?.[key];
2643
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
2644
+ }
2645
+ function isTitleContentNode(node) {
2646
+ if (node.kind !== "text") return false;
2647
+ return metadataString(node.metadata, "elementType") === "title" || metadataString(node.metadata, "sourceUnit") === "title";
2648
+ }
2649
+ function nodePageEnd(node) {
2650
+ return node.pageEnd ?? node.pageStart;
2651
+ }
2652
+ function groupPageContentByTitles(nodes) {
2653
+ const byParent = /* @__PURE__ */ new Map();
2654
+ for (const node of nodes) {
2655
+ const children = byParent.get(node.parentId) ?? [];
2656
+ children.push(node);
2657
+ byParent.set(node.parentId, children);
2658
+ }
2659
+ for (const children of byParent.values()) {
2660
+ children.sort((left, right) => left.order - right.order || left.id.localeCompare(right.id));
2661
+ }
2662
+ const byId = new Map(nodes.map((node) => [node.id, node]));
2663
+ for (const pageNode of nodes.filter((node) => node.kind === "page")) {
2664
+ const children = (byParent.get(pageNode.id) ?? []).filter((child) => child.kind !== "table_row" && child.kind !== "table_cell");
2665
+ let activeTitle;
2666
+ let activeContent = [];
2667
+ const flush = () => {
2668
+ if (!activeTitle || activeContent.length === 0) {
2669
+ activeTitle = void 0;
2670
+ activeContent = [];
2671
+ return;
2672
+ }
2673
+ const contentNodes = activeContent.map((node) => byId.get(node.id) ?? node).filter((node) => node.parentId === pageNode.id);
2674
+ if (contentNodes.length === 0) {
2675
+ activeTitle = void 0;
2676
+ activeContent = [];
2677
+ return;
2678
+ }
2679
+ const evidenceNodes = [activeTitle, ...contentNodes];
2680
+ const title = truncate(activeTitle.textExcerpt ?? activeTitle.title, 120);
2681
+ const pageStarts = evidenceNodes.map((node) => node.pageStart).filter((page) => typeof page === "number");
2682
+ const pageEnds = evidenceNodes.map(nodePageEnd).filter((page) => typeof page === "number");
2683
+ const sourceSpanIds = [...new Set(evidenceNodes.flatMap((node) => node.sourceSpanIds))];
2684
+ const bbox = evidenceNodes.flatMap((node) => node.bbox ?? []).slice(0, 12);
2685
+ byId.set(activeTitle.id, {
2686
+ ...activeTitle,
2687
+ kind: "section",
2688
+ title,
2689
+ description: nodeTextDescription({
2690
+ kind: "section",
2691
+ title,
2692
+ text: evidenceNodes.map((node) => node.textExcerpt).filter(Boolean).join("\n\n"),
2693
+ page: activeTitle.pageStart
2694
+ }),
2695
+ textExcerpt: evidenceNodes.map((node) => node.textExcerpt).filter(Boolean).join("\n\n").slice(0, 1600),
2696
+ sourceSpanIds,
2697
+ pageStart: pageStarts.length ? Math.min(...pageStarts) : activeTitle.pageStart,
2698
+ pageEnd: pageEnds.length ? Math.max(...pageEnds) : activeTitle.pageEnd,
2699
+ bbox,
2700
+ metadata: {
2701
+ ...activeTitle.metadata,
2702
+ sourceTreeVersion: "v3",
2703
+ organizer: "title_block"
2704
+ }
2705
+ });
2706
+ for (const node of contentNodes) {
2707
+ byId.set(node.id, { ...node, parentId: activeTitle.id });
2708
+ }
2709
+ activeTitle = void 0;
2710
+ activeContent = [];
2711
+ };
2712
+ for (const child of children) {
2713
+ if (isTitleContentNode(child)) {
2714
+ flush();
2715
+ activeTitle = child;
2716
+ activeContent = [];
2717
+ continue;
2718
+ }
2719
+ if (activeTitle) activeContent.push(child);
2720
+ }
2721
+ flush();
2722
+ }
2723
+ return nodes.map((node) => byId.get(node.id) ?? node);
2724
+ }
2641
2725
  function sortSpans(left, right) {
2642
2726
  const leftPage = pageStart(left) ?? 0;
2643
2727
  const rightPage = pageStart(right) ?? 0;
@@ -2662,14 +2746,24 @@ function normalizeDocumentSourceTreePaths(nodes) {
2662
2746
  group.sort((left, right) => left.order - right.order || left.id.localeCompare(right.id));
2663
2747
  }
2664
2748
  const result = [];
2665
- const visit = (node, path) => {
2666
- const next = { ...node, path };
2749
+ const visited = /* @__PURE__ */ new Set();
2750
+ const visit = (node, path, ancestors, parentId) => {
2751
+ if (visited.has(node.id) || ancestors.has(node.id)) return;
2752
+ visited.add(node.id);
2753
+ const next = { ...node, parentId, path };
2667
2754
  result.push(next);
2668
2755
  const children = byParent.get(node.id) ?? [];
2669
- children.forEach((child, index) => visit(child, `${path}.${index + 1}`));
2756
+ const nextAncestors = new Set(ancestors);
2757
+ nextAncestors.add(node.id);
2758
+ children.forEach((child, index) => visit(child, `${path}.${index + 1}`, nextAncestors, node.id));
2670
2759
  };
2671
2760
  const roots = byParent.get(void 0) ?? [];
2672
- roots.forEach((root, index) => visit(root, String(index + 1)));
2761
+ roots.forEach((root, index) => visit(root, String(index + 1), /* @__PURE__ */ new Set(), void 0));
2762
+ for (const node of nodes) {
2763
+ if (!visited.has(node.id)) {
2764
+ visit(node, String(result.length + 1), /* @__PURE__ */ new Set(), void 0);
2765
+ }
2766
+ }
2673
2767
  return result;
2674
2768
  }
2675
2769
  function buildDocumentSourceTree(sourceSpans, documentId) {
@@ -2803,7 +2897,7 @@ function buildDocumentSourceTree(sourceSpans, documentId) {
2803
2897
  }
2804
2898
  addStandaloneSpanNode(span, pageParentId);
2805
2899
  }
2806
- return normalizeDocumentSourceTreePaths([...nodes.values()]);
2900
+ return normalizeDocumentSourceTreePaths(groupPageContentByTitles([...nodes.values()]));
2807
2901
  }
2808
2902
 
2809
2903
  // src/source/operational-profile.ts
@@ -8999,7 +9093,9 @@ var ORGANIZABLE_KINDS = [
8999
9093
  "clause"
9000
9094
  ];
9001
9095
  var ORGANIZATION_TOP_LEVEL_BATCH_SIZE = 80;
9002
- var ORGANIZATION_CHILD_CONTEXT_LIMIT = 4;
9096
+ var ORGANIZER_MAX_SOURCE_SPANS = 400;
9097
+ var ORGANIZER_MAX_TOP_LEVEL_NODES = 18;
9098
+ var OUTLINE_CLEANUP_MAX_TOP_LEVEL_NODES = 80;
9003
9099
  var SourceTreeOrganizationSchema = z41.object({
9004
9100
  labels: z41.array(z41.object({
9005
9101
  nodeId: z41.string(),
@@ -9116,6 +9212,22 @@ function endorsementDescription(title, node) {
9116
9212
  title
9117
9213
  );
9118
9214
  }
9215
+ function nodePageEnd2(node) {
9216
+ return node.pageEnd ?? node.pageStart;
9217
+ }
9218
+ function pageRangeForNodes(nodes) {
9219
+ const pageStarts = nodes.map((node) => node.pageStart).filter((page) => typeof page === "number");
9220
+ const pageEnds = nodes.map(nodePageEnd2).filter((page) => typeof page === "number");
9221
+ if (pageStarts.length === 0 || pageEnds.length === 0) return void 0;
9222
+ const start = Math.min(...pageStarts);
9223
+ const end = Math.max(...pageEnds);
9224
+ return start === end ? `page ${start}` : `pages ${start}-${end}`;
9225
+ }
9226
+ function descriptionWithPages(description, nodes) {
9227
+ const range = pageRangeForNodes(nodes);
9228
+ if (!range || new RegExp(`\\b${range.replace("-", "\\-")}\\b`, "i").test(description)) return description;
9229
+ return `${description}; ${range}`;
9230
+ }
9119
9231
  function semanticGroupNodeId(documentId, kind, title, childNodeIds) {
9120
9232
  return [
9121
9233
  documentId.replace(/[^a-zA-Z0-9_.:-]/g, "_"),
@@ -9126,7 +9238,12 @@ function semanticGroupNodeId(documentId, kind, title, childNodeIds) {
9126
9238
  ].join(":");
9127
9239
  }
9128
9240
  function looksLikeDeclarationsStart(node) {
9129
- return /\bdeclarations?\s+(page|schedule)\b/i.test(sourceNodeText(node));
9241
+ const title = cleanText(node.title, "");
9242
+ const text = sourceNodeText(node);
9243
+ if (/\b(important notice|privacy notice|ofac advisory|terrorism risk insurance act|how to report a claim)\b/i.test(text)) {
9244
+ return false;
9245
+ }
9246
+ return /^declarations?$/i.test(title) || /\bdeclarations?\s+(page|schedule|section)\b/i.test(text) || /^declarations?\b/i.test(cleanText(node.textExcerpt, ""));
9130
9247
  }
9131
9248
  function looksLikeDeclarationsContinuation(node) {
9132
9249
  const text = sourceNodeText(node);
@@ -9134,7 +9251,8 @@ function looksLikeDeclarationsContinuation(node) {
9134
9251
  }
9135
9252
  function looksLikePolicyFormStart(node) {
9136
9253
  const text = sourceNodeText(node);
9137
- return /\bpolicy form\b/i.test(node.title) || /\btechnology errors?\s*&?\s*omissions\b/i.test(text) && /\bplease read this entire policy carefully\b/i.test(text) || /\bform\s+[A-Z]{2,}-[A-Z0-9-]+\s+\d{2}\s+\d{2}\b/i.test(text);
9254
+ const excerpt = cleanText(node.textExcerpt, "");
9255
+ return /\bpolicy form\b/i.test(node.title) || /^policy\s+form\b/i.test(excerpt) || /\btechnology errors?\s*&?\s*omissions\b/i.test(text) && /\bplease read this entire policy carefully\b/i.test(text) || /\bform\s+[A-Z]{2,}-[A-Z0-9-]+\s+\d{2}\s+\d{2}\b/i.test(text);
9138
9256
  }
9139
9257
  function looksLikePolicyFormContinuation(node) {
9140
9258
  const text = sourceNodeText(node);
@@ -9160,7 +9278,7 @@ function groupAdjacentChildren(params) {
9160
9278
  parentId,
9161
9279
  kind: params.kind,
9162
9280
  title: params.title,
9163
- description: params.description,
9281
+ description: descriptionWithPages(params.description, children),
9164
9282
  textExcerpt: children.map((child) => child.textExcerpt ?? child.description).filter(Boolean).join("\n\n").slice(0, 1600),
9165
9283
  sourceSpanIds,
9166
9284
  pageStart: pageStarts.length ? Math.min(...pageStarts) : void 0,
@@ -9213,6 +9331,22 @@ function applySemanticPageGrouping(sourceTree) {
9213
9331
  const children = (nodesByParent(relabeled).get(rootId) ?? []).filter((node) => node.kind !== "document").sort((left, right) => left.order - right.order);
9214
9332
  let nextTree = relabeled;
9215
9333
  const declarationsStartIndex = children.findIndex(looksLikeDeclarationsStart);
9334
+ const firstCoreIndex = children.findIndex(
9335
+ (child) => looksLikeDeclarationsStart(child) || looksLikePolicyFormStart(child) || looksLikeEndorsementStart(child)
9336
+ );
9337
+ const frontMatterBoundary = declarationsStartIndex >= 0 ? declarationsStartIndex : firstCoreIndex;
9338
+ if (frontMatterBoundary > 0) {
9339
+ const frontMatterIds = children.slice(0, frontMatterBoundary).map((child) => child.id);
9340
+ nextTree = groupAdjacentChildren({
9341
+ sourceTree: nextTree,
9342
+ children,
9343
+ childIds: frontMatterIds,
9344
+ kind: "page_group",
9345
+ title: "Notices and Jacket",
9346
+ description: "Policy jacket, notices, and administrative pages grouped by source order",
9347
+ organizer: "semantic_front_matter_grouping"
9348
+ });
9349
+ }
9216
9350
  if (declarationsStartIndex >= 0) {
9217
9351
  const declarationIds = [];
9218
9352
  for (let index = declarationsStartIndex; index < children.length; index += 1) {
@@ -9381,17 +9515,19 @@ function normalizeContainerEvidenceFromChildren(sourceTree) {
9381
9515
  return sourceTree.map((node) => byId.get(node.id) ?? node);
9382
9516
  }
9383
9517
  function normalizeSemanticHierarchy(sourceTree) {
9384
- return normalizeDocumentSourceTreePaths(
9385
- normalizeContainerEvidenceFromChildren(
9386
- nestEndorsementContinuationPages(
9387
- normalizePolicyFormStructure(
9388
- normalizeDocumentSourceTreePaths(sourceTree)
9389
- )
9390
- )
9518
+ const normalized = normalizeDocumentSourceTreePaths(
9519
+ normalizePolicyFormStructure(
9520
+ normalizeDocumentSourceTreePaths(sourceTree)
9391
9521
  )
9392
9522
  );
9523
+ const nested = normalizeDocumentSourceTreePaths(nestEndorsementContinuationPages(normalized));
9524
+ const withEvidence = normalizeContainerEvidenceFromChildren(nested);
9525
+ return normalizeDocumentSourceTreePaths(
9526
+ normalizeContainerEvidenceFromChildren(withEvidence)
9527
+ );
9393
9528
  }
9394
9529
  function applyEndorsementGrouping(sourceTree) {
9530
+ const rootId = sourceTreeRootId(sourceTree);
9395
9531
  const relabeledTree = sourceTree.map((node) => {
9396
9532
  if (node.kind === "document" || isEndorsementGroup(node)) return node;
9397
9533
  const title = endorsementStartTitle(node);
@@ -9429,7 +9565,7 @@ function applyEndorsementGrouping(sourceTree) {
9429
9565
  ...node,
9430
9566
  kind: "page_group",
9431
9567
  title: "Endorsements",
9432
- description: cleanText(node.description, "Endorsement forms grouped by source order"),
9568
+ description: descriptionWithPages(cleanText(node.description, "Endorsement forms grouped by source order"), byParent.get(node.id) ?? [node]),
9433
9569
  metadata: {
9434
9570
  ...node.metadata,
9435
9571
  sourceTreeVersion: "v3",
@@ -9456,9 +9592,10 @@ function applyEndorsementGrouping(sourceTree) {
9456
9592
  };
9457
9593
  });
9458
9594
  for (const [parentId, children] of byParent) {
9595
+ if (parentId !== rootId) continue;
9459
9596
  if (endorsementGroupIds.has(parentId ?? "")) continue;
9460
9597
  const endorsementChildren = children.filter((child) => child.kind === "endorsement" && !isEndorsementGroup(child));
9461
- if (endorsementChildren.length < 2) continue;
9598
+ if (endorsementChildren.length < 1) continue;
9462
9599
  const endorsementGroupChildren = [];
9463
9600
  let hasSeenEndorsementStart = false;
9464
9601
  for (const child of children) {
@@ -9471,6 +9608,7 @@ function applyEndorsementGrouping(sourceTree) {
9471
9608
  endorsementGroupChildren.push(child);
9472
9609
  }
9473
9610
  }
9611
+ if (endorsementGroupChildren.length < 1) continue;
9474
9612
  const documentId = endorsementChildren[0].documentId;
9475
9613
  const pageStarts = endorsementGroupChildren.map((child) => child.pageStart).filter((page) => typeof page === "number");
9476
9614
  const pageEnds = endorsementGroupChildren.map((child) => child.pageEnd ?? child.pageStart).filter((page) => typeof page === "number");
@@ -9483,7 +9621,7 @@ function applyEndorsementGrouping(sourceTree) {
9483
9621
  parentId,
9484
9622
  kind: "page_group",
9485
9623
  title: "Endorsements",
9486
- description: "Endorsement forms grouped by source order",
9624
+ description: descriptionWithPages("Endorsement forms grouped by source order", endorsementGroupChildren),
9487
9625
  textExcerpt: void 0,
9488
9626
  sourceSpanIds: [],
9489
9627
  pageStart: pageStarts.length ? Math.min(...pageStarts) : void 0,
@@ -9494,11 +9632,13 @@ function applyEndorsementGrouping(sourceTree) {
9494
9632
  metadata: { sourceTreeVersion: "v3", organizer: "endorsement_grouping" }
9495
9633
  };
9496
9634
  const childSpanIds = [...new Set(endorsementGroupChildren.flatMap((child) => child.sourceSpanIds))];
9635
+ const childPageStart = pageStarts.length ? Math.min(...pageStarts) : void 0;
9636
+ const childPageEnd = pageEnds.length ? Math.max(...pageEnds) : void 0;
9497
9637
  const normalizedGroup = {
9498
9638
  ...groupNode,
9499
9639
  sourceSpanIds: groupNode.sourceSpanIds.length ? groupNode.sourceSpanIds : childSpanIds,
9500
- pageStart: groupNode.pageStart ?? (pageStarts.length ? Math.min(...pageStarts) : void 0),
9501
- pageEnd: groupNode.pageEnd ?? (pageEnds.length ? Math.max(...pageEnds) : void 0),
9640
+ pageStart: childPageStart === void 0 ? groupNode.pageStart : groupNode.pageStart === void 0 ? childPageStart : Math.min(groupNode.pageStart, childPageStart),
9641
+ pageEnd: childPageEnd === void 0 ? groupNode.pageEnd : groupNode.pageEnd === void 0 ? childPageEnd : Math.max(groupNode.pageEnd, childPageEnd),
9502
9642
  order
9503
9643
  };
9504
9644
  groupsByParent.set(parentId, normalizedGroup);
@@ -9538,6 +9678,24 @@ function nodesByParent(sourceTree) {
9538
9678
  function sourceTreeRootId(sourceTree) {
9539
9679
  return sourceTree.find((node) => node.kind === "document")?.id;
9540
9680
  }
9681
+ function rootChildren(sourceTree) {
9682
+ const byParent = nodesByParent(sourceTree);
9683
+ const rootId = sourceTreeRootId(sourceTree);
9684
+ return (byParent.get(rootId) ?? []).filter((node) => node.kind !== "document");
9685
+ }
9686
+ function hasDeterministicSemanticOutline(sourceTree) {
9687
+ const children = rootChildren(sourceTree);
9688
+ const semanticCount = children.filter(
9689
+ (node) => node.kind === "page_group" || node.kind === "form" || node.kind === "endorsement" || node.kind === "section" || node.kind === "schedule"
9690
+ ).length;
9691
+ return semanticCount >= 2 || children.some((node) => node.title === "Declarations") || children.some((node) => node.title === "Policy Form") || children.some((node) => node.title === "Endorsements");
9692
+ }
9693
+ function shouldRunSourceTreeOrganizer(sourceTree, sourceSpans) {
9694
+ const topLevelCount = rootChildren(sourceTree).length;
9695
+ if (sourceSpans.length > ORGANIZER_MAX_SOURCE_SPANS) return false;
9696
+ if (topLevelCount > ORGANIZER_MAX_TOP_LEVEL_NODES) return false;
9697
+ return !hasDeterministicSemanticOutline(sourceTree);
9698
+ }
9541
9699
  function organizationBatches(sourceTree) {
9542
9700
  const byParent = nodesByParent(sourceTree);
9543
9701
  const rootId = sourceTreeRootId(sourceTree);
@@ -9556,10 +9714,6 @@ function organizationBatches(sourceTree) {
9556
9714
  const candidates = /* @__PURE__ */ new Map();
9557
9715
  for (const node of topLevelBatch) {
9558
9716
  candidates.set(node.id, node);
9559
- const childContext = (byParent.get(node.id) ?? []).filter((child) => child.kind !== "text" && child.kind !== "table_cell").slice(0, ORGANIZATION_CHILD_CONTEXT_LIMIT);
9560
- for (const child of childContext) {
9561
- candidates.set(child.id, child);
9562
- }
9563
9717
  }
9564
9718
  batches.push({
9565
9719
  label: `top-level nodes ${index + 1}-${index + topLevelBatch.length} of ${topLevelNodes.length}`,
@@ -9610,6 +9764,41 @@ Rules:
9610
9764
  Source nodes:
9611
9765
  ${JSON.stringify(nodes, null, 2)}
9612
9766
 
9767
+ Return JSON with labels and groups only.`;
9768
+ }
9769
+ function shouldRunOutlineCleanup(sourceTree) {
9770
+ const topLevel = rootChildren(sourceTree);
9771
+ if (topLevel.length === 0 || topLevel.length > OUTLINE_CLEANUP_MAX_TOP_LEVEL_NODES) return false;
9772
+ const genericPages = topLevel.filter((node) => node.kind === "page" && /^Page\s+\d+$/i.test(node.title));
9773
+ const hasDeclarations = topLevel.some((node) => node.title === "Declarations");
9774
+ const hasPolicyForm = topLevel.some((node) => node.title === "Policy Form");
9775
+ const hasEndorsements = topLevel.some((node) => node.title === "Endorsements");
9776
+ return genericPages.length > 0 || topLevel.length > 6 || !hasDeclarations || !hasPolicyForm || !hasEndorsements;
9777
+ }
9778
+ function buildOutlineCleanupPrompt(sourceTree) {
9779
+ const topLevel = rootChildren(sourceTree).slice(0, OUTLINE_CLEANUP_MAX_TOP_LEVEL_NODES);
9780
+ const nodes = topLevel.map((node) => compactNode(node, 900));
9781
+ return `You clean a top-level source outline for an insurance policy.
9782
+
9783
+ Expected product-facing order:
9784
+ 1. Optional front matter: policy jacket, important notices, privacy notices, OFAC notices, TRIA/terrorism notices, marketing/admin pages, signatures, countersignatures, or other pages that are not the declarations, policy wording, or endorsements.
9785
+ 2. Declarations: declarations page(s), schedules, named insured/policy period/premium rows, coverage limit schedules, forms-and-endorsements schedules.
9786
+ 3. Policy Form: the main policy wording, insuring agreements, definitions, exclusions, conditions, claim provisions, and general policy terms.
9787
+ 4. Endorsements: one generic "Endorsements" page_group containing each separately numbered endorsement as its own child.
9788
+
9789
+ Rules:
9790
+ - Use only node IDs from this top-level list: ${JSON.stringify(topLevel.map((node) => node.id))}
9791
+ - Group only adjacent top-level nodes.
9792
+ - Do not invent text, pages, source spans, limits, or policy facts.
9793
+ - Do not merge individually numbered endorsements into one endorsement node; use the generic "Endorsements" parent for the series.
9794
+ - Use canonical terse titles: "Notices and Jacket", "Declarations", "Policy Form", "Endorsements", or the printed endorsement number.
9795
+ - Keep page_group descriptions short and include the page range when pages are known, for example "Declarations pages 6-8".
9796
+ - If a page is an OFAC, privacy, terrorism/TRIA, claim-reporting notice, signature page, or jacket, do not label it as declarations or policy form.
9797
+ - If the existing deterministic outline is already correct, return empty labels and groups.
9798
+
9799
+ Top-level source nodes:
9800
+ ${JSON.stringify(nodes, null, 2)}
9801
+
9613
9802
  Return JSON with labels and groups only.`;
9614
9803
  }
9615
9804
  function buildOperationalProfilePrompt(sourceTree, fallback) {
@@ -9664,7 +9853,7 @@ function applyOrganization(sourceTree, organization) {
9664
9853
  if (!children.every((child) => child.parentId === parentId)) continue;
9665
9854
  const documentId = children[0].documentId;
9666
9855
  const title = simplifyOrganizerTitle(group.title, group.title, group.kind);
9667
- const description = cleanText(group.description, title);
9856
+ const description = descriptionWithPages(cleanText(group.description, title), children);
9668
9857
  const id = groupNodeId(documentId, { ...group, title });
9669
9858
  if (byId.has(id)) continue;
9670
9859
  const sourceSpanIds = [...new Set(children.flatMap((child) => child.sourceSpanIds))];
@@ -9819,7 +10008,7 @@ function materializeDocument(params) {
9819
10008
  }
9820
10009
  async function runSourceTreeExtraction(params) {
9821
10010
  const sourceSpans = normalizeSourceSpans(params.sourceSpans);
9822
- let sourceTree = buildDocumentSourceTree(sourceSpans, params.id);
10011
+ let sourceTree = applySemanticPageGrouping(buildDocumentSourceTree(sourceSpans, params.id));
9823
10012
  const warnings = [];
9824
10013
  let modelCalls = 0;
9825
10014
  let callsWithUsage = 0;
@@ -9841,21 +10030,55 @@ async function runSourceTreeExtraction(params) {
9841
10030
  }
9842
10031
  params.trackUsage(usage, report);
9843
10032
  };
9844
- try {
9845
- const organizations = [];
9846
- const batches = organizationBatches(sourceTree);
9847
- for (const [batchIndex, batch] of batches.entries()) {
9848
- const budget = params.resolveBudget("extraction_source_tree", 4096);
10033
+ if (shouldRunSourceTreeOrganizer(sourceTree, sourceSpans)) {
10034
+ try {
10035
+ const organizations = [];
10036
+ const batches = organizationBatches(sourceTree);
10037
+ for (const [batchIndex, batch] of batches.entries()) {
10038
+ const budget = params.resolveBudget("extraction_source_tree", 4096);
10039
+ const startedAt = Date.now();
10040
+ const response = await safeGenerateObject(
10041
+ params.generateObject,
10042
+ {
10043
+ prompt: buildOrganizationPrompt(batch),
10044
+ schema: SourceTreeOrganizationSchema,
10045
+ maxTokens: budget.maxTokens,
10046
+ taskKind: "extraction_source_tree",
10047
+ budgetDiagnostics: budget
10048
+ },
10049
+ {
10050
+ fallback: { labels: [], groups: [] },
10051
+ log: params.log
10052
+ }
10053
+ );
10054
+ localTrack(response.usage, {
10055
+ taskKind: "extraction_source_tree",
10056
+ label: batches.length > 1 ? `source_tree_organizer_${batchIndex + 1}` : "source_tree_organizer",
10057
+ maxTokens: budget.maxTokens,
10058
+ durationMs: Date.now() - startedAt
10059
+ });
10060
+ organizations.push(response.object);
10061
+ }
10062
+ sourceTree = applyOrganization(sourceTree, mergeOrganizationResults(organizations));
10063
+ } catch (error) {
10064
+ warnings.push(`Source-tree organizer failed; deterministic tree used (${error instanceof Error ? error.message : String(error)})`);
10065
+ }
10066
+ } else {
10067
+ await params.log?.("Deterministic source tree ready; skipped model organizer");
10068
+ }
10069
+ if (shouldRunOutlineCleanup(sourceTree)) {
10070
+ try {
10071
+ const budget = params.resolveBudget("extraction_source_tree", 1600);
10072
+ const maxTokens = Math.min(budget.maxTokens, 1600);
9849
10073
  const startedAt = Date.now();
9850
10074
  const response = await safeGenerateObject(
9851
10075
  params.generateObject,
9852
10076
  {
9853
- prompt: buildOrganizationPrompt(batch),
10077
+ prompt: buildOutlineCleanupPrompt(sourceTree),
9854
10078
  schema: SourceTreeOrganizationSchema,
9855
- maxTokens: budget.maxTokens,
10079
+ maxTokens,
9856
10080
  taskKind: "extraction_source_tree",
9857
- budgetDiagnostics: budget,
9858
- providerOptions: { ...params.providerOptions, sourceSpans }
10081
+ budgetDiagnostics: { ...budget, maxTokens }
9859
10082
  },
9860
10083
  {
9861
10084
  fallback: { labels: [], groups: [] },
@@ -9864,17 +10087,15 @@ async function runSourceTreeExtraction(params) {
9864
10087
  );
9865
10088
  localTrack(response.usage, {
9866
10089
  taskKind: "extraction_source_tree",
9867
- label: batches.length > 1 ? `source_tree_organizer_${batchIndex + 1}` : "source_tree_organizer",
9868
- maxTokens: budget.maxTokens,
10090
+ label: "source_tree_outline_cleanup",
10091
+ maxTokens,
9869
10092
  durationMs: Date.now() - startedAt
9870
10093
  });
9871
- organizations.push(response.object);
10094
+ sourceTree = applyOrganization(sourceTree, response.object);
10095
+ } catch (error) {
10096
+ warnings.push(`Source-tree outline cleanup failed; deterministic tree used (${error instanceof Error ? error.message : String(error)})`);
9872
10097
  }
9873
- sourceTree = applyOrganization(sourceTree, mergeOrganizationResults(organizations));
9874
- } catch (error) {
9875
- warnings.push(`Source-tree organizer failed; deterministic tree used (${error instanceof Error ? error.message : String(error)})`);
9876
10098
  }
9877
- sourceTree = applySemanticPageGrouping(sourceTree);
9878
10099
  const deterministicProfile = buildDeterministicOperationalProfile({
9879
10100
  sourceTree,
9880
10101
  sourceSpans