@duckcodeailabs/dql-agent 1.6.5 → 1.6.7

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.
@@ -15,6 +15,7 @@
15
15
  * full pipeline.
16
16
  */
17
17
  import { buildSkillBlockHints, buildSkillsPrompt } from './skills/loader.js';
18
+ import { validateSqlAgainstLocalContext } from './metadata/sql-context-validation.js';
18
19
  const CERTIFIED_HIT_THRESHOLD = 0.18;
19
20
  const HARD_NEGATIVE_RATIO = 0.5;
20
21
  const EXECUTABLE_ARTIFACT_KINDS = ['block', 'dashboard', 'app', 'notebook'];
@@ -40,7 +41,8 @@ export async function answer(input) {
40
41
  const semanticHits = kg.search({ query: question, domain, kinds: SEMANTIC_KINDS, limit: 12 });
41
42
  const manifestHits = kg.search({ query: question, domain, kinds: MANIFEST_KINDS, limit: 12 });
42
43
  const considered = mergeHits(artifactHits, semanticHits, manifestHits, kg.search({ query: question, domain, limit: 10 })).slice(0, 30);
43
- const intent = classifyAgentIntent({
44
+ const catalogRoute = input.contextPack?.routeDecision;
45
+ const fallbackIntent = classifyAgentIntent({
44
46
  question,
45
47
  followUp: input.followUp,
46
48
  artifactHits,
@@ -48,19 +50,30 @@ export async function answer(input) {
48
50
  manifestHits,
49
51
  schemaContext: input.schemaContext ?? [],
50
52
  });
53
+ const intent = catalogRoute ? agentIntentFromCatalogRoute(catalogRoute) : fallbackIntent;
51
54
  // Stage 1: certified artifact match. Blocks can be executed; dashboards,
52
55
  // Apps, and notebooks are returned as governed citations/navigation targets.
53
- const artifactHit = intent === 'exact_certified_lookup'
54
- ? pickCertifiedArtifact({
55
- artifactHits,
56
+ const drilldownCertifiedHit = input.followUp?.kind === 'drilldown'
57
+ ? pickCertifiedDrilldownArtifact({
56
58
  executableArtifactHits,
57
- businessHits,
58
59
  question,
59
- blockHints: input.followUp?.kind === 'drilldown' ? [] : effectiveBlockHints,
60
+ followUp: input.followUp,
60
61
  excludedArtifactIds,
61
62
  kg,
62
63
  })
63
64
  : null;
65
+ const artifactHit = drilldownCertifiedHit ?? (shouldUseCertifiedRoute(catalogRoute, intent)
66
+ ? certifiedHitFromContextPack(input.contextPack, kg)
67
+ ?? pickCertifiedArtifact({
68
+ artifactHits,
69
+ executableArtifactHits,
70
+ businessHits,
71
+ question,
72
+ blockHints: input.followUp?.kind === 'drilldown' ? [] : effectiveBlockHints,
73
+ excludedArtifactIds,
74
+ kg,
75
+ })
76
+ : null);
64
77
  if (artifactHit) {
65
78
  let result;
66
79
  let executionError;
@@ -89,7 +102,7 @@ export async function answer(input) {
89
102
  const analysisPlan = buildAnalysisPlan({
90
103
  question,
91
104
  intent,
92
- routeReason: 'The question matched a certified DQL artifact closely enough to answer without generating new SQL.',
105
+ routeReason: catalogRoute?.reason ?? 'The question matched a certified DQL artifact closely enough to answer without generating new SQL.',
93
106
  selectedNodes: [artifactHit.node],
94
107
  schemaContext: input.schemaContext ?? [],
95
108
  sql: result?.sql,
@@ -107,6 +120,10 @@ export async function answer(input) {
107
120
  result,
108
121
  executionError,
109
122
  sql: result?.sql,
123
+ trustLabel: input.contextPack?.trustLabel ?? 'certified',
124
+ sourceCertifiedBlock: artifactHit.node.kind === 'block' ? artifactHit.node.name : undefined,
125
+ contextPackId: input.contextPack?.id,
126
+ selectedEvidence: input.contextPack?.evidenceRoles?.slice(0, 12),
110
127
  citations,
111
128
  memoryContext: input.memoryContext,
112
129
  analysisPlan,
@@ -124,19 +141,22 @@ export async function answer(input) {
124
141
  memoryContext: input.memoryContext ?? [],
125
142
  analysisPlan,
126
143
  }),
144
+ contextPack: input.contextPack,
127
145
  considered,
128
146
  providerUsed: provider.name,
129
147
  };
130
148
  }
131
- if (intent === 'clarify') {
132
- const text = composeClarificationText(question, considered, input.schemaContext ?? []);
149
+ if (intent === 'clarify' || catalogRoute?.route === 'clarify') {
150
+ const text = composeCatalogClarificationText(question, catalogRoute) ?? composeClarificationText(question, considered, input.schemaContext ?? []);
133
151
  const analysisPlan = buildAnalysisPlan({
134
152
  question,
135
153
  intent,
136
- routeReason: 'No certified artifact, semantic object, dbt/source table, or runtime schema match was strong enough to safely generate SQL.',
154
+ routeReason: catalogRoute?.reason ?? 'No certified artifact, semantic object, dbt/source table, or runtime schema match was strong enough to safely generate SQL.',
137
155
  selectedNodes: considered.slice(0, 4).map((hit) => hit.node),
138
156
  schemaContext: input.schemaContext ?? [],
139
- assumptions: ['Need a clearer business object, measure, or grain before querying.'],
157
+ assumptions: catalogRoute?.missingContext.length
158
+ ? catalogRoute.missingContext.map((item) => item.message)
159
+ : ['Need a clearer business object, measure, or grain before querying.'],
140
160
  });
141
161
  return {
142
162
  kind: 'no_answer',
@@ -160,17 +180,18 @@ export async function answer(input) {
160
180
  memoryContext: input.memoryContext ?? [],
161
181
  analysisPlan,
162
182
  }),
183
+ contextPack: input.contextPack,
163
184
  considered,
164
185
  providerUsed: provider.name,
165
186
  };
166
187
  }
167
188
  // Stage 2/3: generate only after certified artifacts miss. Semantic context
168
189
  // wins over raw dbt manifest context; memory is appended last as advisory.
169
- const activeTier = semanticHits.length > 0
190
+ const activeTier = sourceTierFromContextPack(input.contextPack) ?? (semanticHits.length > 0
170
191
  ? 'semantic_layer'
171
192
  : manifestHits.length > 0
172
193
  ? 'dbt_manifest'
173
- : 'dbt_manifest';
194
+ : 'dbt_manifest');
174
195
  const reviewRequiredArtifactHits = artifactHits
175
196
  .filter((hit) => hit.score >= CERTIFIED_HIT_THRESHOLD && !isCertifiedHit(hit, kg))
176
197
  .slice(0, 4);
@@ -192,13 +213,19 @@ export async function answer(input) {
192
213
  messages.push({ role: 'system', content: skillsPrompt });
193
214
  messages.push({
194
215
  role: 'system',
195
- content: renderContextPrompt(contextBlocks, contextBusiness, contextOther, activeTier, input.memoryContext ?? [], input.extraContext, input.followUp, input.schemaContext ?? [], intent),
216
+ content: renderContextPrompt(contextBlocks, contextBusiness, contextOther, activeTier, input.memoryContext ?? [], input.extraContext, input.followUp, input.schemaContext ?? [], intent, input.contextPack),
196
217
  });
197
218
  messages.push({ role: 'user', content: question });
198
219
  const localProposal = buildSchemaAwareProposal({
199
220
  question,
200
221
  intent,
201
222
  schemaContext: input.schemaContext ?? [],
223
+ followUp: input.followUp,
224
+ contextPack: input.contextPack,
225
+ }) ?? buildContextPackAwareProposal({
226
+ question,
227
+ intent,
228
+ contextPack: input.contextPack,
202
229
  });
203
230
  let proposed = '';
204
231
  let parsed;
@@ -231,6 +258,7 @@ export async function answer(input) {
231
258
  considered,
232
259
  memoryContext: input.memoryContext ?? [],
233
260
  }),
261
+ contextPack: input.contextPack,
234
262
  considered,
235
263
  providerUsed: provider.name,
236
264
  };
@@ -259,11 +287,67 @@ export async function answer(input) {
259
287
  considered,
260
288
  memoryContext: input.memoryContext ?? [],
261
289
  }),
290
+ contextPack: input.contextPack,
262
291
  considered,
263
292
  providerUsed: provider.name,
264
293
  };
265
294
  }
295
+ const contextValidation = validateSqlAgainstLocalContext(parsed.sql, input.contextPack, {
296
+ question,
297
+ intent,
298
+ filterValues: input.followUp?.filters,
299
+ });
300
+ if (!contextValidation.ok) {
301
+ const text = `I could not safely prepare this generated SQL from the inspected context. ${contextValidation.error}`;
302
+ const analysisPlan = buildAnalysisPlan({
303
+ question,
304
+ intent,
305
+ routeReason: catalogRoute?.reason ?? 'Generated SQL failed metadata context validation before preview execution or draft capture.',
306
+ selectedNodes: contextNodes,
307
+ schemaContext: input.schemaContext ?? [],
308
+ sql: parsed.sql,
309
+ suggestedViz: parsed.viz ?? 'table',
310
+ assumptions: [
311
+ 'Generated SQL was rejected before execution because it did not match inspected metadata context.',
312
+ ...contextValidation.warnings,
313
+ ],
314
+ });
315
+ return {
316
+ kind: 'no_answer',
317
+ sourceTier: 'no_answer',
318
+ certification: 'analyst_review_required',
319
+ reviewStatus: 'none',
320
+ confidence: 0.15,
321
+ text,
322
+ answer: text,
323
+ proposedSql: parsed.sql,
324
+ sql: parsed.sql,
325
+ trustLabel: input.contextPack?.trustLabel,
326
+ sourceCertifiedBlock: followUpSourceBlock?.name ?? input.followUp?.sourceBlockName,
327
+ contextPackId: input.contextPack?.id,
328
+ validationWarnings: contextValidation.warnings,
329
+ selectedEvidence: input.contextPack?.evidenceRoles?.slice(0, 12),
330
+ citations: [],
331
+ memoryContext: input.memoryContext,
332
+ analysisPlan,
333
+ evidence: buildNoAnswerEvidence({
334
+ question,
335
+ reason: contextValidation.error,
336
+ artifactHits,
337
+ businessHits,
338
+ semanticHits,
339
+ manifestHits,
340
+ considered,
341
+ memoryContext: input.memoryContext ?? [],
342
+ analysisPlan,
343
+ }),
344
+ contextPack: input.contextPack,
345
+ considered,
346
+ providerUsed: localProposal ? 'schema_planner' : provider.name,
347
+ };
348
+ }
266
349
  const generatedCitations = [
350
+ ...contextPackCitations(input.contextPack, 4),
267
351
  ...contextNodes.slice(0, 4).map((n) => ({
268
352
  nodeId: n.nodeId,
269
353
  kind: n.kind,
@@ -334,33 +418,74 @@ export async function answer(input) {
334
418
  const analysisPlan = buildAnalysisPlan({
335
419
  question,
336
420
  intent,
337
- routeReason: intent === 'drillthrough'
421
+ routeReason: catalogRoute?.reason ?? (intent === 'drillthrough'
338
422
  ? 'The user asked for a drill-through or follow-up, so DQL generated review-required SQL from the prior context and current metadata.'
339
- : 'The question asks for a custom analysis, ranking, breakdown, comparison, or grain that should not be answered by a loose certified block match.',
423
+ : 'The question asks for a custom analysis, ranking, breakdown, comparison, or grain that should not be answered by a loose certified block match.'),
340
424
  selectedNodes: contextNodes,
341
425
  schemaContext: input.schemaContext ?? [],
342
426
  sql: parsed.sql,
343
427
  suggestedViz: parsed.viz ?? 'table',
344
428
  assumptions: [
345
429
  'Generated SQL is an uncertified preview until an analyst reviews and promotes it.',
346
- ...(localProposal ? ['A schema-aware local planner selected the customer ranking grain before provider generation.'] : []),
430
+ ...(localProposal ? ['A local metadata planner selected a review-required SQL grain before provider generation.'] : []),
347
431
  ...(executionError ? ['The preview execution error must be reviewed before reuse.'] : []),
348
432
  ],
349
433
  repairAttempts,
350
434
  });
435
+ const validationWarnings = [
436
+ ...(input.contextPack?.warnings ?? []),
437
+ ...(executionError ? ['The preview execution error must be reviewed before reuse.'] : []),
438
+ ];
439
+ let draftBlock;
440
+ let draftCaptureError;
441
+ if (input.captureGeneratedDraft && parsed.sql) {
442
+ try {
443
+ draftBlock = await input.captureGeneratedDraft({
444
+ question,
445
+ sql: parsed.sql,
446
+ intent,
447
+ followUp: input.followUp,
448
+ contextPack: input.contextPack,
449
+ sourceBlock: followUpSourceBlock ?? undefined,
450
+ validationWarnings,
451
+ });
452
+ }
453
+ catch (err) {
454
+ draftCaptureError = err instanceof Error ? err.message : String(err);
455
+ validationWarnings.push(`Draft capture failed: ${draftCaptureError}`);
456
+ }
457
+ }
458
+ const sourceCertifiedBlock = followUpSourceBlock?.name ?? input.followUp?.sourceBlockName;
459
+ const trustExplanation = generatedTrustExplanation({
460
+ followUp: input.followUp,
461
+ sourceCertifiedBlock,
462
+ draftBlock,
463
+ });
464
+ const cleanedSummary = cleanGeneratedSummary(parsed.text);
465
+ const generatedText = trustExplanation
466
+ ? [trustExplanation, cleanedSummary].filter(Boolean).join('\n\n')
467
+ : cleanedSummary;
351
468
  return {
352
469
  kind: 'uncertified',
353
470
  sourceTier: activeTier,
354
471
  certification: 'ai_generated',
355
472
  reviewStatus: 'draft_ready',
356
473
  confidence: activeTier === 'semantic_layer' ? 0.72 : 0.55,
357
- text: parsed.text,
358
- answer: parsed.text,
474
+ text: generatedText,
475
+ answer: generatedText,
359
476
  proposedSql: parsed.sql,
360
477
  sql: parsed.sql,
361
478
  result,
362
479
  executionError,
363
480
  suggestedViz: parsed.viz ?? 'table',
481
+ draftBlock,
482
+ draftBlockId: draftBlock?.path,
483
+ promoteCommand: draftBlock ? `dql certify --from-draft ${draftBlock.path}` : undefined,
484
+ trustLabel: input.contextPack?.trustLabel,
485
+ sourceCertifiedBlock,
486
+ contextPackId: input.contextPack?.id,
487
+ validationWarnings,
488
+ selectedEvidence: input.contextPack?.evidenceRoles?.slice(0, 12),
364
489
  citations: generatedCitations,
365
490
  memoryContext: input.memoryContext,
366
491
  analysisPlan,
@@ -382,6 +507,7 @@ export async function answer(input) {
382
507
  executorWasAvailable: Boolean(input.executeGeneratedSql),
383
508
  analysisPlan,
384
509
  }),
510
+ contextPack: input.contextPack,
385
511
  considered,
386
512
  providerUsed: localProposal ? 'schema_planner' : provider.name,
387
513
  };
@@ -409,8 +535,11 @@ Rules:
409
535
  proposed SQL. Do not emit INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, COPY,
410
536
  PRAGMA, SET, or multiple statements.
411
537
  8. If the schema is insufficient to answer, say so explicitly and ask a
412
- clarifying question instead of guessing.`;
413
- function renderContextPrompt(blocks, businessContext, others, activeTier, memoryContext, extraContext, followUp, schemaContext = [], intent = 'ad_hoc_analysis') {
538
+ clarifying question instead of guessing.
539
+ 9. Write directly to the analyst. Do not say "the user is asking", "the user
540
+ requested", "I will generate", or describe internal routing. State the
541
+ answer, the certified context used, and the review requirement.`;
542
+ function renderContextPrompt(blocks, businessContext, others, activeTier, memoryContext, extraContext, followUp, schemaContext = [], intent = 'ad_hoc_analysis', contextPack) {
414
543
  const intentSection = `## Routing intent\n\nintent: ${intent}\n${intent === 'exact_certified_lookup'
415
544
  ? 'Use a certified artifact only if it exactly answers the question.'
416
545
  : 'Generate review-required SQL for this question. Certified blocks are trusted context, not a reason to answer the wrong grain.'}`;
@@ -458,7 +587,146 @@ function renderContextPrompt(blocks, businessContext, others, activeTier, memory
458
587
  const followUpSection = followUp
459
588
  ? `\n\n## Follow-up routing context\n\n${renderFollowUpContext(followUp)}`
460
589
  : '';
461
- return `${intentSection}\n\n${blockSection}${businessSection}${otherSection}${schemaSection}${memorySection}${extraSection}${followUpSection}`;
590
+ const contextPackSection = contextPack
591
+ ? `\n\n## Local metadata context pack\n\n${renderContextPackForPrompt(contextPack)}`
592
+ : '';
593
+ return `${intentSection}\n\n${blockSection}${businessSection}${otherSection}${schemaSection}${contextPackSection}${memorySection}${extraSection}${followUpSection}`;
594
+ }
595
+ function renderContextPackForPrompt(contextPack) {
596
+ const warnings = contextPack.warnings.length
597
+ ? `Warnings:\n${contextPack.warnings.slice(0, 8).map((warning) => `- ${warning}`).join('\n')}\n`
598
+ : '';
599
+ const objects = contextPack.objects.slice(0, 18).map((object) => {
600
+ const detail = [
601
+ object.objectType,
602
+ object.domain ? `domain: ${object.domain}` : '',
603
+ object.status ? `status: ${object.status}` : '',
604
+ object.description ? `description: ${object.description}` : '',
605
+ ].filter(Boolean).join('; ');
606
+ return `- ${object.objectKey} (${detail})`;
607
+ }).join('\n');
608
+ const conflicts = contextPack.retrievalDiagnostics.candidateConflicts.length
609
+ ? `\nCandidate conflicts:\n${contextPack.retrievalDiagnostics.candidateConflicts.slice(0, 4).map((conflict) => `- ${conflict.reason} ${conflict.prompt}`).join('\n')}`
610
+ : '';
611
+ const route = contextPack.routeDecision
612
+ ? `\nRoute decision: ${contextPack.routeDecision.route} / ${contextPack.routeDecision.intent}\nReason: ${contextPack.routeDecision.reason}\nMissing context: ${contextPack.routeDecision.missingContext.map((item) => item.message).join(' ') || 'none'}`
613
+ : '';
614
+ const allowed = contextPack.allowedSqlContext?.relations.length
615
+ ? `\nAllowed SQL relations:\n${contextPack.allowedSqlContext.relations.slice(0, 12).map((relation) => `- ${relation.relation}: ${relation.columns.slice(0, 24).map((column) => column.name).join(', ') || '(columns unavailable)'}`).join('\n')}`
616
+ : '';
617
+ return [
618
+ `context_pack_id: ${contextPack.id}`,
619
+ `trust_label: ${contextPack.trustLabel}`,
620
+ route.trim(),
621
+ warnings.trim(),
622
+ `Selected evidence:\n${objects || '- none'}`,
623
+ allowed.trim(),
624
+ conflicts.trim(),
625
+ ].filter(Boolean).join('\n');
626
+ }
627
+ function contextPackCitations(contextPack, limit) {
628
+ if (!contextPack)
629
+ return [];
630
+ return contextPack.objects.slice(0, limit).map((object) => ({
631
+ nodeId: object.objectKey,
632
+ kind: metadataObjectKindForCitation(object.objectType),
633
+ name: object.name,
634
+ sourceTier: metadataObjectSourceTier(object.objectType),
635
+ provenance: object.sourceSystem,
636
+ }));
637
+ }
638
+ function agentIntentFromCatalogRoute(route) {
639
+ if (route.route === 'clarify')
640
+ return 'clarify';
641
+ if (route.route === 'certified')
642
+ return route.intent === 'definition_lookup' ? 'definition_lookup' : 'exact_certified_lookup';
643
+ return route.intent;
644
+ }
645
+ function shouldUseCertifiedRoute(route, intent) {
646
+ if (route)
647
+ return route.route === 'certified';
648
+ return intent === 'exact_certified_lookup' || intent === 'definition_lookup';
649
+ }
650
+ function certifiedHitFromContextPack(contextPack, kg) {
651
+ const key = contextPack?.routeDecision.exactObjectKey;
652
+ if (!key)
653
+ return null;
654
+ const object = contextPack.objects.find((item) => item.objectKey === key);
655
+ if (!object)
656
+ return null;
657
+ const nodeId = object.objectType === 'dql_block'
658
+ ? `block:${object.name}`
659
+ : object.objectType === 'dql_term'
660
+ ? `term:${object.name}`
661
+ : object.objectType === 'business_view'
662
+ ? `business_view:${object.name}`
663
+ : undefined;
664
+ const node = nodeId ? kg.getNode(nodeId) : null;
665
+ return node ? { node, score: 1, snippet: object.snippet } : null;
666
+ }
667
+ function composeCatalogClarificationText(question, route) {
668
+ if (!route?.missingContext.length)
669
+ return undefined;
670
+ const missing = route.missingContext.map((item) => item.message).join(' ');
671
+ const followUp = route.followUps[0] ? ` ${route.followUps[0]}?` : '';
672
+ return `I need one more detail before querying "${question}". ${missing}${followUp}`;
673
+ }
674
+ function sourceTierFromContextPack(contextPack) {
675
+ if (!contextPack)
676
+ return undefined;
677
+ if (contextPack.objects.some((object) => object.objectType === 'semantic_metric'))
678
+ return 'semantic_layer';
679
+ if (contextPack.objects.some((object) => object.objectType.startsWith('dbt_') || object.objectType === 'warehouse_table' || object.objectType === 'runtime_table'))
680
+ return 'dbt_manifest';
681
+ if (contextPack.objects.some((object) => object.objectType === 'dql_term' || object.objectType === 'business_view'))
682
+ return 'business_context';
683
+ if (contextPack.objects.some((object) => object.objectType === 'dql_block'))
684
+ return 'certified_artifact';
685
+ return undefined;
686
+ }
687
+ function isGeneratedAgentIntent(intent) {
688
+ return intent === 'ad_hoc_analysis'
689
+ || intent === 'drillthrough'
690
+ || intent === 'ad_hoc_ranking'
691
+ || intent === 'driver_breakdown'
692
+ || intent === 'diagnose_change'
693
+ || intent === 'segment_compare'
694
+ || intent === 'entity_drilldown'
695
+ || intent === 'anomaly_investigation';
696
+ }
697
+ function metadataObjectKindForCitation(objectType) {
698
+ if (objectType === 'dql_block')
699
+ return 'block';
700
+ if (objectType === 'dql_term')
701
+ return 'term';
702
+ if (objectType === 'business_view')
703
+ return 'business_view';
704
+ if (objectType === 'semantic_metric')
705
+ return 'metric';
706
+ if (objectType === 'semantic_dimension')
707
+ return 'dimension';
708
+ if (objectType === 'dbt_model')
709
+ return 'dbt_model';
710
+ if (objectType === 'dbt_source' || objectType === 'warehouse_table')
711
+ return 'dbt_source';
712
+ if (objectType === 'notebook')
713
+ return 'notebook';
714
+ if (objectType === 'dashboard')
715
+ return 'dashboard';
716
+ if (objectType === 'app')
717
+ return 'app';
718
+ return 'runtime_schema';
719
+ }
720
+ function metadataObjectSourceTier(objectType) {
721
+ if (objectType === 'dql_block')
722
+ return 'certified_artifact';
723
+ if (objectType === 'dql_term' || objectType === 'business_view')
724
+ return 'business_context';
725
+ if (objectType.startsWith('semantic_'))
726
+ return 'semantic_layer';
727
+ if (objectType.startsWith('dbt_') || objectType === 'warehouse_table')
728
+ return 'dbt_manifest';
729
+ return 'business_context';
462
730
  }
463
731
  function renderFollowUpContext(followUp) {
464
732
  const parts = [
@@ -474,6 +742,30 @@ function renderFollowUpContext(followUp) {
474
742
  : 'routing rule: reuse the prior certified block when the user asks a generic follow-up.';
475
743
  return [...parts, rule].join('\n');
476
744
  }
745
+ function generatedTrustExplanation(input) {
746
+ if (input.followUp?.kind !== 'drilldown')
747
+ return undefined;
748
+ const source = input.sourceCertifiedBlock
749
+ ? ` I used the certified \`${input.sourceCertifiedBlock}\` block for the business definition,`
750
+ : ' I used certified context where available,';
751
+ const filters = [
752
+ ...(input.followUp.filters ?? []),
753
+ ...(input.followUp.dimensions ?? []),
754
+ ];
755
+ const grain = filters.length ? ` at the requested ${filters.join('/')} grain` : ' at the requested drilldown grain';
756
+ const draft = input.draftBlock
757
+ ? ` The draft was saved at \`${input.draftBlock.path}\` for review.`
758
+ : ' The generated SQL still needs analyst review before certification.';
759
+ return `This is an uncertified drilldown.${source} then generated new SQL${grain}.${draft}`;
760
+ }
761
+ function cleanGeneratedSummary(text) {
762
+ return text
763
+ .trim()
764
+ .replace(/^(?:the user (?:is asking|asked|wants|requested)[^.]*\.\s*)+/i, '')
765
+ .replace(/\s*(?:therefore,\s*)?i will generate review-required sql[^.]*\.\s*/gi, ' ')
766
+ .replace(/\s*(?:therefore,\s*)?i will generate[^.]*\.\s*/gi, ' ')
767
+ .trim();
768
+ }
477
769
  /**
478
770
  * Public for tests. Pulls the first ```sql block and an optional Viz: line
479
771
  * out of an LLM response.
@@ -491,8 +783,12 @@ export function parseProposal(raw) {
491
783
  return { text, sql, viz };
492
784
  }
493
785
  function buildSchemaAwareProposal(input) {
494
- if (input.intent !== 'ad_hoc_analysis' && input.intent !== 'drillthrough')
786
+ if (!isGeneratedAgentIntent(input.intent))
495
787
  return undefined;
788
+ const schemaContext = schemaContextWithAllowedSqlContext(input.schemaContext, input.contextPack);
789
+ const drilldownProposal = buildMatchedEntityDrilldownProposal({ ...input, schemaContext });
790
+ if (drilldownProposal)
791
+ return drilldownProposal;
496
792
  if (isFilteredEntityQuestion(input.question))
497
793
  return undefined;
498
794
  const lower = input.question.toLowerCase();
@@ -501,7 +797,7 @@ function buildSchemaAwareProposal(input) {
501
797
  && !/\b(order details|specific orders|each order|all orders|order line|line item)\b/.test(lower);
502
798
  if (!asksForCustomerPerformance)
503
799
  return undefined;
504
- const customers = findSchemaTable(input.schemaContext, ['customers', 'customer']);
800
+ const customers = findSchemaTable(schemaContext, ['customers', 'customer']);
505
801
  if (!customers)
506
802
  return undefined;
507
803
  const customerName = findSchemaColumn(customers, ['customer_name', 'name', 'full_name']);
@@ -523,7 +819,7 @@ function buildSchemaAwareProposal(input) {
523
819
  };
524
820
  }
525
821
  const customerId = findSchemaColumn(customers, ['customer_id', 'id']);
526
- const orders = findSchemaTable(input.schemaContext, ['orders', 'order']);
822
+ const orders = findSchemaTable(schemaContext, ['orders', 'order']);
527
823
  if (!orders || !customerName || !customerId)
528
824
  return undefined;
529
825
  const orderCustomerId = findSchemaColumn(orders, ['customer_id', 'customer']);
@@ -548,6 +844,341 @@ function buildSchemaAwareProposal(input) {
548
844
  viz: 'table',
549
845
  };
550
846
  }
847
+ function buildMatchedEntityDrilldownProposal(input) {
848
+ if (input.intent !== 'entity_drilldown' && input.followUp?.kind !== 'drilldown')
849
+ return undefined;
850
+ const table = pickDrilldownTable(input.schemaContext, input.question, input.followUp);
851
+ if (!table)
852
+ return undefined;
853
+ const dimension = inferDrilldownDimension(table, input.question, input.followUp);
854
+ if (!dimension)
855
+ return undefined;
856
+ const entityFilters = matchedEntityFiltersForQuestion(table, input.question, input.followUp);
857
+ if (entityFilters.length === 0)
858
+ return undefined;
859
+ if (entityFilters.some((filter) => namesEqualLoose(filter.column, dimension)))
860
+ return undefined;
861
+ const sourceSql = selectSourceBlockSql(input.contextPack, input.followUp?.sourceBlockName);
862
+ const metric = inferDrilldownMetric(table, input.question, sourceSql);
863
+ if (!metric)
864
+ return undefined;
865
+ const timePredicates = drilldownTimePredicates({
866
+ question: input.question,
867
+ followUp: input.followUp,
868
+ table,
869
+ sourceSql,
870
+ });
871
+ if (mentionsRelativeTime(input.question, input.followUp) && timePredicates.length === 0)
872
+ return undefined;
873
+ const predicates = [
874
+ ...entityFilters.map((filter) => `${sqlIdentifier(filter.column)} = ${sqlStringLiteral(filter.value)}`),
875
+ ...timePredicates,
876
+ ];
877
+ const where = predicates.length ? [`WHERE ${predicates.join(' AND ')}`] : [];
878
+ return {
879
+ text: [
880
+ `Prepared a review-required ${humanizeIdentifier(dimension)} drilldown from inspected metadata.`,
881
+ `The entity filter uses ${entityFilters.map((filter) => `${filter.column} = ${filter.value}`).join(', ')} from matched sample values.`,
882
+ 'This result is uncertified until reviewed and promoted.',
883
+ ].join(' '),
884
+ sql: [
885
+ 'SELECT',
886
+ ` ${sqlIdentifier(dimension)} AS ${sqlIdentifier(dimension)},`,
887
+ ` ${metric.expression} AS ${sqlIdentifier(metric.alias)}`,
888
+ `FROM ${sqlRelation(table.relation)}`,
889
+ ...where,
890
+ `GROUP BY ${sqlIdentifier(dimension)}`,
891
+ `ORDER BY ${sqlIdentifier(metric.alias)} DESC`,
892
+ 'LIMIT 50',
893
+ ].join('\n'),
894
+ viz: 'bar',
895
+ };
896
+ }
897
+ function schemaContextWithAllowedSqlContext(schemaContext, contextPack) {
898
+ const byRelation = new Map();
899
+ for (const table of schemaContext) {
900
+ byRelation.set(normalizeRelationKey(table.relation), {
901
+ ...table,
902
+ columns: table.columns.map((column) => ({ ...column, sampleValues: column.sampleValues?.slice() })),
903
+ });
904
+ }
905
+ for (const relation of contextPack?.allowedSqlContext?.relations ?? []) {
906
+ const key = normalizeRelationKey(relation.relation);
907
+ const existing = byRelation.get(key);
908
+ if (!existing) {
909
+ byRelation.set(key, {
910
+ relation: relation.relation,
911
+ name: relation.name,
912
+ columns: relation.columns.map((column) => ({
913
+ name: column.name,
914
+ type: column.type,
915
+ description: column.description,
916
+ sampleValues: column.sampleValues?.slice(),
917
+ })),
918
+ source: relation.source,
919
+ });
920
+ continue;
921
+ }
922
+ const columns = new Map(existing.columns.map((column) => [column.name.toLowerCase(), column]));
923
+ for (const column of relation.columns) {
924
+ const existingColumn = columns.get(column.name.toLowerCase());
925
+ if (!existingColumn) {
926
+ existing.columns.push({
927
+ name: column.name,
928
+ type: column.type,
929
+ description: column.description,
930
+ sampleValues: column.sampleValues?.slice(),
931
+ });
932
+ continue;
933
+ }
934
+ existingColumn.sampleValues = uniqueDrilldownStrings([
935
+ ...(existingColumn.sampleValues ?? []),
936
+ ...(column.sampleValues ?? []),
937
+ ]).slice(0, 8);
938
+ existingColumn.type ??= column.type;
939
+ existingColumn.description ??= column.description;
940
+ }
941
+ }
942
+ return Array.from(byRelation.values());
943
+ }
944
+ function pickDrilldownTable(schemaContext, question, followUp) {
945
+ const scored = schemaContext
946
+ .map((table) => ({
947
+ table,
948
+ filters: matchedEntityFiltersForQuestion(table, question, followUp).length,
949
+ dimension: inferDrilldownDimension(table, question, followUp) ? 1 : 0,
950
+ measure: inferDrilldownMetric(table, question, undefined) ? 1 : 0,
951
+ }))
952
+ .filter((candidate) => candidate.filters > 0 && candidate.dimension > 0 && candidate.measure > 0)
953
+ .sort((a, b) => (b.filters + b.dimension + b.measure) - (a.filters + a.dimension + a.measure));
954
+ return scored[0]?.table;
955
+ }
956
+ function inferDrilldownDimension(table, question, followUp) {
957
+ const lower = question.toLowerCase();
958
+ const requested = [
959
+ ...(followUp?.dimensions ?? []),
960
+ ...Array.from(lower.matchAll(/\bby\s+([a-z][a-z0-9_ -]{1,40})/g)).map((match) => match[1] ?? ''),
961
+ ]
962
+ .flatMap((value) => value.split(/\band\b|,|\//i))
963
+ .map((value) => value.replace(/\b(last|this|next|previous|prior|current)\s+(day|week|month|quarter|year)\b/gi, '').trim())
964
+ .filter(Boolean);
965
+ const direct = findSchemaColumn(table, requested);
966
+ if (direct)
967
+ return direct;
968
+ if (/\bcustomers?\b/.test(lower)) {
969
+ const customer = findSchemaColumn(table, ['customer', 'customer_name', 'account', 'account_name']);
970
+ if (customer)
971
+ return customer;
972
+ }
973
+ if (/\bsegments?\b/.test(lower)) {
974
+ const segment = findSchemaColumn(table, ['segment', 'customer_segment', 'market_segment']);
975
+ if (segment)
976
+ return segment;
977
+ }
978
+ if (/\bproducts?\b/.test(lower)) {
979
+ const product = findSchemaColumn(table, ['product', 'product_name', 'sku']);
980
+ if (product)
981
+ return product;
982
+ }
983
+ if (/\bregions?\b/.test(lower)) {
984
+ const region = findSchemaColumn(table, ['region', 'market', 'geo']);
985
+ if (region)
986
+ return region;
987
+ }
988
+ return undefined;
989
+ }
990
+ function matchedEntityFiltersForQuestion(table, question, followUp) {
991
+ const text = normalizeForEntityMatch([
992
+ question,
993
+ ...(followUp?.filters ?? []),
994
+ ].join(' '));
995
+ const filters = [];
996
+ for (const column of table.columns) {
997
+ for (const sampleValue of column.sampleValues ?? []) {
998
+ if (isTemporalDrilldownValue(sampleValue))
999
+ continue;
1000
+ const needle = normalizeForEntityMatch(sampleValue);
1001
+ if (!needle || !text.includes(needle))
1002
+ continue;
1003
+ filters.push({ column: column.name, value: sampleValue });
1004
+ }
1005
+ }
1006
+ return uniqueMatchedEntityFilters(filters);
1007
+ }
1008
+ function inferDrilldownMetric(table, question, sourceSql) {
1009
+ const sourceMetric = sourceSql ? aggregateMetricFromSourceSql(table, sourceSql) : undefined;
1010
+ if (sourceMetric)
1011
+ return sourceMetric;
1012
+ const lower = question.toLowerCase();
1013
+ const candidates = /\brevenue|arr|mrr|sales\b/.test(lower)
1014
+ ? ['revenue', 'net_revenue', 'gross_revenue', 'amount', 'order_total', 'total_amount', 'sales', 'arr', 'mrr']
1015
+ : ['amount', 'revenue', 'order_total', 'total_amount', 'value', 'spend'];
1016
+ const column = findSchemaColumn(table, candidates);
1017
+ if (!column)
1018
+ return undefined;
1019
+ const alias = /\brevenue|arr|mrr|sales\b/.test(lower) ? 'revenue_total' : `${column}_total`;
1020
+ return {
1021
+ expression: `SUM(${sqlIdentifier(column)})`,
1022
+ alias,
1023
+ };
1024
+ }
1025
+ function aggregateMetricFromSourceSql(table, sourceSql) {
1026
+ for (const column of table.columns) {
1027
+ const columnPattern = sqlIdentifierPattern(column.name);
1028
+ const aggregatePattern = new RegExp(`\\b(SUM|COUNT|AVG|MIN|MAX)\\s*\\(\\s*(?:["\`]?\\w+["\`]?\\s*\\.\\s*)?(${columnPattern}|\\*)\\s*\\)\\s*(?:AS\\s+(["\`]?\\w+["\`]?))?`, 'i');
1029
+ const match = sourceSql.match(aggregatePattern);
1030
+ if (!match)
1031
+ continue;
1032
+ const fn = (match[1] ?? 'SUM').toUpperCase();
1033
+ const target = match[2] === '*' ? '*' : sqlIdentifier(column.name);
1034
+ const alias = cleanSqlIdentifier(match[3] ?? defaultMetricAlias(fn, column.name));
1035
+ return {
1036
+ expression: `${fn}(${target})`,
1037
+ alias,
1038
+ };
1039
+ }
1040
+ return undefined;
1041
+ }
1042
+ function drilldownTimePredicates(input) {
1043
+ if (!mentionsRelativeTime(input.question, input.followUp))
1044
+ return [];
1045
+ if (!input.sourceSql)
1046
+ return [];
1047
+ const timeColumns = input.table.columns.map((column) => column.name).filter(isTimeLikeDrilldownColumn);
1048
+ if (timeColumns.length === 0)
1049
+ return [];
1050
+ return extractWherePredicates(input.sourceSql)
1051
+ .map(stripSqlAliasQualifiers)
1052
+ .filter((predicate) => isReusableSqlPredicate(predicate))
1053
+ .filter((predicate) => timeColumns.some((column) => predicateReferencesColumn(predicate, column)))
1054
+ .slice(0, 3);
1055
+ }
1056
+ function selectSourceBlockSql(contextPack, sourceBlockName) {
1057
+ const sourceSql = contextPack?.allowedSqlContext?.sourceBlockSql ?? [];
1058
+ if (sourceSql.length === 0)
1059
+ return undefined;
1060
+ const preferred = sourceBlockName
1061
+ ? sourceSql.find((source) => namesEqualLoose(source.name, sourceBlockName))
1062
+ : undefined;
1063
+ return (preferred ?? sourceSql.find((source) => source.status === 'certified') ?? sourceSql[0])?.sql;
1064
+ }
1065
+ function extractWherePredicates(sql) {
1066
+ const match = sql.match(/\bWHERE\b([\s\S]*?)(\bGROUP\s+BY\b|\bHAVING\b|\bORDER\s+BY\b|\bLIMIT\b|$)/i);
1067
+ if (!match)
1068
+ return [];
1069
+ return (match[1] ?? '')
1070
+ .split(/\s+AND\s+/i)
1071
+ .map((part) => part.trim().replace(/^\(+|\)+$/g, '').replace(/\s+/g, ' '))
1072
+ .filter(Boolean);
1073
+ }
1074
+ function mentionsRelativeTime(question, followUp) {
1075
+ const text = [question, ...(followUp?.filters ?? [])].join(' ');
1076
+ return /\b(last|this|next|previous|prior|current)\s+(day|week|month|quarter|year)\b/i.test(text)
1077
+ || /\b(today|yesterday|tomorrow|ytd|mtd|qtd|wtd)\b/i.test(text);
1078
+ }
1079
+ function isTimeLikeDrilldownColumn(name) {
1080
+ return /\b(date|time|day|week|month|quarter|year|period|created_at|updated_at)\b/i.test(name);
1081
+ }
1082
+ function stripSqlAliasQualifiers(predicate) {
1083
+ return predicate.replace(/\b["`]?\w+["`]?\s*\.\s*(["`]?\w+["`]?)/g, '$1');
1084
+ }
1085
+ function isReusableSqlPredicate(predicate) {
1086
+ if (!predicate || predicate.length > 240)
1087
+ return false;
1088
+ if (/[;]/.test(predicate) || /--|\/\*/.test(predicate))
1089
+ return false;
1090
+ return !/\b(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|COPY|PRAGMA|SET)\b/i.test(predicate);
1091
+ }
1092
+ function predicateReferencesColumn(predicate, column) {
1093
+ return new RegExp(`(^|[^\\w])${sqlIdentifierPattern(column)}([^\\w]|$)`, 'i').test(predicate);
1094
+ }
1095
+ function sqlIdentifierPattern(identifier) {
1096
+ const escaped = escapeRegExp(identifier);
1097
+ return `(?:"${escaped}"|\`${escaped}\`|${escaped})`;
1098
+ }
1099
+ function sqlStringLiteral(value) {
1100
+ return `'${value.replace(/'/g, "''")}'`;
1101
+ }
1102
+ function defaultMetricAlias(fn, column) {
1103
+ if (fn === 'SUM' && /revenue|amount|sales|arr|mrr/i.test(column))
1104
+ return 'revenue_total';
1105
+ return `${column}_${fn.toLowerCase()}`;
1106
+ }
1107
+ function cleanSqlIdentifier(identifier) {
1108
+ return identifier.replace(/^["`]|["`]$/g, '').trim();
1109
+ }
1110
+ function namesEqualLoose(a, b) {
1111
+ return cleanSqlIdentifier(a).replace(/[_-]+/g, ' ').toLowerCase() === cleanSqlIdentifier(b).replace(/[_-]+/g, ' ').toLowerCase();
1112
+ }
1113
+ function normalizeRelationKey(relation) {
1114
+ return relation.replace(/["`]/g, '').replace(/\s*\.\s*/g, '.').toLowerCase().trim();
1115
+ }
1116
+ function normalizeForEntityMatch(value) {
1117
+ return value.toLowerCase().replace(/[^a-z0-9.%+-]+/g, ' ').replace(/\s+/g, ' ').trim();
1118
+ }
1119
+ function uniqueMatchedEntityFilters(filters) {
1120
+ const seen = new Set();
1121
+ const unique = [];
1122
+ for (const filter of filters) {
1123
+ const key = `${filter.column.toLowerCase()}\0${filter.value.toLowerCase()}`;
1124
+ if (seen.has(key))
1125
+ continue;
1126
+ seen.add(key);
1127
+ unique.push(filter);
1128
+ }
1129
+ return unique;
1130
+ }
1131
+ function uniqueDrilldownStrings(values) {
1132
+ return Array.from(new Set(values));
1133
+ }
1134
+ function isTemporalDrilldownValue(value) {
1135
+ return mentionsRelativeTime(value, undefined) || /^\d{4}-\d{2}-\d{2}/.test(value);
1136
+ }
1137
+ function escapeRegExp(value) {
1138
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1139
+ }
1140
+ function buildContextPackAwareProposal(input) {
1141
+ if (!isGeneratedAgentIntent(input.intent))
1142
+ return undefined;
1143
+ if (!input.contextPack)
1144
+ return undefined;
1145
+ const lower = input.question.toLowerCase();
1146
+ if (!/\b(least|lowest|fewest|bottom|min(?:imum)?)\b/.test(lower))
1147
+ return undefined;
1148
+ for (const object of input.contextPack.objects) {
1149
+ if (object.objectType !== 'dql_block' || object.status !== 'certified')
1150
+ continue;
1151
+ const sql = typeof object.payload?.sql === 'string' ? object.payload.sql.trim() : '';
1152
+ if (!sql || !/\border\s+by\b/i.test(sql) || !/\bdesc\b/i.test(sql))
1153
+ continue;
1154
+ const inverted = invertRankingSql(sql);
1155
+ if (!inverted || inverted === sql)
1156
+ continue;
1157
+ return {
1158
+ text: `Generated a review-required least-ranking query by using certified block "${object.name}" as context and reversing its ranking direction. This result is uncertified until reviewed and promoted.`,
1159
+ sql: ensurePreviewLimit(inverted, 10),
1160
+ viz: 'table',
1161
+ };
1162
+ }
1163
+ return undefined;
1164
+ }
1165
+ function invertRankingSql(sql) {
1166
+ const withoutTrailingSemicolon = sql.replace(/;\s*$/, '').trim();
1167
+ const inverted = withoutTrailingSemicolon.replace(/\border\s+by\s+([\s\S]*?)(\blimit\b|$)/i, (match, orderExpr, limitKeyword) => {
1168
+ if (!/\bdesc\b/i.test(orderExpr))
1169
+ return match;
1170
+ const nextExpr = orderExpr
1171
+ .replace(/\bDESC\b/gi, 'ASC')
1172
+ .replace(/\bNULLS\s+FIRST\b/gi, 'NULLS LAST');
1173
+ return `ORDER BY ${nextExpr}${limitKeyword}`;
1174
+ });
1175
+ return inverted !== withoutTrailingSemicolon ? inverted : undefined;
1176
+ }
1177
+ function ensurePreviewLimit(sql, limit) {
1178
+ if (/\blimit\s+\d+\b/i.test(sql))
1179
+ return sql;
1180
+ return `${sql.replace(/;\s*$/, '').trim()}\nLIMIT ${limit}`;
1181
+ }
551
1182
  function findSchemaTable(schemaContext, names) {
552
1183
  return schemaContext.find((table) => {
553
1184
  const tableNames = new Set([table.name, table.relation.split('.').at(-1) ?? table.relation].map((name) => name.toLowerCase()));
@@ -588,7 +1219,7 @@ function pickCertifiedArtifact(input) {
588
1219
  // user at a specific block. We still validate it's certified.
589
1220
  for (const hint of input.blockHints) {
590
1221
  const node = input.kg.getNode(`block:${hint}`);
591
- if (node && node.status === 'certified') {
1222
+ if (node && node.status === 'certified' && hasCompatibleCertifiedBlockMatch(input.question, node)) {
592
1223
  return { node, score: 1, snippet: undefined };
593
1224
  }
594
1225
  }
@@ -628,12 +1259,46 @@ function pickFirstCertifiedHit(hits, kg, excludedNodeIds, question) {
628
1259
  continue;
629
1260
  if (!isCertifiedHit(hit, kg))
630
1261
  continue;
631
- if (question && hit.node.kind === 'block' && !hasMeaningfulCertifiedBlockSignal(question, hit.node))
1262
+ if (question && hit.node.kind === 'block' && !hasCompatibleCertifiedBlockMatch(question, hit.node))
632
1263
  continue;
633
1264
  return hit;
634
1265
  }
635
1266
  return null;
636
1267
  }
1268
+ function pickCertifiedDrilldownArtifact(input) {
1269
+ const requestedTerms = meaningfulTokens([
1270
+ input.question,
1271
+ ...(input.followUp.filters ?? []),
1272
+ ...(input.followUp.dimensions ?? []),
1273
+ ].join(' '));
1274
+ for (const hit of input.executableArtifactHits) {
1275
+ if (hit.score < CERTIFIED_HIT_THRESHOLD)
1276
+ break;
1277
+ if (input.excludedArtifactIds?.has(hit.node.nodeId))
1278
+ continue;
1279
+ if (hit.node.kind !== 'block')
1280
+ continue;
1281
+ if (!isCertifiedHit(hit, input.kg))
1282
+ continue;
1283
+ if (!hasCompatibleCertifiedBlockMatch(input.question, hit.node))
1284
+ continue;
1285
+ if (!hasRequestedDrilldownOverlap(hit.node, requestedTerms))
1286
+ continue;
1287
+ return hit;
1288
+ }
1289
+ return null;
1290
+ }
1291
+ function hasRequestedDrilldownOverlap(node, requestedTerms) {
1292
+ if (requestedTerms.size === 0)
1293
+ return false;
1294
+ const nodeTerms = meaningfulTokens(certifiedBlockSignalText(node));
1295
+ let overlaps = 0;
1296
+ for (const term of requestedTerms) {
1297
+ if (nodeTerms.has(term))
1298
+ overlaps += 1;
1299
+ }
1300
+ return overlaps >= 2 || (requestedTerms.size === 1 && overlaps === 1);
1301
+ }
637
1302
  function shouldDeferCertifiedArtifactForReviewPath(input) {
638
1303
  if (!isBreakdownOrDrilldownQuestion(input.question))
639
1304
  return false;
@@ -711,6 +1376,44 @@ function hasMeaningfulCertifiedBlockSignal(question, node) {
711
1376
  }
712
1377
  return false;
713
1378
  }
1379
+ function hasCompatibleCertifiedBlockMatch(question, node) {
1380
+ return hasMeaningfulCertifiedBlockSignal(question, node)
1381
+ && hasCompatibleRankingDirection(question, node);
1382
+ }
1383
+ function hasCompatibleRankingDirection(question, node) {
1384
+ const questionDirection = rankingDirectionFromText(question);
1385
+ if (!questionDirection)
1386
+ return true;
1387
+ const blockDirection = rankingDirectionFromText(certifiedBlockSignalText(node));
1388
+ if (!blockDirection)
1389
+ return true;
1390
+ return questionDirection === blockDirection;
1391
+ }
1392
+ function rankingDirectionFromText(text) {
1393
+ const lower = text.toLowerCase();
1394
+ const hasBottomSignal = /\b(bottom|least|fewest|lowest|minimum|min|smallest|worst|underperform(?:ing|ed|er|ers)?)\b/.test(lower);
1395
+ const hasTopSignal = /\b(top|most|highest|maximum|max|greatest|best|leader|leaders|leading)\b/.test(lower);
1396
+ if (hasBottomSignal && !hasTopSignal)
1397
+ return 'bottom';
1398
+ if (hasTopSignal && !hasBottomSignal)
1399
+ return 'top';
1400
+ return undefined;
1401
+ }
1402
+ function certifiedBlockSignalText(node) {
1403
+ const examples = (node.examples ?? [])
1404
+ .flatMap((example) => [example.question, example.sql ?? '']);
1405
+ return [
1406
+ node.name,
1407
+ node.domain ?? '',
1408
+ node.description ?? '',
1409
+ node.llmContext ?? '',
1410
+ node.provenance ?? '',
1411
+ ...(node.tags ?? []),
1412
+ ...(node.businessRules ?? []),
1413
+ ...(node.caveats ?? []),
1414
+ ...examples,
1415
+ ].join(' ');
1416
+ }
714
1417
  function hasExactExecutableArtifactSignal(question, node) {
715
1418
  if (!EXECUTABLE_ARTIFACT_KINDS.includes(node.kind))
716
1419
  return false;
@@ -876,7 +1579,7 @@ function isAdHocAnalysisQuestion(question) {
876
1579
  const lower = question.toLowerCase();
877
1580
  if (isBusinessDefinitionQuestion(question))
878
1581
  return false;
879
- return /\b(break\s*down|breakdown|drill\s*(?:down|into)|slice|segment|split|compare|versus|vs\.?|trend|over time|top|bottom|best|worst|highest|lowest|rank|ranking|performed better|better performing|why|what drove|driver|drivers|top movers?|changed?|change|dropped?|drop|decreased?|decrease|declined?|decline|increased?|increase|anomal(?:y|ies)|exceptions?|root cause|contribut(?:e|ed|ion)|variance|delta|by\s+[a-z][\w\s-]{1,40})\b/i.test(lower)
1582
+ return /\b(break\s*down|breakdown|drill\s*(?:down|into)|slice|segment|split|compare|versus|vs\.?|trend|over time|top|bottom|best|worst|highest|lowest|least|fewest|minimum|min|smallest|rank|ranking|performed better|better performing|why|what drove|driver|drivers|top movers?|changed?|change|dropped?|drop|decreased?|decrease|declined?|decline|increased?|increase|anomal(?:y|ies)|exceptions?|root cause|contribut(?:e|ed|ion)|variance|delta|by\s+[a-z][\w\s-]{1,40})\b/i.test(lower)
880
1583
  || /\b(show|list|find|give)\b.+\b(account|accounts|customer|customers|product|products|order|orders|region|location|month|week|day|user|users)\b/i.test(lower);
881
1584
  }
882
1585
  function isFilteredEntityQuestion(question) {