@duckcodeailabs/dql-agent 1.6.31 → 1.6.33

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.
Files changed (160) hide show
  1. package/dist/agent-run-engine.d.ts +42 -6
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +133 -79
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/agent-run-gates.d.ts.map +1 -1
  6. package/dist/agent-run-gates.js +72 -5
  7. package/dist/agent-run-gates.js.map +1 -1
  8. package/dist/agent-run-planner.d.ts.map +1 -1
  9. package/dist/agent-run-planner.js +13 -0
  10. package/dist/agent-run-planner.js.map +1 -1
  11. package/dist/answer-loop.d.ts +110 -8
  12. package/dist/answer-loop.d.ts.map +1 -1
  13. package/dist/answer-loop.js +1679 -1299
  14. package/dist/answer-loop.js.map +1 -1
  15. package/dist/answer-shape.d.ts +16 -0
  16. package/dist/answer-shape.d.ts.map +1 -0
  17. package/dist/answer-shape.js +85 -0
  18. package/dist/answer-shape.js.map +1 -0
  19. package/dist/cascade/budgets.d.ts +101 -0
  20. package/dist/cascade/budgets.d.ts.map +1 -0
  21. package/dist/cascade/budgets.js +184 -0
  22. package/dist/cascade/budgets.js.map +1 -0
  23. package/dist/cascade/cascade.d.ts +86 -0
  24. package/dist/cascade/cascade.d.ts.map +1 -0
  25. package/dist/cascade/cascade.js +115 -0
  26. package/dist/cascade/cascade.js.map +1 -0
  27. package/dist/cascade/route-policy.d.ts +14 -0
  28. package/dist/cascade/route-policy.d.ts.map +1 -0
  29. package/dist/cascade/route-policy.js +48 -0
  30. package/dist/cascade/route-policy.js.map +1 -0
  31. package/dist/cascade/triage.d.ts +20 -0
  32. package/dist/cascade/triage.d.ts.map +1 -0
  33. package/dist/cascade/triage.js +25 -0
  34. package/dist/cascade/triage.js.map +1 -0
  35. package/dist/conversation/rolling-summary.d.ts +13 -0
  36. package/dist/conversation/rolling-summary.d.ts.map +1 -0
  37. package/dist/conversation/rolling-summary.js +43 -0
  38. package/dist/conversation/rolling-summary.js.map +1 -0
  39. package/dist/conversation/session-store.d.ts +110 -0
  40. package/dist/conversation/session-store.d.ts.map +1 -0
  41. package/dist/conversation/session-store.js +387 -0
  42. package/dist/conversation/session-store.js.map +1 -0
  43. package/dist/conversation/snapshot.d.ts +59 -0
  44. package/dist/conversation/snapshot.d.ts.map +1 -0
  45. package/dist/conversation/snapshot.js +153 -0
  46. package/dist/conversation/snapshot.js.map +1 -0
  47. package/dist/conversation/working-state.d.ts +49 -0
  48. package/dist/conversation/working-state.d.ts.map +1 -0
  49. package/dist/conversation/working-state.js +249 -0
  50. package/dist/conversation/working-state.js.map +1 -0
  51. package/dist/embeddings/provider.d.ts +62 -0
  52. package/dist/embeddings/provider.d.ts.map +1 -1
  53. package/dist/embeddings/provider.js +98 -0
  54. package/dist/embeddings/provider.js.map +1 -1
  55. package/dist/grounding/context-ledger.d.ts +30 -0
  56. package/dist/grounding/context-ledger.d.ts.map +1 -0
  57. package/dist/grounding/context-ledger.js +54 -0
  58. package/dist/grounding/context-ledger.js.map +1 -0
  59. package/dist/grounding/regrounding.d.ts +24 -0
  60. package/dist/grounding/regrounding.d.ts.map +1 -0
  61. package/dist/grounding/regrounding.js +219 -0
  62. package/dist/grounding/regrounding.js.map +1 -0
  63. package/dist/grounding/value-index.d.ts +32 -0
  64. package/dist/grounding/value-index.d.ts.map +1 -0
  65. package/dist/grounding/value-index.js +67 -0
  66. package/dist/grounding/value-index.js.map +1 -0
  67. package/dist/index.d.ts +45 -14
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +102 -11
  70. package/dist/index.js.map +1 -1
  71. package/dist/intent-controller.d.ts +7 -0
  72. package/dist/intent-controller.d.ts.map +1 -1
  73. package/dist/intent-controller.js +22 -1
  74. package/dist/intent-controller.js.map +1 -1
  75. package/dist/kg/build.d.ts.map +1 -1
  76. package/dist/kg/build.js +44 -17
  77. package/dist/kg/build.js.map +1 -1
  78. package/dist/kg/sqlite-fts.d.ts +3 -1
  79. package/dist/kg/sqlite-fts.d.ts.map +1 -1
  80. package/dist/kg/sqlite-fts.js +16 -2
  81. package/dist/kg/sqlite-fts.js.map +1 -1
  82. package/dist/kg/types.d.ts +23 -1
  83. package/dist/kg/types.d.ts.map +1 -1
  84. package/dist/memory/fts-query.d.ts +7 -0
  85. package/dist/memory/fts-query.d.ts.map +1 -0
  86. package/dist/memory/fts-query.js +36 -0
  87. package/dist/memory/fts-query.js.map +1 -0
  88. package/dist/memory/sqlite-memory.d.ts.map +1 -1
  89. package/dist/memory/sqlite-memory.js +1 -30
  90. package/dist/memory/sqlite-memory.js.map +1 -1
  91. package/dist/metadata/analysis-planner.d.ts +23 -0
  92. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  93. package/dist/metadata/analysis-planner.js +432 -2
  94. package/dist/metadata/analysis-planner.js.map +1 -1
  95. package/dist/metadata/block-fit.d.ts +24 -0
  96. package/dist/metadata/block-fit.d.ts.map +1 -0
  97. package/dist/metadata/block-fit.js +373 -0
  98. package/dist/metadata/block-fit.js.map +1 -0
  99. package/dist/metadata/catalog.d.ts +72 -3
  100. package/dist/metadata/catalog.d.ts.map +1 -1
  101. package/dist/metadata/catalog.js +1452 -72
  102. package/dist/metadata/catalog.js.map +1 -1
  103. package/dist/metadata/drafts.d.ts +25 -0
  104. package/dist/metadata/drafts.d.ts.map +1 -1
  105. package/dist/metadata/drafts.js +112 -2
  106. package/dist/metadata/drafts.js.map +1 -1
  107. package/dist/metadata/metric-match.d.ts +55 -6
  108. package/dist/metadata/metric-match.d.ts.map +1 -1
  109. package/dist/metadata/metric-match.js +158 -13
  110. package/dist/metadata/metric-match.js.map +1 -1
  111. package/dist/metadata/sql-context-validation.d.ts +13 -1
  112. package/dist/metadata/sql-context-validation.d.ts.map +1 -1
  113. package/dist/metadata/sql-context-validation.js +172 -19
  114. package/dist/metadata/sql-context-validation.js.map +1 -1
  115. package/dist/metadata/sql-retrieval.d.ts +3 -2
  116. package/dist/metadata/sql-retrieval.d.ts.map +1 -1
  117. package/dist/metadata/sql-retrieval.js +6 -4
  118. package/dist/metadata/sql-retrieval.js.map +1 -1
  119. package/dist/providers/claude.d.ts +2 -1
  120. package/dist/providers/claude.d.ts.map +1 -1
  121. package/dist/providers/claude.js +95 -0
  122. package/dist/providers/claude.js.map +1 -1
  123. package/dist/providers/index.d.ts +1 -1
  124. package/dist/providers/index.d.ts.map +1 -1
  125. package/dist/providers/index.js.map +1 -1
  126. package/dist/providers/openai.d.ts +2 -1
  127. package/dist/providers/openai.d.ts.map +1 -1
  128. package/dist/providers/openai.js +143 -0
  129. package/dist/providers/openai.js.map +1 -1
  130. package/dist/providers/tool-output.d.ts +2 -0
  131. package/dist/providers/tool-output.d.ts.map +1 -0
  132. package/dist/providers/tool-output.js +22 -0
  133. package/dist/providers/tool-output.js.map +1 -0
  134. package/dist/providers/types.d.ts +23 -0
  135. package/dist/providers/types.d.ts.map +1 -1
  136. package/dist/providers/types.js.map +1 -1
  137. package/dist/semantic-bridge/compose.d.ts +89 -0
  138. package/dist/semantic-bridge/compose.d.ts.map +1 -0
  139. package/dist/semantic-bridge/compose.js +528 -0
  140. package/dist/semantic-bridge/compose.js.map +1 -0
  141. package/dist/semantic-bridge/member-select.d.ts +23 -0
  142. package/dist/semantic-bridge/member-select.d.ts.map +1 -0
  143. package/dist/semantic-bridge/member-select.js +106 -0
  144. package/dist/semantic-bridge/member-select.js.map +1 -0
  145. package/dist/skills/defaults.d.ts.map +1 -1
  146. package/dist/skills/defaults.js +5 -0
  147. package/dist/skills/defaults.js.map +1 -1
  148. package/dist/synthesize.d.ts +26 -0
  149. package/dist/synthesize.d.ts.map +1 -1
  150. package/dist/synthesize.js +72 -3
  151. package/dist/synthesize.js.map +1 -1
  152. package/dist/tools/registry.d.ts +15 -0
  153. package/dist/tools/registry.d.ts.map +1 -0
  154. package/dist/tools/registry.js +692 -0
  155. package/dist/tools/registry.js.map +1 -0
  156. package/dist/trust/stamp.d.ts +26 -0
  157. package/dist/trust/stamp.d.ts.map +1 -0
  158. package/dist/trust/stamp.js +33 -0
  159. package/dist/trust/stamp.js.map +1 -0
  160. package/package.json +4 -4
@@ -26,7 +26,10 @@ export function buildAnalysisQuestionPlan(question, followUp) {
26
26
  const normalizedQuestion = normalizeSearchText(cleanQuestion);
27
27
  const lower = cleanQuestion.toLowerCase();
28
28
  const entities = extractEntities(cleanQuestion);
29
- const metricTerms = extractMetricTerms(cleanQuestion);
29
+ const metricTerms = uniqueStrings([
30
+ ...extractMetricTerms(cleanQuestion),
31
+ ...extractFollowUpMetricTerms(followUp, lower),
32
+ ]);
30
33
  const dimensionTerms = extractDimensionTerms(cleanQuestion);
31
34
  const filterTerms = extractFilterTerms(cleanQuestion, entities);
32
35
  const timeTerms = extractTimeTerms(cleanQuestion);
@@ -36,6 +39,15 @@ export function buildAnalysisQuestionPlan(question, followUp) {
36
39
  const shouldConsiderCertifiedExact = certifiedExactIsPlausible(mode, entities);
37
40
  const needsGeneratedSql = generatedSqlIsLikely(mode, shouldConsiderCertifiedExact);
38
41
  const needsResearchWorkspace = researchWorkspaceIsLikely(mode, lower);
42
+ const requestedShape = buildRequestedAnswerShape(cleanQuestion, {
43
+ lower,
44
+ mode,
45
+ metricTerms,
46
+ dimensionTerms,
47
+ filterTerms,
48
+ timeTerms,
49
+ followUp,
50
+ });
39
51
  const searchTerms = uniqueStrings([
40
52
  ...tokenize(cleanQuestion),
41
53
  ...entities.flatMap((entity) => tokenize(entity.text)),
@@ -72,6 +84,7 @@ export function buildAnalysisQuestionPlan(question, followUp) {
72
84
  needsResearchWorkspace,
73
85
  searchQueries,
74
86
  searchTerms,
87
+ requestedShape,
75
88
  confidence: planConfidence(mode, entities, metricTerms, dimensionTerms),
76
89
  reasons,
77
90
  };
@@ -209,6 +222,12 @@ export function scoreAllowedSqlRelationWithAnalysisPlan(relation, plan) {
209
222
  : 0;
210
223
  const usabilityBonus = generatedSqlUsabilityScore(relation, plan);
211
224
  const columnShapeBonus = relationColumnShapeScore(relation, plan);
225
+ const semanticColumnBonus = semanticColumnMapScore(relation, plan);
226
+ const incompleteSourceShapePenalty = relation.source.includes('certified source SQL') &&
227
+ sourceShapeMustCoverRequestedOutputs(plan) &&
228
+ semanticColumnBonus.missingRequired.length > 0
229
+ ? Math.min(42, semanticColumnBonus.missingRequired.length * 34)
230
+ : 0;
212
231
  const score = Number((search.score * 3 +
213
232
  metrics.score * 8 +
214
233
  dimensions.score * 6 +
@@ -217,6 +236,8 @@ export function scoreAllowedSqlRelationWithAnalysisPlan(relation, plan) {
217
236
  sourceBonus +
218
237
  usabilityBonus +
219
238
  columnShapeBonus +
239
+ semanticColumnBonus.score +
240
+ -incompleteSourceShapePenalty +
220
241
  Math.min(relation.columns.length, 24) * 0.2).toFixed(3));
221
242
  const reasons = [
222
243
  search.matched.length ? `analysis terms matched: ${search.matched.slice(0, 5).join(', ')}` : '',
@@ -228,6 +249,8 @@ export function scoreAllowedSqlRelationWithAnalysisPlan(relation, plan) {
228
249
  usabilityBonus > 0 ? 'relation has usable columns for generated SQL' : '',
229
250
  usabilityBonus < 0 ? 'relation has no inspected/projected columns for generated SQL' : '',
230
251
  columnShapeBonus > 0 ? 'columns match requested analytical shape' : '',
252
+ semanticColumnBonus.matched.length ? `semantic column map matched: ${semanticColumnBonus.matched.slice(0, 6).join(', ')}` : '',
253
+ incompleteSourceShapePenalty > 0 ? `certified source SQL shape is missing requested output(s): ${semanticColumnBonus.missingRequired.join(', ')}` : '',
231
254
  relation.columns.length > 0 ? `${relation.columns.length} inspected/projected columns` : '',
232
255
  ].filter(Boolean);
233
256
  return {
@@ -266,6 +289,152 @@ function relationColumnShapeScore(relation, plan) {
266
289
  }
267
290
  return score;
268
291
  }
292
+ function semanticColumnMapScore(relation, plan) {
293
+ if (relation.columns.length === 0)
294
+ return { score: 0, matched: [], missingRequired: [] };
295
+ const requiredConcepts = uniqueStrings(plan.requestedShape.requiredOutputs.map(canonicalColumnConcept).filter(Boolean));
296
+ const requestedOutputs = uniqueStrings([
297
+ ...requiredConcepts,
298
+ ...plan.requestedShape.dimensions,
299
+ ...plan.requestedShape.measures,
300
+ ...plan.dimensionTerms,
301
+ ...plan.metricTerms,
302
+ ].map(canonicalColumnConcept).filter(Boolean));
303
+ const matched = [];
304
+ const missingRequired = [];
305
+ let score = 0;
306
+ for (const concept of requestedOutputs) {
307
+ const best = bestColumnConceptScore(concept, relation.columns);
308
+ if (best.score <= 0) {
309
+ if (requiredConcepts.includes(concept)) {
310
+ missingRequired.push(concept);
311
+ score -= 12;
312
+ }
313
+ continue;
314
+ }
315
+ matched.push(`${concept}->${best.column}`);
316
+ score += best.score * (requiredConcepts.includes(concept) ? 1.35 : 1);
317
+ }
318
+ const capped = Math.max(-36, Math.min(score, plan.needsGeneratedSql ? 44 : 28));
319
+ return { score: capped, matched: uniqueStrings(matched), missingRequired: uniqueStrings(missingRequired) };
320
+ }
321
+ function sourceShapeMustCoverRequestedOutputs(plan) {
322
+ return plan.mode === 'ranking' ||
323
+ plan.mode === 'driver_breakdown' ||
324
+ plan.mode === 'comparison' ||
325
+ plan.mode === 'trend' ||
326
+ plan.mode === 'general_analysis';
327
+ }
328
+ function canonicalColumnConcept(value) {
329
+ return normalizeTerm(value)
330
+ .replace(/\bname\b$/i, '')
331
+ .replace(/\s+/g, ' ')
332
+ .trim();
333
+ }
334
+ function bestColumnConceptScore(concept, columns) {
335
+ let best = { score: 0, column: '' };
336
+ for (const column of columns) {
337
+ const score = columnConceptScore(concept, column);
338
+ if (score > best.score)
339
+ best = { score, column: column.name };
340
+ }
341
+ return best;
342
+ }
343
+ function columnConceptScore(concept, column) {
344
+ const normalizedConcept = normalizeTerm(concept.replace(/_name$/, ''));
345
+ if (!normalizedConcept)
346
+ return 0;
347
+ const columnName = normalizeSearchText(column.name);
348
+ const columnTokens = tokenize(column.name);
349
+ const columnText = normalizeSearchText([
350
+ column.name,
351
+ column.description ?? '',
352
+ ].join(' '));
353
+ const aliases = semanticColumnAliases(normalizedConcept);
354
+ let score = 0;
355
+ if (columnName === normalizedConcept || columnName === `${normalizedConcept} name`)
356
+ score += 12;
357
+ if (columnTokens.includes(normalizedConcept))
358
+ score += 9;
359
+ for (const alias of aliases) {
360
+ const aliasText = normalizeSearchText(alias);
361
+ const aliasTokens = tokenize(alias).filter((token) => !GENERIC_COLUMN_ALIAS_TOKENS.has(token));
362
+ if (!aliasText)
363
+ continue;
364
+ if (columnName === aliasText)
365
+ score += 11;
366
+ else if (columnText.includes(aliasText))
367
+ score += 7;
368
+ else if (aliasTokens.some((token) => columnTokens.includes(token)))
369
+ score += 5;
370
+ }
371
+ if (lineageAliasesIncludeConcept(column.description, normalizedConcept, aliases))
372
+ score += 12;
373
+ if (isCertifiedSourceShapeDescription(column.description))
374
+ score -= 16;
375
+ if (isMetricConcept(normalizedConcept) && isIdentifierLikeColumn(column.name))
376
+ score -= 8;
377
+ if (isDimensionConcept(normalizedConcept) && isMetricLikeColumn(column.name))
378
+ score -= 6;
379
+ return Math.max(0, score);
380
+ }
381
+ const GENERIC_COLUMN_ALIAS_TOKENS = new Set(['name', 'title', 'type', 'value', 'total']);
382
+ function lineageAliasesIncludeConcept(description, concept, aliases) {
383
+ const match = /governed aliases from lineage:\s*([^.]*)/i.exec(description ?? '');
384
+ if (!match?.[1])
385
+ return false;
386
+ const aliasText = normalizeSearchText(match[1]);
387
+ const candidates = [concept, ...aliases].map(normalizeSearchText).filter(Boolean);
388
+ return candidates.some((candidate) => aliasText === candidate ||
389
+ aliasText.split(/\s*,\s*/).some((alias) => normalizeSearchText(alias) === candidate));
390
+ }
391
+ function isCertifiedSourceShapeDescription(description) {
392
+ return /projected by certified source sql shape/i.test(description ?? '');
393
+ }
394
+ function semanticColumnAliases(concept) {
395
+ switch (concept) {
396
+ case 'revenue':
397
+ case 'sale':
398
+ case 'sales':
399
+ return ['revenue', 'sales', 'amount', 'gross amount', 'net amount', 'price', 'product price', 'order amount', 'total'];
400
+ case 'usage':
401
+ return ['usage', 'use', 'consumption', 'activity', 'events', 'sessions', 'volume'];
402
+ case 'order':
403
+ case 'orders':
404
+ return ['order count', 'orders', 'order total', 'count lifetime orders'];
405
+ case 'product':
406
+ return ['product', 'product name', 'product title', 'sku', 'item', 'item name'];
407
+ case 'category':
408
+ return ['category', 'category name', 'product category', 'product type', 'type', 'class'];
409
+ case 'customer':
410
+ return ['customer', 'customer name', 'buyer', 'client', 'account name', 'full name'];
411
+ case 'segment':
412
+ return ['segment', 'customer segment', 'market segment'];
413
+ case 'region':
414
+ case 'market':
415
+ return ['region', 'market', 'geo', 'location', 'country', 'state'];
416
+ case 'player':
417
+ return ['player', 'player name', 'athlete', 'athlete name'];
418
+ case 'month':
419
+ case 'week':
420
+ case 'year':
421
+ return [concept, `${concept} date`, `${concept} start`, 'date', 'period'];
422
+ default:
423
+ return [concept.replace(/_/g, ' ')];
424
+ }
425
+ }
426
+ function isMetricConcept(concept) {
427
+ return METRIC_WORDS.includes(concept) || ['sale', 'sales', 'order', 'orders'].includes(concept);
428
+ }
429
+ function isDimensionConcept(concept) {
430
+ return DIMENSION_WORDS.includes(concept);
431
+ }
432
+ function isIdentifierLikeColumn(column) {
433
+ return /\b(id|key|uuid|code|hash)\b/i.test(column.replace(/_/g, ' '));
434
+ }
435
+ function isMetricLikeColumn(column) {
436
+ return /\b(revenue|sales|amount|price|spend|cost|total|count|score|points?|quantity|value|rate|volume)\b/i.test(column.replace(/_/g, ' '));
437
+ }
269
438
  function inferQuestionMode(input) {
270
439
  const { lower } = input;
271
440
  if (followUpKind(input.followUp) === 'drilldown')
@@ -375,6 +544,12 @@ function extractMetricTerms(question) {
375
544
  terms.add('score');
376
545
  terms.add('scoring');
377
546
  }
547
+ if (/\b(how many|number of)\b/i.test(lower)) {
548
+ terms.add('count');
549
+ }
550
+ if (/\bhow much\b/i.test(lower)) {
551
+ terms.add('amount');
552
+ }
378
553
  for (const word of METRIC_WORDS) {
379
554
  if (new RegExp(`\\b${escapeRegExp(word)}s?\\b`, 'i').test(lower))
380
555
  terms.add(normalizeTerm(word));
@@ -388,8 +563,10 @@ function extractMetricTerms(question) {
388
563
  function extractDimensionTerms(question) {
389
564
  const lower = question.toLowerCase();
390
565
  const terms = new Set();
566
+ if (/\b(?:cusomers?|custmers?|costomers?|clients?|buyers?)\b/i.test(lower))
567
+ terms.add('customer');
391
568
  for (const word of DIMENSION_WORDS) {
392
- if (new RegExp(`\\b${escapeRegExp(word)}s?\\b`, 'i').test(lower))
569
+ if (new RegExp(`\\b(?:${escapeRegExp(word)}|${escapeRegExp(pluralizeDimensionWord(word))})\\b`, 'i').test(lower))
393
570
  terms.add(normalizeTerm(word));
394
571
  }
395
572
  for (const match of lower.matchAll(/\bby\s+([a-z][a-z0-9_ -]{1,40})/g)) {
@@ -399,7 +576,15 @@ function extractDimensionTerms(question) {
399
576
  }
400
577
  return uniqueStrings([...terms]).slice(0, 16);
401
578
  }
579
+ function pluralizeDimensionWord(word) {
580
+ if (word.endsWith('y'))
581
+ return `${word.slice(0, -1)}ies`;
582
+ return `${word}s`;
583
+ }
402
584
  function extractFilterTerms(question, entities) {
585
+ // Filter VALUES come from the question's own entities and time phrases only.
586
+ // (Real value binding happens later against the runtime value index / sampled
587
+ // column values — never from a hard-coded project-specific vocabulary.)
403
588
  return uniqueStrings([
404
589
  ...entities.flatMap((entity) => tokenize(entity.text)),
405
590
  ...Array.from(question.matchAll(/\b(?:last|this|next|previous|prior|current)\s+(day|week|month|quarter|year|season)\b/gi)).map((match) => match[0].toLowerCase()),
@@ -416,6 +601,249 @@ function extractTimeTerms(question) {
416
601
  }
417
602
  return uniqueStrings(terms).slice(0, 12);
418
603
  }
604
+ function extractFollowUpMetricTerms(followUp, lowerQuestion) {
605
+ if (!followUp || typeof followUp !== 'object' || Array.isArray(followUp))
606
+ return [];
607
+ const record = followUp;
608
+ // Advisory 'contextual' carry must not inject prior measures into a genuinely-new
609
+ // question's plan (it would bias retrieval and the block-fit gate toward the old
610
+ // topic). Carry prior measures only when the question textually refers back.
611
+ if (record.kind === 'contextual' && lowerQuestion !== undefined
612
+ && !/\b(these|those|that|them|this|same|prior|previous|above)\b/.test(lowerQuestion)) {
613
+ return [];
614
+ }
615
+ return uniqueStrings([
616
+ ...cleanStringArray(record.priorMeasures),
617
+ ...cleanStringArray(record.priorResultColumns).filter((column) => METRIC_WORDS.some((word) => new RegExp(`\\b${word}\\b`, 'i').test(column.replace(/_/g, ' ')))),
618
+ ]).slice(0, 8);
619
+ }
620
+ function cleanStringArray(value) {
621
+ if (!Array.isArray(value))
622
+ return [];
623
+ return value
624
+ .map((item) => typeof item === 'string' ? item.trim() : '')
625
+ .filter(Boolean);
626
+ }
627
+ function buildRequestedAnswerShape(question, input) {
628
+ const topN = parseTopN(question);
629
+ const followUpReferences = extractFollowUpReferences(question, input.followUp);
630
+ const rawDimensions = uniqueStrings([
631
+ ...input.dimensionTerms.map(canonicalShapeTerm),
632
+ ...followUpDimensionsForRequestedShape(input.followUp, followUpReferences),
633
+ ].filter(Boolean));
634
+ const dimensions = scalarValueQuestionUsesEntitiesAsMeasures(input.mode, input.lower)
635
+ ? []
636
+ : rawDimensions;
637
+ const measures = uniqueStrings(input.metricTerms.map(canonicalShapeTerm).filter(Boolean));
638
+ const requiredOutputs = extractRequiredOutputs(question, dimensions, measures);
639
+ const filters = uniqueStrings([
640
+ ...input.filterTerms,
641
+ ...followUpFiltersForRequestedShape(input.followUp, followUpReferences),
642
+ ]);
643
+ const ambiguities = /\bimpact(?:ed|s|ing)?\b/i.test(question)
644
+ ? [{
645
+ term: 'impacted',
646
+ defaultInterpretation: 'highest contribution to the requested metric',
647
+ requiresClarification: false,
648
+ }]
649
+ : [];
650
+ return {
651
+ grain: inferRequestedShapeGrain(dimensions, input.timeTerms),
652
+ dimensions,
653
+ measures,
654
+ requiredOutputs,
655
+ filters,
656
+ ...(topN ? { topN } : {}),
657
+ rankingDirection: rankingDirection(question),
658
+ followUpReferences,
659
+ ambiguities,
660
+ };
661
+ }
662
+ function scalarValueQuestionUsesEntitiesAsMeasures(mode, lower) {
663
+ if (mode !== 'exact_lookup' && mode !== 'general_analysis')
664
+ return false;
665
+ return /\b(how many|how much|what is|what was|total|count|number of)\b/.test(lower)
666
+ && !/\b(by|per|each|every|top|bottom|rank|ranking|list|which|who|break\s*down|breakdown|split|segment|trend|over time)\b/.test(lower);
667
+ }
668
+ function parseTopN(question) {
669
+ const lower = question.toLowerCase();
670
+ const numeric = lower.match(/\b(?:top|bottom|first|last)\s+(\d{1,3})\b/);
671
+ const n = numeric ? Number(numeric[1]) : wordNumberFromTopN(lower);
672
+ if (!n || n <= 0)
673
+ return undefined;
674
+ const scope = /\b(?:per|for each|within each)\s+\w+/i.test(lower)
675
+ || /\bby\s+(?:category|segment|region|channel|product|customer|account|user|month|week|year)\b/i.test(lower)
676
+ ? 'per_group'
677
+ : 'overall';
678
+ return { n, scope };
679
+ }
680
+ function wordNumberFromTopN(lower) {
681
+ const words = {
682
+ one: 1,
683
+ two: 2,
684
+ three: 3,
685
+ four: 4,
686
+ five: 5,
687
+ six: 6,
688
+ seven: 7,
689
+ eight: 8,
690
+ nine: 9,
691
+ ten: 10,
692
+ };
693
+ const match = lower.match(/\b(?:top|bottom|first|last)\s+(one|two|three|four|five|six|seven|eight|nine|ten)\b/);
694
+ return match ? words[match[1]] : undefined;
695
+ }
696
+ function extractRequiredOutputs(question, dimensions, measures) {
697
+ const lower = question.toLowerCase();
698
+ const outputs = new Set();
699
+ for (const dim of dimensions)
700
+ outputs.add(dim);
701
+ for (const measure of measures)
702
+ outputs.add(measure);
703
+ if (/\bproduct\s+name\b/i.test(lower))
704
+ outputs.add('product_name');
705
+ if (/\bcustomer\s+name\b/i.test(lower))
706
+ outputs.add('customer_name');
707
+ if (/\bcategory\s+name\b/i.test(lower))
708
+ outputs.add('category_name');
709
+ if (/\b(?:name|names)\b/i.test(lower)) {
710
+ if (dimensions.includes('product'))
711
+ outputs.add('product_name');
712
+ if (dimensions.includes('customer'))
713
+ outputs.add('customer_name');
714
+ if (dimensions.includes('category'))
715
+ outputs.add('category_name');
716
+ }
717
+ return uniqueStrings([...outputs]).slice(0, 24);
718
+ }
719
+ function extractFollowUpReferences(question, followUp) {
720
+ const refs = [];
721
+ const lower = question.toLowerCase();
722
+ const record = followUpRecord(followUp);
723
+ const hasFollowUp = Boolean(record);
724
+ for (const match of lower.matchAll(/\b(these|those|that|same|prior|previous)\s+([a-z][a-z0-9_ -]{1,30})\b/g)) {
725
+ const phrase = match[0];
726
+ const noun = canonicalShapeTerm(match[2]);
727
+ let kind = 'ambiguous';
728
+ if (/period|date|day|week|month|quarter|year|time/.test(noun))
729
+ kind = 'prior_timeframe';
730
+ else if (hasFollowUp && /\b(category|product|customer|account|user|region|segment|channel)\b/.test(noun))
731
+ kind = 'prior_dimension_values';
732
+ else if (hasFollowUp)
733
+ kind = 'prior_entities';
734
+ refs.push({
735
+ phrase,
736
+ kind,
737
+ ...resolvedFollowUpValues(record, noun, kind),
738
+ });
739
+ }
740
+ if (refs.length === 0 && record) {
741
+ const bare = lower.match(/\b(these|those|that|them|same)\b/);
742
+ const dimension = bare ? singleFollowUpDimension(record) : undefined;
743
+ if (bare && dimension) {
744
+ refs.push({
745
+ phrase: bare[0],
746
+ kind: 'prior_dimension_values',
747
+ ...resolvedFollowUpValues(record, dimension, 'prior_dimension_values'),
748
+ });
749
+ }
750
+ }
751
+ return refs.slice(0, 8);
752
+ }
753
+ function followUpDimensionsForRequestedShape(followUp, refs) {
754
+ const record = followUpRecord(followUp);
755
+ if (!record || refs.every((ref) => ref.kind !== 'prior_dimension_values'))
756
+ return [];
757
+ const explicit = cleanStringArray(record.dimensions).map(contextDimensionTerm).filter(Boolean);
758
+ if (explicit.length > 0)
759
+ return uniqueStrings(explicit);
760
+ const single = singleFollowUpDimension(record);
761
+ return single ? [single] : [];
762
+ }
763
+ function followUpFiltersForRequestedShape(followUp, refs) {
764
+ const record = followUpRecord(followUp);
765
+ if (!record || refs.every((ref) => ref.kind !== 'prior_dimension_values'))
766
+ return [];
767
+ return uniqueStrings([
768
+ ...cleanStringArray(record.filters),
769
+ ...refs.flatMap((ref) => ref.resolvedValues ?? []),
770
+ ]);
771
+ }
772
+ function resolvedFollowUpValues(record, dimension, kind) {
773
+ if (!record || kind !== 'prior_dimension_values')
774
+ return {};
775
+ const values = followUpValuesForDimension(record, dimension);
776
+ return values.length > 0 ? { resolvedValues: values } : {};
777
+ }
778
+ function followUpValuesForDimension(record, dimension) {
779
+ const valuesByDimension = cleanStringRecord(record.priorResultValues);
780
+ const canonicalDimension = dimension ? contextDimensionTerm(dimension) : singleFollowUpDimension(record);
781
+ const values = canonicalDimension
782
+ ? valuesByDimension[canonicalDimension] ?? valuesByDimension[`${canonicalDimension}_name`] ?? []
783
+ : [];
784
+ return uniqueStrings([
785
+ ...values,
786
+ ...cleanStringArray(record.filters),
787
+ ]).slice(0, 24);
788
+ }
789
+ function singleFollowUpDimension(record) {
790
+ const explicit = uniqueStrings(cleanStringArray(record.dimensions).map(contextDimensionTerm).filter(Boolean));
791
+ if (explicit.length === 1)
792
+ return explicit[0];
793
+ if (explicit.length > 1)
794
+ return undefined;
795
+ const valuesByDimension = cleanStringRecord(record.priorResultValues);
796
+ const inferred = uniqueStrings(Object.keys(valuesByDimension)
797
+ .map(contextDimensionTerm)
798
+ .filter((term) => term && !METRIC_WORDS.includes(term)));
799
+ return inferred.length === 1 ? inferred[0] : undefined;
800
+ }
801
+ function contextDimensionTerm(value) {
802
+ const term = canonicalShapeTerm(value);
803
+ if (term.includes('category'))
804
+ return 'category';
805
+ if (term.includes('product'))
806
+ return 'product';
807
+ if (term.includes('customer'))
808
+ return 'customer';
809
+ if (term.includes('account'))
810
+ return 'account';
811
+ if (term.includes('user'))
812
+ return 'user';
813
+ if (term.includes('region'))
814
+ return 'region';
815
+ if (term.includes('segment'))
816
+ return 'segment';
817
+ if (term.includes('channel'))
818
+ return 'channel';
819
+ return term;
820
+ }
821
+ function cleanStringRecord(value) {
822
+ if (!value || typeof value !== 'object' || Array.isArray(value))
823
+ return {};
824
+ const out = {};
825
+ for (const [key, raw] of Object.entries(value)) {
826
+ const cleanKey = contextDimensionTerm(key);
827
+ const values = cleanStringArray(raw).slice(0, 24);
828
+ if (cleanKey && values.length > 0)
829
+ out[cleanKey] = values;
830
+ }
831
+ return out;
832
+ }
833
+ function followUpRecord(value) {
834
+ return value && typeof value === 'object' && !Array.isArray(value)
835
+ ? value
836
+ : undefined;
837
+ }
838
+ function inferRequestedShapeGrain(dimensions, timeTerms) {
839
+ if (dimensions.length > 0)
840
+ return dimensions[0];
841
+ const time = timeTerms.find((term) => /\b(hour|day|week|month|quarter|year|season|period|date)\b/i.test(term));
842
+ return time ? canonicalShapeTerm(time) : undefined;
843
+ }
844
+ function canonicalShapeTerm(value) {
845
+ return normalizeTerm(value).replace(/\s+/g, '_');
846
+ }
419
847
  function buildSearchQueries(input) {
420
848
  const entityText = input.entities.map((entity) => entity.text).join(' ');
421
849
  const profileTerms = input.mode === 'entity_profile' ? PROFILE_WORDS.join(' ') : '';
@@ -599,6 +1027,8 @@ function normalizeSearchText(value) {
599
1027
  }
600
1028
  function normalizeTerm(value) {
601
1029
  const clean = normalizeSearchText(value);
1030
+ if (/^(cusomer|custmer|costomer|client|buyer)s?$/.test(clean))
1031
+ return 'customer';
602
1032
  if (clean.endsWith('ies') && clean.length > 4)
603
1033
  return `${clean.slice(0, -3)}y`;
604
1034
  if (clean.endsWith('s') && clean.length > 4)