@duckcodeailabs/dql-cli 1.8.1 → 1.8.2

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.
@@ -145,6 +145,7 @@ export function parseAgentRunRequestBody(body) {
145
145
  return {
146
146
  request: {
147
147
  question,
148
+ selectedEvidenceId: agentRunString(record.selectedEvidenceId),
148
149
  requestedMode,
149
150
  audience,
150
151
  intent: agentRunString(record.intent),
@@ -197,6 +198,10 @@ export function shouldSynthesizeAgentRunAnswer(governedAnswer) {
197
198
  return false;
198
199
  return true;
199
200
  }
201
+ export function agentAnswerHasExecutionFailure(governedAnswer) {
202
+ return typeof governedAnswer.executionError === 'string'
203
+ && governedAnswer.executionError.trim().length > 0;
204
+ }
200
205
  function businessNarrativeGaps(warnings) {
201
206
  const businessRelevant = (warnings ?? []).filter((warning) => !(/missing requested output column/i.test(warning)
202
207
  || /\bquery plan\b/i.test(warning)
@@ -280,16 +285,20 @@ function recordConversationTurn(store, threadId, run) {
280
285
  // Conversation persistence is additive; a failed write must not fail the run.
281
286
  }
282
287
  }
283
- function conversationTurnInputFromRun(run) {
288
+ export function conversationTurnInputFromRun(run) {
284
289
  const artifact = run.artifacts.find((candidate) => candidate.kind === 'answer')
285
290
  ?? run.artifacts.find((candidate) => candidate.kind === 'research_run')
286
291
  ?? run.artifacts[0];
287
292
  const payload = agentRunRecord(artifact?.payload);
288
293
  const result = agentRunRecord(payload?.result);
289
294
  const columns = conversationResultColumns(result?.columns);
290
- const rows = Array.isArray(result?.rows)
291
- ? result.rows.filter((row) => Boolean(row && typeof row === 'object' && !Array.isArray(row))).slice(0, 8)
295
+ // The visual preview stays tiny, but member resolution needs a wider bounded
296
+ // value window. Deriving dimensions from only the eight preview rows caused a
297
+ // valid row 9/10 member to disappear before the next turn (AGT-012/E2E-010).
298
+ const memberRows = Array.isArray(result?.rows)
299
+ ? result.rows.filter((row) => Boolean(row && typeof row === 'object' && !Array.isArray(row))).slice(0, 24)
292
300
  : [];
301
+ const rows = memberRows.slice(0, 8);
293
302
  const rowsSample = rows.map((row) => columns.map((column) => row[column]));
294
303
  const contextPack = agentRunRecord(payload?.contextPack);
295
304
  const questionPlan = agentRunRecord(contextPack?.questionPlan);
@@ -313,7 +322,7 @@ function conversationTurnInputFromRun(run) {
313
322
  ? {
314
323
  columns,
315
324
  rowsSample,
316
- dimensionValues: conversationDimensionValues(columns, rows),
325
+ dimensionValues: conversationDimensionValues(columns, memberRows),
317
326
  measureColumns,
318
327
  rowCount: typeof rowCountRaw === 'number' ? rowCountRaw : rows.length || undefined,
319
328
  }
@@ -1445,6 +1454,7 @@ export async function startLocalServer(opts) {
1445
1454
  || governedAnswer.sourceCertifiedBlock
1446
1455
  || governedAnswer.dqlArtifact?.kind === 'certified_block'));
1447
1456
  const isExploratory = Boolean(governedAnswer.exploratoryCandidate);
1457
+ const isExecutionFailure = agentAnswerHasExecutionFailure(governedAnswer);
1448
1458
  const isGroundingGap = governedAnswer.kind === 'no_answer'
1449
1459
  && governedAnswer.refusalCode === 'grounding_gap'
1450
1460
  && !isExploratory;
@@ -1465,7 +1475,7 @@ export async function startLocalServer(opts) {
1465
1475
  const needsClarification = governedAnswer.kind === 'no_answer'
1466
1476
  && !isGroundingGap && !isProviderError && !isModelDeclined && !isPolicyBlocked;
1467
1477
  const sql = governedAnswer.proposedSql ?? governedAnswer.sql;
1468
- const runnableSql = governedAnswer.kind === 'no_answer' || (isExploratory && !governedAnswer.result)
1478
+ const runnableSql = governedAnswer.kind === 'no_answer' || isExecutionFailure || (isExploratory && !governedAnswer.result)
1469
1479
  ? undefined
1470
1480
  : sql;
1471
1481
  // Render executed rows deterministically for ordinary lookups. A second LLM
@@ -1504,14 +1514,14 @@ export async function startLocalServer(opts) {
1504
1514
  synthesizedAnswer = undefined;
1505
1515
  }
1506
1516
  }
1507
- const status = isProviderError
1517
+ const status = isProviderError || isExecutionFailure
1508
1518
  ? 'blocked'
1509
1519
  : needsClarification
1510
1520
  ? 'needs_clarification'
1511
1521
  : isCertified || isSemantic
1512
1522
  ? 'completed'
1513
1523
  : 'needs_review';
1514
- const trustState = isProviderError
1524
+ const trustState = isProviderError || isExecutionFailure
1515
1525
  ? 'blocked'
1516
1526
  : needsClarification
1517
1527
  ? 'not_applicable'
@@ -1520,7 +1530,7 @@ export async function startLocalServer(opts) {
1520
1530
  : isSemantic
1521
1531
  ? 'governed'
1522
1532
  : 'review_required';
1523
- const stopReason = isProviderError
1533
+ const stopReason = isProviderError || isExecutionFailure
1524
1534
  ? 'blocked'
1525
1535
  : needsClarification
1526
1536
  ? 'needs_clarification'
@@ -1531,33 +1541,41 @@ export async function startLocalServer(opts) {
1531
1541
  : 'human_review_required';
1532
1542
  const nextActions = needsClarification
1533
1543
  ? [{ id: 'clarify', label: 'Clarify question', route: 'generated_answer' }]
1534
- : [
1535
- { id: 'create-block', label: governedAnswer.dqlArtifact ? 'Review DQL draft' : 'Create DQL draft', route: 'dql_block_draft', artifactKind: 'dql_block_draft' },
1536
- { id: 'research-gap', label: 'Research deeper', route: 'research' },
1537
- ...(runnableSql ? [{
1538
- id: 'insert-sql',
1539
- label: governedAnswer.dqlArtifact ? 'Insert as DQL cell' : 'Insert SQL preview',
1540
- route: 'sql_cell',
1541
- artifactKind: 'sql_cell',
1542
- }] : []),
1543
- ];
1544
+ : isExecutionFailure
1545
+ ? [{ id: 'retry-after-connection', label: 'Retry after fixing the database connection', route: 'generated_answer' }]
1546
+ : isGroundingGap
1547
+ ? [{ id: 'research-gap', label: 'Research missing metadata coverage', route: 'research', artifactKind: 'research_run' }]
1548
+ : [
1549
+ { id: 'create-block', label: governedAnswer.dqlArtifact ? 'Review DQL draft' : 'Create DQL draft', route: 'dql_block_draft', artifactKind: 'dql_block_draft' },
1550
+ { id: 'research-gap', label: 'Research deeper', route: 'research' },
1551
+ ...(runnableSql ? [{
1552
+ id: 'insert-sql',
1553
+ label: governedAnswer.dqlArtifact ? 'Insert as DQL cell' : 'Insert SQL preview',
1554
+ route: 'sql_cell',
1555
+ artifactKind: 'sql_cell',
1556
+ }] : []),
1557
+ ];
1544
1558
  return {
1545
1559
  resolvedRoute,
1546
1560
  answerRefusalCode: governedAnswer.kind === 'no_answer' ? governedAnswer.refusalCode : undefined,
1547
1561
  answerTier: governedAnswer.route?.tier,
1548
- summary: governedAnswer.route?.label ?? (isCertified ? 'Answered from certified DQL context.' : isExploratory ? 'Exploratory DBT-grounded analysis requires review.' : 'Answered with review-required generated analysis.'),
1562
+ summary: isExecutionFailure
1563
+ ? 'The governed query could not be executed.'
1564
+ : governedAnswer.route?.label ?? (isCertified ? 'Answered from certified DQL context.' : isExploratory ? 'Exploratory DBT-grounded analysis requires review.' : 'Answered with review-required generated analysis.'),
1549
1565
  answer: synthesizedAnswer ?? governedAnswer.answer ?? governedAnswer.text,
1550
1566
  status,
1551
1567
  trustState,
1552
1568
  stopReason,
1553
- artifacts: governedAnswer.kind === 'no_answer'
1554
- // A refusal still keeps the DQL draft the answer loop produced (when any),
1555
- // so the "Review DQL draft" next-action isn't a dead link and the user can
1556
- // see the SQL that was about to run. Provider outages carry no draft.
1557
- ? (governedAnswer.dqlArtifact && !isProviderError
1558
- ? [agentRunArtifact('dql_block_draft', 'DQL draft (review required)', governedAnswer.dqlArtifact, undefined, 'review_required')]
1559
- : [])
1560
- : [agentRunArtifact('answer', isCertified ? 'Certified answer' : isSemantic ? 'Governed semantic answer' : isExploratory ? 'Exploratory DBT-grounded answer' : 'Review-required answer', governedAnswer, governedAnswer.sourceCertifiedBlock ?? governedAnswer.block?.name, isCertified ? 'certified' : isSemantic ? 'governed' : 'review_required')],
1569
+ artifacts: isExecutionFailure
1570
+ ? []
1571
+ : governedAnswer.kind === 'no_answer'
1572
+ // A refusal still keeps the DQL draft the answer loop produced (when any),
1573
+ // so the "Review DQL draft" next-action isn't a dead link and the user can
1574
+ // see the SQL that was about to run. Provider outages carry no draft.
1575
+ ? (governedAnswer.dqlArtifact && !isProviderError && !isGroundingGap && !isModelDeclined && !isPolicyBlocked
1576
+ ? [agentRunArtifact('dql_block_draft', 'DQL draft (review required)', governedAnswer.dqlArtifact, undefined, 'review_required')]
1577
+ : [])
1578
+ : [agentRunArtifact('answer', isCertified ? 'Certified answer' : isSemantic ? 'Governed semantic answer' : isExploratory ? 'Exploratory DBT-grounded answer' : 'Review-required answer', governedAnswer, governedAnswer.sourceCertifiedBlock ?? governedAnswer.block?.name, isCertified ? 'certified' : isSemantic ? 'governed' : 'review_required')],
1561
1579
  evaluations: [
1562
1580
  agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to governed answer.', {
1563
1581
  plannedRoute: route,
@@ -1586,6 +1604,7 @@ export async function startLocalServer(opts) {
1586
1604
  : isExploratory
1587
1605
  ? 'The answer used bounded DBT/schema evidence because no certified modeled relationship path covered the request. It is review-required and was not certified.'
1588
1606
  : 'The answer is generated or semantic-layer backed and remains review-required.', governedAnswer.route),
1607
+ ...(isExecutionFailure ? [agentRunEvaluation('query-execution', 'Query execution', false, 'blocking', `The governed query failed before it produced a result: ${governedAnswer.executionError}`)] : []),
1589
1608
  ...(isGroundingGap ? [
1590
1609
  {
1591
1610
  ...agentRunEvaluation('grounding-gap', 'Metadata grounding', false, 'warning', 'The bounded lookup could not prove the required metadata grounding. No automatic retry or Research escalation was started.', {
@@ -2171,6 +2190,7 @@ export async function startLocalServer(opts) {
2171
2190
  : undefined;
2172
2191
  const task = buildLocalContextPack(projectRoot, {
2173
2192
  question: request.question,
2193
+ focusObjectKey: request.selectedEvidenceId,
2174
2194
  followUp,
2175
2195
  priorContextPackId: agentRunString(request.conversationContext?.contextPackId),
2176
2196
  conversationTopicRelation: topicRelation === 'continuation'
@@ -2246,9 +2266,9 @@ export async function startLocalServer(opts) {
2246
2266
  };
2247
2267
  }
2248
2268
  if ((candidate.kind === 'semantic_metric' || candidate.kind === 'semantic_member')
2249
- && semanticEvidence.has(candidate.id)
2250
- && pack.routeDecision.route !== 'clarify'
2251
- && pack.routeDecision.route !== 'conflict') {
2269
+ && (semanticEvidence.has(candidate.id) || request.selectedEvidenceId === candidate.id)
2270
+ && (request.selectedEvidenceId === candidate.id
2271
+ || (pack.routeDecision.route !== 'clarify' && pack.routeDecision.route !== 'conflict'))) {
2252
2272
  const requestedDimensions = pack.questionPlan.requestedShape.dimensions.map((dimension) => dimension.toLowerCase());
2253
2273
  const availableDimensions = (candidate.dimensions ?? []).map((dimension) => dimension.toLowerCase());
2254
2274
  const dimensionsFit = requestedDimensions.length === 0 || requestedDimensions.every((requested) => availableDimensions.some((available) => available === requested || available.endsWith(`.${requested}`)));
@@ -8722,6 +8742,12 @@ export async function startLocalServer(opts) {
8722
8742
  }));
8723
8743
  return;
8724
8744
  }
8745
+ const provider = semanticConfig?.provider ?? semanticDetectedProvider ?? 'dql';
8746
+ const dbtManifestReady = provider === 'dbt'
8747
+ ? hasDbtSemanticManifest(projectRoot, semanticConfig?.projectPath)
8748
+ : false;
8749
+ const metricFlowReady = provider === 'dbt' ? hasMetricFlowCli() : false;
8750
+ const dbtExecutionReady = dbtManifestReady && metricFlowReady;
8725
8751
  const metrics = semanticLayer.listMetrics().map((m) => ({
8726
8752
  name: m.name,
8727
8753
  label: m.label,
@@ -8736,6 +8762,7 @@ export async function startLocalServer(opts) {
8736
8762
  typeParams: m.typeParams ?? null,
8737
8763
  filter: m.filter ?? null,
8738
8764
  source: m.source ?? null,
8765
+ execution: semanticMetricExecutionCapability(m.name, semanticLayer, provider, metricFlowReady, connection?.driver),
8739
8766
  }));
8740
8767
  const measures = semanticLayer.listMeasures().map((m) => ({
8741
8768
  name: m.name,
@@ -8833,12 +8860,6 @@ export async function startLocalServer(opts) {
8833
8860
  owner: q.owner ?? null,
8834
8861
  source: q.source ?? null,
8835
8862
  }));
8836
- const provider = semanticConfig?.provider ?? semanticDetectedProvider ?? 'dql';
8837
- const dbtManifestReady = provider === 'dbt'
8838
- ? hasDbtSemanticManifest(projectRoot, semanticConfig?.projectPath)
8839
- : false;
8840
- const metricFlowReady = provider === 'dbt' ? hasMetricFlowCli() : false;
8841
- const dbtExecutionReady = dbtManifestReady && metricFlowReady;
8842
8863
  const dbtExecutionSetup = dbtExecutionReady
8843
8864
  ? null
8844
8865
  : !dbtManifestReady && !metricFlowReady
@@ -9753,8 +9774,20 @@ export async function startLocalServer(opts) {
9753
9774
  tableMapping,
9754
9775
  });
9755
9776
  if (!composed) {
9777
+ const provider = semanticConfig?.provider ?? semanticDetectedProvider ?? 'dql';
9778
+ const metricFlowReady = provider === 'dbt' && hasMetricFlowCli();
9779
+ const blocked = metrics.map((metricName) => ({
9780
+ metric: metricName,
9781
+ ...semanticMetricExecutionCapability(metricName, semanticLayer, provider, metricFlowReady, driver),
9782
+ })).filter((capability) => capability.status !== 'ready');
9756
9783
  res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
9757
- res.end(serializeJSON({ error: `Could not compose query for metrics: [${metrics.join(', ')}]` }));
9784
+ res.end(serializeJSON({
9785
+ error: blocked.length > 0
9786
+ ? blocked.map((capability) => `${capability.metric}: ${capability.reason}`).join(' ')
9787
+ : 'The selected dimensions do not share a governed join path with every selected metric.',
9788
+ code: blocked.length > 0 ? 'SEMANTIC_RUNTIME_REQUIRED' : 'SEMANTIC_FIELDS_INCOMPATIBLE',
9789
+ details: { metrics, dimensions, blocked },
9790
+ }));
9758
9791
  return;
9759
9792
  }
9760
9793
  // Execute the composed SQL against the resolved connection
@@ -9839,8 +9872,20 @@ export async function startLocalServer(opts) {
9839
9872
  tableMapping,
9840
9873
  });
9841
9874
  if (!composed) {
9875
+ const provider = semanticConfig?.provider ?? semanticDetectedProvider ?? 'dql';
9876
+ const metricFlowReady = provider === 'dbt' && hasMetricFlowCli();
9877
+ const blocked = metrics.map((metricName) => ({
9878
+ metric: metricName,
9879
+ ...semanticMetricExecutionCapability(metricName, semanticLayer, provider, metricFlowReady, driver),
9880
+ })).filter((capability) => capability.status !== 'ready');
9842
9881
  res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
9843
- res.end(serializeJSON({ error: 'Could not compose semantic block preview SQL.' }));
9882
+ res.end(serializeJSON({
9883
+ error: blocked.length > 0
9884
+ ? blocked.map((capability) => `${capability.metric}: ${capability.reason}`).join(' ')
9885
+ : 'The selected dimensions do not share a governed join path with every selected metric.',
9886
+ code: blocked.length > 0 ? 'SEMANTIC_RUNTIME_REQUIRED' : 'SEMANTIC_FIELDS_INCOMPATIBLE',
9887
+ details: { metrics, dimensions, blocked },
9888
+ }));
9844
9889
  return;
9845
9890
  }
9846
9891
  const prepared = prepareLocalExecution(composed.sql, targetConnection, projectRoot, projectConfig);
@@ -13671,6 +13716,28 @@ export function openBlockStudioDocument(projectRoot, relativePath, semanticLayer
13671
13716
  validation: validateBlockStudioSource(source, semanticLayer),
13672
13717
  };
13673
13718
  }
13719
+ function semanticMetricExecutionCapability(metricName, semanticLayer, provider, metricFlowReady, driver) {
13720
+ if (provider === 'dbt' && metricFlowReady) {
13721
+ return { status: 'ready', engine: 'metricflow', reason: null };
13722
+ }
13723
+ const native = semanticLayer.composeQuery({ metrics: [metricName], dimensions: [], driver });
13724
+ if (native)
13725
+ return { status: 'ready', engine: 'native', reason: null };
13726
+ const metric = semanticLayer.getMetric(metricName);
13727
+ if (provider === 'dbt') {
13728
+ const kind = metric?.metricType || metric?.aggregation || metric?.type || 'metric';
13729
+ return {
13730
+ status: 'requires_setup',
13731
+ engine: null,
13732
+ reason: `${kind} metric requires MetricFlow execution. Install/configure the MetricFlow CLI, then refresh the semantic layer.`,
13733
+ };
13734
+ }
13735
+ return {
13736
+ status: 'unsupported',
13737
+ engine: null,
13738
+ reason: 'The metric does not have enough composable measure and relation metadata.',
13739
+ };
13740
+ }
13674
13741
  function parseBlockStudioArrayField(source, key) {
13675
13742
  const match = source.match(new RegExp(`\\b${key}\\s*=\\s*\\[([\\s\\S]*?)\\]`, 'i'));
13676
13743
  if (!match)
@@ -13905,10 +13972,18 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
13905
13972
  return { sql: null, diagnostics, semanticRefs };
13906
13973
  }
13907
13974
  if (!composed) {
13975
+ const provider = options?.projectConfig && isDbtSemanticRuntime(options.projectConfig, options.detectedProvider, semanticLayer) ? 'dbt' : (options?.detectedProvider ?? 'dql');
13976
+ const metricFlowReady = provider === 'dbt' && hasMetricFlowCli();
13977
+ const reasons = metrics.map((metricName) => {
13978
+ const capability = semanticMetricExecutionCapability(metricName, semanticLayer, provider, metricFlowReady, options?.driver);
13979
+ return capability.status === 'ready' ? null : `${metricName}: ${capability.reason}`;
13980
+ }).filter((reason) => Boolean(reason));
13908
13981
  diagnostics.push({
13909
13982
  severity: 'error',
13910
13983
  code: 'semantic_compose_failed',
13911
- message: `Could not compose SQL for semantic block metrics: [${metrics.join(', ')}].`,
13984
+ message: reasons.length > 0
13985
+ ? `Could not compose SQL for semantic block metrics. ${reasons.join(' ')}`
13986
+ : `Could not compose SQL for semantic block metrics: [${metrics.join(', ')}]. Check that the selected dimensions share a governed join path.`,
13912
13987
  });
13913
13988
  return { sql: null, diagnostics, semanticRefs };
13914
13989
  }