@duckcodeailabs/dql-cli 1.12.8 → 1.12.10

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 (63) hide show
  1. package/dist/assets/dql-notebook/assets/{AgentAnswerCard-D1VcQOTJ.js → AgentAnswerCard-Dp4odOLE.js} +1 -1
  2. package/dist/assets/dql-notebook/assets/{AgentLogPage-DKvtW4Fq.js → AgentLogPage-uvVs284-.js} +1 -1
  3. package/dist/assets/dql-notebook/assets/{AiBuildDialog-apJ4HoK3.js → AiBuildDialog-DEUS19WF.js} +1 -1
  4. package/dist/assets/dql-notebook/assets/{AiBuildResult-CvNitmsw.js → AiBuildResult-B9KmBRsh.js} +1 -1
  5. package/dist/assets/dql-notebook/assets/{AiSidePanel-B2cqbPJr.js → AiSidePanel-C7BZVSkM.js} +1 -1
  6. package/dist/assets/dql-notebook/assets/{AnalyticsHome-D5I-Okbz.js → AnalyticsHome-4TgtH6SB.js} +1 -1
  7. package/dist/assets/dql-notebook/assets/{AppsView-DLtjogVz.js → AppsView-CnfNtk5W.js} +3 -3
  8. package/dist/assets/dql-notebook/assets/{BlockStudio-BmNsIfNK.js → BlockStudio-DUXN3MKz.js} +1 -1
  9. package/dist/assets/dql-notebook/assets/{BusinessArtifactView-Bvn5X8uW.js → BusinessArtifactView-D8FIgrkF.js} +1 -1
  10. package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-DnpVGZJv.js → DbtFirstModelingPage-DR8hTZ95.js} +1 -1
  11. package/dist/assets/dql-notebook/assets/{GitPage-C_5u2aCj.js → GitPage-BS2sm9zh.js} +1 -1
  12. package/dist/assets/dql-notebook/assets/{GlobalAiRail-ZOHo4H6j.js → GlobalAiRail-B_MZsB0z.js} +1 -1
  13. package/dist/assets/dql-notebook/assets/{GovernedContextPage-CUaYIqfd.js → GovernedContextPage-CNM5UCAH.js} +1 -1
  14. package/dist/assets/dql-notebook/assets/{HelpDocsPage-6SmmBvJh.js → HelpDocsPage-Bc5pLakt.js} +1 -1
  15. package/dist/assets/dql-notebook/assets/{HomePage-LDu8z_nW.js → HomePage-DQSo-K4y.js} +1 -1
  16. package/dist/assets/dql-notebook/assets/{LineageDAG-VFSCvevO.js → LineageDAG-RdpWmGmR.js} +1 -1
  17. package/dist/assets/dql-notebook/assets/{LineageDetailView-CuGG_JDv.js → LineageDetailView-CZEx2r1H.js} +1 -1
  18. package/dist/assets/dql-notebook/assets/{LineageDrawer-B2SVsEw1.js → LineageDrawer-P7ZdOW8-.js} +1 -1
  19. package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-qWhGM3bh.js → LineagePathBreadcrumb-DTUMFkJ8.js} +1 -1
  20. package/dist/assets/dql-notebook/assets/{MiniLineageGraph-BHxUFWDi.js → MiniLineageGraph-Ctp_yFh8.js} +1 -1
  21. package/dist/assets/dql-notebook/assets/{NewBlockModal-oo3fU5OW.js → NewBlockModal-BLtjuWGy.js} +1 -1
  22. package/dist/assets/dql-notebook/assets/{NewNotebookModal-Bl78Nnjz.js → NewNotebookModal-xgaKB2kI.js} +1 -1
  23. package/dist/assets/dql-notebook/assets/NotebookEditor-D6RgLQw-.js +202 -0
  24. package/dist/assets/dql-notebook/assets/{ReadinessPage-CL2EJLKo.js → ReadinessPage-C8hATaT_.js} +1 -1
  25. package/dist/assets/dql-notebook/assets/{SetupOnboarding-Cqw2NuAn.js → SetupOnboarding-DX2f53J2.js} +1 -1
  26. package/dist/assets/dql-notebook/assets/{TrustBadge-C4jIUuEE.js → TrustBadge-DPnKzRSY.js} +1 -1
  27. package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-CwxQ4T19.js +83 -0
  28. package/dist/assets/dql-notebook/assets/{answer-to-notebook-mhY-MBsX.js → answer-to-notebook-Bm9oByWA.js} +1 -1
  29. package/dist/assets/dql-notebook/assets/{arrow-left-CwMW-Dnz.js → arrow-left-D2FbLWqh.js} +1 -1
  30. package/dist/assets/dql-notebook/assets/{arrow-right-CJ_6d0z7.js → arrow-right-OYItAZ-s.js} +1 -1
  31. package/dist/assets/dql-notebook/assets/{book-open-text-B2q44mou.js → book-open-text-BnnirfAc.js} +1 -1
  32. package/dist/assets/dql-notebook/assets/{bot-CbsSLtXR.js → bot-BJNxPYFm.js} +1 -1
  33. package/dist/assets/dql-notebook/assets/{chevron-up-DSMkEFrK.js → chevron-up-DM5LX-26.js} +1 -1
  34. package/dist/assets/dql-notebook/assets/{circle-x-C5hi2hCq.js → circle-x-Dic_-4tF.js} +1 -1
  35. package/dist/assets/dql-notebook/assets/{dagre.esm-BT65gOyC.js → dagre.esm-C9zxjHHX.js} +1 -1
  36. package/dist/assets/dql-notebook/assets/{external-link-sDmXu1pG.js → external-link-DHxAcoCE.js} +1 -1
  37. package/dist/assets/dql-notebook/assets/{grip-vertical-Ca_E_fRZ.js → grip-vertical-02I-EOjL.js} +1 -1
  38. package/dist/assets/dql-notebook/assets/{index-CcZ8Dw84.js → index-Ca3UeUWI.js} +3 -3
  39. package/dist/assets/dql-notebook/assets/{link-2-DaVaJfdX.js → link-2-Delh-lai.js} +1 -1
  40. package/dist/assets/dql-notebook/assets/{list-tree-DMGqPCsV.js → list-tree-BrIZg6l6.js} +1 -1
  41. package/dist/assets/dql-notebook/assets/{maximize-2-CVJP65sF.js → maximize-2-CQOA-bbN.js} +1 -1
  42. package/dist/assets/dql-notebook/assets/{minimize-2-BnsbPUr5.js → minimize-2-CUDBPkwe.js} +1 -1
  43. package/dist/assets/dql-notebook/assets/{panel-right-open-BTTos1w4.js → panel-right-open-CwhZfkKJ.js} +1 -1
  44. package/dist/assets/dql-notebook/assets/{pencil-BDR0kEX-.js → pencil-Cf6-MgpT.js} +1 -1
  45. package/dist/assets/dql-notebook/assets/{rotate-ccw-DrQw5ZrW.js → rotate-ccw-DndbJAiz.js} +1 -1
  46. package/dist/assets/dql-notebook/assets/{semantic-fields-CgqeBot0.js → semantic-fields-DwKPY_mU.js} +1 -1
  47. package/dist/assets/dql-notebook/assets/{star-DBVkbHOE.js → star-DvcMC9wZ.js} +1 -1
  48. package/dist/assets/dql-notebook/assets/{triangle-alert-_FtKJhjm.js → triangle-alert-DUf4Ook0.js} +1 -1
  49. package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-BG1TZXp_.js → usePersistedAgentThreadId-DbdBi0oH.js} +1 -1
  50. package/dist/assets/dql-notebook/assets/{user-round-DkUToep7.js → user-round-DSt0xU3K.js} +1 -1
  51. package/dist/assets/dql-notebook/assets/{wand-sparkles-TgacCrJg.js → wand-sparkles-ro8Cq0ro.js} +1 -1
  52. package/dist/assets/dql-notebook/assets/{workflow-z-B8gUF9.js → workflow-DVJXehjU.js} +1 -1
  53. package/dist/assets/dql-notebook/assets/{wrench-CgXGBYJ-.js → wrench-DxUnoHtz.js} +1 -1
  54. package/dist/assets/dql-notebook/assets/{x-FQc3jbQ8.js → x-9Gtb3EAd.js} +1 -1
  55. package/dist/assets/dql-notebook/index.html +1 -1
  56. package/dist/local-runtime.d.ts +71 -1
  57. package/dist/local-runtime.d.ts.map +1 -1
  58. package/dist/local-runtime.js +558 -54
  59. package/dist/local-runtime.js.map +1 -1
  60. package/dist/package.json +10 -10
  61. package/package.json +10 -10
  62. package/dist/assets/dql-notebook/assets/NotebookEditor-BWMX6Xfi.js +0 -202
  63. package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-CZjUHLl5.js +0 -83
@@ -21,7 +21,7 @@ import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from '
21
21
  import { resolveRetrievalHealthStatus } from './retrieval-health.js';
22
22
  import { createDqlAgentProviderRunner, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
23
23
  import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
24
- import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, prepareConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, propose, proposePlan, recordGovernedCorrection, HintStore, defaultHintIndexPath, ensureHintIndexFresh, listHintsFromGit, getHintEvaluationFromGit, getCorrectionTraceFromGit, inspectGovernedHint, editGovernedHintCandidate, reopenGovernedHint, retireHint, supersedeHint, hintsConflict, mineJoinPatterns, reviewGovernedHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, internalRelationIdsInSql, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, projectEmbeddingProvider, isHashedEmbeddingProvider, clearProjectEmbeddingCache, upgradeVectorIndexForProject, openMetadataCatalog, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, isTrustedConversationTurn, analyticalError, withAnalyticalErrorOrigin, withAnalyticalErrorOriginSync, } from '@duckcodeailabs/dql-agent';
24
+ import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, prepareConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, normalizeWarehouseSqlFailure, parseProposal, propose, proposePlan, recordGovernedCorrection, HintStore, defaultHintIndexPath, ensureHintIndexFresh, listHintsFromGit, getHintEvaluationFromGit, getCorrectionTraceFromGit, inspectGovernedHint, editGovernedHintCandidate, reopenGovernedHint, retireHint, supersedeHint, hintsConflict, mineJoinPatterns, reviewGovernedHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, internalRelationIdsInSql, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, projectEmbeddingProvider, isHashedEmbeddingProvider, clearProjectEmbeddingCache, upgradeVectorIndexForProject, openMetadataCatalog, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, isTrustedConversationTurn, resolveInternalRelationIds, analyticalError, tagAnalyticalError, withAnalyticalErrorOrigin, withAnalyticalErrorOriginSync, } from '@duckcodeailabs/dql-agent';
25
25
  import { addSqlResultFilter, filterableResultColumns, replaceBlockStudioSql } from './sql-result-filter.js';
26
26
  import { gatherProposeEnrichment } from './propose-enrich.js';
27
27
  import { handleAppsApi, proposeAppAiBuild, recommendVisualization } from './apps-api.js';
@@ -403,6 +403,36 @@ export function analyticalFailedRunFromAgentRun(run) {
403
403
  }
404
404
  return undefined;
405
405
  }
406
+ /** Exact failed statement retained on an immutable run, if one exists. */
407
+ export function repairableSqlFromAgentRun(run) {
408
+ for (const artifact of run.artifacts) {
409
+ const payload = agentRunRecord(artifact.payload);
410
+ if (!payload)
411
+ continue;
412
+ const dqlArtifact = normalizeDqlArtifactReference(payload.dqlArtifact);
413
+ const sql = agentRunString(payload.proposedSql)
414
+ ?? agentRunString(payload.sql)
415
+ ?? dqlArtifact?.compiledSql;
416
+ if (sql?.trim())
417
+ return sql.trim();
418
+ }
419
+ return undefined;
420
+ }
421
+ /** Structured producer failure retained on the source run. */
422
+ export function warehouseFailureFromAgentRun(run) {
423
+ for (const artifact of run.artifacts) {
424
+ const payload = agentRunRecord(artifact.payload);
425
+ const failure = agentRunRecord(payload?.warehouseFailure);
426
+ if (failure?.version !== 1 || typeof failure.redactedMessage !== 'string')
427
+ continue;
428
+ return failure;
429
+ }
430
+ return undefined;
431
+ }
432
+ /** Permission, authentication, policy and unsafe-query failures are never AI-repaired. */
433
+ export function analyticalFailureAllowsDeterministicRetry(failure) {
434
+ return !failure || !['permission', 'authentication', 'unsafe', 'cancelled'].includes(failure.category);
435
+ }
406
436
  function businessNarrativeGaps(warnings) {
407
437
  const businessRelevant = (warnings ?? []).filter((warning) => !(/missing requested output column/i.test(warning)
408
438
  || /\bquery plan\b/i.test(warning)
@@ -1996,6 +2026,7 @@ export async function startLocalServer(opts) {
1996
2026
  sql: exploration.sql,
1997
2027
  exploratoryCandidate: repairedExploratoryCandidate,
1998
2028
  executionError: exploration.error,
2029
+ warehouseFailure: exploration.warehouseFailure,
1999
2030
  text: explorationAnswer,
2000
2031
  answer: explorationAnswer,
2001
2032
  trustLabel: 'Exploratory · DBT-grounded',
@@ -3242,10 +3273,21 @@ export async function startLocalServer(opts) {
3242
3273
  const semanticError = semanticCompose?.diagnostics.find((diagnostic) => diagnostic.severity === 'error')?.message;
3243
3274
  throw analyticalError(semanticError ?? `DQL artifact "${metadata.name ?? 'draft'}" produced no executable SQL.`, { origin: 'dql_compilation', stage: 'compile' });
3244
3275
  }
3245
- const prepared = prepareLocalExecution(semanticCompose?.sql ?? plan.sql, activeConnection, projectRoot, projectConfig);
3246
- const executionSql = invocationInput?.rowLimit
3247
- ? buildAgentPreviewSql(prepared.sql, invocationInput.rowLimit)
3248
- : prepared.sql;
3276
+ const preparation = await prepareAnalyticalExecutionSql({
3277
+ sql: semanticCompose?.sql ?? plan.sql,
3278
+ subject: 'DQL artifact query',
3279
+ executor,
3280
+ connection: activeConnection,
3281
+ projectRoot,
3282
+ projectConfig,
3283
+ // Preserve the existing contract: previewed Ask artifacts are explicitly
3284
+ // read-only and bounded; saved/manual DQL execution keeps its compiler-
3285
+ // governed behavior when no preview bound was requested.
3286
+ enforceReadOnly: invocationInput?.rowLimit !== undefined,
3287
+ rowLimit: invocationInput?.rowLimit,
3288
+ });
3289
+ const prepared = { sql: preparation.preparedSql, connection: preparation.connection };
3290
+ const executionSql = preparation.executedSql;
3249
3291
  const app = loadRuntimeApp(projectRoot, activePersonaAppId());
3250
3292
  const sourceDomain = metadata.domain ?? source.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1];
3251
3293
  assertAppAccess({ app, domain: sourceDomain ?? app?.domain, level: 'execute' });
@@ -3454,13 +3496,18 @@ export async function startLocalServer(opts) {
3454
3496
  if (semantic.unresolvedRefs.length > 0) {
3455
3497
  throw analyticalError(`Unknown semantic reference${semantic.unresolvedRefs.length > 1 ? 's' : ''}: ${semantic.unresolvedRefs.join(', ')}`, { origin: 'dql_compilation', stage: 'compile', code: 'semantic_ref' });
3456
3498
  }
3457
- const prepared = prepareLocalExecution(semantic.sql, activeConnection, projectRoot, projectConfig);
3458
- // Read-only enforcement runs on the statement that will ACTUALLY execute.
3459
- const readOnlyError = readOnlySqlValidationError(prepared.sql, 'Generated SQL', activeConnection.driver);
3460
- if (readOnlyError) {
3461
- throw analyticalError(readOnlyError, { origin: 'governance_gate', stage: 'validation', code: 'unsafe_sql' });
3462
- }
3463
- const bounded = buildRowBoundedSql(prepared.sql, rowBound, activeConnection.driver);
3499
+ const preparation = await prepareAnalyticalExecutionSql({
3500
+ sql: semantic.sql,
3501
+ subject: 'Generated SQL',
3502
+ executor,
3503
+ connection: activeConnection,
3504
+ projectRoot,
3505
+ projectConfig,
3506
+ enforceReadOnly: true,
3507
+ rowLimit: rowBound,
3508
+ });
3509
+ const prepared = { sql: preparation.preparedSql, connection: preparation.connection };
3510
+ const bounded = preparation.rowBound;
3464
3511
  const app = loadRuntimeApp(projectRoot, activePersonaAppId());
3465
3512
  // Generated SQL is governed as 'uncategorized' unless its seed declares a
3466
3513
  // domain — the same value `generatedSqlArtifactContract` stamps into the
@@ -3488,7 +3535,7 @@ export async function startLocalServer(opts) {
3488
3535
  })
3489
3536
  : null;
3490
3537
  const rawResult = semanticExecution?.result
3491
- ?? await executor.executeQuery(bounded.sql, [], runtimeVariables({}), prepared.connection);
3538
+ ?? await executor.executeQuery(preparation.executedSql, [], runtimeVariables({}), preparation.connection);
3492
3539
  const normalized = normalizeQueryResult(rawResult, semantic.semanticRefs);
3493
3540
  // The authoritative bound. `buildRowBoundedSql` may legitimately decline to
3494
3541
  // touch the statement, and only one of 15 drivers honours a maxRows hint, so
@@ -3528,34 +3575,65 @@ export async function startLocalServer(opts) {
3528
3575
  */
3529
3576
  const executeExploratoryCandidate = async (candidate, schemaContext = [], question = '', requestedTopN, executionConnection) => {
3530
3577
  const activeConnection = requireActiveConnection(executionConnection);
3531
- const preflight = repairExploratorySqlBeforeExecution(candidate.sql, schemaContext, question, activeConnection.driver);
3578
+ const failed = (message, sql, repairs, proofs = [], code = 'exploratory_validation_failed') => ({
3579
+ proofs,
3580
+ sql,
3581
+ repairs,
3582
+ error: message,
3583
+ warehouseFailure: normalizeWarehouseSqlFailure(analyticalError(message, {
3584
+ origin: 'governance_gate',
3585
+ stage: 'validation',
3586
+ code,
3587
+ }), activeConnection.driver),
3588
+ });
3589
+ let normalizedCandidateSql;
3590
+ try {
3591
+ normalizedCandidateSql = (await prepareAnalyticalExecutionSql({
3592
+ sql: candidate.sql,
3593
+ subject: question || 'this exploratory query',
3594
+ executor,
3595
+ connection: activeConnection,
3596
+ projectRoot,
3597
+ projectConfig,
3598
+ enforceReadOnly: true,
3599
+ })).preparedSql;
3600
+ }
3601
+ catch (error) {
3602
+ const failure = normalizeWarehouseSqlFailure(error, activeConnection.driver);
3603
+ return {
3604
+ proofs: [],
3605
+ sql: candidate.sql,
3606
+ repairs: [],
3607
+ error: failure.redactedMessage,
3608
+ warehouseFailure: failure,
3609
+ };
3610
+ }
3611
+ const preflight = repairExploratorySqlBeforeExecution(normalizedCandidateSql, schemaContext, question, activeConnection.driver);
3532
3612
  const boundedSql = applyRequestedTopNToExploratorySql(preflight.sql, requestedTopN);
3533
3613
  const repairs = boundedSql === preflight.sql
3534
3614
  ? [...preflight.repairs]
3535
3615
  : [...preflight.repairs, `Applied the requested overall top-${requestedTopN} bound before exploratory execution.`];
3536
3616
  if (preflight.blockedReason) {
3537
- return {
3538
- proofs: [],
3539
- sql: boundedSql,
3540
- repairs,
3541
- error: preflight.blockedReason,
3542
- };
3617
+ return failed(preflight.blockedReason, boundedSql, repairs, [], 'exploratory_preflight_blocked');
3543
3618
  }
3544
3619
  const analysis = analyzeSqlReferences(boundedSql, activeConnection.driver);
3620
+ if (!analysis.parsed && sqlMayContainJoin(boundedSql)) {
3621
+ return failed('DQL could not parse the exploratory SQL to validate its join predicates. Nothing was sent to the warehouse.', boundedSql, repairs, [], 'exploratory_join_parse_failed');
3622
+ }
3545
3623
  if (!analysis.parsed) {
3546
- return { proofs: [], sql: boundedSql, repairs, error: 'DQL could not parse the exploratory SQL to validate its join predicates.' };
3624
+ repairs.push('The local DQL parser could not inspect this join-free statement; warehouse read-only execution will provide the authoritative syntax check.');
3547
3625
  }
3548
- const probeableJoins = probeableExploratoryJoins(analysis.joins, analysis.ctes, analysis.derivedRelations);
3626
+ const probeableJoins = probeableExploratoryJoins(analysis.parsed ? analysis.joins : [], analysis.parsed ? analysis.ctes : [], analysis.parsed ? analysis.derivedRelations : []);
3549
3627
  // A normal customer → order → order-item → product question needs three
3550
3628
  // joins. Keep the lane bounded, but do not reject this common dbt-star path
3551
3629
  // solely because the old two-join demo limit was too small. CTE-internal
3552
3630
  // joins are counted physically (they appear in analysis.joins with their
3553
3631
  // physical endpoints); joins ON a CTE are restructuring, not new paths.
3554
3632
  if (probeableJoins.length > 4) {
3555
- return { proofs: [], sql: boundedSql, repairs, error: 'This exploratory query has more than four joins and requires an analyst to review the relationship path.' };
3633
+ return failed('This exploratory query has more than four joins and requires an analyst to review the relationship path.', boundedSql, repairs, [], 'exploratory_join_limit');
3556
3634
  }
3557
3635
  if (probeableJoins.some((join) => !join.leftRelation || !join.rightRelation)) {
3558
- return { proofs: [], sql: boundedSql, repairs, error: 'This exploratory query has a join endpoint DQL could not resolve for bounded validation.' };
3636
+ return failed('This exploratory query has a join endpoint DQL could not resolve for bounded validation.', boundedSql, repairs, [], 'exploratory_join_unresolved');
3559
3637
  }
3560
3638
  // Structural binding: when the candidate carries a declared draft join path,
3561
3639
  // every SQL join must ride one of its edges on the declared key pair. A join
@@ -3568,22 +3646,12 @@ export async function startLocalServer(opts) {
3568
3646
  const edge = declaredEdges.find((value) => declaredExploratoryRelationMatches(join.leftRelation, value.fromRelation) && declaredExploratoryRelationMatches(join.rightRelation, value.toRelation)
3569
3647
  || declaredExploratoryRelationMatches(join.leftRelation, value.toRelation) && declaredExploratoryRelationMatches(join.rightRelation, value.fromRelation));
3570
3648
  if (!edge) {
3571
- return {
3572
- proofs: [],
3573
- sql: boundedSql,
3574
- repairs,
3575
- error: `The join between ${join.leftRelation} and ${join.rightRelation} is not part of the declared relationship path, so DQL did not execute it. Declare the relationship in the DQL model to enable it.`,
3576
- };
3649
+ return failed(`The join between ${join.leftRelation} and ${join.rightRelation} is not part of the declared relationship path, so DQL did not execute it. Declare the relationship in the DQL model to enable it.`, boundedSql, repairs, [], 'undeclared_relationship');
3577
3650
  }
3578
3651
  const keyMatches = edge.keys.some((key) => exploratoryColumnsMatch(join.leftColumn, key.from) && exploratoryColumnsMatch(join.rightColumn, key.to)
3579
3652
  || exploratoryColumnsMatch(join.leftColumn, key.to) && exploratoryColumnsMatch(join.rightColumn, key.from));
3580
3653
  if (!keyMatches) {
3581
- return {
3582
- proofs: [],
3583
- sql: boundedSql,
3584
- repairs,
3585
- error: `The join ${join.leftColumn} = ${join.rightColumn} uses different keys than relationship "${edge.relationshipId}" declares (${edge.keys.map((key) => `${key.from}=${key.to}`).join(', ')}). Review the declared keys before executing.`,
3586
- };
3654
+ return failed(`The join ${join.leftColumn} = ${join.rightColumn} uses different keys than relationship "${edge.relationshipId}" declares (${edge.keys.map((key) => `${key.from}=${key.to}`).join(', ')}). Review the declared keys before executing.`, boundedSql, repairs, [], 'relationship_key_mismatch');
3587
3655
  }
3588
3656
  joinEdges.set(index, edge);
3589
3657
  }
@@ -3594,7 +3662,7 @@ export async function startLocalServer(opts) {
3594
3662
  }
3595
3663
  }
3596
3664
  }
3597
- if (probeableJoins.length < analysis.joins.length) {
3665
+ if (analysis.parsed && probeableJoins.length < analysis.joins.length) {
3598
3666
  repairs.push(`Skipped join probes for ${analysis.joins.length - probeableJoins.length} join(s) on derived (CTE) relations; the final query execution validates them.`);
3599
3667
  }
3600
3668
  const proofs = [];
@@ -3615,14 +3683,21 @@ export async function startLocalServer(opts) {
3615
3683
  // answer — these gates only inspect the raw key samples.
3616
3684
  const contradiction = exploratoryProbeContradiction(raw.rows, join, joinEdges.get(index));
3617
3685
  if (contradiction) {
3618
- return { proofs, sql: boundedSql, repairs, error: contradiction };
3686
+ return failed(contradiction, boundedSql, repairs, proofs, 'relationship_probe_contradiction');
3619
3687
  }
3620
3688
  }
3621
3689
  const result = await executeGeneratedArtifactForAgent(question, boundedSql, undefined, activeConnection);
3622
3690
  return { result, proofs, sql: boundedSql, repairs };
3623
3691
  }
3624
3692
  catch (error) {
3625
- return { proofs, sql: boundedSql, repairs, error: error instanceof Error ? error.message : String(error) };
3693
+ const failure = normalizeWarehouseSqlFailure(error, activeConnection.driver);
3694
+ return {
3695
+ proofs,
3696
+ sql: boundedSql,
3697
+ repairs,
3698
+ error: failure.redactedMessage,
3699
+ warehouseFailure: failure,
3700
+ };
3626
3701
  }
3627
3702
  };
3628
3703
  /**
@@ -4784,9 +4859,10 @@ export async function startLocalServer(opts) {
4784
4859
  */
4785
4860
  const MIN_COMPRESS_BYTES = 1024;
4786
4861
  const COMPRESSIBLE = /^(?:text\/(?!event-stream)|application\/(?:json|javascript|xml|wasm)|image\/svg)/i;
4787
- const withCompression = (request, response) => {
4862
+ const withCompression = (request, rawResponse) => {
4863
+ const response = rawResponse;
4788
4864
  if (!/\bgzip\b/i.test(String(request.headers['accept-encoding'] ?? '')))
4789
- return response;
4865
+ return rawResponse;
4790
4866
  const writeHead = response.writeHead.bind(response);
4791
4867
  const end = response.end.bind(response);
4792
4868
  const write = response.write.bind(response);
@@ -4857,7 +4933,7 @@ export async function startLocalServer(opts) {
4857
4933
  });
4858
4934
  return end(gz);
4859
4935
  });
4860
- return response;
4936
+ return rawResponse;
4861
4937
  };
4862
4938
  const server = createServer((req, res) => {
4863
4939
  res = withCompression(req, res);
@@ -5710,7 +5786,7 @@ export async function startLocalServer(opts) {
5710
5786
  if (!body.expectedSnapshotId || body.expectedSnapshotId !== snapshot.snapshotId)
5711
5787
  throw Object.assign(new Error('Project sources changed before relationship validation.'), { code: 'SOURCE_CHANGED' });
5712
5788
  const manifest = snapshot.manifest;
5713
- const activeConnection = requireActiveConnection();
5789
+ const activeConnection = await resolveExecutionConnection(body);
5714
5790
  const evidence = await validateModelingRelationship(body.relationship, manifest, (sql) => executor.executeQuery(sql, [], {}, activeConnection), (identifier) => getDialect(activeConnection.driver).quoteIdentifier(identifier));
5715
5791
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
5716
5792
  res.end(serializeJSON({ requestId, snapshotId: snapshot.snapshotId, evidence }));
@@ -5735,6 +5811,267 @@ export async function startLocalServer(opts) {
5735
5811
  res.end(serializeJSON({ runs, total: agentRunStore.list().length, limit }));
5736
5812
  return;
5737
5813
  }
5814
+ /**
5815
+ * One-click bounded execution repair.
5816
+ *
5817
+ * The source run is immutable. First, retry its exact SQL through the
5818
+ * shared deterministic preparation boundary (this repairs internal IDs and
5819
+ * dialect-safe row bounds without an LLM). Only a connector-classified
5820
+ * model-repair failure may spend one provider call to rewrite the SQL. The
5821
+ * derived SQL is still read-only, bounded, and executed on the source run's
5822
+ * exact target before a replacement card is returned.
5823
+ *
5824
+ * Acceptance: API-003, API-007, UI-012, UI-013, SEC-004, E2E-014.
5825
+ */
5826
+ if (req.method === 'POST' && /^\/api\/agent-runs\/[^/]+\/repair-execution$/.test(path)) {
5827
+ const match = path.match(/^\/api\/agent-runs\/([^/]+)\/repair-execution$/);
5828
+ const sourceRunId = decodeURIComponent(match?.[1] ?? '');
5829
+ const sourceRun = await agentRunStore.get(sourceRunId);
5830
+ if (!sourceRun) {
5831
+ res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
5832
+ res.end(serializeJSON({ error: 'Agent run not found.' }));
5833
+ return;
5834
+ }
5835
+ const derivedRunId = `${sourceRun.id}:repair:1`;
5836
+ const existing = await agentRunStore.get(derivedRunId);
5837
+ if (existing) {
5838
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
5839
+ res.end(serializeJSON({ run: existing }));
5840
+ return;
5841
+ }
5842
+ const sourceSql = repairableSqlFromAgentRun(sourceRun);
5843
+ const retainedFailure = warehouseFailureFromAgentRun(sourceRun);
5844
+ if (!sourceSql || !analyticalFailureAllowsDeterministicRetry(retainedFailure)) {
5845
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
5846
+ res.end(serializeJSON({
5847
+ error: sourceSql
5848
+ ? 'This failure requires authorized access or a policy change and cannot be repaired by AI.'
5849
+ : 'This run has no retained SQL statement to repair.',
5850
+ ...(retainedFailure ? { warehouseFailure: retainedFailure } : {}),
5851
+ }));
5852
+ return;
5853
+ }
5854
+ const body = agentRunRecord(await readJSON(req).catch(() => null));
5855
+ const threadId = agentRunString(body?.threadId);
5856
+ let targetConnection;
5857
+ try {
5858
+ targetConnection = await resolveAgentRunExecutionConnection({
5859
+ question: sourceRun.question,
5860
+ executionTarget: sourceRun.executionTarget,
5861
+ });
5862
+ }
5863
+ catch (error) {
5864
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
5865
+ res.end(serializeJSON({ error: apiErrorMessage(error) }));
5866
+ return;
5867
+ }
5868
+ const targetConnectionName = sourceRun.executionTarget?.target === 'connection'
5869
+ ? sourceRun.executionTarget.connectionName
5870
+ : sourceRun.executionTarget?.target === 'local'
5871
+ ? 'local'
5872
+ : projectConfig.defaultConnectionName;
5873
+ let result;
5874
+ let repairedSql = sourceSql;
5875
+ let repairMode = 'deterministic';
5876
+ let firstFailure;
5877
+ try {
5878
+ result = await executeGeneratedSqlDirect(sourceRun.question, sourceSql, undefined, targetConnection);
5879
+ }
5880
+ catch (error) {
5881
+ firstFailure = normalizeWarehouseSqlFailure(error, targetConnection.driver);
5882
+ }
5883
+ if (!result) {
5884
+ if (firstFailure?.retryDisposition !== 'model_repair') {
5885
+ res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
5886
+ res.end(serializeJSON({
5887
+ error: firstFailure?.redactedMessage ?? 'The deterministic repair did not produce an executable query.',
5888
+ ...(firstFailure ? { warehouseFailure: firstFailure } : {}),
5889
+ }));
5890
+ return;
5891
+ }
5892
+ const provider = await createBlockStudioAssistProvider(projectRoot);
5893
+ if (!provider) {
5894
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
5895
+ res.end(serializeJSON({ error: 'Configure an available AI provider in Settings to repair this query.' }));
5896
+ return;
5897
+ }
5898
+ let schemaContext;
5899
+ try {
5900
+ schemaContext = await getSchemaContextForAgent(sourceRun.question, undefined, targetConnection, targetConnectionName);
5901
+ }
5902
+ catch (error) {
5903
+ const failure = normalizeWarehouseSqlFailure(error, targetConnection.driver);
5904
+ res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
5905
+ res.end(serializeJSON({ error: failure.redactedMessage, warehouseFailure: failure }));
5906
+ return;
5907
+ }
5908
+ const boundedSchema = schemaContext.slice(0, 16).map((table) => ({
5909
+ relation: table.relation,
5910
+ columns: table.columns.slice(0, 40).map((column) => ({ name: column.name, type: column.type })),
5911
+ }));
5912
+ let raw;
5913
+ try {
5914
+ raw = await provider.generate([
5915
+ {
5916
+ role: 'system',
5917
+ content: [
5918
+ 'Repair one failed read-only analytical SQL statement.',
5919
+ 'Return exactly one JSON object in a ```json fence with keys summary, sql, viz, outputs.',
5920
+ 'Preserve the user question, selected grain, filters, and aggregation semantics.',
5921
+ 'Use only relations and columns present in the supplied schema context.',
5922
+ 'Never emit internal DQL relation identities such as source::, model::, seed::, or snapshot::.',
5923
+ 'Do not add writes, DDL, multiple statements, or commentary outside the JSON fence.',
5924
+ ].join(' '),
5925
+ },
5926
+ {
5927
+ role: 'user',
5928
+ content: JSON.stringify({
5929
+ question: sourceRun.question,
5930
+ driver: targetConnection.driver,
5931
+ failure: firstFailure.redactedMessage,
5932
+ sql: sourceSql,
5933
+ schema: boundedSchema,
5934
+ }),
5935
+ },
5936
+ ], { maxTokens: 1200, temperature: 0 });
5937
+ }
5938
+ catch {
5939
+ res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
5940
+ res.end(serializeJSON({ error: 'The AI provider could not complete this repair. Check Provider Settings and retry.' }));
5941
+ return;
5942
+ }
5943
+ const proposal = parseProposal(raw);
5944
+ if (!proposal.sql?.trim()) {
5945
+ res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
5946
+ res.end(serializeJSON({ error: 'The AI repair did not return an executable SQL statement.' }));
5947
+ return;
5948
+ }
5949
+ repairedSql = proposal.sql.trim();
5950
+ repairMode = 'ai';
5951
+ try {
5952
+ result = await executeGeneratedSqlDirect(sourceRun.question, repairedSql, undefined, targetConnection);
5953
+ }
5954
+ catch (error) {
5955
+ const failure = normalizeWarehouseSqlFailure(error, targetConnection.driver);
5956
+ res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
5957
+ res.end(serializeJSON({
5958
+ error: failure.redactedMessage,
5959
+ warehouseFailure: failure,
5960
+ }));
5961
+ return;
5962
+ }
5963
+ }
5964
+ const completedAt = new Date().toISOString();
5965
+ const preview = agentResultToSynthesisPreview(result);
5966
+ const synthesis = await synthesizeAnswer({
5967
+ question: sourceRun.question,
5968
+ audience: 'stakeholder',
5969
+ resultPreview: preview,
5970
+ sql: repairedSql,
5971
+ draftText: `The repaired query returned ${result.rowCount} row${result.rowCount === 1 ? '' : 's'}.`,
5972
+ ...(preview ? { columnFormats: agentColumnDisplayFormats(projectRoot, preview.columns) } : {}),
5973
+ });
5974
+ const sourceFailure = analyticalFailedRunFromAgentRun(sourceRun)?.failure;
5975
+ const artifact = agentRunArtifact('answer', 'Repaired analytical answer', {
5976
+ kind: 'uncertified',
5977
+ certification: 'analyst_review_required',
5978
+ reviewStatus: 'analyst_review_required',
5979
+ trustLabel: 'AI-generated · review required',
5980
+ answer: synthesis.text,
5981
+ text: synthesis.text,
5982
+ result,
5983
+ proposedSql: repairedSql,
5984
+ sql: result.sql ?? repairedSql,
5985
+ dqlArtifact: result.dqlArtifact,
5986
+ previousAttempt: {
5987
+ sourceRunId: sourceRun.id,
5988
+ sql: sourceSql,
5989
+ failure: retainedFailure ?? firstFailure,
5990
+ },
5991
+ repair: { attempt: 1, mode: repairMode },
5992
+ }, undefined, 'review_required');
5993
+ const evaluation = {
5994
+ id: `${derivedRunId}:execution`,
5995
+ label: 'Repaired query execution',
5996
+ passed: true,
5997
+ severity: 'info',
5998
+ message: `The ${repairMode === 'ai' ? 'AI-repaired' : 'deterministically normalized'} read-only query executed on the original target.`,
5999
+ };
6000
+ const repairStep = {
6001
+ id: `${derivedRunId}:step`,
6002
+ index: 0,
6003
+ route: 'generated_answer',
6004
+ resolvedRoute: 'generated_answer',
6005
+ goal: 'Repair and execute the failed analytical query on its original target.',
6006
+ successCriteria: ['Read-only validation passes.', 'The original execution target returns a bounded result.'],
6007
+ status: 'repaired',
6008
+ attempts: 1,
6009
+ summary: evaluation.message,
6010
+ evaluations: [evaluation],
6011
+ artifacts: [artifact],
6012
+ };
6013
+ const events = [
6014
+ { id: `${derivedRunId}:repair`, runId: derivedRunId, type: 'repair.attempted', at: completedAt, message: 'Started one bounded query repair.', route: 'generated_answer' },
6015
+ { id: `${derivedRunId}:artifact`, runId: derivedRunId, type: 'artifact.created', at: completedAt, message: 'Created the repaired result artifact.', route: 'generated_answer', trustState: 'review_required' },
6016
+ { id: `${derivedRunId}:complete`, runId: derivedRunId, type: 'run.completed', at: completedAt, message: 'The repaired query executed successfully.', route: 'generated_answer', status: 'needs_review', trustState: 'review_required' },
6017
+ ];
6018
+ const derivedRun = {
6019
+ id: derivedRunId,
6020
+ question: sourceRun.question,
6021
+ requestedMode: sourceRun.requestedMode,
6022
+ route: 'generated_answer',
6023
+ status: 'needs_review',
6024
+ trustState: 'review_required',
6025
+ stopReason: 'generated_review_required',
6026
+ startedAt: completedAt,
6027
+ completedAt,
6028
+ selectedObject: sourceRun.selectedObject,
6029
+ executionTarget: sourceRun.executionTarget,
6030
+ routeDecision: sourceRun.routeDecision,
6031
+ plan: sourceRun.plan,
6032
+ steps: [repairStep],
6033
+ summary: synthesis.text,
6034
+ answer: synthesis.text,
6035
+ answerKind: 'governed',
6036
+ artifacts: [artifact],
6037
+ evaluations: [evaluation],
6038
+ events,
6039
+ nextActions: sourceRun.nextActions,
6040
+ repairAttempts: 1,
6041
+ lifecycle: {
6042
+ version: 1,
6043
+ state: 'terminal',
6044
+ phase: 'run.completed',
6045
+ revision: 1,
6046
+ eventCursor: events.length,
6047
+ startedAt: completedAt,
6048
+ updatedAt: completedAt,
6049
+ completedAt,
6050
+ },
6051
+ diagnosticReceipt: {
6052
+ version: 1,
6053
+ runId: derivedRunId,
6054
+ phase: 'run.completed',
6055
+ route: 'generated_answer',
6056
+ plan: sourceRun.plan,
6057
+ steps: [repairStep],
6058
+ artifacts: [artifact],
6059
+ evaluations: [evaluation],
6060
+ },
6061
+ derivation: {
6062
+ version: 1,
6063
+ kind: 'analytical_repair',
6064
+ sourceRunId: sourceRun.id,
6065
+ ...(sourceFailure?.failureId ? { sourceFailureId: sourceFailure.failureId } : {}),
6066
+ attempt: 1,
6067
+ },
6068
+ };
6069
+ await agentRunStore.save(derivedRun);
6070
+ recordConversationTurn(threadId ? getConversationStore() : null, threadId, derivedRun);
6071
+ res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' });
6072
+ res.end(serializeJSON({ run: derivedRun }));
6073
+ return;
6074
+ }
5738
6075
  if (req.method === 'POST' && /^\/api\/agent-runs\/[^/]+\/analytical-repair$/.test(path)) {
5739
6076
  const match = path.match(/^\/api\/agent-runs\/([^/]+)\/analytical-repair$/);
5740
6077
  const id = decodeURIComponent(match?.[1] ?? '');
@@ -12005,7 +12342,15 @@ export async function startLocalServer(opts) {
12005
12342
  }));
12006
12343
  return;
12007
12344
  }
12008
- const prepared = prepareLocalExecution(semantic.sql, executionConnection, projectRoot, projectConfig);
12345
+ const preparation = await prepareAnalyticalExecutionSql({
12346
+ sql: semantic.sql,
12347
+ subject: 'Notebook query',
12348
+ executor,
12349
+ connection: executionConnection,
12350
+ projectRoot,
12351
+ projectConfig,
12352
+ });
12353
+ const prepared = { sql: preparation.preparedSql, connection: preparation.connection };
12009
12354
  const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
12010
12355
  const domain = typeof body.domain === 'string' ? body.domain : app?.domain;
12011
12356
  assertAppAccess({ app, domain, level: 'execute' });
@@ -12033,8 +12378,8 @@ export async function startLocalServer(opts) {
12033
12378
  ...normalized,
12034
12379
  executionContext: notebookExecutionIdentity(execContext),
12035
12380
  executionTarget: executionTargetDescriptor(body),
12036
- compiledSql: semantic.sql,
12037
- executedSql: prepared.sql,
12381
+ compiledSql: preparation.decodedSql,
12382
+ executedSql: preparation.executedSql,
12038
12383
  });
12039
12384
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
12040
12385
  res.end(payload);
@@ -12705,6 +13050,11 @@ export async function startLocalServer(opts) {
12705
13050
  if (req.method === 'POST' && path === '/api/dql/artifacts/execute') {
12706
13051
  try {
12707
13052
  const body = await readJSON(req);
13053
+ const targetConnection = await resolveExecutionConnection(body);
13054
+ const targetDescriptor = executionTargetDescriptor(body);
13055
+ const targetConnectionName = targetDescriptor.target === 'connection'
13056
+ ? targetDescriptor.connectionName
13057
+ : 'local';
12708
13058
  const requestedArtifact = normalizeDqlArtifactReference(body.artifact);
12709
13059
  const requestedBlockName = typeof body.blockName === 'string' ? body.blockName.trim() : '';
12710
13060
  let source = requestedArtifact?.source ?? '';
@@ -12762,7 +13112,7 @@ export async function startLocalServer(opts) {
12762
13112
  parameters,
12763
13113
  question: typeof body.question === 'string' ? body.question : undefined,
12764
13114
  rowLimit: requestedArtifact?.limit,
12765
- });
13115
+ }, targetConnection, targetConnectionName);
12766
13116
  const actualReceipt = result.executionReceipt;
12767
13117
  // Same reasoning: if the canonical block was reloaded, its compiled SQL
12768
13118
  // is EXPECTED to differ, so this determinism check would fire on a
@@ -12917,10 +13267,16 @@ export async function startLocalServer(opts) {
12917
13267
  res.end(serializeJSON({ cellType: cell.type, result: null }));
12918
13268
  return;
12919
13269
  }
12920
- const internalRelationError = internalDqlRelationIdValidationError(executableSql, cell.type === 'dql' ? 'DQL query' : 'Notebook query');
12921
- if (internalRelationError)
12922
- throw new DqlInternalRelationIdError(internalRelationError);
12923
- const prepared = prepareLocalExecution(executableSql, cellConnection, projectRoot, projectConfig);
13270
+ const preparation = await prepareAnalyticalExecutionSql({
13271
+ sql: executableSql,
13272
+ subject: cell.type === 'dql' ? 'DQL query' : 'Notebook query',
13273
+ executor,
13274
+ connection: cellConnection,
13275
+ projectRoot,
13276
+ projectConfig,
13277
+ });
13278
+ const decodedSql = preparation.decodedSql;
13279
+ const prepared = { sql: preparation.preparedSql, connection: preparation.connection };
12924
13280
  const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
12925
13281
  assertAppAccess({ app, domain: resolved.domain ?? app?.domain, level: 'execute' });
12926
13282
  const semanticExecution = semanticCompose?.compiled
@@ -12980,7 +13336,10 @@ export async function startLocalServer(opts) {
12980
13336
  tests: plan?.tests,
12981
13337
  result: normalized,
12982
13338
  executionTarget: executionTargetDescriptor(body),
12983
- compiledSql: semanticCompose?.compiled?.sql ?? executableSql,
13339
+ // Report the statement that ACTUALLY ran. `executableSql` is the
13340
+ // pre-decode text, so showing it would put a `source::` identity in
13341
+ // front of the user that the warehouse never saw.
13342
+ compiledSql: semanticCompose?.compiled?.sql ?? decodedSql,
12984
13343
  engine: semanticCompose?.compiled?.engine,
12985
13344
  ...(semanticExecution ? {
12986
13345
  semanticTrace: semanticExecution.semanticTrace,
@@ -15262,6 +15621,60 @@ export function prepareLocalExecution(sql, connection, projectRoot, projectConfi
15262
15621
  connection: normalizedConnection,
15263
15622
  };
15264
15623
  }
15624
+ /**
15625
+ * One host-owned SQL preparation boundary shared by Ask and Notebook.
15626
+ *
15627
+ * Surface-specific compilation happens before this function and surface policy
15628
+ * (for example exploratory join proof) may run around it, but no analytical
15629
+ * entrypoint may decode internal IDs, resolve dbt paths, enforce read-only
15630
+ * execution, or append a preview bound differently anymore.
15631
+ *
15632
+ * Acceptance: API-003, API-006, API-007, EXP-001, E2E-014.
15633
+ */
15634
+ export async function prepareAnalyticalExecutionSql(input) {
15635
+ const sourceSql = input.sql.trim();
15636
+ const rewrites = [];
15637
+ let decodedSql = resolveInternalRelationIds(sourceSql, EMPTY_SCHEMA_GROUNDING, 'qualified', rewrites);
15638
+ let bareResolution = null;
15639
+ if (internalDqlRelationIdValidationError(decodedSql, 'x')) {
15640
+ bareResolution = await resolveBareInternalRelationIds(decodedSql, input.executor, input.connection);
15641
+ decodedSql = bareResolution.sql;
15642
+ rewrites.push(...bareResolution.resolved);
15643
+ }
15644
+ const internalRelationError = internalDqlRelationIdValidationError(decodedSql, input.subject, bareResolution
15645
+ ? { probed: true, ambiguous: bareResolution.ambiguous }
15646
+ : undefined);
15647
+ if (internalRelationError) {
15648
+ throw tagAnalyticalError(new DqlInternalRelationIdError(internalRelationError), {
15649
+ origin: bareResolution?.ambiguous.length ? 'ambiguity' : 'retrieval_gap',
15650
+ stage: 'grounding',
15651
+ code: bareResolution?.ambiguous.length ? 'ambiguous_relation' : 'unknown_relation',
15652
+ });
15653
+ }
15654
+ const prepared = prepareLocalExecution(decodedSql, input.connection, input.projectRoot, input.projectConfig);
15655
+ if (input.enforceReadOnly) {
15656
+ const readOnlyError = readOnlySqlValidationError(prepared.sql, input.subject, input.connection.driver);
15657
+ if (readOnlyError) {
15658
+ throw analyticalError(readOnlyError, {
15659
+ origin: 'governance_gate',
15660
+ stage: 'validation',
15661
+ code: 'unsafe_sql',
15662
+ });
15663
+ }
15664
+ }
15665
+ const rowBound = input.rowLimit === undefined
15666
+ ? undefined
15667
+ : buildRowBoundedSql(prepared.sql, input.rowLimit, input.connection.driver);
15668
+ return {
15669
+ sourceSql,
15670
+ decodedSql,
15671
+ preparedSql: prepared.sql,
15672
+ executedSql: rowBound?.sql ?? prepared.sql,
15673
+ connection: prepared.connection,
15674
+ rewrites,
15675
+ ...(rowBound ? { rowBound } : {}),
15676
+ };
15677
+ }
15265
15678
  export function dashboardRuntimeVariables(dashboard, overrides = {}) {
15266
15679
  const variables = {};
15267
15680
  for (const param of dashboard.params ?? []) {
@@ -15667,6 +16080,8 @@ const AGENT_PREVIEW_FORBIDDEN_SQL = [
15667
16080
  function withAnalyticalCompilationOrigin(stage, fn) {
15668
16081
  return withAnalyticalErrorOriginSync({ origin: 'dql_compilation', stage }, fn);
15669
16082
  }
16083
+ /** No dbt grounding — decoding an internal id needs none for a qualified suffix. */
16084
+ const EMPTY_SCHEMA_GROUNDING = { tables: [], joinKeys: [], byKey: new Map() };
15670
16085
  export function clampAnalyticalRowBound(rowLimit) {
15671
16086
  return Number.isFinite(rowLimit) ? Math.max(1, Math.min(10_000, Math.floor(rowLimit))) : 200;
15672
16087
  }
@@ -16155,11 +16570,19 @@ function readOnlySqlValidationError(sql, subject, dialect = 'duckdb') {
16155
16570
  }
16156
16571
  return null;
16157
16572
  }
16158
- function internalDqlRelationIdValidationError(sql, subject) {
16573
+ function internalDqlRelationIdValidationError(sql, subject, probe) {
16159
16574
  const internalIds = internalRelationIdsInSql(stripSqlStringsAndComments(sql));
16160
16575
  if (internalIds.length === 0)
16161
16576
  return null;
16162
- return `${subject} contains internal DQL graph relation identifier${internalIds.length === 1 ? '' : 's'} ${internalIds.join(', ')}. Use the physical database.schema.table relation instead, or choose Ask AI to fix from the notebook error.`;
16577
+ const plural = internalIds.length === 1 ? '' : 's';
16578
+ const head = `${subject} contains internal DQL graph relation identifier${plural} ${internalIds.join(', ')}.`;
16579
+ if (probe?.ambiguous.length) {
16580
+ return `${head} ${probe.ambiguous.join(', ')} matched more than one schema in this connection, so the name alone does not identify a relation. Qualify it as database.schema.table.`;
16581
+ }
16582
+ if (probe?.probed) {
16583
+ return `${head} DQL checked this connection and no relation with that name is visible to it. Qualify it as database.schema.table, or connect to the source that holds it.`;
16584
+ }
16585
+ return `${head} Use the physical database.schema.table relation instead, or choose Ask AI to fix from the notebook error.`;
16163
16586
  }
16164
16587
  function parseHintLessonInput(value) {
16165
16588
  if (!value || typeof value !== 'object' || Array.isArray(value))
@@ -23303,6 +23726,87 @@ ${predicates.map((predicate) => ` ${predicate}`).join(' OR\n')}
23303
23726
  ORDER BY table_schema, table_name, ordinal_position
23304
23727
  LIMIT 400`;
23305
23728
  }
23729
+ /**
23730
+ * Conservative fallback for deciding whether an unparsed statement still
23731
+ * needs DQL's structural join validation. Join-free SELECT syntax may be
23732
+ * newer than the local parser and can safely continue to the read-only
23733
+ * warehouse boundary; an unparsed join must never bypass relationship proof.
23734
+ */
23735
+ export function sqlMayContainJoin(sql) {
23736
+ const normalized = stripSqlStringsAndComments(sql)
23737
+ .replace(/\s+/g, ' ')
23738
+ .trim();
23739
+ if (/\bjoin\b/i.test(normalized))
23740
+ return true;
23741
+ const fromMatch = /\bfrom\b/i.exec(normalized);
23742
+ if (!fromMatch)
23743
+ return false;
23744
+ const tail = normalized.slice(fromMatch.index + fromMatch[0].length);
23745
+ const clauseMatch = /\b(where|group\s+by|having|qualify|order\s+by|limit|fetch|union|intersect|except)\b/i.exec(tail);
23746
+ const fromClause = clauseMatch ? tail.slice(0, clauseMatch.index) : tail;
23747
+ let depth = 0;
23748
+ for (const char of fromClause) {
23749
+ if (char === '(')
23750
+ depth += 1;
23751
+ else if (char === ')')
23752
+ depth = Math.max(0, depth - 1);
23753
+ else if (char === ',' && depth === 0)
23754
+ return true;
23755
+ }
23756
+ return false;
23757
+ }
23758
+ /**
23759
+ * Resolve a BARE internal graph identity (`source::orders` — a name with no
23760
+ * database or schema) against the warehouse.
23761
+ *
23762
+ * A qualified identity decodes mechanically, but a bare one carries no
23763
+ * location, so the old behaviour was to reject the cell and hand the user an
23764
+ * "Ask AI to fix" button. That was an LLM round trip for a lookup: the name is
23765
+ * unambiguous whenever exactly one relation in the connection answers to it.
23766
+ * Probe for it, and only refuse when the answer is genuinely zero or many —
23767
+ * at which point the refusal can say which it was.
23768
+ *
23769
+ * Runs ONLY after a decode has already failed, so the happy path pays nothing.
23770
+ */
23771
+ export async function resolveBareInternalRelationIds(sql, executor, connection) {
23772
+ const bare = internalRelationIdsInSql(stripSqlStringsAndComments(sql))
23773
+ .filter((id) => !id.slice(id.indexOf('::') + 2).includes('.'));
23774
+ if (bare.length === 0)
23775
+ return { sql, resolved: [], ambiguous: [] };
23776
+ const names = bare.map((id) => id.slice(id.indexOf('::') + 2));
23777
+ const probeSql = buildNamedRelationProbeSql(names);
23778
+ if (!probeSql)
23779
+ return { sql, resolved: [], ambiguous: [] };
23780
+ let rows = [];
23781
+ try {
23782
+ const probe = await executor.executeQuery(probeSql, [], {}, connection);
23783
+ rows = (probe?.rows ?? []);
23784
+ }
23785
+ catch {
23786
+ // The probe is best-effort. A warehouse that cannot answer it leaves the
23787
+ // identity unresolved, which is the same outcome as not probing at all.
23788
+ return { sql, resolved: [], ambiguous: [] };
23789
+ }
23790
+ const resolved = [];
23791
+ const ambiguous = [];
23792
+ let out = sql;
23793
+ for (const id of bare) {
23794
+ const name = id.slice(id.indexOf('::') + 2).toLowerCase();
23795
+ const matches = new Set(rows
23796
+ .filter((row) => String(row.table_name ?? '').toLowerCase() === name)
23797
+ .map((row) => `${String(row.table_schema ?? '')}.${String(row.table_name ?? '')}`));
23798
+ if (matches.size !== 1) {
23799
+ if (matches.size > 1)
23800
+ ambiguous.push(id);
23801
+ continue;
23802
+ }
23803
+ const target = [...matches][0];
23804
+ const idPattern = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
23805
+ out = out.replace(new RegExp(`\\b${idPattern}`, 'g'), target);
23806
+ resolved.push({ from: id, to: target });
23807
+ }
23808
+ return { sql: out, resolved, ambiguous };
23809
+ }
23306
23810
  export function buildRuntimeSchemaSearchSql(question) {
23307
23811
  const terms = Array.from(agentSchemaTokens(question))
23308
23812
  .map((term) => term.toLowerCase().replace(/[^a-z0-9_]/g, ''))