@duckcodeailabs/dql-cli 1.11.1 → 1.11.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.
@@ -1811,6 +1811,20 @@ export async function startLocalServer(opts) {
1811
1811
  status: 'blocked',
1812
1812
  trustState: 'blocked',
1813
1813
  stopReason: 'blocked',
1814
+ artifacts: [agentRunArtifact('answer', 'AI answer provider failed', {
1815
+ kind: 'no_answer',
1816
+ refusalCode: 'provider_error',
1817
+ text: message,
1818
+ answer: message,
1819
+ ...(routeDecision?.resolvedAnalyticalPlan
1820
+ ? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
1821
+ : {}),
1822
+ providerFailure: {
1823
+ code: 'AI_PROVIDER_FAILURE',
1824
+ message,
1825
+ recoverable: true,
1826
+ },
1827
+ }, undefined, 'blocked')],
1814
1828
  evaluations: [
1815
1829
  agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to governed answer.'),
1816
1830
  agentRunEvaluation('ai-provider', 'AI provider', false, 'blocking', message, { originalErrorType: error instanceof Error ? error.name : typeof error }),
@@ -1836,12 +1850,12 @@ export async function startLocalServer(opts) {
1836
1850
  && !isExploratory;
1837
1851
  const isProviderError = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'provider_error';
1838
1852
  const isAnalyticalFailure = Boolean(governedAnswer.analyticalFailure);
1839
- const isTerminalFailure = isProviderError || isExecutionFailure || isAnalyticalFailure;
1840
1853
  // AGT-004: a rejected attribution/export/proof policy is a deliberate
1841
1854
  // governance boundary, not an ambiguous user question and not a repairable
1842
1855
  // retrieval miss. Keep the precise policy detail visible, but never burn two
1843
1856
  // more provider calls retrying the same incompatible candidate.
1844
1857
  const isPolicyBlocked = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'policy_blocked';
1858
+ const isTerminalFailure = isProviderError || isExecutionFailure || isAnalyticalFailure || isPolicyBlocked;
1845
1859
  // The model tried to compose a governed query and declined despite having usable
1846
1860
  // context (e.g. it wasn't confident about a multi-table join). The answer loop
1847
1861
  // has already spent its one evidence-aware repair. Keep this terminal and
@@ -1933,7 +1947,16 @@ export async function startLocalServer(opts) {
1933
1947
  : [{ id: 'review-analytical-failure', label: 'Review the plan, DQL, SQL, and safe repair actions', route: 'blocked' }]
1934
1948
  : isProviderError
1935
1949
  ? [{ id: 'retry-after-provider', label: 'Retry after fixing the AI provider', route: 'generated_answer' }]
1936
- : [{ id: 'retry-after-connection', label: 'Retry after fixing the database connection', route: 'generated_answer' }];
1950
+ : isPolicyBlocked
1951
+ ? [
1952
+ ...(governedAnswer.dqlArtifact
1953
+ ? [{ id: 'edit_dql', label: 'Open DQL to repair', route: 'dql_block_draft', artifactKind: 'dql_block_draft' }]
1954
+ : []),
1955
+ ...(sql
1956
+ ? [{ id: 'open_sql_notebook', label: 'Open SQL in Notebook', route: 'sql_cell', artifactKind: 'sql_cell' }]
1957
+ : []),
1958
+ ]
1959
+ : [{ id: 'retry-after-connection', label: 'Retry after fixing the database connection', route: 'generated_answer' }];
1937
1960
  const nextActions = needsClarification
1938
1961
  ? [{ id: 'clarify', label: 'Clarify question', route: 'generated_answer' }]
1939
1962
  : isTerminalFailure
@@ -1955,7 +1978,11 @@ export async function startLocalServer(opts) {
1955
1978
  answerRefusalCode: governedAnswer.kind === 'no_answer' ? governedAnswer.refusalCode : undefined,
1956
1979
  answerTier: governedAnswer.route?.tier,
1957
1980
  summary: isTerminalFailure
1958
- ? 'The governed query could not be executed.'
1981
+ ? governedAnswer.analyticalFailure?.message
1982
+ ?? governedAnswer.executionError
1983
+ ?? governedAnswer.answer
1984
+ ?? governedAnswer.text
1985
+ ?? 'The governed query could not be executed.'
1959
1986
  : governedAnswer.route?.label ?? (isCertified ? 'Answered from certified DQL context.' : isExploratory ? 'Exploratory DBT-grounded analysis requires review.' : 'Answered with review-required generated analysis.'),
1960
1987
  answer: synthesizedAnswer ?? governedAnswer.answer ?? governedAnswer.text,
1961
1988
  ...(governedAnswer.clarificationOptions?.length
@@ -2384,87 +2411,88 @@ export async function startLocalServer(opts) {
2384
2411
  ],
2385
2412
  };
2386
2413
  },
2387
- dql_block_draft: async ({ request, routeDecision, attempt, repairHint }) => {
2414
+ dql_block_draft: async ({ request, routeDecision, attempt, repairHint, emit, runId, maxRepairAttempts, }) => {
2388
2415
  const contextRecord = agentRunRecord(request.conversationContext);
2389
2416
  const carriedDqlArtifact = normalizeDqlArtifactReference(contextRecord?.dqlArtifact)
2390
2417
  ?? normalizeDqlArtifactReference(request.workspaceContext?.dqlArtifact);
2391
- if (carriedDqlArtifact && (attempt ?? 0) === 0) {
2392
- const sourceQuestion = agentRunString(contextRecord?.sourceQuestion) ?? request.question;
2393
- const session = await createDqlArtifactGenerationSessionForProject(projectRoot, {
2394
- question: sourceQuestion,
2395
- dqlArtifact: carriedDqlArtifact,
2396
- inputMode: 'upload',
2397
- inputPath: carriedDqlArtifact.sourcePath,
2398
- domain: agentRunWorkspaceValue(request, 'domain'),
2399
- owner: agentRunWorkspaceValue(request, 'owner'),
2400
- tags: ['agent-run', 'review-required'],
2401
- contextPackId: agentRunString(contextRecord?.contextPackId),
2402
- routeIntent: agentRunString(contextRecord?.route),
2403
- sourceBlock: carriedDqlArtifact.sourcePath,
2404
- }, semanticLayer);
2405
- const candidate = session.candidates[0];
2406
- const validation = candidate?.validation && typeof candidate.validation === 'object'
2407
- ? candidate.validation
2408
- : undefined;
2409
- const ready = validation?.valid !== false;
2410
- const payload = {
2411
- target: 'block',
2412
- name: candidate?.name ?? carriedDqlArtifact.name ?? 'DQL block draft',
2413
- path: candidate?.draftSave?.path ?? candidate?.savedPath,
2414
- importId: session.id,
2415
- candidateId: candidate?.id,
2416
- source: candidate?.dqlSource,
2417
- dqlSource: candidate?.dqlSource,
2418
- dqlArtifact: carriedDqlArtifact,
2419
- certifierVerdict: {
2420
- ready,
2421
- diagnostics: validation?.diagnostics ?? [],
2422
- },
2423
- generation: session.generation,
2424
- };
2425
- return {
2426
- summary: ready
2427
- ? 'Saved the generated DQL artifact as a review-required block draft.'
2428
- : 'Saved the generated DQL artifact as a block draft with review blockers or warnings.',
2429
- artifacts: [agentRunArtifact('dql_block_draft', payload.name, payload, payload.path)],
2418
+ // UI-016 / E2E-015: a cold Block AI request uses the same governed answer
2419
+ // pipeline as Ask and Notebook. Generation is write-free. The returned
2420
+ // ownerless artifact is persisted only when the user explicitly chooses
2421
+ // Add to Block Studio.
2422
+ const governedResult = carriedDqlArtifact && (attempt ?? 0) === 0
2423
+ ? {
2424
+ summary: 'Reused the governed answer artifact as a transient Block AI draft.',
2425
+ answer: 'Prepared the existing governed DQL as an ownerless review draft. No file was changed.',
2426
+ artifacts: [agentRunArtifact('answer', carriedDqlArtifact.name ?? 'Governed DQL answer', { dqlArtifact: carriedDqlArtifact }, undefined, carriedDqlArtifact.trustState)],
2430
2427
  evaluations: [
2431
- agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to DQL block draft generation.'),
2432
- agentRunEvaluation('dql-artifact-reuse', 'Used generated DQL artifact', true, 'info', 'Persisted the existing DQL artifact without regenerating it from SQL.', { importId: session.id, path: payload.path }),
2433
- agentRunEvaluation('certification-boundary', 'Certification boundary', ready, 'warning', ready
2434
- ? 'The draft has no automatic validation blockers, but certification still requires human review.'
2435
- : 'The draft has validation blockers that must be resolved before certification review.', payload),
2436
- ],
2437
- nextActions: [
2438
- { id: 'open-review', label: 'Open review checklist', artifactKind: 'dql_block_draft' },
2439
- { id: 'build-app', label: 'Build app from block', route: 'app_build', artifactKind: 'app_draft' },
2428
+ agentRunEvaluation('dql-artifact-reuse', 'Reused governed DQL artifact', true, 'info', 'Reused the exact answer artifact without regenerating SQL or writing a block file.'),
2440
2429
  ],
2441
- };
2430
+ }
2431
+ : await answerRunExecutor({
2432
+ request,
2433
+ route: 'generated_answer',
2434
+ routeDecision,
2435
+ attempt,
2436
+ repairHint,
2437
+ emit,
2438
+ runId,
2439
+ maxRepairAttempts,
2440
+ });
2441
+ if (governedResult.status === 'blocked' || governedResult.status === 'needs_clarification') {
2442
+ return governedResult;
2443
+ }
2444
+ const governedPayload = (governedResult.artifacts ?? [])
2445
+ .map((artifact) => agentRunRecord(artifact.payload))
2446
+ .find((payload) => Boolean(normalizeDqlArtifactReference(payload?.dqlArtifact)
2447
+ ?? normalizeDqlArtifactReference(payload)));
2448
+ const directDqlArtifact = normalizeDqlArtifactReference(governedPayload);
2449
+ const governedAnswer = governedPayload
2450
+ ? directDqlArtifact
2451
+ ? { dqlArtifact: directDqlArtifact }
2452
+ : governedPayload
2453
+ : undefined;
2454
+ const dqlArtifact = governedAnswer
2455
+ ? ownerlessReviewDqlArtifactFromAnswer(governedAnswer, request.question)
2456
+ : undefined;
2457
+ if (!dqlArtifact) {
2458
+ return governedResult;
2442
2459
  }
2443
- // P4 (partial): a direct "build a block from a cold prompt" still uses the
2444
- // tool-less buildFromPrompt path. Unlike a SQL cell, a block needs SQL→DQL-block
2445
- // CONSTRUCTION (name, outputs, grain, certifier verdict, reflection), which
2446
- // buildFromPrompt owns — routing this through answer() cleanly requires driving
2447
- // that construction from the tool-rich pipeline (thread captureGeneratedDraft
2448
- // through the governed answer), a larger change deferred to its own session.
2449
- // NOTE: the two-step "ask a question → Create DQL draft" flow ALREADY routes
2450
- // block creation through the tool-rich governed pipeline via the carried-artifact
2451
- // branch above, so only this cold-start direct-build case remains tool-less.
2452
- const result = await buildAgentPromptArtifact(request, 'block', { attempt, repairHint });
2453
- const ready = result.target === 'block' ? result.certifierVerdict.ready : false;
2460
+ const sql = dqlArtifact.compiledSql
2461
+ ?? agentRunString(governedPayload?.proposedSql)
2462
+ ?? agentRunString(governedPayload?.sql);
2463
+ const result = {
2464
+ target: 'block',
2465
+ name: dqlArtifact.name ?? deriveGeneratedDraftSlug(request.question),
2466
+ source: dqlArtifact.source,
2467
+ dqlSource: dqlArtifact.source,
2468
+ dqlArtifact,
2469
+ ...(sql ? { sql, sqlPreview: sql } : {}),
2470
+ ...(governedPayload?.result ? { result: governedPayload.result } : {}),
2471
+ description: request.question,
2472
+ dimensions: dqlArtifact.dimensions ?? [],
2473
+ outputs: [...(dqlArtifact.dimensions ?? []), ...(dqlArtifact.metrics ?? [])],
2474
+ certifierVerdict: {
2475
+ ready: true,
2476
+ blocking: [],
2477
+ warnings: [
2478
+ 'Ownership is intentionally absent from the AI draft and is assigned by a human during promotion.',
2479
+ 'Review the generated DQL, compiled SQL, grain, and result before promotion.',
2480
+ ],
2481
+ },
2482
+ };
2454
2483
  return {
2455
- summary: ready
2456
- ? 'Created a DQL block draft that is ready for human certification review.'
2457
- : 'Created a DQL block draft with review blockers or warnings.',
2458
- artifacts: [agentRunArtifact('dql_block_draft', result.target === 'block' ? result.name : 'DQL block draft', result, result.target === 'block' ? result.path : undefined)],
2484
+ summary: 'Prepared an ownerless, review-required DQL block draft. No file was changed.',
2485
+ answer: governedResult.answer,
2486
+ status: 'needs_review',
2487
+ trustState: 'review_required',
2488
+ stopReason: 'human_review_required',
2489
+ artifacts: [agentRunArtifact('dql_block_draft', result.name, result, undefined, 'review_required')],
2459
2490
  evaluations: [
2460
- agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to DQL block draft generation.'),
2461
- agentRunEvaluation('certification-boundary', 'Certification boundary', ready, 'warning', ready
2462
- ? 'The draft has no automatic certifier blockers, but certification still requires human review.'
2463
- : 'The draft has certifier blockers that must be resolved before certification review.', result),
2491
+ ...(governedResult.evaluations ?? []),
2492
+ agentRunEvaluation('block-commit-boundary', 'Explicit Block Studio commit', true, 'warning', 'Generation produced an ownerless transient DQL artifact. Add to Block Studio is the only action that writes the review draft.', result),
2464
2493
  ],
2465
2494
  nextActions: [
2466
- { id: 'open-review', label: 'Open review checklist', artifactKind: 'dql_block_draft' },
2467
- { id: 'build-app', label: 'Build app from block', route: 'app_build', artifactKind: 'app_draft' },
2495
+ { id: 'add-to-block-studio', label: 'Add to Block Studio', artifactKind: 'dql_block_draft' },
2468
2496
  ],
2469
2497
  };
2470
2498
  },
@@ -15188,7 +15216,7 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
15188
15216
  diagnostics.push({
15189
15217
  severity: 'error',
15190
15218
  code: 'semantic_metric_missing',
15191
- message: 'Semantic block is missing a metric. Add metric = "metric_name" or metrics = ["metric_name"].',
15219
+ message: 'Semantic block is missing a metric. Add metrics = ["metric_name"].',
15192
15220
  });
15193
15221
  return { sql: null, diagnostics, semanticRefs };
15194
15222
  }
@@ -15214,12 +15242,9 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
15214
15242
  if (diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
15215
15243
  return { sql: null, diagnostics, semanticRefs };
15216
15244
  }
15217
- // Import-adapter shape: a semantic block may carry a pre-compiled `query` (the
15218
- // governed metric already expanded to runnable SQL). When present, RUN THAT —
15219
- // the `metric` field is provenance/governance, not something to recompile. This
15220
- // keeps metric-bound blocks runnable offline (the full MetricFlow engine may be
15221
- // unavailable). We still validated the metric exists above. Returning the query
15222
- // as the composed SQL lets the normal {{ ref() }} resolution + execution apply.
15245
+ // Legacy import-adapter compatibility: older semantic blocks may carry a
15246
+ // pre-compiled `query`. Current writers keep compiled SQL outside semantic DQL,
15247
+ // but existing files remain executable while they are migrated by the formatter.
15223
15248
  const precompiledQuery = extractBlockStudioSql(source);
15224
15249
  if (precompiledQuery) {
15225
15250
  return { sql: precompiledQuery, diagnostics, semanticRefs };
@@ -15903,6 +15928,95 @@ function saveDqlGenerationDraftForProject(projectRoot, importId, candidate) {
15903
15928
  export function sanitizeAgentBlockDraftSource(source) {
15904
15929
  return source.replace(/^\s*owner\s*=\s*.+(?:\r?\n|$)/gmi, '');
15905
15930
  }
15931
+ /**
15932
+ * UI-016 / CONTRACT-002 / AGT-021 — derive the transient Block AI artifact
15933
+ * from the exact governed answer artifact. Semantic drafts are re-rendered
15934
+ * through the one canonical metrics/dimensions writer; custom SQL drafts retain
15935
+ * their query body. Both become ownerless, review-required drafts and never
15936
+ * inherit a saved/certified identity from their answer source.
15937
+ */
15938
+ export function ownerlessReviewDqlArtifactFromAnswer(answer, question) {
15939
+ const artifact = normalizeDqlArtifactReference(answer.result?.dqlArtifact)
15940
+ ?? normalizeDqlArtifactReference(answer.dqlArtifact);
15941
+ if (!artifact?.source)
15942
+ return undefined;
15943
+ const parsed = parseSemanticBlockConfig(artifact.source);
15944
+ const metrics = Array.from(new Set([
15945
+ ...(artifact.metrics ?? []),
15946
+ ...parsed.metrics,
15947
+ ...(parsed.metric ? [parsed.metric] : []),
15948
+ ].map((metric) => metric.trim()).filter(Boolean)));
15949
+ const dimensions = Array.from(new Set([
15950
+ ...(artifact.dimensions ?? []),
15951
+ ...parsed.dimensions,
15952
+ ].map((dimension) => dimension.trim()).filter(Boolean)));
15953
+ const name = artifact.name
15954
+ ?? artifact.source.match(/\bblock\s+"([^"]+)"/i)?.[1]
15955
+ ?? deriveGeneratedDraftSlug(question);
15956
+ const compiledSql = artifact.compiledSql
15957
+ ?? answer.result?.sql
15958
+ ?? answer.proposedSql
15959
+ ?? answer.sql;
15960
+ if (artifact.kind === 'semantic_block' || parsed.blockType === 'semantic' || metrics.length > 0) {
15961
+ const source = renderSemanticBlockSource({
15962
+ name,
15963
+ status: 'draft',
15964
+ domain: parseBlockStudioStringField(artifact.source, 'domain'),
15965
+ description: question,
15966
+ tags: ['ai-generated', 'review-required'],
15967
+ metrics,
15968
+ dimensions,
15969
+ timeDimension: artifact.timeDimension ?? (parsed.timeDimension && parsed.granularity
15970
+ ? { name: parsed.timeDimension, granularity: parsed.granularity }
15971
+ : undefined),
15972
+ requestedFilters: (artifact.filters ?? parsed.filters).flatMap((filter) => filter.values.length > 0
15973
+ ? filter.values.map((value) => `${filter.dimension}=${value}`)
15974
+ : [filter.dimension]),
15975
+ orderBy: artifact.orderBy,
15976
+ limit: artifact.limit ?? parsed.limit,
15977
+ parameters: artifact.parameters?.map((parameter) => ({
15978
+ name: parameter.name,
15979
+ type: parameter.type,
15980
+ default: parameter.default,
15981
+ policy: parameter.policy,
15982
+ binding: parameter.binding?.kind === 'semantic_filter'
15983
+ ? parameter.binding.field
15984
+ : parameter.binding?.kind === 'limit'
15985
+ ? 'limit'
15986
+ : undefined,
15987
+ })),
15988
+ visualization: metrics.length > 1 ? { chart: 'table' } : undefined,
15989
+ });
15990
+ return {
15991
+ ...artifact,
15992
+ kind: 'semantic_block',
15993
+ name,
15994
+ source,
15995
+ sourcePath: undefined,
15996
+ metrics,
15997
+ dimensions,
15998
+ persistence: 'transient',
15999
+ trustState: 'review_required',
16000
+ ...(compiledSql ? { compiledSql } : {}),
16001
+ };
16002
+ }
16003
+ let source = sanitizeAgentBlockDraftSource(artifact.source);
16004
+ if (/\bstatus\s*=\s*"[^"]*"/i.test(source)) {
16005
+ source = source.replace(/\bstatus\s*=\s*"[^"]*"/i, 'status = "draft"');
16006
+ }
16007
+ else {
16008
+ source = source.replace(/(\bblock\s+"[^"]+"\s*\{)/i, '$1\n status = "draft"');
16009
+ }
16010
+ return {
16011
+ ...artifact,
16012
+ name,
16013
+ source,
16014
+ sourcePath: undefined,
16015
+ persistence: 'transient',
16016
+ trustState: 'review_required',
16017
+ ...(compiledSql ? { compiledSql } : {}),
16018
+ };
16019
+ }
15906
16020
  export function saveBlockStudioArtifacts(projectRoot, options) {
15907
16021
  const slug = options.name.toLowerCase().replace(/[^a-z0-9_]+/g, '-').replace(/^[-_]+|[-_]+$/g, '') || 'block';
15908
16022
  const safeDomain = (options.domain ?? '')