@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.js CHANGED
@@ -2957,7 +2957,7 @@ function normalizeNodeKind(span) {
2957
2957
  if (unit === "table_cell") return "table_cell";
2958
2958
  if (unit === "key_value") return "schedule";
2959
2959
  if (unit === "section") return "section";
2960
- if (element === "title" || element === "section_candidate") {
2960
+ if (element === "section_candidate") {
2961
2961
  const text = span.text.toLowerCase();
2962
2962
  if (/endorsement/.test(text)) return "endorsement";
2963
2963
  if (/schedule|declarations?/.test(text)) return "schedule";
@@ -2993,6 +2993,90 @@ function makeNode(params) {
2993
2993
  metadata: params.metadata
2994
2994
  };
2995
2995
  }
2996
+ function metadataString(metadata, key) {
2997
+ const value = metadata?.[key];
2998
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
2999
+ }
3000
+ function isTitleContentNode(node) {
3001
+ if (node.kind !== "text") return false;
3002
+ return metadataString(node.metadata, "elementType") === "title" || metadataString(node.metadata, "sourceUnit") === "title";
3003
+ }
3004
+ function nodePageEnd(node) {
3005
+ return node.pageEnd ?? node.pageStart;
3006
+ }
3007
+ function groupPageContentByTitles(nodes) {
3008
+ const byParent = /* @__PURE__ */ new Map();
3009
+ for (const node of nodes) {
3010
+ const children = byParent.get(node.parentId) ?? [];
3011
+ children.push(node);
3012
+ byParent.set(node.parentId, children);
3013
+ }
3014
+ for (const children of byParent.values()) {
3015
+ children.sort((left, right) => left.order - right.order || left.id.localeCompare(right.id));
3016
+ }
3017
+ const byId = new Map(nodes.map((node) => [node.id, node]));
3018
+ for (const pageNode of nodes.filter((node) => node.kind === "page")) {
3019
+ const children = (byParent.get(pageNode.id) ?? []).filter((child) => child.kind !== "table_row" && child.kind !== "table_cell");
3020
+ let activeTitle;
3021
+ let activeContent = [];
3022
+ const flush = () => {
3023
+ if (!activeTitle || activeContent.length === 0) {
3024
+ activeTitle = void 0;
3025
+ activeContent = [];
3026
+ return;
3027
+ }
3028
+ const contentNodes = activeContent.map((node) => byId.get(node.id) ?? node).filter((node) => node.parentId === pageNode.id);
3029
+ if (contentNodes.length === 0) {
3030
+ activeTitle = void 0;
3031
+ activeContent = [];
3032
+ return;
3033
+ }
3034
+ const evidenceNodes = [activeTitle, ...contentNodes];
3035
+ const title = truncate(activeTitle.textExcerpt ?? activeTitle.title, 120);
3036
+ const pageStarts = evidenceNodes.map((node) => node.pageStart).filter((page) => typeof page === "number");
3037
+ const pageEnds = evidenceNodes.map(nodePageEnd).filter((page) => typeof page === "number");
3038
+ const sourceSpanIds = [...new Set(evidenceNodes.flatMap((node) => node.sourceSpanIds))];
3039
+ const bbox = evidenceNodes.flatMap((node) => node.bbox ?? []).slice(0, 12);
3040
+ byId.set(activeTitle.id, {
3041
+ ...activeTitle,
3042
+ kind: "section",
3043
+ title,
3044
+ description: nodeTextDescription({
3045
+ kind: "section",
3046
+ title,
3047
+ text: evidenceNodes.map((node) => node.textExcerpt).filter(Boolean).join("\n\n"),
3048
+ page: activeTitle.pageStart
3049
+ }),
3050
+ textExcerpt: evidenceNodes.map((node) => node.textExcerpt).filter(Boolean).join("\n\n").slice(0, 1600),
3051
+ sourceSpanIds,
3052
+ pageStart: pageStarts.length ? Math.min(...pageStarts) : activeTitle.pageStart,
3053
+ pageEnd: pageEnds.length ? Math.max(...pageEnds) : activeTitle.pageEnd,
3054
+ bbox,
3055
+ metadata: {
3056
+ ...activeTitle.metadata,
3057
+ sourceTreeVersion: "v3",
3058
+ organizer: "title_block"
3059
+ }
3060
+ });
3061
+ for (const node of contentNodes) {
3062
+ byId.set(node.id, { ...node, parentId: activeTitle.id });
3063
+ }
3064
+ activeTitle = void 0;
3065
+ activeContent = [];
3066
+ };
3067
+ for (const child of children) {
3068
+ if (isTitleContentNode(child)) {
3069
+ flush();
3070
+ activeTitle = child;
3071
+ activeContent = [];
3072
+ continue;
3073
+ }
3074
+ if (activeTitle) activeContent.push(child);
3075
+ }
3076
+ flush();
3077
+ }
3078
+ return nodes.map((node) => byId.get(node.id) ?? node);
3079
+ }
2996
3080
  function sortSpans(left, right) {
2997
3081
  const leftPage = pageStart(left) ?? 0;
2998
3082
  const rightPage = pageStart(right) ?? 0;
@@ -3017,14 +3101,24 @@ function normalizeDocumentSourceTreePaths(nodes) {
3017
3101
  group.sort((left, right) => left.order - right.order || left.id.localeCompare(right.id));
3018
3102
  }
3019
3103
  const result = [];
3020
- const visit = (node, path) => {
3021
- const next = { ...node, path };
3104
+ const visited = /* @__PURE__ */ new Set();
3105
+ const visit = (node, path, ancestors, parentId) => {
3106
+ if (visited.has(node.id) || ancestors.has(node.id)) return;
3107
+ visited.add(node.id);
3108
+ const next = { ...node, parentId, path };
3022
3109
  result.push(next);
3023
3110
  const children = byParent.get(node.id) ?? [];
3024
- children.forEach((child, index) => visit(child, `${path}.${index + 1}`));
3111
+ const nextAncestors = new Set(ancestors);
3112
+ nextAncestors.add(node.id);
3113
+ children.forEach((child, index) => visit(child, `${path}.${index + 1}`, nextAncestors, node.id));
3025
3114
  };
3026
3115
  const roots = byParent.get(void 0) ?? [];
3027
- roots.forEach((root, index) => visit(root, String(index + 1)));
3116
+ roots.forEach((root, index) => visit(root, String(index + 1), /* @__PURE__ */ new Set(), void 0));
3117
+ for (const node of nodes) {
3118
+ if (!visited.has(node.id)) {
3119
+ visit(node, String(result.length + 1), /* @__PURE__ */ new Set(), void 0);
3120
+ }
3121
+ }
3028
3122
  return result;
3029
3123
  }
3030
3124
  function buildDocumentSourceTree(sourceSpans, documentId) {
@@ -3158,7 +3252,7 @@ function buildDocumentSourceTree(sourceSpans, documentId) {
3158
3252
  }
3159
3253
  addStandaloneSpanNode(span, pageParentId);
3160
3254
  }
3161
- return normalizeDocumentSourceTreePaths([...nodes.values()]);
3255
+ return normalizeDocumentSourceTreePaths(groupPageContentByTitles([...nodes.values()]));
3162
3256
  }
3163
3257
 
3164
3258
  // src/source/operational-profile.ts
@@ -9346,7 +9440,9 @@ var ORGANIZABLE_KINDS = [
9346
9440
  "clause"
9347
9441
  ];
9348
9442
  var ORGANIZATION_TOP_LEVEL_BATCH_SIZE = 80;
9349
- var ORGANIZATION_CHILD_CONTEXT_LIMIT = 4;
9443
+ var ORGANIZER_MAX_SOURCE_SPANS = 400;
9444
+ var ORGANIZER_MAX_TOP_LEVEL_NODES = 18;
9445
+ var OUTLINE_CLEANUP_MAX_TOP_LEVEL_NODES = 80;
9350
9446
  var SourceTreeOrganizationSchema = import_zod41.z.object({
9351
9447
  labels: import_zod41.z.array(import_zod41.z.object({
9352
9448
  nodeId: import_zod41.z.string(),
@@ -9463,6 +9559,22 @@ function endorsementDescription(title, node) {
9463
9559
  title
9464
9560
  );
9465
9561
  }
9562
+ function nodePageEnd2(node) {
9563
+ return node.pageEnd ?? node.pageStart;
9564
+ }
9565
+ function pageRangeForNodes(nodes) {
9566
+ const pageStarts = nodes.map((node) => node.pageStart).filter((page) => typeof page === "number");
9567
+ const pageEnds = nodes.map(nodePageEnd2).filter((page) => typeof page === "number");
9568
+ if (pageStarts.length === 0 || pageEnds.length === 0) return void 0;
9569
+ const start = Math.min(...pageStarts);
9570
+ const end = Math.max(...pageEnds);
9571
+ return start === end ? `page ${start}` : `pages ${start}-${end}`;
9572
+ }
9573
+ function descriptionWithPages(description, nodes) {
9574
+ const range = pageRangeForNodes(nodes);
9575
+ if (!range || new RegExp(`\\b${range.replace("-", "\\-")}\\b`, "i").test(description)) return description;
9576
+ return `${description}; ${range}`;
9577
+ }
9466
9578
  function semanticGroupNodeId(documentId, kind, title, childNodeIds) {
9467
9579
  return [
9468
9580
  documentId.replace(/[^a-zA-Z0-9_.:-]/g, "_"),
@@ -9473,7 +9585,12 @@ function semanticGroupNodeId(documentId, kind, title, childNodeIds) {
9473
9585
  ].join(":");
9474
9586
  }
9475
9587
  function looksLikeDeclarationsStart(node) {
9476
- return /\bdeclarations?\s+(page|schedule)\b/i.test(sourceNodeText(node));
9588
+ const title = cleanText(node.title, "");
9589
+ const text = sourceNodeText(node);
9590
+ if (/\b(important notice|privacy notice|ofac advisory|terrorism risk insurance act|how to report a claim)\b/i.test(text)) {
9591
+ return false;
9592
+ }
9593
+ return /^declarations?$/i.test(title) || /\bdeclarations?\s+(page|schedule|section)\b/i.test(text) || /^declarations?\b/i.test(cleanText(node.textExcerpt, ""));
9477
9594
  }
9478
9595
  function looksLikeDeclarationsContinuation(node) {
9479
9596
  const text = sourceNodeText(node);
@@ -9481,7 +9598,8 @@ function looksLikeDeclarationsContinuation(node) {
9481
9598
  }
9482
9599
  function looksLikePolicyFormStart(node) {
9483
9600
  const text = sourceNodeText(node);
9484
- 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);
9601
+ const excerpt = cleanText(node.textExcerpt, "");
9602
+ 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);
9485
9603
  }
9486
9604
  function looksLikePolicyFormContinuation(node) {
9487
9605
  const text = sourceNodeText(node);
@@ -9507,7 +9625,7 @@ function groupAdjacentChildren(params) {
9507
9625
  parentId,
9508
9626
  kind: params.kind,
9509
9627
  title: params.title,
9510
- description: params.description,
9628
+ description: descriptionWithPages(params.description, children),
9511
9629
  textExcerpt: children.map((child) => child.textExcerpt ?? child.description).filter(Boolean).join("\n\n").slice(0, 1600),
9512
9630
  sourceSpanIds,
9513
9631
  pageStart: pageStarts.length ? Math.min(...pageStarts) : void 0,
@@ -9560,6 +9678,22 @@ function applySemanticPageGrouping(sourceTree) {
9560
9678
  const children = (nodesByParent(relabeled).get(rootId) ?? []).filter((node) => node.kind !== "document").sort((left, right) => left.order - right.order);
9561
9679
  let nextTree = relabeled;
9562
9680
  const declarationsStartIndex = children.findIndex(looksLikeDeclarationsStart);
9681
+ const firstCoreIndex = children.findIndex(
9682
+ (child) => looksLikeDeclarationsStart(child) || looksLikePolicyFormStart(child) || looksLikeEndorsementStart(child)
9683
+ );
9684
+ const frontMatterBoundary = declarationsStartIndex >= 0 ? declarationsStartIndex : firstCoreIndex;
9685
+ if (frontMatterBoundary > 0) {
9686
+ const frontMatterIds = children.slice(0, frontMatterBoundary).map((child) => child.id);
9687
+ nextTree = groupAdjacentChildren({
9688
+ sourceTree: nextTree,
9689
+ children,
9690
+ childIds: frontMatterIds,
9691
+ kind: "page_group",
9692
+ title: "Notices and Jacket",
9693
+ description: "Policy jacket, notices, and administrative pages grouped by source order",
9694
+ organizer: "semantic_front_matter_grouping"
9695
+ });
9696
+ }
9563
9697
  if (declarationsStartIndex >= 0) {
9564
9698
  const declarationIds = [];
9565
9699
  for (let index = declarationsStartIndex; index < children.length; index += 1) {
@@ -9728,17 +9862,19 @@ function normalizeContainerEvidenceFromChildren(sourceTree) {
9728
9862
  return sourceTree.map((node) => byId.get(node.id) ?? node);
9729
9863
  }
9730
9864
  function normalizeSemanticHierarchy(sourceTree) {
9731
- return normalizeDocumentSourceTreePaths(
9732
- normalizeContainerEvidenceFromChildren(
9733
- nestEndorsementContinuationPages(
9734
- normalizePolicyFormStructure(
9735
- normalizeDocumentSourceTreePaths(sourceTree)
9736
- )
9737
- )
9865
+ const normalized = normalizeDocumentSourceTreePaths(
9866
+ normalizePolicyFormStructure(
9867
+ normalizeDocumentSourceTreePaths(sourceTree)
9738
9868
  )
9739
9869
  );
9870
+ const nested = normalizeDocumentSourceTreePaths(nestEndorsementContinuationPages(normalized));
9871
+ const withEvidence = normalizeContainerEvidenceFromChildren(nested);
9872
+ return normalizeDocumentSourceTreePaths(
9873
+ normalizeContainerEvidenceFromChildren(withEvidence)
9874
+ );
9740
9875
  }
9741
9876
  function applyEndorsementGrouping(sourceTree) {
9877
+ const rootId = sourceTreeRootId(sourceTree);
9742
9878
  const relabeledTree = sourceTree.map((node) => {
9743
9879
  if (node.kind === "document" || isEndorsementGroup(node)) return node;
9744
9880
  const title = endorsementStartTitle(node);
@@ -9776,7 +9912,7 @@ function applyEndorsementGrouping(sourceTree) {
9776
9912
  ...node,
9777
9913
  kind: "page_group",
9778
9914
  title: "Endorsements",
9779
- description: cleanText(node.description, "Endorsement forms grouped by source order"),
9915
+ description: descriptionWithPages(cleanText(node.description, "Endorsement forms grouped by source order"), byParent.get(node.id) ?? [node]),
9780
9916
  metadata: {
9781
9917
  ...node.metadata,
9782
9918
  sourceTreeVersion: "v3",
@@ -9803,9 +9939,10 @@ function applyEndorsementGrouping(sourceTree) {
9803
9939
  };
9804
9940
  });
9805
9941
  for (const [parentId, children] of byParent) {
9942
+ if (parentId !== rootId) continue;
9806
9943
  if (endorsementGroupIds.has(parentId ?? "")) continue;
9807
9944
  const endorsementChildren = children.filter((child) => child.kind === "endorsement" && !isEndorsementGroup(child));
9808
- if (endorsementChildren.length < 2) continue;
9945
+ if (endorsementChildren.length < 1) continue;
9809
9946
  const endorsementGroupChildren = [];
9810
9947
  let hasSeenEndorsementStart = false;
9811
9948
  for (const child of children) {
@@ -9818,6 +9955,7 @@ function applyEndorsementGrouping(sourceTree) {
9818
9955
  endorsementGroupChildren.push(child);
9819
9956
  }
9820
9957
  }
9958
+ if (endorsementGroupChildren.length < 1) continue;
9821
9959
  const documentId = endorsementChildren[0].documentId;
9822
9960
  const pageStarts = endorsementGroupChildren.map((child) => child.pageStart).filter((page) => typeof page === "number");
9823
9961
  const pageEnds = endorsementGroupChildren.map((child) => child.pageEnd ?? child.pageStart).filter((page) => typeof page === "number");
@@ -9830,7 +9968,7 @@ function applyEndorsementGrouping(sourceTree) {
9830
9968
  parentId,
9831
9969
  kind: "page_group",
9832
9970
  title: "Endorsements",
9833
- description: "Endorsement forms grouped by source order",
9971
+ description: descriptionWithPages("Endorsement forms grouped by source order", endorsementGroupChildren),
9834
9972
  textExcerpt: void 0,
9835
9973
  sourceSpanIds: [],
9836
9974
  pageStart: pageStarts.length ? Math.min(...pageStarts) : void 0,
@@ -9841,11 +9979,13 @@ function applyEndorsementGrouping(sourceTree) {
9841
9979
  metadata: { sourceTreeVersion: "v3", organizer: "endorsement_grouping" }
9842
9980
  };
9843
9981
  const childSpanIds = [...new Set(endorsementGroupChildren.flatMap((child) => child.sourceSpanIds))];
9982
+ const childPageStart = pageStarts.length ? Math.min(...pageStarts) : void 0;
9983
+ const childPageEnd = pageEnds.length ? Math.max(...pageEnds) : void 0;
9844
9984
  const normalizedGroup = {
9845
9985
  ...groupNode,
9846
9986
  sourceSpanIds: groupNode.sourceSpanIds.length ? groupNode.sourceSpanIds : childSpanIds,
9847
- pageStart: groupNode.pageStart ?? (pageStarts.length ? Math.min(...pageStarts) : void 0),
9848
- pageEnd: groupNode.pageEnd ?? (pageEnds.length ? Math.max(...pageEnds) : void 0),
9987
+ pageStart: childPageStart === void 0 ? groupNode.pageStart : groupNode.pageStart === void 0 ? childPageStart : Math.min(groupNode.pageStart, childPageStart),
9988
+ pageEnd: childPageEnd === void 0 ? groupNode.pageEnd : groupNode.pageEnd === void 0 ? childPageEnd : Math.max(groupNode.pageEnd, childPageEnd),
9849
9989
  order
9850
9990
  };
9851
9991
  groupsByParent.set(parentId, normalizedGroup);
@@ -9885,6 +10025,24 @@ function nodesByParent(sourceTree) {
9885
10025
  function sourceTreeRootId(sourceTree) {
9886
10026
  return sourceTree.find((node) => node.kind === "document")?.id;
9887
10027
  }
10028
+ function rootChildren(sourceTree) {
10029
+ const byParent = nodesByParent(sourceTree);
10030
+ const rootId = sourceTreeRootId(sourceTree);
10031
+ return (byParent.get(rootId) ?? []).filter((node) => node.kind !== "document");
10032
+ }
10033
+ function hasDeterministicSemanticOutline(sourceTree) {
10034
+ const children = rootChildren(sourceTree);
10035
+ const semanticCount = children.filter(
10036
+ (node) => node.kind === "page_group" || node.kind === "form" || node.kind === "endorsement" || node.kind === "section" || node.kind === "schedule"
10037
+ ).length;
10038
+ return semanticCount >= 2 || children.some((node) => node.title === "Declarations") || children.some((node) => node.title === "Policy Form") || children.some((node) => node.title === "Endorsements");
10039
+ }
10040
+ function shouldRunSourceTreeOrganizer(sourceTree, sourceSpans) {
10041
+ const topLevelCount = rootChildren(sourceTree).length;
10042
+ if (sourceSpans.length > ORGANIZER_MAX_SOURCE_SPANS) return false;
10043
+ if (topLevelCount > ORGANIZER_MAX_TOP_LEVEL_NODES) return false;
10044
+ return !hasDeterministicSemanticOutline(sourceTree);
10045
+ }
9888
10046
  function organizationBatches(sourceTree) {
9889
10047
  const byParent = nodesByParent(sourceTree);
9890
10048
  const rootId = sourceTreeRootId(sourceTree);
@@ -9903,10 +10061,6 @@ function organizationBatches(sourceTree) {
9903
10061
  const candidates = /* @__PURE__ */ new Map();
9904
10062
  for (const node of topLevelBatch) {
9905
10063
  candidates.set(node.id, node);
9906
- const childContext = (byParent.get(node.id) ?? []).filter((child) => child.kind !== "text" && child.kind !== "table_cell").slice(0, ORGANIZATION_CHILD_CONTEXT_LIMIT);
9907
- for (const child of childContext) {
9908
- candidates.set(child.id, child);
9909
- }
9910
10064
  }
9911
10065
  batches.push({
9912
10066
  label: `top-level nodes ${index + 1}-${index + topLevelBatch.length} of ${topLevelNodes.length}`,
@@ -9957,6 +10111,41 @@ Rules:
9957
10111
  Source nodes:
9958
10112
  ${JSON.stringify(nodes, null, 2)}
9959
10113
 
10114
+ Return JSON with labels and groups only.`;
10115
+ }
10116
+ function shouldRunOutlineCleanup(sourceTree) {
10117
+ const topLevel = rootChildren(sourceTree);
10118
+ if (topLevel.length === 0 || topLevel.length > OUTLINE_CLEANUP_MAX_TOP_LEVEL_NODES) return false;
10119
+ const genericPages = topLevel.filter((node) => node.kind === "page" && /^Page\s+\d+$/i.test(node.title));
10120
+ const hasDeclarations = topLevel.some((node) => node.title === "Declarations");
10121
+ const hasPolicyForm = topLevel.some((node) => node.title === "Policy Form");
10122
+ const hasEndorsements = topLevel.some((node) => node.title === "Endorsements");
10123
+ return genericPages.length > 0 || topLevel.length > 6 || !hasDeclarations || !hasPolicyForm || !hasEndorsements;
10124
+ }
10125
+ function buildOutlineCleanupPrompt(sourceTree) {
10126
+ const topLevel = rootChildren(sourceTree).slice(0, OUTLINE_CLEANUP_MAX_TOP_LEVEL_NODES);
10127
+ const nodes = topLevel.map((node) => compactNode(node, 900));
10128
+ return `You clean a top-level source outline for an insurance policy.
10129
+
10130
+ Expected product-facing order:
10131
+ 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.
10132
+ 2. Declarations: declarations page(s), schedules, named insured/policy period/premium rows, coverage limit schedules, forms-and-endorsements schedules.
10133
+ 3. Policy Form: the main policy wording, insuring agreements, definitions, exclusions, conditions, claim provisions, and general policy terms.
10134
+ 4. Endorsements: one generic "Endorsements" page_group containing each separately numbered endorsement as its own child.
10135
+
10136
+ Rules:
10137
+ - Use only node IDs from this top-level list: ${JSON.stringify(topLevel.map((node) => node.id))}
10138
+ - Group only adjacent top-level nodes.
10139
+ - Do not invent text, pages, source spans, limits, or policy facts.
10140
+ - Do not merge individually numbered endorsements into one endorsement node; use the generic "Endorsements" parent for the series.
10141
+ - Use canonical terse titles: "Notices and Jacket", "Declarations", "Policy Form", "Endorsements", or the printed endorsement number.
10142
+ - Keep page_group descriptions short and include the page range when pages are known, for example "Declarations pages 6-8".
10143
+ - 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.
10144
+ - If the existing deterministic outline is already correct, return empty labels and groups.
10145
+
10146
+ Top-level source nodes:
10147
+ ${JSON.stringify(nodes, null, 2)}
10148
+
9960
10149
  Return JSON with labels and groups only.`;
9961
10150
  }
9962
10151
  function buildOperationalProfilePrompt(sourceTree, fallback) {
@@ -10011,7 +10200,7 @@ function applyOrganization(sourceTree, organization) {
10011
10200
  if (!children.every((child) => child.parentId === parentId)) continue;
10012
10201
  const documentId = children[0].documentId;
10013
10202
  const title = simplifyOrganizerTitle(group.title, group.title, group.kind);
10014
- const description = cleanText(group.description, title);
10203
+ const description = descriptionWithPages(cleanText(group.description, title), children);
10015
10204
  const id = groupNodeId(documentId, { ...group, title });
10016
10205
  if (byId.has(id)) continue;
10017
10206
  const sourceSpanIds = [...new Set(children.flatMap((child) => child.sourceSpanIds))];
@@ -10166,7 +10355,7 @@ function materializeDocument(params) {
10166
10355
  }
10167
10356
  async function runSourceTreeExtraction(params) {
10168
10357
  const sourceSpans = normalizeSourceSpans(params.sourceSpans);
10169
- let sourceTree = buildDocumentSourceTree(sourceSpans, params.id);
10358
+ let sourceTree = applySemanticPageGrouping(buildDocumentSourceTree(sourceSpans, params.id));
10170
10359
  const warnings = [];
10171
10360
  let modelCalls = 0;
10172
10361
  let callsWithUsage = 0;
@@ -10188,21 +10377,55 @@ async function runSourceTreeExtraction(params) {
10188
10377
  }
10189
10378
  params.trackUsage(usage, report);
10190
10379
  };
10191
- try {
10192
- const organizations = [];
10193
- const batches = organizationBatches(sourceTree);
10194
- for (const [batchIndex, batch] of batches.entries()) {
10195
- const budget = params.resolveBudget("extraction_source_tree", 4096);
10380
+ if (shouldRunSourceTreeOrganizer(sourceTree, sourceSpans)) {
10381
+ try {
10382
+ const organizations = [];
10383
+ const batches = organizationBatches(sourceTree);
10384
+ for (const [batchIndex, batch] of batches.entries()) {
10385
+ const budget = params.resolveBudget("extraction_source_tree", 4096);
10386
+ const startedAt = Date.now();
10387
+ const response = await safeGenerateObject(
10388
+ params.generateObject,
10389
+ {
10390
+ prompt: buildOrganizationPrompt(batch),
10391
+ schema: SourceTreeOrganizationSchema,
10392
+ maxTokens: budget.maxTokens,
10393
+ taskKind: "extraction_source_tree",
10394
+ budgetDiagnostics: budget
10395
+ },
10396
+ {
10397
+ fallback: { labels: [], groups: [] },
10398
+ log: params.log
10399
+ }
10400
+ );
10401
+ localTrack(response.usage, {
10402
+ taskKind: "extraction_source_tree",
10403
+ label: batches.length > 1 ? `source_tree_organizer_${batchIndex + 1}` : "source_tree_organizer",
10404
+ maxTokens: budget.maxTokens,
10405
+ durationMs: Date.now() - startedAt
10406
+ });
10407
+ organizations.push(response.object);
10408
+ }
10409
+ sourceTree = applyOrganization(sourceTree, mergeOrganizationResults(organizations));
10410
+ } catch (error) {
10411
+ warnings.push(`Source-tree organizer failed; deterministic tree used (${error instanceof Error ? error.message : String(error)})`);
10412
+ }
10413
+ } else {
10414
+ await params.log?.("Deterministic source tree ready; skipped model organizer");
10415
+ }
10416
+ if (shouldRunOutlineCleanup(sourceTree)) {
10417
+ try {
10418
+ const budget = params.resolveBudget("extraction_source_tree", 1600);
10419
+ const maxTokens = Math.min(budget.maxTokens, 1600);
10196
10420
  const startedAt = Date.now();
10197
10421
  const response = await safeGenerateObject(
10198
10422
  params.generateObject,
10199
10423
  {
10200
- prompt: buildOrganizationPrompt(batch),
10424
+ prompt: buildOutlineCleanupPrompt(sourceTree),
10201
10425
  schema: SourceTreeOrganizationSchema,
10202
- maxTokens: budget.maxTokens,
10426
+ maxTokens,
10203
10427
  taskKind: "extraction_source_tree",
10204
- budgetDiagnostics: budget,
10205
- providerOptions: { ...params.providerOptions, sourceSpans }
10428
+ budgetDiagnostics: { ...budget, maxTokens }
10206
10429
  },
10207
10430
  {
10208
10431
  fallback: { labels: [], groups: [] },
@@ -10211,17 +10434,15 @@ async function runSourceTreeExtraction(params) {
10211
10434
  );
10212
10435
  localTrack(response.usage, {
10213
10436
  taskKind: "extraction_source_tree",
10214
- label: batches.length > 1 ? `source_tree_organizer_${batchIndex + 1}` : "source_tree_organizer",
10215
- maxTokens: budget.maxTokens,
10437
+ label: "source_tree_outline_cleanup",
10438
+ maxTokens,
10216
10439
  durationMs: Date.now() - startedAt
10217
10440
  });
10218
- organizations.push(response.object);
10441
+ sourceTree = applyOrganization(sourceTree, response.object);
10442
+ } catch (error) {
10443
+ warnings.push(`Source-tree outline cleanup failed; deterministic tree used (${error instanceof Error ? error.message : String(error)})`);
10219
10444
  }
10220
- sourceTree = applyOrganization(sourceTree, mergeOrganizationResults(organizations));
10221
- } catch (error) {
10222
- warnings.push(`Source-tree organizer failed; deterministic tree used (${error instanceof Error ? error.message : String(error)})`);
10223
10445
  }
10224
- sourceTree = applySemanticPageGrouping(sourceTree);
10225
10446
  const deterministicProfile = buildDeterministicOperationalProfile({
10226
10447
  sourceTree,
10227
10448
  sourceSpans