@duckcodeailabs/dql-cli 1.13.2 → 1.13.3

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 (67) hide show
  1. package/dist/apps-api.d.ts +25 -3
  2. package/dist/apps-api.d.ts.map +1 -1
  3. package/dist/apps-api.js +1037 -175
  4. package/dist/apps-api.js.map +1 -1
  5. package/dist/assets/dql-notebook/assets/{AgentLogPage-BlUE3qsX.js → AgentLogPage-B3ugEb3H.js} +1 -1
  6. package/dist/assets/dql-notebook/assets/{AiBuildDialog-O88iK6UW.js → AiBuildDialog-BK_TDNcM.js} +1 -1
  7. package/dist/assets/dql-notebook/assets/{AiBuildResult-D-6G4Sh3.js → AiBuildResult-Cob_Lj05.js} +1 -1
  8. package/dist/assets/dql-notebook/assets/{AiSidePanel-MRSGtQDV.js → AiSidePanel-CzIgxmP9.js} +1 -1
  9. package/dist/assets/dql-notebook/assets/{AnalyticsHome-CcRVO6zN.js → AnalyticsHome-C2vJ2fxY.js} +1 -1
  10. package/dist/assets/dql-notebook/assets/AppsView-DEWQ0sHT.js +5588 -0
  11. package/dist/assets/dql-notebook/assets/BlockStudio-CaDNTKMM.js +55 -0
  12. package/dist/assets/dql-notebook/assets/{BusinessArtifactView-itEIUrYy.js → BusinessArtifactView-zwUOtddi.js} +1 -1
  13. package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-CbAqOpNc.js → DbtFirstModelingPage-BlnhTMu3.js} +4 -4
  14. package/dist/assets/dql-notebook/assets/{GitPage-BxOqRYu2.js → GitPage-Coevr9NJ.js} +3 -3
  15. package/dist/assets/dql-notebook/assets/{GlobalAiRail-Tfkx76CI.js → GlobalAiRail-BTZS11x5.js} +1 -1
  16. package/dist/assets/dql-notebook/assets/{GovernedContextPage-CeaDbepP.js → GovernedContextPage-CjyyM_ne.js} +3 -3
  17. package/dist/assets/dql-notebook/assets/{HelpDocsPage-D1lAryYv.js → HelpDocsPage-C1UeP85v.js} +1 -1
  18. package/dist/assets/dql-notebook/assets/{HomePage-GB6se6xW.js → HomePage-zGPFIvOY.js} +1 -1
  19. package/dist/assets/dql-notebook/assets/{LineageDAG-zCyN-hdg.js → LineageDAG-DbXnQWu3.js} +1 -1
  20. package/dist/assets/dql-notebook/assets/{LineageDetailView-Cg0TgG2Y.js → LineageDetailView-Ch1Db01I.js} +1 -1
  21. package/dist/assets/dql-notebook/assets/{LineageDrawer-DwMcenDf.js → LineageDrawer-DLxqJO6D.js} +1 -1
  22. package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-DUZx3ksU.js → LineagePathBreadcrumb-B2tNdkr_.js} +3 -3
  23. package/dist/assets/dql-notebook/assets/{MiniLineageGraph-DT_ycl6s.js → MiniLineageGraph-0gY-ZwCy.js} +1 -1
  24. package/dist/assets/dql-notebook/assets/{NewBlockModal-qdhzeJPF.js → NewBlockModal-DKVkXD20.js} +1 -1
  25. package/dist/assets/dql-notebook/assets/{NewNotebookModal-BnaDzs1A.js → NewNotebookModal-BUIByvYT.js} +1 -1
  26. package/dist/assets/dql-notebook/assets/{NotebookEditor-CouPFzdo.js → NotebookEditor-D2fIUxjE.js} +1 -1
  27. package/dist/assets/dql-notebook/assets/{ReadinessPage-DB_KbH9Y.js → ReadinessPage-Ci4LyNiQ.js} +1 -1
  28. package/dist/assets/dql-notebook/assets/{SetupOnboarding-CamQq_N2.js → SetupOnboarding-BF7ai_0t.js} +1 -1
  29. package/dist/assets/dql-notebook/assets/{SkillsPage--mYkOAK2.js → SkillsPage-BgYrgirJ.js} +1 -1
  30. package/dist/assets/dql-notebook/assets/{TrustBadge-ClEYiYeA.js → TrustBadge-Bob1Laiq.js} +1 -1
  31. package/dist/assets/dql-notebook/assets/{UnifiedAgentRunPanel-DBfsmXes.js → UnifiedAgentRunPanel-IxDUKSG8.js} +1 -1
  32. package/dist/assets/dql-notebook/assets/{answer-to-notebook-Cg3UyGwh.js → answer-to-notebook-Dk42N0pU.js} +1 -1
  33. package/dist/assets/dql-notebook/assets/{arrow-left-DnYPGqlr.js → arrow-left-DKuuTw9l.js} +1 -1
  34. package/dist/assets/dql-notebook/assets/{arrow-right-Ct_YGrmU.js → arrow-right-TH8_ufKG.js} +1 -1
  35. package/dist/assets/dql-notebook/assets/{book-open-text-DULlwbj6.js → book-open-text-B-dcHHn0.js} +1 -1
  36. package/dist/assets/dql-notebook/assets/{circle-x-ByYxoiS9.js → circle-x-BOc2KCZQ.js} +1 -1
  37. package/dist/assets/dql-notebook/assets/{dagre.esm-BjYSO53F.js → dagre.esm-CPT_vn5p.js} +1 -1
  38. package/dist/assets/dql-notebook/assets/{external-link-DprWh9Yf.js → external-link-DtOfpYc6.js} +1 -1
  39. package/dist/assets/dql-notebook/assets/{grip-vertical-CrEEYkXN.js → grip-vertical-B8joDDH9.js} +1 -1
  40. package/dist/assets/dql-notebook/assets/index-DJRMz64S.js +635 -0
  41. package/dist/assets/dql-notebook/assets/{link-2-BkW-rkEC.js → link-2-DMZPHawg.js} +1 -1
  42. package/dist/assets/dql-notebook/assets/{list-tree-BipBLmIE.js → list-tree-Dw0qlxPK.js} +1 -1
  43. package/dist/assets/dql-notebook/assets/{minimize-2-BhXsglrO.js → minimize-2-DUiSrPS3.js} +1 -1
  44. package/dist/assets/dql-notebook/assets/{panel-right-open-BoCSilYB.js → panel-right-open-BrrFrRZQ.js} +1 -1
  45. package/dist/assets/dql-notebook/assets/{play-mdoZawPu.js → play-Ced0HT9g.js} +1 -1
  46. package/dist/assets/dql-notebook/assets/{rotate-ccw-ELiDRUmj.js → rotate-ccw-D_9U9o7X.js} +1 -1
  47. package/dist/assets/dql-notebook/assets/{semantic-fields-DVNRLfeU.js → semantic-fields-BlDOaonV.js} +1 -1
  48. package/dist/assets/dql-notebook/assets/{sliders-horizontal-GeUzpmKz.js → sliders-horizontal-BDt6KRGR.js} +1 -1
  49. package/dist/assets/dql-notebook/assets/{star-D63O81Wp.js → star-CxUbF_BW.js} +1 -1
  50. package/dist/assets/dql-notebook/assets/{triangle-alert-BWrCxQVE.js → triangle-alert-COWnsSdh.js} +1 -1
  51. package/dist/assets/dql-notebook/assets/{upload-CI-ldXhC.js → upload-DSFC2kJX.js} +1 -1
  52. package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-Cv7Yq2hK.js → usePersistedAgentThreadId-DYdx896E.js} +1 -1
  53. package/dist/assets/dql-notebook/assets/{user-round-Do21aFGo.js → user-round-Bfu7fosn.js} +1 -1
  54. package/dist/assets/dql-notebook/assets/{wand-sparkles-DT7u7F_x.js → wand-sparkles-DSpG0c0j.js} +1 -1
  55. package/dist/assets/dql-notebook/assets/{workflow-DHh2mMbn.js → workflow-B1_pj9gq.js} +1 -1
  56. package/dist/assets/dql-notebook/assets/{wrench-7tYr_GKb.js → wrench-CvfM03KZ.js} +1 -1
  57. package/dist/assets/dql-notebook/assets/{x-CcYceFMz.js → x-B4coLiOl.js} +1 -1
  58. package/dist/assets/dql-notebook/index.html +1 -1
  59. package/dist/local-runtime.d.ts +47 -19
  60. package/dist/local-runtime.d.ts.map +1 -1
  61. package/dist/local-runtime.js +1015 -566
  62. package/dist/local-runtime.js.map +1 -1
  63. package/dist/package.json +10 -10
  64. package/package.json +10 -10
  65. package/dist/assets/dql-notebook/assets/AppsView-BTkGC-wl.js +0 -5588
  66. package/dist/assets/dql-notebook/assets/BlockStudio-BrTNaDpb.js +0 -55
  67. package/dist/assets/dql-notebook/assets/index-9w93YPF9.js +0 -635
@@ -968,6 +968,36 @@ export async function startLocalServer(opts) {
968
968
  // rather than treating compiled semantic SQL as unrelated generic SQL.
969
969
  const compiledSemanticQueries = new Map();
970
970
  const dashboardRunEvidence = new Map();
971
+ const loadAppBuildPreviewEvidence = (runId) => {
972
+ const memoryEvidence = dashboardRunEvidence.get(runId);
973
+ if (memoryEvidence && memoryEvidence.expiresAt >= Date.now())
974
+ return memoryEvidence;
975
+ if (memoryEvidence)
976
+ dashboardRunEvidence.delete(runId);
977
+ const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
978
+ try {
979
+ const persisted = storage.getAppPreviewEvidence(runId);
980
+ if (!persisted)
981
+ return null;
982
+ return {
983
+ appId: persisted.draftId,
984
+ dashboardId: persisted.dashboardId,
985
+ snapshotId: persisted.snapshotId,
986
+ filterFingerprint: persisted.filterFingerprint,
987
+ resultFingerprint: persisted.resultFingerprint,
988
+ personaFingerprint: persisted.personaFingerprint,
989
+ successfulTileIds: persisted.successfulTileIds,
990
+ semanticApprovalEligibleTileIds: persisted.semanticApprovalEligibleTileIds,
991
+ // Persisted receipts are bound again by draft revision, source
992
+ // fingerprints, filters and snapshot during preflight. They therefore
993
+ // survive a process restart without making result rows durable.
994
+ expiresAt: Number.POSITIVE_INFINITY,
995
+ };
996
+ }
997
+ finally {
998
+ storage.close();
999
+ }
1000
+ };
971
1001
  const dbtNodeDetailCache = new Map();
972
1002
  const onboardingJobs = new Map();
973
1003
  const metricFlowInstaller = new ManagedMetricFlowInstaller(projectRoot);
@@ -5653,7 +5683,10 @@ export async function startLocalServer(opts) {
5653
5683
  parameters: invocation.values,
5654
5684
  })
5655
5685
  : null;
5656
- const validation = validateBlockStudioSource(source, semanticLayer);
5686
+ const staticValidation = validateBlockStudioSource(source, semanticLayer);
5687
+ const validation = semanticCompose
5688
+ ? reconcileBlockStudioRuntimeValidation(staticValidation, semanticCompose)
5689
+ : staticValidation;
5657
5690
  const executableSql = semanticCompose?.sql ?? validation.executableSql;
5658
5691
  if (!executableSql) {
5659
5692
  const message = semanticCompose?.diagnostics.find((item) => item.severity === 'error')?.message
@@ -5675,6 +5708,7 @@ export async function startLocalServer(opts) {
5675
5708
  chartConfig: plan?.chartConfig ?? validation.chartConfig ?? null,
5676
5709
  invocation,
5677
5710
  tests: plan?.tests ?? [],
5711
+ validation,
5678
5712
  };
5679
5713
  };
5680
5714
  const runBlockStudioTestSummary = async (source, targetConnection, executedPreview) => {
@@ -5775,12 +5809,13 @@ export async function startLocalServer(opts) {
5775
5809
  };
5776
5810
  };
5777
5811
  const certifyBlockStudioSource = async (source, blockPath, options = {}) => {
5778
- const validation = validateBlockStudioSource(source, semanticLayer);
5812
+ let validation = validateBlockStudioSource(source, semanticLayer);
5779
5813
  let preview = null;
5780
5814
  let testResults = null;
5781
5815
  const blockers = [];
5782
5816
  try {
5783
5817
  preview = await runBlockStudioPreviewSource(source);
5818
+ validation = preview.validation;
5784
5819
  }
5785
5820
  catch (error) {
5786
5821
  blockers.push(error instanceof Error ? error.message : String(error));
@@ -9272,633 +9307,659 @@ export async function startLocalServer(opts) {
9272
9307
  ? body.tileId.trim()
9273
9308
  : undefined;
9274
9309
  const tiles = [];
9275
- let localApps = null;
9276
- for (const item of loaded.dashboard.layout.items) {
9277
- if (requestedTileId && item.i !== requestedTileId)
9278
- continue;
9279
- if (item.text) {
9280
- tiles.push({
9281
- tileId: item.i,
9282
- status: 'ok',
9283
- tileType: 'text',
9284
- title: item.title,
9285
- viz: item.viz,
9286
- text: item.text,
9287
- });
9288
- continue;
9289
- }
9290
- if (item.aiPin) {
9291
- try {
9292
- localApps ??= new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
9293
- }
9294
- catch (err) {
9295
- tiles.push({
9296
- tileId: item.i,
9297
- status: 'error',
9298
- tileType: 'aiPin',
9299
- error: err instanceof Error ? err.message : String(err),
9300
- });
9310
+ const localApps = { current: null };
9311
+ const itemsToRun = loaded.dashboard.layout.items.filter((item) => !requestedTileId || item.i === requestedTileId);
9312
+ const tileOrder = new Map(itemsToRun.map((item, index) => [item.i, index]));
9313
+ let nextTileIndex = 0;
9314
+ // App preview is a multi-source execution, so isolate each tile's
9315
+ // failure and run a small fixed pool. Four avoids serial 4k-catalog
9316
+ // behavior without flooding the configured warehouse.
9317
+ const runTileWorker = async () => {
9318
+ while (nextTileIndex < itemsToRun.length) {
9319
+ const item = itemsToRun[nextTileIndex++];
9320
+ if (!item)
9301
9321
  continue;
9302
- }
9303
- let pin = localApps.getAiPin(item.aiPin.id);
9304
- if (!pin) {
9322
+ if (item.text) {
9305
9323
  tiles.push({
9306
9324
  tileId: item.i,
9307
- status: 'unresolved',
9308
- tileType: 'aiPin',
9309
- error: `AI pin "${item.aiPin.id}" could not be found`,
9325
+ status: 'ok',
9326
+ tileType: 'text',
9327
+ title: item.title,
9328
+ viz: item.viz,
9329
+ text: item.text,
9310
9330
  });
9311
9331
  continue;
9312
9332
  }
9313
- const pinPlan = pin.analysisPlan && typeof pin.analysisPlan === 'object' && !Array.isArray(pin.analysisPlan)
9314
- ? pin.analysisPlan
9315
- : undefined;
9316
- const pinArtifact = normalizeDqlArtifactReference(pinPlan?.dqlArtifact);
9317
- let renderedPinResult = pin.result;
9318
- let renderedPinChartConfig = pin.chartConfig;
9319
- if (pinArtifact) {
9333
+ if (item.aiPin) {
9320
9334
  try {
9321
- const boundParameters = dashboardTileParameterValues({
9322
- item,
9323
- dashboardValues: dashboardVariables,
9324
- requestValues: variables,
9325
- });
9326
- const executed = await executeDqlArtifactSourceForAgent(pinArtifact.source, { name: pinArtifact.name ?? pin.title, path: pinArtifact.sourcePath }, { question: pin.question, parameters: { ...(pinArtifact.parameterValues ?? {}), ...boundParameters } });
9327
- renderedPinResult = executed;
9328
- renderedPinChartConfig = executed.chartConfig && typeof executed.chartConfig === 'object'
9329
- ? executed.chartConfig
9330
- : renderedPinChartConfig;
9335
+ localApps.current ??= new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
9331
9336
  }
9332
9337
  catch (err) {
9333
9338
  tiles.push({
9334
9339
  tileId: item.i,
9335
9340
  status: 'error',
9336
9341
  tileType: 'aiPin',
9337
- title: item.title ?? pin.title,
9338
9342
  error: err instanceof Error ? err.message : String(err),
9339
- trustState: pinArtifact.trustState ?? 'review_required',
9340
9343
  });
9341
9344
  continue;
9342
9345
  }
9343
- }
9344
- else if (pin.refreshCadence === 'daily' && pin.sql && isAiPinRefreshDue(pin.lastRefreshedAt)) {
9345
- try {
9346
- const refreshed = await executeLocalSqlForStoredResult(pin.sql);
9347
- pin = localApps.updateAiPinResult(pin.id, refreshed) ?? pin;
9348
- }
9349
- catch (err) {
9350
- pin = localApps.updateAiPinResult(pin.id, pin.result, err instanceof Error ? err.message : String(err)) ?? pin;
9346
+ let pin = localApps.current.getAiPin(item.aiPin.id);
9347
+ if (!pin) {
9348
+ tiles.push({
9349
+ tileId: item.i,
9350
+ status: 'unresolved',
9351
+ tileType: 'aiPin',
9352
+ error: `AI pin "${item.aiPin.id}" could not be found`,
9353
+ });
9354
+ continue;
9351
9355
  }
9352
- }
9353
- tiles.push({
9354
- tileId: item.i,
9355
- status: 'ok',
9356
- tileType: 'aiPin',
9357
- title: item.title ?? pin.title,
9358
- viz: item.viz,
9359
- chartConfig: mergeDashboardChartConfig(renderedPinChartConfig, item),
9360
- result: renderedPinResult,
9361
- aiPin: pin,
9362
- citation: {
9363
- kind: 'ai_pin',
9364
- name: pin.title,
9365
- },
9366
- artifact: {
9367
- version: 1,
9368
- sourceKind: 'ai_pin',
9369
- name: pinArtifact?.name ?? pin.title,
9370
- ...(pinArtifact?.sourcePath ? { sourcePath: pinArtifact.sourcePath } : {}),
9371
- ...(pinArtifact?.source ? { dql: pinArtifact.source.slice(0, 40_000) } : {}),
9372
- ...(pinArtifact?.compiledSql || pin.sql ? { sql: (pinArtifact?.compiledSql ?? pin.sql).slice(0, 40_000) } : {}),
9373
- trustState: pinArtifact?.trustState === 'certified' || pin.certification === 'certified'
9374
- ? 'certified'
9375
- : 'review_required',
9376
- explanation: [
9377
- 'This result comes from a saved App insight.',
9378
- pinArtifact ? 'The DQL artifact was executed again with the current App parameters.' : 'The saved bounded result is displayed with its original trust state.',
9379
- ],
9380
- executionTarget: executionTargetDescriptor(body),
9381
- },
9382
- });
9383
- continue;
9384
- }
9385
- if (item.draftAnalysis) {
9386
- let draftSource;
9387
- let draftAppRelativePath;
9388
- let draftParameters = {};
9389
- let draftTargetConnection;
9390
- let draftRepairPlan;
9391
- try {
9392
- const localDraftPath = loaded.draftArtifactsDir
9393
- ? join(loaded.draftArtifactsDir, item.draftAnalysis.ref)
9356
+ const pinPlan = pin.analysisPlan && typeof pin.analysisPlan === 'object' && !Array.isArray(pin.analysisPlan)
9357
+ ? pin.analysisPlan
9394
9358
  : undefined;
9395
- const draftRoot = localDraftPath && existsSync(localDraftPath)
9396
- ? loaded.draftArtifactsDir
9397
- : loaded.appDir;
9398
- const draftPath = join(draftRoot, item.draftAnalysis.ref);
9399
- draftAppRelativePath = relative(draftRoot, draftPath);
9400
- if (draftAppRelativePath.startsWith('..') || draftAppRelativePath.startsWith('/')) {
9401
- throw new Error('App-scoped analysis reference escapes the App directory.');
9359
+ const pinArtifact = normalizeDqlArtifactReference(pinPlan?.dqlArtifact);
9360
+ let renderedPinResult = pin.result;
9361
+ let renderedPinChartConfig = pin.chartConfig;
9362
+ if (pinArtifact) {
9363
+ try {
9364
+ const boundParameters = dashboardTileParameterValues({
9365
+ item,
9366
+ dashboardValues: dashboardVariables,
9367
+ requestValues: variables,
9368
+ });
9369
+ const executed = await executeDqlArtifactSourceForAgent(pinArtifact.source, { name: pinArtifact.name ?? pin.title, path: pinArtifact.sourcePath }, { question: pin.question, parameters: { ...(pinArtifact.parameterValues ?? {}), ...boundParameters } });
9370
+ renderedPinResult = executed;
9371
+ renderedPinChartConfig = executed.chartConfig && typeof executed.chartConfig === 'object'
9372
+ ? executed.chartConfig
9373
+ : renderedPinChartConfig;
9374
+ }
9375
+ catch (err) {
9376
+ tiles.push({
9377
+ tileId: item.i,
9378
+ status: 'error',
9379
+ tileType: 'aiPin',
9380
+ title: item.title ?? pin.title,
9381
+ error: err instanceof Error ? err.message : String(err),
9382
+ trustState: pinArtifact.trustState ?? 'review_required',
9383
+ });
9384
+ continue;
9385
+ }
9402
9386
  }
9403
- draftSource = readFileSync(draftPath, 'utf-8');
9404
- const fingerprint = `sha256:${createHash('sha256').update(draftSource).digest('hex')}`;
9405
- if (fingerprint !== item.draftAnalysis.artifactFingerprint) {
9406
- throw new Error('App-scoped analysis changed after approval. Review the draft again.');
9387
+ else if (pin.refreshCadence === 'daily' && pin.sql && isAiPinRefreshDue(pin.lastRefreshedAt)) {
9388
+ try {
9389
+ const refreshed = await executeLocalSqlForStoredResult(pin.sql);
9390
+ pin = localApps.current.updateAiPinResult(pin.id, refreshed) ?? pin;
9391
+ }
9392
+ catch (err) {
9393
+ pin = localApps.current.updateAiPinResult(pin.id, pin.result, err instanceof Error ? err.message : String(err)) ?? pin;
9394
+ }
9407
9395
  }
9408
- draftParameters = dashboardTileParameterValues({
9409
- item,
9410
- dashboardValues: dashboardVariables,
9411
- requestValues: variables,
9412
- });
9413
- draftTargetConnection = isConnectionConfig(body.connection)
9414
- ? body.connection
9415
- : connection ?? undefined;
9416
- draftRepairPlan = buildExecutionPlan({ id: item.i, type: 'dql', source: draftSource, title: item.title ?? item.i }, {
9417
- semanticLayer,
9418
- driver: draftTargetConnection?.driver ?? 'duckdb',
9419
- parameters: draftParameters,
9396
+ tiles.push({
9397
+ tileId: item.i,
9398
+ status: 'ok',
9399
+ tileType: 'aiPin',
9400
+ title: item.title ?? pin.title,
9401
+ viz: item.viz,
9402
+ chartConfig: mergeDashboardChartConfig(renderedPinChartConfig, item),
9403
+ result: renderedPinResult,
9404
+ aiPin: pin,
9405
+ citation: {
9406
+ kind: 'ai_pin',
9407
+ name: pin.title,
9408
+ },
9409
+ artifact: {
9410
+ version: 1,
9411
+ sourceKind: 'ai_pin',
9412
+ name: pinArtifact?.name ?? pin.title,
9413
+ ...(pinArtifact?.sourcePath ? { sourcePath: pinArtifact.sourcePath } : {}),
9414
+ ...(pinArtifact?.source ? { dql: pinArtifact.source.slice(0, 40_000) } : {}),
9415
+ ...(pinArtifact?.compiledSql || pin.sql ? { sql: (pinArtifact?.compiledSql ?? pin.sql).slice(0, 40_000) } : {}),
9416
+ trustState: pinArtifact?.trustState === 'certified' || pin.certification === 'certified'
9417
+ ? 'certified'
9418
+ : 'review_required',
9419
+ explanation: [
9420
+ 'This result comes from a saved App insight.',
9421
+ pinArtifact ? 'The DQL artifact was executed again with the current App parameters.' : 'The saved bounded result is displayed with its original trust state.',
9422
+ ],
9423
+ executionTarget: executionTargetDescriptor(body),
9424
+ },
9420
9425
  });
9421
- let result;
9422
- let repair;
9423
- let repairedSql;
9424
- let repairedDql;
9426
+ continue;
9427
+ }
9428
+ if (item.draftAnalysis) {
9429
+ let draftSource;
9430
+ let draftAppRelativePath;
9431
+ let draftParameters = {};
9432
+ let draftTargetConnection;
9433
+ let draftRepairPlan;
9425
9434
  try {
9426
- result = await executeDqlArtifactSourceForAgent(draftSource, { name: item.title ?? item.i, path: draftAppRelativePath }, { question: item.title, parameters: draftParameters });
9435
+ const localDraftPath = loaded.draftArtifactsDir
9436
+ ? join(loaded.draftArtifactsDir, item.draftAnalysis.ref)
9437
+ : undefined;
9438
+ const draftRoot = localDraftPath && existsSync(localDraftPath)
9439
+ ? loaded.draftArtifactsDir
9440
+ : loaded.appDir;
9441
+ const draftPath = join(draftRoot, item.draftAnalysis.ref);
9442
+ draftAppRelativePath = relative(draftRoot, draftPath);
9443
+ if (draftAppRelativePath.startsWith('..') || draftAppRelativePath.startsWith('/')) {
9444
+ throw new Error('App-scoped analysis reference escapes the App directory.');
9445
+ }
9446
+ draftSource = readFileSync(draftPath, 'utf-8');
9447
+ const fingerprint = `sha256:${createHash('sha256').update(draftSource).digest('hex')}`;
9448
+ if (fingerprint !== item.draftAnalysis.artifactFingerprint) {
9449
+ throw new Error('App-scoped analysis changed after approval. Review the draft again.');
9450
+ }
9451
+ draftParameters = dashboardTileParameterValues({
9452
+ item,
9453
+ dashboardValues: dashboardVariables,
9454
+ requestValues: variables,
9455
+ });
9456
+ draftTargetConnection = isConnectionConfig(body.connection)
9457
+ ? body.connection
9458
+ : connection ?? undefined;
9459
+ draftRepairPlan = buildExecutionPlan({ id: item.i, type: 'dql', source: draftSource, title: item.title ?? item.i }, {
9460
+ semanticLayer,
9461
+ driver: draftTargetConnection?.driver ?? 'duckdb',
9462
+ parameters: draftParameters,
9463
+ });
9464
+ let result;
9465
+ let repair;
9466
+ let repairedSql;
9467
+ let repairedDql;
9468
+ try {
9469
+ result = await executeDqlArtifactSourceForAgent(draftSource, { name: item.title ?? item.i, path: draftAppRelativePath }, { question: item.title, parameters: draftParameters });
9470
+ }
9471
+ catch (executionError) {
9472
+ const repaired = await repairFailedAppTileExecution({
9473
+ source: 'draft_analysis',
9474
+ question: item.title ?? item.i,
9475
+ sourceSql: draftRepairPlan?.sql,
9476
+ sourceDql: draftSource,
9477
+ artifactName: item.title ?? item.i,
9478
+ error: executionError,
9479
+ targetConnection: draftTargetConnection,
9480
+ targetConnectionName: projectConfig.defaultConnectionName,
9481
+ sqlParams: draftRepairPlan?.sqlParams,
9482
+ variables: { ...(draftRepairPlan?.variables ?? {}), ...draftParameters },
9483
+ });
9484
+ if (!repaired.ok) {
9485
+ tiles.push({
9486
+ tileId: item.i,
9487
+ status: 'error',
9488
+ tileType: 'draftAnalysis',
9489
+ title: item.title ?? item.i,
9490
+ error: executionError instanceof Error ? executionError.message : String(executionError),
9491
+ trustState: 'review_required',
9492
+ repair: repaired.repair,
9493
+ artifact: {
9494
+ version: 1,
9495
+ sourceKind: 'draft_analysis',
9496
+ name: item.title ?? item.i,
9497
+ sourcePath: draftAppRelativePath,
9498
+ dql: draftSource.slice(0, 40_000),
9499
+ ...(draftRepairPlan?.sql ? { sql: draftRepairPlan.sql.slice(0, 40_000) } : {}),
9500
+ trustState: 'review_required',
9501
+ explanation: ['This App-scoped analysis remains review-required.', 'Automatic repair could not produce a safe executable result.'],
9502
+ executionTarget: executionTargetDescriptor(body),
9503
+ },
9504
+ });
9505
+ continue;
9506
+ }
9507
+ result = repaired.result;
9508
+ repair = repaired.repair;
9509
+ repairedSql = repaired.repairedSql;
9510
+ repairedDql = repaired.repairedDql;
9511
+ }
9512
+ tiles.push({
9513
+ tileId: item.i,
9514
+ status: 'ok',
9515
+ tileType: 'draftAnalysis',
9516
+ title: item.title ?? item.i,
9517
+ viz: item.viz,
9518
+ chartConfig: mergeDashboardChartConfig(result.chartConfig, item),
9519
+ result,
9520
+ trustState: 'review_required',
9521
+ citation: { kind: repair ? 'app_draft_analysis_repaired' : 'app_draft_analysis', name: item.title ?? item.i, path: draftAppRelativePath },
9522
+ ...(repair ? { repair } : {}),
9523
+ artifact: {
9524
+ version: 1,
9525
+ sourceKind: 'draft_analysis',
9526
+ name: item.title ?? item.i,
9527
+ sourcePath: draftAppRelativePath,
9528
+ dql: (repairedDql ?? draftSource).slice(0, 40_000),
9529
+ ...(repairedSql ?? draftRepairPlan?.sql ? { sql: (repairedSql ?? draftRepairPlan.sql).slice(0, 40_000) } : {}),
9530
+ trustState: 'review_required',
9531
+ explanation: [
9532
+ 'This is private App analysis and remains review-required.',
9533
+ repair ? 'A bounded repair ran on the original data target without changing the saved source file.' : 'The saved App-scoped DQL was executed with the current filters.',
9534
+ ],
9535
+ executionTarget: executionTargetDescriptor(body),
9536
+ },
9537
+ });
9427
9538
  }
9428
- catch (executionError) {
9539
+ catch (err) {
9429
9540
  const repaired = await repairFailedAppTileExecution({
9430
9541
  source: 'draft_analysis',
9431
9542
  question: item.title ?? item.i,
9432
9543
  sourceSql: draftRepairPlan?.sql,
9433
9544
  sourceDql: draftSource,
9434
9545
  artifactName: item.title ?? item.i,
9435
- error: executionError,
9546
+ error: err,
9436
9547
  targetConnection: draftTargetConnection,
9437
9548
  targetConnectionName: projectConfig.defaultConnectionName,
9438
9549
  sqlParams: draftRepairPlan?.sqlParams,
9439
9550
  variables: { ...(draftRepairPlan?.variables ?? {}), ...draftParameters },
9440
9551
  });
9441
- if (!repaired.ok) {
9552
+ if (repaired.ok && draftSource) {
9442
9553
  tiles.push({
9443
9554
  tileId: item.i,
9444
- status: 'error',
9555
+ status: 'ok',
9445
9556
  tileType: 'draftAnalysis',
9446
9557
  title: item.title ?? item.i,
9447
- error: executionError instanceof Error ? executionError.message : String(executionError),
9558
+ viz: item.viz,
9559
+ chartConfig: mergeDashboardChartConfig(repaired.result.chartConfig, item),
9560
+ result: repaired.result,
9448
9561
  trustState: 'review_required',
9562
+ citation: { kind: 'app_draft_analysis_repaired', name: item.title ?? item.i, path: draftAppRelativePath },
9449
9563
  repair: repaired.repair,
9450
9564
  artifact: {
9451
9565
  version: 1,
9452
9566
  sourceKind: 'draft_analysis',
9453
9567
  name: item.title ?? item.i,
9454
- sourcePath: draftAppRelativePath,
9455
- dql: draftSource.slice(0, 40_000),
9456
- ...(draftRepairPlan?.sql ? { sql: draftRepairPlan.sql.slice(0, 40_000) } : {}),
9568
+ ...(draftAppRelativePath ? { sourcePath: draftAppRelativePath } : {}),
9569
+ dql: (repaired.repairedDql ?? draftSource).slice(0, 40_000),
9570
+ sql: repaired.repairedSql.slice(0, 40_000),
9457
9571
  trustState: 'review_required',
9458
- explanation: ['This App-scoped analysis remains review-required.', 'Automatic repair could not produce a safe executable result.'],
9572
+ explanation: ['A bounded DQL repair executed on the original data target. The saved App analysis was not changed.'],
9459
9573
  executionTarget: executionTargetDescriptor(body),
9460
9574
  },
9461
9575
  });
9462
9576
  continue;
9463
9577
  }
9464
- result = repaired.result;
9465
- repair = repaired.repair;
9466
- repairedSql = repaired.repairedSql;
9467
- repairedDql = repaired.repairedDql;
9468
- }
9469
- tiles.push({
9470
- tileId: item.i,
9471
- status: 'ok',
9472
- tileType: 'draftAnalysis',
9473
- title: item.title ?? item.i,
9474
- viz: item.viz,
9475
- chartConfig: mergeDashboardChartConfig(result.chartConfig, item),
9476
- result,
9477
- trustState: 'review_required',
9478
- citation: { kind: repair ? 'app_draft_analysis_repaired' : 'app_draft_analysis', name: item.title ?? item.i, path: draftAppRelativePath },
9479
- ...(repair ? { repair } : {}),
9480
- artifact: {
9481
- version: 1,
9482
- sourceKind: 'draft_analysis',
9483
- name: item.title ?? item.i,
9484
- sourcePath: draftAppRelativePath,
9485
- dql: (repairedDql ?? draftSource).slice(0, 40_000),
9486
- ...(repairedSql ?? draftRepairPlan?.sql ? { sql: (repairedSql ?? draftRepairPlan.sql).slice(0, 40_000) } : {}),
9578
+ tiles.push({
9579
+ tileId: item.i,
9580
+ status: 'error',
9581
+ tileType: 'draftAnalysis',
9582
+ title: item.title ?? item.i,
9583
+ error: err instanceof Error ? err.message : String(err),
9487
9584
  trustState: 'review_required',
9488
- explanation: [
9489
- 'This is private App analysis and remains review-required.',
9490
- repair ? 'A bounded repair ran on the original data target without changing the saved source file.' : 'The saved App-scoped DQL was executed with the current filters.',
9491
- ],
9492
- executionTarget: executionTargetDescriptor(body),
9493
- },
9494
- });
9585
+ repair: repaired.repair,
9586
+ ...(draftSource ? {
9587
+ artifact: {
9588
+ version: 1,
9589
+ sourceKind: 'draft_analysis',
9590
+ name: item.title ?? item.i,
9591
+ ...(draftAppRelativePath ? { sourcePath: draftAppRelativePath } : {}),
9592
+ dql: draftSource.slice(0, 40_000),
9593
+ ...(draftRepairPlan?.sql ? { sql: draftRepairPlan.sql.slice(0, 40_000) } : {}),
9594
+ trustState: 'review_required',
9595
+ explanation: ['Automatic repair could not produce a safe executable result. The saved App analysis was not changed.'],
9596
+ executionTarget: executionTargetDescriptor(body),
9597
+ },
9598
+ } : {}),
9599
+ });
9600
+ }
9601
+ continue;
9495
9602
  }
9496
- catch (err) {
9497
- const repaired = await repairFailedAppTileExecution({
9498
- source: 'draft_analysis',
9499
- question: item.title ?? item.i,
9500
- sourceSql: draftRepairPlan?.sql,
9501
- sourceDql: draftSource,
9502
- artifactName: item.title ?? item.i,
9503
- error: err,
9504
- targetConnection: draftTargetConnection,
9505
- targetConnectionName: projectConfig.defaultConnectionName,
9506
- sqlParams: draftRepairPlan?.sqlParams,
9507
- variables: { ...(draftRepairPlan?.variables ?? {}), ...draftParameters },
9508
- });
9509
- if (repaired.ok && draftSource) {
9603
+ if (item.semantic) {
9604
+ try {
9605
+ if (!semanticLayer)
9606
+ throw new Error('Semantic layer is not available for this governed query.');
9607
+ const knownMetrics = new Set(semanticLayer.listMetrics().map((metric) => metric.name));
9608
+ const missingMetrics = item.semantic.metrics.filter((metric) => !knownMetrics.has(metric));
9609
+ if (missingMetrics.length)
9610
+ throw new Error(`Semantic definition changed; missing metric(s): ${missingMetrics.join(', ')}`);
9611
+ const knownModels = new Set(semanticLayer.listSemanticModels().map((model) => model.name));
9612
+ const missingModels = item.semantic.semanticModelRefs.filter((model) => !knownModels.has(model));
9613
+ if (missingModels.length)
9614
+ throw new Error(`Semantic definition changed; missing model(s): ${missingModels.join(', ')}`);
9615
+ const targetConnection = requireActiveConnection(isConnectionConfig(body.connection) ? body.connection : connection);
9616
+ const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer, projectRoot);
9617
+ const activeFilters = dashboardSemanticFiltersForTile(loaded.dashboard, item, dashboardVariables);
9618
+ const staticFilters = (item.semantic.filters ?? []).map((filter) => ({
9619
+ dimension: filter.field,
9620
+ operator: filter.operator,
9621
+ values: Array.isArray(filter.value) ? filter.value.map(String) : [String(filter.value)],
9622
+ }));
9623
+ const composed = await composeRuntimeSemanticQuery({
9624
+ metrics: item.semantic.metrics,
9625
+ dimensions: item.semantic.dimensions ?? [],
9626
+ filters: [...staticFilters, ...activeFilters],
9627
+ ...(item.semantic.timeDimension ? { timeDimension: { name: item.semantic.timeDimension, granularity: 'month' } } : {}),
9628
+ orderBy: item.semantic.orderBy?.map((order) => ({ name: order.field, direction: order.direction })),
9629
+ limit: item.semantic.limit,
9630
+ engine: item.semantic.provider,
9631
+ }, semanticLayer, {
9632
+ projectRoot,
9633
+ projectConfig,
9634
+ detectedProvider: semanticDetectedProvider,
9635
+ driver: targetConnection.driver,
9636
+ tableMapping,
9637
+ });
9638
+ if (!composed)
9639
+ throw new Error('The governed semantic query could not be composed.');
9640
+ const prepared = prepareLocalExecution(composed.sql, targetConnection, projectRoot, projectConfig);
9641
+ let normalizedResult;
9642
+ let repair;
9643
+ let executedSql = composed.sql;
9644
+ try {
9645
+ const result = await executor.executeQuery(prepared.sql, [], {}, prepared.connection);
9646
+ normalizedResult = normalizeQueryResult(result);
9647
+ }
9648
+ catch (executionError) {
9649
+ const repaired = await repairFailedAppTileExecution({
9650
+ source: 'semantic_query',
9651
+ question: item.title ?? item.semantic.id,
9652
+ sourceSql: composed.sql,
9653
+ error: executionError,
9654
+ targetConnection,
9655
+ });
9656
+ if (!repaired.ok) {
9657
+ tiles.push({
9658
+ tileId: item.i,
9659
+ status: 'error',
9660
+ tileType: 'semantic',
9661
+ title: item.title ?? item.semantic.id,
9662
+ error: executionError instanceof Error ? executionError.message : String(executionError),
9663
+ trustState: 'review_required',
9664
+ repair: repaired.repair,
9665
+ artifact: {
9666
+ version: 1,
9667
+ sourceKind: 'semantic_query',
9668
+ name: item.title ?? item.semantic.id,
9669
+ sql: composed.sql.slice(0, 40_000),
9670
+ trustState: 'review_required',
9671
+ explanation: ['Automatic repair could not execute the governed semantic query safely.'],
9672
+ executionTarget: executionTargetDescriptor(body),
9673
+ },
9674
+ });
9675
+ continue;
9676
+ }
9677
+ normalizedResult = normalizeQueryResult(repaired.result);
9678
+ repair = repaired.repair;
9679
+ executedSql = repaired.repairedSql;
9680
+ }
9510
9681
  tiles.push({
9511
9682
  tileId: item.i,
9512
9683
  status: 'ok',
9513
- tileType: 'draftAnalysis',
9514
- title: item.title ?? item.i,
9684
+ tileType: 'semantic',
9685
+ title: item.title ?? item.semantic.id,
9515
9686
  viz: item.viz,
9516
- chartConfig: mergeDashboardChartConfig(repaired.result.chartConfig, item),
9517
- result: repaired.result,
9687
+ result: normalizedResult,
9518
9688
  trustState: 'review_required',
9519
- citation: { kind: 'app_draft_analysis_repaired', name: item.title ?? item.i, path: draftAppRelativePath },
9520
- repair: repaired.repair,
9689
+ citation: { kind: repair ? 'semantic_query_repaired' : 'semantic_query', name: item.semantic.id },
9690
+ ...(repair ? { repair } : {}),
9521
9691
  artifact: {
9522
9692
  version: 1,
9523
- sourceKind: 'draft_analysis',
9524
- name: item.title ?? item.i,
9525
- ...(draftAppRelativePath ? { sourcePath: draftAppRelativePath } : {}),
9526
- dql: (repaired.repairedDql ?? draftSource).slice(0, 40_000),
9527
- sql: repaired.repairedSql.slice(0, 40_000),
9693
+ sourceKind: 'semantic_query',
9694
+ name: item.title ?? item.semantic.id,
9695
+ sql: executedSql.slice(0, 40_000),
9528
9696
  trustState: 'review_required',
9529
- explanation: ['A bounded DQL repair executed on the original data target. The saved App analysis was not changed.'],
9697
+ explanation: [
9698
+ `Metrics: ${item.semantic.metrics.join(', ') || 'none'}.`,
9699
+ `Dimensions: ${item.semantic.dimensions?.join(', ') || 'none'}.`,
9700
+ repair ? 'The composed SQL needed bounded repair and therefore remains review-required.' : 'The governed semantic intent was composed for the active warehouse and remains review-required until approval.',
9701
+ ],
9530
9702
  executionTarget: executionTargetDescriptor(body),
9531
9703
  },
9532
9704
  });
9533
- continue;
9534
9705
  }
9706
+ catch (err) {
9707
+ tiles.push({
9708
+ tileId: item.i,
9709
+ status: 'error',
9710
+ tileType: 'semantic',
9711
+ title: item.title ?? item.semantic.id,
9712
+ error: err instanceof Error ? err.message : String(err),
9713
+ trustState: 'review_required',
9714
+ });
9715
+ }
9716
+ continue;
9717
+ }
9718
+ const block = resolveDashboardItemBlock(item, manifest);
9719
+ if (!block) {
9535
9720
  tiles.push({
9536
9721
  tileId: item.i,
9537
- status: 'error',
9538
- tileType: 'draftAnalysis',
9539
- title: item.title ?? item.i,
9540
- error: err instanceof Error ? err.message : String(err),
9541
- trustState: 'review_required',
9542
- repair: repaired.repair,
9543
- ...(draftSource ? {
9544
- artifact: {
9545
- version: 1,
9546
- sourceKind: 'draft_analysis',
9547
- name: item.title ?? item.i,
9548
- ...(draftAppRelativePath ? { sourcePath: draftAppRelativePath } : {}),
9549
- dql: draftSource.slice(0, 40_000),
9550
- ...(draftRepairPlan?.sql ? { sql: draftRepairPlan.sql.slice(0, 40_000) } : {}),
9551
- trustState: 'review_required',
9552
- explanation: ['Automatic repair could not produce a safe executable result. The saved App analysis was not changed.'],
9553
- executionTarget: executionTargetDescriptor(body),
9554
- },
9555
- } : {}),
9722
+ status: 'unresolved',
9723
+ blockRef: item.block ? (isBlockIdRef(item.block) ? item.block.blockId : item.block.ref) : '(missing source)',
9724
+ error: 'Block reference could not be resolved',
9556
9725
  });
9726
+ continue;
9557
9727
  }
9558
- continue;
9559
- }
9560
- if (item.semantic) {
9728
+ let blockSource;
9729
+ let blockTargetConnection;
9730
+ let blockPlan;
9731
+ let blockFilterApplication;
9732
+ let blockInvocation;
9561
9733
  try {
9562
- if (!semanticLayer)
9563
- throw new Error('Semantic layer is not available for this governed query.');
9564
- const knownMetrics = new Set(semanticLayer.listMetrics().map((metric) => metric.name));
9565
- const missingMetrics = item.semantic.metrics.filter((metric) => !knownMetrics.has(metric));
9566
- if (missingMetrics.length)
9567
- throw new Error(`Semantic definition changed; missing metric(s): ${missingMetrics.join(', ')}`);
9568
- const knownModels = new Set(semanticLayer.listSemanticModels().map((model) => model.name));
9569
- const missingModels = item.semantic.semanticModelRefs.filter((model) => !knownModels.has(model));
9570
- if (missingModels.length)
9571
- throw new Error(`Semantic definition changed; missing model(s): ${missingModels.join(', ')}`);
9572
- const targetConnection = requireActiveConnection(isConnectionConfig(body.connection) ? body.connection : connection);
9573
- const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer, projectRoot);
9574
- const activeFilters = dashboardSemanticFiltersForTile(loaded.dashboard, item, dashboardVariables);
9575
- const staticFilters = (item.semantic.filters ?? []).map((filter) => ({
9576
- dimension: filter.field,
9577
- operator: filter.operator,
9578
- values: Array.isArray(filter.value) ? filter.value.map(String) : [String(filter.value)],
9579
- }));
9580
- const composed = await composeRuntimeSemanticQuery({
9581
- metrics: item.semantic.metrics,
9582
- dimensions: item.semantic.dimensions ?? [],
9583
- filters: [...staticFilters, ...activeFilters],
9584
- ...(item.semantic.timeDimension ? { timeDimension: { name: item.semantic.timeDimension, granularity: 'month' } } : {}),
9585
- orderBy: item.semantic.orderBy?.map((order) => ({ name: order.field, direction: order.direction })),
9586
- limit: item.semantic.limit,
9587
- engine: item.semantic.provider,
9588
- }, semanticLayer, {
9589
- projectRoot,
9590
- projectConfig,
9591
- detectedProvider: semanticDetectedProvider,
9592
- driver: targetConnection.driver,
9593
- tableMapping,
9734
+ assertAppAccess({
9735
+ app: loaded.app,
9736
+ domain: block.domain ?? loaded.dashboard.metadata.domain ?? loaded.app.domain,
9737
+ level: 'execute',
9594
9738
  });
9595
- if (!composed)
9596
- throw new Error('The governed semantic query could not be composed.');
9597
- const prepared = prepareLocalExecution(composed.sql, targetConnection, projectRoot, projectConfig);
9598
- let normalizedResult;
9599
- let repair;
9600
- let executedSql = composed.sql;
9601
- try {
9602
- const result = await executor.executeQuery(prepared.sql, [], {}, prepared.connection);
9603
- normalizedResult = normalizeQueryResult(result);
9739
+ const absBlockPath = join(projectRoot, block.filePath);
9740
+ blockSource = readFileSync(absBlockPath, 'utf-8');
9741
+ const currentSourceRevision = `sha256:${createHash('sha256').update(blockSource).digest('hex')}`;
9742
+ if (item.sourceRevision && item.sourceRevision !== currentSourceRevision) {
9743
+ tiles.push({
9744
+ tileId: item.i,
9745
+ status: 'error',
9746
+ tileType: 'block',
9747
+ blockId: block.name,
9748
+ blockPath: block.filePath,
9749
+ certificationStatus: block.status ?? null,
9750
+ title: item.title ?? block.name,
9751
+ trustState: 'review_required',
9752
+ error: 'This App source changed after it was selected. Refresh the source binding and rerun preview.',
9753
+ });
9754
+ continue;
9604
9755
  }
9605
- catch (executionError) {
9606
- const repaired = await repairFailedAppTileExecution({
9607
- source: 'semantic_query',
9608
- question: item.title ?? item.semantic.id,
9609
- sourceSql: composed.sql,
9610
- error: executionError,
9611
- targetConnection,
9756
+ blockTargetConnection = requireActiveConnection(isConnectionConfig(body.connection) ? body.connection : connection);
9757
+ const tableMapping = await resolveSemanticTableMapping(executor, blockTargetConnection, semanticLayer, projectRoot);
9758
+ const boundParameters = dashboardTileParameterValues({
9759
+ item,
9760
+ dashboardValues: dashboardVariables,
9761
+ requestValues: variables,
9762
+ });
9763
+ blockInvocation = prepareBlockInvocation({
9764
+ block: block.name,
9765
+ source: blockSource,
9766
+ parameters: { ...dashboardVariables, ...boundParameters },
9767
+ parameterSources: Object.fromEntries(Object.keys(boundParameters).map((name) => [name, 'surface'])),
9768
+ surface: 'app',
9769
+ });
9770
+ if (blockInvocation.errors.length || blockInvocation.unresolvedParameters.length) {
9771
+ tiles.push({
9772
+ tileId: item.i,
9773
+ status: 'unresolved',
9774
+ tileType: 'block',
9775
+ blockId: block.name,
9776
+ title: item.title ?? block.name,
9777
+ error: blockInvocation.errors[0] ?? `Needs values for: ${blockInvocation.unresolvedParameters.join(', ')}`,
9778
+ invocation: {
9779
+ resolvedParameters: blockInvocation.resolvedParameters,
9780
+ unresolvedParameters: blockInvocation.unresolvedParameters,
9781
+ auditId: blockInvocation.auditId,
9782
+ },
9612
9783
  });
9613
- if (!repaired.ok) {
9614
- tiles.push({
9615
- tileId: item.i,
9616
- status: 'error',
9617
- tileType: 'semantic',
9618
- title: item.title ?? item.semantic.id,
9619
- error: executionError instanceof Error ? executionError.message : String(executionError),
9620
- trustState: 'review_required',
9621
- repair: repaired.repair,
9622
- artifact: {
9623
- version: 1,
9624
- sourceKind: 'semantic_query',
9625
- name: item.title ?? item.semantic.id,
9626
- sql: composed.sql.slice(0, 40_000),
9627
- trustState: 'review_required',
9628
- explanation: ['Automatic repair could not execute the governed semantic query safely.'],
9629
- executionTarget: executionTargetDescriptor(body),
9630
- },
9631
- });
9632
- continue;
9633
- }
9634
- normalizedResult = normalizeQueryResult(repaired.result);
9635
- repair = repaired.repair;
9636
- executedSql = repaired.repairedSql;
9784
+ continue;
9637
9785
  }
9638
- tiles.push({
9639
- tileId: item.i,
9640
- status: 'ok',
9641
- tileType: 'semantic',
9642
- title: item.title ?? item.semantic.id,
9643
- viz: item.viz,
9644
- result: normalizedResult,
9645
- trustState: 'review_required',
9646
- citation: { kind: repair ? 'semantic_query_repaired' : 'semantic_query', name: item.semantic.id },
9647
- ...(repair ? { repair } : {}),
9648
- artifact: {
9649
- version: 1,
9650
- sourceKind: 'semantic_query',
9651
- name: item.title ?? item.semantic.id,
9652
- sql: executedSql.slice(0, 40_000),
9653
- trustState: 'review_required',
9654
- explanation: [
9655
- `Metrics: ${item.semantic.metrics.join(', ') || 'none'}.`,
9656
- `Dimensions: ${item.semantic.dimensions?.join(', ') || 'none'}.`,
9657
- repair ? 'The composed SQL needed bounded repair and therefore remains review-required.' : 'The governed semantic intent was composed for the active warehouse and remains review-required until approval.',
9658
- ],
9659
- executionTarget: executionTargetDescriptor(body),
9660
- },
9786
+ const semanticCompose = semanticLayer
9787
+ ? await composeSemanticBlockSqlForRuntime(blockSource, semanticLayer, {
9788
+ driver: blockTargetConnection.driver,
9789
+ tableMapping,
9790
+ projectRoot,
9791
+ projectConfig,
9792
+ detectedProvider: semanticDetectedProvider,
9793
+ parameters: blockInvocation.values,
9794
+ })
9795
+ : null;
9796
+ blockPlan = buildExecutionPlan({ id: item.i, type: 'dql', source: blockSource, title: item.title ?? block.name }, {
9797
+ semanticLayer,
9798
+ driver: blockTargetConnection.driver,
9799
+ tableMapping,
9800
+ parameters: blockInvocation.values,
9801
+ semanticSql: semanticCompose?.sql ?? undefined,
9661
9802
  });
9662
- }
9663
- catch (err) {
9664
- tiles.push({
9803
+ if (!blockPlan && !semanticCompose?.sql) {
9804
+ tiles.push({
9805
+ tileId: item.i,
9806
+ status: 'error',
9807
+ blockId: block.name,
9808
+ error: semanticCompose?.diagnostics.find((diagnostic) => diagnostic.severity === 'error')?.message ?? 'Block produced no executable plan',
9809
+ });
9810
+ continue;
9811
+ }
9812
+ blockFilterApplication = applyDashboardFiltersToBlockExecution({
9813
+ sql: semanticCompose?.sql ?? blockPlan.sql,
9814
+ sqlParams: blockPlan?.sqlParams ?? [],
9815
+ variables: { ...(blockPlan?.variables ?? {}), ...dashboardVariables },
9816
+ block,
9817
+ dashboard: loaded.dashboard,
9665
9818
  tileId: item.i,
9666
- status: 'error',
9667
- tileType: 'semantic',
9668
- title: item.title ?? item.semantic.id,
9669
- error: err instanceof Error ? err.message : String(err),
9670
- trustState: 'review_required',
9819
+ tileFilterBindings: item.filterBindings,
9671
9820
  });
9672
- }
9673
- continue;
9674
- }
9675
- const block = resolveDashboardItemBlock(item, manifest);
9676
- if (!block) {
9677
- tiles.push({
9678
- tileId: item.i,
9679
- status: 'unresolved',
9680
- blockRef: item.block ? (isBlockIdRef(item.block) ? item.block.blockId : item.block.ref) : '(missing source)',
9681
- error: 'Block reference could not be resolved',
9682
- });
9683
- continue;
9684
- }
9685
- let blockSource;
9686
- let blockTargetConnection;
9687
- let blockPlan;
9688
- let blockFilterApplication;
9689
- let blockInvocation;
9690
- try {
9691
- assertAppAccess({
9692
- app: loaded.app,
9693
- domain: block.domain ?? loaded.dashboard.metadata.domain ?? loaded.app.domain,
9694
- level: 'execute',
9695
- });
9696
- const absBlockPath = join(projectRoot, block.filePath);
9697
- blockSource = readFileSync(absBlockPath, 'utf-8');
9698
- blockTargetConnection = requireActiveConnection(isConnectionConfig(body.connection) ? body.connection : connection);
9699
- const tableMapping = await resolveSemanticTableMapping(executor, blockTargetConnection, semanticLayer, projectRoot);
9700
- const boundParameters = dashboardTileParameterValues({
9701
- item,
9702
- dashboardValues: dashboardVariables,
9703
- requestValues: variables,
9704
- });
9705
- blockInvocation = prepareBlockInvocation({
9706
- block: block.name,
9707
- source: blockSource,
9708
- parameters: { ...dashboardVariables, ...boundParameters },
9709
- parameterSources: Object.fromEntries(Object.keys(boundParameters).map((name) => [name, 'surface'])),
9710
- surface: 'app',
9711
- });
9712
- if (blockInvocation.errors.length || blockInvocation.unresolvedParameters.length) {
9821
+ const prepared = prepareLocalExecution(blockFilterApplication.sql, blockTargetConnection, projectRoot, projectConfig);
9822
+ const result = await executor.executeQuery(prepared.sql, blockFilterApplication.sqlParams, runtimeVariables({ ...blockFilterApplication.variables, ...blockInvocation.values }), prepared.connection);
9713
9823
  tiles.push({
9714
9824
  tileId: item.i,
9715
- status: 'unresolved',
9716
- tileType: 'block',
9825
+ status: 'ok',
9717
9826
  blockId: block.name,
9827
+ blockPath: block.filePath,
9828
+ certificationStatus: block.status ?? null,
9718
9829
  title: item.title ?? block.name,
9719
- error: blockInvocation.errors[0] ?? `Needs values for: ${blockInvocation.unresolvedParameters.join(', ')}`,
9830
+ viz: item.viz,
9831
+ chartConfig: mergeDashboardChartConfig(blockPlan?.chartConfig, item),
9832
+ result: normalizeQueryResult(result),
9833
+ filters: {
9834
+ applied: blockFilterApplication.appliedFilters,
9835
+ skipped: blockFilterApplication.skippedFilters,
9836
+ },
9720
9837
  invocation: {
9721
9838
  resolvedParameters: blockInvocation.resolvedParameters,
9722
9839
  unresolvedParameters: blockInvocation.unresolvedParameters,
9723
9840
  auditId: blockInvocation.auditId,
9724
9841
  },
9725
- });
9726
- continue;
9727
- }
9728
- const semanticCompose = semanticLayer
9729
- ? await composeSemanticBlockSqlForRuntime(blockSource, semanticLayer, {
9730
- driver: blockTargetConnection.driver,
9731
- tableMapping,
9732
- projectRoot,
9733
- projectConfig,
9734
- detectedProvider: semanticDetectedProvider,
9735
- parameters: blockInvocation.values,
9736
- })
9737
- : null;
9738
- blockPlan = buildExecutionPlan({ id: item.i, type: 'dql', source: blockSource, title: item.title ?? block.name }, {
9739
- semanticLayer,
9740
- driver: blockTargetConnection.driver,
9741
- tableMapping,
9742
- parameters: blockInvocation.values,
9743
- semanticSql: semanticCompose?.sql ?? undefined,
9744
- });
9745
- if (!blockPlan && !semanticCompose?.sql) {
9746
- tiles.push({
9747
- tileId: item.i,
9748
- status: 'error',
9749
- blockId: block.name,
9750
- error: semanticCompose?.diagnostics.find((diagnostic) => diagnostic.severity === 'error')?.message ?? 'Block produced no executable plan',
9751
- });
9752
- continue;
9753
- }
9754
- blockFilterApplication = applyDashboardFiltersToBlockExecution({
9755
- sql: semanticCompose?.sql ?? blockPlan.sql,
9756
- sqlParams: blockPlan?.sqlParams ?? [],
9757
- variables: { ...(blockPlan?.variables ?? {}), ...dashboardVariables },
9758
- block,
9759
- dashboard: loaded.dashboard,
9760
- tileId: item.i,
9761
- tileFilterBindings: item.filterBindings,
9762
- });
9763
- const prepared = prepareLocalExecution(blockFilterApplication.sql, blockTargetConnection, projectRoot, projectConfig);
9764
- const result = await executor.executeQuery(prepared.sql, blockFilterApplication.sqlParams, runtimeVariables({ ...blockFilterApplication.variables, ...blockInvocation.values }), prepared.connection);
9765
- tiles.push({
9766
- tileId: item.i,
9767
- status: 'ok',
9768
- blockId: block.name,
9769
- blockPath: block.filePath,
9770
- certificationStatus: block.status ?? null,
9771
- title: item.title ?? block.name,
9772
- viz: item.viz,
9773
- chartConfig: mergeDashboardChartConfig(blockPlan?.chartConfig, item),
9774
- result: normalizeQueryResult(result),
9775
- filters: {
9776
- applied: blockFilterApplication.appliedFilters,
9777
- skipped: blockFilterApplication.skippedFilters,
9778
- },
9779
- invocation: {
9780
- resolvedParameters: blockInvocation.resolvedParameters,
9781
- unresolvedParameters: blockInvocation.unresolvedParameters,
9782
- auditId: blockInvocation.auditId,
9783
- },
9784
- trustState: block.status === 'certified' ? 'certified' : 'review_required',
9785
- citation: {
9786
- kind: 'block',
9787
- name: block.name,
9788
- path: block.filePath,
9789
- },
9790
- artifact: {
9791
- version: 1,
9792
- sourceKind: 'certified_block',
9793
- name: block.name,
9794
- sourcePath: block.filePath,
9795
- dql: blockSource.slice(0, 40_000),
9796
- sql: blockFilterApplication.sql.slice(0, 40_000),
9797
9842
  trustState: block.status === 'certified' ? 'certified' : 'review_required',
9798
- explanation: [
9799
- block.status === 'certified' ? 'This tile executed a certified project block.' : 'This project block is not certified and remains review-required.',
9800
- 'Dashboard filters and typed parameters were bound before read-only execution.',
9801
- ],
9802
- executionTarget: executionTargetDescriptor(body),
9803
- },
9804
- });
9805
- }
9806
- catch (err) {
9807
- if (err instanceof DQLAccessDeniedError) {
9808
- tiles.push({
9809
- tileId: item.i,
9810
- status: 'unauthorized',
9811
- blockId: block.name,
9812
- error: err.message,
9813
- });
9814
- }
9815
- else {
9816
- const repaired = await repairFailedAppTileExecution({
9817
- source: 'certified_block',
9818
- question: item.title ?? block.name,
9819
- sourceSql: blockFilterApplication?.sql ?? blockPlan?.sql,
9820
- sourceDql: blockSource,
9821
- artifactName: block.name,
9822
- error: err,
9823
- targetConnection: blockTargetConnection,
9824
- targetConnectionName: projectConfig.defaultConnectionName,
9825
- sqlParams: blockFilterApplication?.sqlParams ?? blockPlan?.sqlParams,
9826
- variables: {
9827
- ...(blockPlan?.variables ?? {}),
9828
- ...(blockFilterApplication?.variables ?? {}),
9829
- ...(blockInvocation?.values ?? {}),
9843
+ citation: {
9844
+ kind: 'block',
9845
+ name: block.name,
9846
+ path: block.filePath,
9847
+ },
9848
+ artifact: {
9849
+ version: 1,
9850
+ sourceKind: block.status === 'certified' ? 'certified_block' : 'review_block',
9851
+ name: block.name,
9852
+ sourcePath: block.filePath,
9853
+ dql: blockSource.slice(0, 40_000),
9854
+ sql: blockFilterApplication.sql.slice(0, 40_000),
9855
+ trustState: block.status === 'certified' ? 'certified' : 'review_required',
9856
+ explanation: [
9857
+ block.status === 'certified' ? 'This tile executed a certified project block.' : 'This project block is not certified and remains review-required.',
9858
+ 'Dashboard filters and typed parameters were bound before read-only execution.',
9859
+ ],
9860
+ executionTarget: executionTargetDescriptor(body),
9830
9861
  },
9831
9862
  });
9832
- if (repaired.ok) {
9863
+ }
9864
+ catch (err) {
9865
+ if (err instanceof DQLAccessDeniedError) {
9833
9866
  tiles.push({
9834
9867
  tileId: item.i,
9835
- status: 'ok',
9836
- tileType: 'block',
9868
+ status: 'unauthorized',
9837
9869
  blockId: block.name,
9838
- blockPath: block.filePath,
9839
- certificationStatus: block.status ?? null,
9840
- title: item.title ?? block.name,
9841
- viz: item.viz,
9842
- chartConfig: mergeDashboardChartConfig(blockPlan?.chartConfig, item),
9843
- result: normalizeQueryResult(repaired.result),
9844
- ...(blockFilterApplication ? {
9845
- filters: {
9846
- applied: blockFilterApplication.appliedFilters,
9847
- skipped: blockFilterApplication.skippedFilters,
9870
+ error: err.message,
9871
+ });
9872
+ }
9873
+ else {
9874
+ const repaired = await repairFailedAppTileExecution({
9875
+ source: block.status === 'certified' ? 'certified_block' : 'review_block',
9876
+ question: item.title ?? block.name,
9877
+ sourceSql: blockFilterApplication?.sql ?? blockPlan?.sql,
9878
+ sourceDql: blockSource,
9879
+ artifactName: block.name,
9880
+ error: err,
9881
+ targetConnection: blockTargetConnection,
9882
+ targetConnectionName: projectConfig.defaultConnectionName,
9883
+ sqlParams: blockFilterApplication?.sqlParams ?? blockPlan?.sqlParams,
9884
+ variables: {
9885
+ ...(blockPlan?.variables ?? {}),
9886
+ ...(blockFilterApplication?.variables ?? {}),
9887
+ ...(blockInvocation?.values ?? {}),
9888
+ },
9889
+ });
9890
+ if (repaired.ok) {
9891
+ tiles.push({
9892
+ tileId: item.i,
9893
+ status: 'ok',
9894
+ tileType: 'block',
9895
+ blockId: block.name,
9896
+ blockPath: block.filePath,
9897
+ certificationStatus: block.status ?? null,
9898
+ title: item.title ?? block.name,
9899
+ viz: item.viz,
9900
+ chartConfig: mergeDashboardChartConfig(blockPlan?.chartConfig, item),
9901
+ result: normalizeQueryResult(repaired.result),
9902
+ ...(blockFilterApplication ? {
9903
+ filters: {
9904
+ applied: blockFilterApplication.appliedFilters,
9905
+ skipped: blockFilterApplication.skippedFilters,
9906
+ },
9907
+ } : {}),
9908
+ ...(blockInvocation ? {
9909
+ invocation: {
9910
+ resolvedParameters: blockInvocation.resolvedParameters,
9911
+ unresolvedParameters: blockInvocation.unresolvedParameters,
9912
+ auditId: blockInvocation.auditId,
9913
+ },
9914
+ } : {}),
9915
+ trustState: 'review_required',
9916
+ repair: repaired.repair,
9917
+ citation: { kind: 'block_repaired', name: block.name, path: block.filePath },
9918
+ artifact: {
9919
+ version: 1,
9920
+ sourceKind: block.status === 'certified' ? 'certified_block' : 'review_block',
9921
+ name: block.name,
9922
+ sourcePath: block.filePath,
9923
+ ...(repaired.repairedDql ?? blockSource ? { dql: (repaired.repairedDql ?? blockSource).slice(0, 40_000) } : {}),
9924
+ sql: repaired.repairedSql.slice(0, 40_000),
9925
+ trustState: 'review_required',
9926
+ explanation: [
9927
+ 'The saved certified block was not changed.',
9928
+ 'A bounded repair executed on the original data target; this repaired result requires human review.',
9929
+ ],
9930
+ executionTarget: executionTargetDescriptor(body),
9848
9931
  },
9849
- } : {}),
9850
- ...(blockInvocation ? {
9851
- invocation: {
9852
- resolvedParameters: blockInvocation.resolvedParameters,
9853
- unresolvedParameters: blockInvocation.unresolvedParameters,
9854
- auditId: blockInvocation.auditId,
9932
+ });
9933
+ continue;
9934
+ }
9935
+ tiles.push({
9936
+ tileId: item.i,
9937
+ status: 'error',
9938
+ tileType: 'block',
9939
+ blockId: block.name,
9940
+ error: err instanceof Error ? err.message : String(err),
9941
+ repair: repaired.repair,
9942
+ ...(blockSource ? {
9943
+ artifact: {
9944
+ version: 1,
9945
+ sourceKind: block.status === 'certified' ? 'certified_block' : 'review_block',
9946
+ name: block.name,
9947
+ sourcePath: block.filePath,
9948
+ dql: blockSource.slice(0, 40_000),
9949
+ ...(blockFilterApplication?.sql ?? blockPlan?.sql ? { sql: (blockFilterApplication?.sql ?? blockPlan.sql).slice(0, 40_000) } : {}),
9950
+ trustState: 'review_required',
9951
+ explanation: ['Automatic repair could not produce a safe executable result. The saved block was not changed.'],
9952
+ executionTarget: executionTargetDescriptor(body),
9855
9953
  },
9856
9954
  } : {}),
9857
- trustState: 'review_required',
9858
- repair: repaired.repair,
9859
- citation: { kind: 'block_repaired', name: block.name, path: block.filePath },
9860
- artifact: {
9861
- version: 1,
9862
- sourceKind: 'certified_block',
9863
- name: block.name,
9864
- sourcePath: block.filePath,
9865
- ...(repaired.repairedDql ?? blockSource ? { dql: (repaired.repairedDql ?? blockSource).slice(0, 40_000) } : {}),
9866
- sql: repaired.repairedSql.slice(0, 40_000),
9867
- trustState: 'review_required',
9868
- explanation: [
9869
- 'The saved certified block was not changed.',
9870
- 'A bounded repair executed on the original data target; this repaired result requires human review.',
9871
- ],
9872
- executionTarget: executionTargetDescriptor(body),
9873
- },
9874
9955
  });
9875
- continue;
9876
9956
  }
9877
- tiles.push({
9878
- tileId: item.i,
9879
- status: 'error',
9880
- tileType: 'block',
9881
- blockId: block.name,
9882
- error: err instanceof Error ? err.message : String(err),
9883
- repair: repaired.repair,
9884
- ...(blockSource ? {
9885
- artifact: {
9886
- version: 1,
9887
- sourceKind: 'certified_block',
9888
- name: block.name,
9889
- sourcePath: block.filePath,
9890
- dql: blockSource.slice(0, 40_000),
9891
- ...(blockFilterApplication?.sql ?? blockPlan?.sql ? { sql: (blockFilterApplication?.sql ?? blockPlan.sql).slice(0, 40_000) } : {}),
9892
- trustState: 'review_required',
9893
- explanation: ['Automatic repair could not produce a safe executable result. The saved block was not changed.'],
9894
- executionTarget: executionTargetDescriptor(body),
9895
- },
9896
- } : {}),
9897
- });
9898
9957
  }
9899
9958
  }
9900
- }
9901
- localApps?.close();
9959
+ };
9960
+ await Promise.all(Array.from({ length: Math.min(4, itemsToRun.length) }, () => runTileWorker()));
9961
+ tiles.sort((left, right) => (tileOrder.get(left.tileId) ?? 0) - (tileOrder.get(right.tileId) ?? 0));
9962
+ localApps.current?.close();
9902
9963
  // Which of each tile's result columns a viewer may safely filter on.
9903
9964
  // Decided here, from a real dialect parse of the SQL that actually ran,
9904
9965
  // for the same reason Ask does it server-side: an aggregate output needs
@@ -9920,7 +9981,7 @@ export async function startLocalServer(opts) {
9920
9981
  try {
9921
9982
  const dashboardItem = loaded.dashboard.layout.items.find((item) => item.i === tile.tileId);
9922
9983
  const block = dashboardItem ? resolveDashboardItemBlock(dashboardItem, manifest) : null;
9923
- const declaredDimensions = block?.status === 'certified' ? block.dimensions ?? [] : [];
9984
+ const declaredDimensions = block?.dimensions ?? [];
9924
9985
  const filterable = dashboardFilterableResultColumns(executedSql, columns, declaredDimensions, runDialect);
9925
9986
  return filterable.length > 0 ? { ...tile, filterableColumns: filterable } : tile;
9926
9987
  }
@@ -9951,7 +10012,7 @@ export async function startLocalServer(opts) {
9951
10012
  eligibleTileIds: loaded.dashboard.story?.eligibleTileIds,
9952
10013
  driverTileIds: loaded.dashboard.story?.driverTileIds,
9953
10014
  });
9954
- dashboardRunEvidence.set(runId, {
10015
+ const runEvidence = {
9955
10016
  appId,
9956
10017
  dashboardId,
9957
10018
  snapshotId: snapshot.snapshotId,
@@ -9965,7 +10026,28 @@ export async function startLocalServer(opts) {
9965
10026
  facts: storyResult.facts,
9966
10027
  story: storyResult.story,
9967
10028
  expiresAt: Date.now() + 15 * 60_000,
9968
- });
10029
+ };
10030
+ dashboardRunEvidence.set(runId, runEvidence);
10031
+ if (runSurface === 'app-builds') {
10032
+ const receiptStorage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
10033
+ try {
10034
+ receiptStorage.saveAppPreviewEvidence({
10035
+ runId,
10036
+ draftId: appId,
10037
+ dashboardId,
10038
+ snapshotId: snapshot.snapshotId,
10039
+ filterFingerprint,
10040
+ resultFingerprint,
10041
+ personaFingerprint,
10042
+ successfulTileIds: runEvidence.successfulTileIds,
10043
+ semanticApprovalEligibleTileIds: runEvidence.semanticApprovalEligibleTileIds,
10044
+ createdAt: new Date().toISOString(),
10045
+ });
10046
+ }
10047
+ finally {
10048
+ receiptStorage.close();
10049
+ }
10050
+ }
9969
10051
  for (const [id, evidence] of dashboardRunEvidence)
9970
10052
  if (evidence.expiresAt < Date.now())
9971
10053
  dashboardRunEvidence.delete(id);
@@ -10019,6 +10101,17 @@ export async function startLocalServer(opts) {
10019
10101
  // questions. Throws when no provider is configured — propose degrades
10020
10102
  // gracefully by listing the gap instead.
10021
10103
  generateGovernedAnswer: (question, appContext) => generateAppBuildAnswerWithRepair(question, appContext),
10104
+ // App Builder owns its orchestration and structured state. It shares
10105
+ // only the configured provider adapter with Ask/Notebook.
10106
+ planAppBuild: async ({ system, user }) => {
10107
+ const provider = await createBlockStudioAssistProvider(projectRoot);
10108
+ if (!provider)
10109
+ return undefined;
10110
+ return provider.generate([
10111
+ { role: 'system', content: system },
10112
+ { role: 'user', content: user },
10113
+ ], { maxTokens: 2_400, temperature: 0.1 });
10114
+ },
10022
10115
  queueOperation: (input, task) => {
10023
10116
  const operation = operationCoordinator.create(input);
10024
10117
  void operationCoordinator.run(operation.id, task);
@@ -10043,9 +10136,9 @@ export async function startLocalServer(opts) {
10043
10136
  return { ok: true, snapshotId: evidence.snapshotId, resultFingerprint: evidence.resultFingerprint };
10044
10137
  },
10045
10138
  verifyAppBuildPreview: ({ runId, draftId, dashboardId, tileIds }) => {
10046
- const evidence = dashboardRunEvidence.get(runId);
10047
- if (!evidence || evidence.expiresAt < Date.now())
10048
- return { ok: false, error: 'The App preview receipt expired. Run the local draft again.' };
10139
+ const evidence = loadAppBuildPreviewEvidence(runId);
10140
+ if (!evidence)
10141
+ return { ok: false, error: 'The App preview receipt is unavailable. Run the local draft again.' };
10049
10142
  if (evidence.appId !== draftId || evidence.dashboardId !== dashboardId)
10050
10143
  return { ok: false, error: 'The App preview receipt does not match this local draft page.' };
10051
10144
  const successful = new Set(evidence.successfulTileIds);
@@ -11606,13 +11699,12 @@ export async function startLocalServer(opts) {
11606
11699
  res.end(serializeJSON({ error: 'Block name is required.' }));
11607
11700
  return;
11608
11701
  }
11609
- const canonicalPath = currentPath && !isDraftBlockPath(currentPath) ? currentPath : undefined;
11702
+ const existingCanonicalDraft = Boolean(currentPath && !isDraftBlockPath(currentPath));
11610
11703
  const stableSuffix = createHash('sha256')
11611
11704
  .update(currentPath ?? `${name}:${draftSource}`)
11612
11705
  .digest('hex')
11613
11706
  .slice(0, 16);
11614
- const draftPath = saveBlockStudioDraftArtifacts(projectRoot, {
11615
- currentPath: currentPath && isDraftBlockPath(currentPath) ? currentPath : undefined,
11707
+ const draftSaveOptions = {
11616
11708
  source: draftSource,
11617
11709
  name,
11618
11710
  domain: typeof suppliedMetadata.domain === 'string' ? suppliedMetadata.domain : parsed.domain,
@@ -11620,8 +11712,22 @@ export async function startLocalServer(opts) {
11620
11712
  owner: typeof suppliedMetadata.owner === 'string' ? suppliedMetadata.owner : parsed.owner,
11621
11713
  tags: Array.isArray(suppliedMetadata.tags) ? suppliedMetadata.tags.map(String) : parsed.tags,
11622
11714
  lineage: Array.isArray(suppliedMetadata.lineage) ? suppliedMetadata.lineage.map(String) : undefined,
11623
- stableSuffix,
11624
- });
11715
+ };
11716
+ // A saved draft already has a durable identity, even when it lives in
11717
+ // the canonical domain blocks directory. Reuse that identity for the
11718
+ // certification attempt. Creating a second `_drafts` artifact here
11719
+ // made a failed gate duplicate the block in the library.
11720
+ const draftPath = existingCanonicalDraft
11721
+ ? saveBlockStudioArtifacts(projectRoot, {
11722
+ currentPath: currentPath,
11723
+ ...draftSaveOptions,
11724
+ })
11725
+ : saveBlockStudioDraftArtifacts(projectRoot, {
11726
+ currentPath: currentPath && isDraftBlockPath(currentPath) ? currentPath : undefined,
11727
+ ...draftSaveOptions,
11728
+ stableSuffix,
11729
+ });
11730
+ const canonicalPath = existingCanonicalDraft ? draftPath : undefined;
11625
11731
  const operation = operationCoordinator.create({
11626
11732
  type: 'block_certification',
11627
11733
  scope: `block:${draftPath}`,
@@ -11647,13 +11753,22 @@ export async function startLocalServer(opts) {
11647
11753
  if (signal.aborted)
11648
11754
  throw Object.assign(new Error('Certification cancelled.'), { code: 'OPERATION_CANCELLED' });
11649
11755
  if (!result.certification.certified || blockers.length > 0) {
11756
+ if (result.preview)
11757
+ writeBlockStudioRunSummary(projectRoot, draftPath, draftSource, result.preview.result);
11758
+ const openedDraft = openBlockStudioDocument(projectRoot, draftPath, semanticLayer);
11650
11759
  return {
11651
11760
  outcome: 'draft_saved_with_blockers',
11652
11761
  oldPath: currentPath ?? draftPath,
11653
11762
  draftPath,
11654
11763
  blockers,
11655
11764
  checklist: result.checklist,
11656
- block: openBlockStudioDocument(projectRoot, draftPath, semanticLayer),
11765
+ runSummary: result.preview ? {
11766
+ rowCount: result.preview.result.rowCount ?? result.preview.result.rows.length,
11767
+ executionTime: result.preview.result.executionTime,
11768
+ columns: result.preview.result.columns,
11769
+ ranAt: new Date().toISOString(),
11770
+ } : undefined,
11771
+ block: { ...openedDraft, validation: result.validation },
11657
11772
  };
11658
11773
  }
11659
11774
  report({ phase: 'publishing', progress: 80, message: 'Publishing the certified block.' });
@@ -11699,13 +11814,22 @@ export async function startLocalServer(opts) {
11699
11814
  /* best-effort: learning capture must never block certification */
11700
11815
  }
11701
11816
  const refreshOperation = scheduleProjectRefresh('block-studio-certification');
11817
+ if (result.preview)
11818
+ writeBlockStudioRunSummary(projectRoot, savedPath, certifiedSource, result.preview.result);
11819
+ const openedCertified = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
11702
11820
  return {
11703
11821
  outcome: 'certified',
11704
11822
  oldPath: currentPath ?? draftPath,
11705
11823
  draftPath,
11706
11824
  newPath: savedPath,
11707
- block: openBlockStudioDocument(projectRoot, savedPath, semanticLayer),
11825
+ block: { ...openedCertified, validation: result.validation },
11708
11826
  checklist: result.checklist,
11827
+ runSummary: result.preview ? {
11828
+ rowCount: result.preview.result.rowCount ?? result.preview.result.rows.length,
11829
+ executionTime: result.preview.result.executionTime,
11830
+ columns: result.preview.result.columns,
11831
+ ranAt: new Date().toISOString(),
11832
+ } : undefined,
11709
11833
  indexRefresh: { status: 'queued', operationId: refreshOperation.id },
11710
11834
  };
11711
11835
  });
@@ -11723,6 +11847,32 @@ export async function startLocalServer(opts) {
11723
11847
  }
11724
11848
  return;
11725
11849
  }
11850
+ if (req.method === 'POST' && path === '/api/block-studio/certification-check') {
11851
+ try {
11852
+ const body = await readJSON(req);
11853
+ const source = typeof body.source === 'string' ? body.source : '';
11854
+ if (!source.trim()) {
11855
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
11856
+ res.end(serializeJSON({ error: 'source is required' }));
11857
+ return;
11858
+ }
11859
+ // Read-only certification assessment for repair/review workflows. It
11860
+ // deliberately does not write source, receipts, learning, or indexes.
11861
+ const result = await certifyBlockStudioSource(source, null, { enterprise: body.enterprise === true });
11862
+ const blockers = Array.from(new Set(result.checklist.blockers));
11863
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
11864
+ res.end(serializeJSON({
11865
+ ok: result.certification.certified && blockers.length === 0,
11866
+ ...result,
11867
+ blockers,
11868
+ }));
11869
+ }
11870
+ catch (error) {
11871
+ res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
11872
+ res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
11873
+ }
11874
+ return;
11875
+ }
11726
11876
  if (req.method === 'POST' && path === '/api/block-studio/certify') {
11727
11877
  try {
11728
11878
  const body = await readJSON(req);
@@ -11740,6 +11890,8 @@ export async function startLocalServer(opts) {
11740
11890
  const result = await certifyBlockStudioSource(source, blockPath, { enterprise: body.enterprise === true });
11741
11891
  const blockers = Array.from(new Set(result.checklist.blockers));
11742
11892
  if (!result.certification.certified || blockers.length > 0) {
11893
+ if (blockPath && result.preview)
11894
+ writeBlockStudioRunSummary(projectRoot, blockPath, source, result.preview.result);
11743
11895
  res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
11744
11896
  res.end(serializeJSON({ ok: false, ...result, blockers }));
11745
11897
  return;
@@ -11759,10 +11911,14 @@ export async function startLocalServer(opts) {
11759
11911
  owner: parsed.owner,
11760
11912
  tags: parsed.tags,
11761
11913
  });
11914
+ if (result.preview)
11915
+ writeBlockStudioRunSummary(projectRoot, savedPath, certifiedSource, result.preview.result);
11762
11916
  certifiedPayload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
11763
11917
  }
11764
11918
  else {
11765
11919
  setBlockStudioStatus(projectRoot, normalizedBlockPath, 'certified');
11920
+ if (result.preview)
11921
+ writeBlockStudioRunSummary(projectRoot, normalizedBlockPath, certifiedSource, result.preview.result);
11766
11922
  certifiedPayload = openBlockStudioDocument(projectRoot, normalizedBlockPath, semanticLayer);
11767
11923
  }
11768
11924
  }
@@ -11802,7 +11958,7 @@ export async function startLocalServer(opts) {
11802
11958
  source: certifiedPayload?.source ?? certifiedSource,
11803
11959
  metadata: certifiedPayload?.metadata,
11804
11960
  companionPath: certifiedPayload?.companionPath ?? null,
11805
- validation: certifiedPayload?.validation ?? result.validation,
11961
+ validation: result.validation,
11806
11962
  lineageRefresh: { status: 'queued', operationId: refreshOperation.id },
11807
11963
  }));
11808
11964
  }
@@ -12568,15 +12724,26 @@ export async function startLocalServer(opts) {
12568
12724
  ? body.parameters
12569
12725
  : {};
12570
12726
  const preview = await runBlockStudioPreviewSource(source, targetConnection, parameters);
12727
+ const blockPath = typeof body.path === 'string' ? body.path : null;
12728
+ if (blockPath)
12729
+ writeBlockStudioRunSummary(projectRoot, blockPath, source, preview.result);
12571
12730
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
12572
12731
  res.end(serializeJSON(preview));
12573
12732
  }
12574
12733
  catch (error) {
12575
12734
  const raw = error instanceof Error ? error.message : String(error);
12735
+ const friendlyMessage = compactBlockStudioRuntimeFailure(raw);
12576
12736
  res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
12577
- // `error` stays the full compiler output (the UI shows it in a
12578
- // collapsible); `friendlyMessage` is the one actionable sentence.
12579
- res.end(serializeJSON({ error: raw, friendlyMessage: compactSemanticRuntimeFailure(raw) }));
12737
+ // Keep the result surface concise. Raw compiler output remains available
12738
+ // for support and AI repair, but is never the primary end-user message.
12739
+ res.end(serializeJSON({
12740
+ error: raw,
12741
+ friendlyMessage,
12742
+ code: 'BLOCK_PREVIEW_FAILED',
12743
+ recoverable: true,
12744
+ details: { technicalDetails: raw },
12745
+ nextActions: ['Review the named field or metric.', 'Fix the draft and run it again.'],
12746
+ }));
12580
12747
  }
12581
12748
  return;
12582
12749
  }
@@ -16240,7 +16407,8 @@ function resolveDashboardItemBlock(item, manifest) {
16240
16407
  return manifest.blocks[item.block.blockId] ?? null;
16241
16408
  }
16242
16409
  const normalizedRef = normalize(item.block.ref).replaceAll('\\', '/');
16243
- return Object.values(manifest.blocks).find((b) => normalize(b.filePath).replaceAll('\\', '/') === normalizedRef) ?? null;
16410
+ return (manifest.blockDeclarations ?? Object.values(manifest.blocks))
16411
+ .find((b) => normalize(b.filePath).replaceAll('\\', '/') === normalizedRef) ?? null;
16244
16412
  }
16245
16413
  function mergeDashboardChartConfig(base, item) {
16246
16414
  const options = item.viz.options ?? {};
@@ -20708,7 +20876,62 @@ export function openBlockStudioDocument(projectRoot, relativePath, semanticLayer
20708
20876
  metadata,
20709
20877
  companionPath: companionPath && existsSync(join(projectRoot, companionPath)) ? companionPath : null,
20710
20878
  validation: validateBlockStudioSource(source, semanticLayer),
20879
+ lastRun: readBlockStudioRunSummary(projectRoot, normalizedPath, source) ?? undefined,
20880
+ };
20881
+ }
20882
+ function blockStudioExecutionFingerprint(source) {
20883
+ return createHash('sha256')
20884
+ .update(setBlockStudioStatusInSource(source, 'draft').trim())
20885
+ .digest('hex');
20886
+ }
20887
+ function blockStudioRunSummaryPath(projectRoot, blockPath) {
20888
+ const normalizedPath = normalize(blockPath).replace(/^\/+/, '').replaceAll('\\', '/');
20889
+ const pathKey = createHash('sha256').update(normalizedPath).digest('hex').slice(0, 32);
20890
+ return join(projectRoot, '.dql', 'runs', 'block-studio', `${pathKey}.json`);
20891
+ }
20892
+ function writeBlockStudioRunSummary(projectRoot, blockPath, source, result) {
20893
+ const normalizedPath = normalize(blockPath).replace(/^\/+/, '').replaceAll('\\', '/');
20894
+ if (!isBlockStudioBlockPath(normalizedPath))
20895
+ return;
20896
+ const receiptPath = blockStudioRunSummaryPath(projectRoot, normalizedPath);
20897
+ mkdirSync(dirname(receiptPath), { recursive: true });
20898
+ const receipt = {
20899
+ version: 1,
20900
+ blockPath: normalizedPath,
20901
+ sourceFingerprint: blockStudioExecutionFingerprint(source),
20902
+ rowCount: result.rowCount ?? result.rows.length,
20903
+ executionTime: result.executionTime,
20904
+ columns: result.columns,
20905
+ ranAt: new Date().toISOString(),
20711
20906
  };
20907
+ const tempPath = `${receiptPath}.${process.pid}.tmp`;
20908
+ writeFileSync(tempPath, JSON.stringify(receipt, null, 2) + '\n', 'utf-8');
20909
+ renameSync(tempPath, receiptPath);
20910
+ }
20911
+ function readBlockStudioRunSummary(projectRoot, blockPath, source) {
20912
+ try {
20913
+ const normalizedPath = normalize(blockPath).replace(/^\/+/, '').replaceAll('\\', '/');
20914
+ const receiptPath = blockStudioRunSummaryPath(projectRoot, normalizedPath);
20915
+ if (!existsSync(receiptPath))
20916
+ return null;
20917
+ const receipt = JSON.parse(readFileSync(receiptPath, 'utf-8'));
20918
+ if (receipt.version !== 1
20919
+ || receipt.blockPath !== normalizedPath
20920
+ || receipt.sourceFingerprint !== blockStudioExecutionFingerprint(source)
20921
+ || !Number.isFinite(receipt.rowCount)
20922
+ || !Array.isArray(receipt.columns)
20923
+ || typeof receipt.ranAt !== 'string')
20924
+ return null;
20925
+ return {
20926
+ rowCount: Number(receipt.rowCount),
20927
+ executionTime: typeof receipt.executionTime === 'number' ? receipt.executionTime : undefined,
20928
+ columns: receipt.columns.filter((column) => typeof column === 'string'),
20929
+ ranAt: receipt.ranAt,
20930
+ };
20931
+ }
20932
+ catch {
20933
+ return null;
20934
+ }
20712
20935
  }
20713
20936
  const BLOCK_STUDIO_DRAFT_RUNTIME_DIAGNOSTICS = new Set([
20714
20937
  'metricflow_unavailable',
@@ -20716,6 +20939,161 @@ const BLOCK_STUDIO_DRAFT_RUNTIME_DIAGNOSTICS = new Set([
20716
20939
  'semantic_layer_missing',
20717
20940
  'semantic_runtime_required',
20718
20941
  ]);
20942
+ const BLOCK_STUDIO_RUNTIME_COMPILE_DIAGNOSTICS = new Set([
20943
+ 'metricflow_unavailable',
20944
+ 'semantic_compose_failed',
20945
+ 'semantic_layer_missing',
20946
+ 'semantic_runtime_check_required',
20947
+ 'semantic_runtime_required',
20948
+ ]);
20949
+ function staticSemanticAuthoringDiagnostic(diagnostic) {
20950
+ if (!BLOCK_STUDIO_RUNTIME_COMPILE_DIAGNOSTICS.has(diagnostic.code ?? ''))
20951
+ return diagnostic;
20952
+ return {
20953
+ severity: 'warning',
20954
+ code: 'semantic_runtime_check_required',
20955
+ title: 'Run required for semantic validation',
20956
+ field: 'Runtime',
20957
+ message: 'The block structure and references are valid, but only the configured semantic runtime can prove this metric combination.',
20958
+ correction: 'Run the block to validate compilation and data access before certification.',
20959
+ technicalDetails: diagnostic.message,
20960
+ action: 'run_again',
20961
+ };
20962
+ }
20963
+ function presentBlockStudioDiagnostic(diagnostic, semanticRefs) {
20964
+ if (diagnostic.title || diagnostic.correction)
20965
+ return diagnostic;
20966
+ const raw = diagnostic.message.trim();
20967
+ const locationMatch = /(?:line\s+)?(\d+)[: ,]+(?:column\s+)?(\d+)/i.exec(raw);
20968
+ const technicalDetails = raw.length > 300 || raw.includes('\n') ? raw : undefined;
20969
+ const base = {
20970
+ ...diagnostic,
20971
+ ...(technicalDetails ? { technicalDetails } : {}),
20972
+ ...(locationMatch ? { location: { line: Number(locationMatch[1]), column: Number(locationMatch[2]) } } : {}),
20973
+ };
20974
+ switch (diagnostic.code) {
20975
+ case 'semantic_compose_failed':
20976
+ return {
20977
+ ...base,
20978
+ title: 'Selected metrics cannot be compiled',
20979
+ field: 'Metrics',
20980
+ references: semanticRefs.metrics,
20981
+ message: semanticRefs.metrics.length > 0
20982
+ ? `${semanticRefs.metrics.length} selected metric${semanticRefs.metrics.length === 1 ? '' : 's'} could not be compiled by the configured semantic runtime.`
20983
+ : 'The selected metrics and dimensions do not share a governed query path.',
20984
+ correction: 'Review the listed metrics in Modeling. Add the missing governed measure or relationship metadata, or replace the incompatible metric, then run the block again.',
20985
+ technicalDetails: raw,
20986
+ action: 'review_metrics',
20987
+ };
20988
+ case 'metricflow_unavailable':
20989
+ case 'semantic_runtime_required':
20990
+ case 'semantic_layer_missing':
20991
+ return {
20992
+ ...base,
20993
+ title: 'Semantic runtime is not ready',
20994
+ field: 'Runtime',
20995
+ references: semanticRefs.metrics,
20996
+ message: 'This semantic block needs a configured dbt Cloud, local MetricFlow, or DQL semantic runtime before it can run or be certified.',
20997
+ correction: 'Open semantic runtime settings, connect the project runtime, then run the block again.',
20998
+ technicalDetails: raw,
20999
+ action: 'configure_runtime',
21000
+ };
21001
+ case 'semantic_metric_missing':
21002
+ return {
21003
+ ...base,
21004
+ title: 'Choose at least one metric',
21005
+ field: 'Metrics',
21006
+ message: 'This semantic block does not select a metric yet.',
21007
+ correction: 'Add one or more governed metrics in the visual builder or DQL source.',
21008
+ action: 'review_metrics',
21009
+ };
21010
+ case 'semantic_ref': {
21011
+ const reference = raw.split(':').slice(1).join(':').trim();
21012
+ return {
21013
+ ...base,
21014
+ title: 'Semantic reference was not found',
21015
+ field: 'Metrics and dimensions',
21016
+ references: reference ? [reference] : [...semanticRefs.metrics, ...semanticRefs.dimensions],
21017
+ message: reference ? `“${reference}” is not available in the current semantic snapshot.` : compactSemanticRuntimeFailure(raw),
21018
+ correction: 'Choose a current metric or dimension from the semantic catalog, then run validation again.',
21019
+ action: 'review_metrics',
21020
+ };
21021
+ }
21022
+ case 'semantic_granularity_missing':
21023
+ return {
21024
+ ...base,
21025
+ title: 'Choose a time grain',
21026
+ field: 'Time',
21027
+ message: 'A time dimension is selected without a granularity.',
21028
+ correction: 'Choose day, week, month, quarter, or year for the selected time dimension.',
21029
+ action: 'review_metrics',
21030
+ };
21031
+ case 'parameter_parse':
21032
+ return {
21033
+ ...base,
21034
+ title: 'Run input is invalid',
21035
+ field: 'Parameters',
21036
+ message: compactSemanticRuntimeFailure(raw),
21037
+ correction: 'Correct the named parameter, default value, or binding, then run validation again.',
21038
+ action: 'edit_parameters',
21039
+ };
21040
+ case 'syntax':
21041
+ case 'semantic_shape':
21042
+ case 'semantic_block_has_query':
21043
+ case 'sql_missing':
21044
+ case 'sql_read_only':
21045
+ return {
21046
+ ...base,
21047
+ title: diagnostic.code === 'sql_read_only' ? 'Only read-only SQL is allowed' : 'DQL source needs attention',
21048
+ field: 'DQL source',
21049
+ message: compactSemanticRuntimeFailure(raw),
21050
+ correction: diagnostic.code === 'sql_read_only'
21051
+ ? 'Replace write or DDL statements with one read-only query.'
21052
+ : 'Open DQL source, correct the highlighted declaration, then validate again.',
21053
+ action: 'edit_source',
21054
+ };
21055
+ default:
21056
+ return base;
21057
+ }
21058
+ }
21059
+ export function compactBlockStudioRuntimeFailure(failure) {
21060
+ const semanticPrefix = 'Could not compose SQL for semantic block metrics.';
21061
+ const semanticIndex = failure.indexOf(semanticPrefix);
21062
+ if (semanticIndex >= 0) {
21063
+ const remainder = failure.slice(semanticIndex + semanticPrefix.length);
21064
+ const references = Array.from(remainder.matchAll(/(?:^|\s)([A-Za-z_][\w.:/-]*)\s*:/g), (match) => match[1]);
21065
+ const uniqueReferences = Array.from(new Set(references));
21066
+ if (uniqueReferences.length > 0) {
21067
+ return `${uniqueReferences.length} selected metric${uniqueReferences.length === 1 ? '' : 's'} cannot be compiled by the current semantic runtime: ${uniqueReferences.join(', ')}. Review the measure and relationship metadata, then run again.`;
21068
+ }
21069
+ return 'The selected metrics and dimensions do not share a governed query path. Review the semantic relationships, then run again.';
21070
+ }
21071
+ return compactSemanticRuntimeFailure(failure);
21072
+ }
21073
+ function presentBlockStudioDiagnostics(diagnostics, semanticRefs) {
21074
+ return diagnostics.map((diagnostic) => presentBlockStudioDiagnostic(diagnostic, semanticRefs));
21075
+ }
21076
+ /**
21077
+ * Certification and preview must use the same compiler verdict. A successful
21078
+ * dbt/MetricFlow compile supersedes the native compiler's capability warning;
21079
+ * syntax, unsafe SQL, unknown references, and parameter errors remain intact.
21080
+ */
21081
+ export function reconcileBlockStudioRuntimeValidation(validation, runtime) {
21082
+ const diagnostics = [
21083
+ ...validation.diagnostics.filter((diagnostic) => !BLOCK_STUDIO_RUNTIME_COMPILE_DIAGNOSTICS.has(diagnostic.code ?? '')),
21084
+ ...runtime.diagnostics,
21085
+ ].filter((diagnostic, index, all) => all.findIndex((candidate) => (candidate.severity === diagnostic.severity
21086
+ && candidate.code === diagnostic.code
21087
+ && candidate.message === diagnostic.message)) === index);
21088
+ const presented = presentBlockStudioDiagnostics(diagnostics, validation.semanticRefs);
21089
+ return {
21090
+ ...validation,
21091
+ valid: presented.every((diagnostic) => diagnostic.severity !== 'error'),
21092
+ saveable: blockStudioValidationAllowsDraftSave({ diagnostics: presented }),
21093
+ diagnostics: presented,
21094
+ executableSql: runtime.sql ?? validation.executableSql,
21095
+ };
21096
+ }
20719
21097
  /**
20720
21098
  * Draft persistence is stricter than loose text storage but does not require a
20721
21099
  * configured execution runtime. Syntax, unsafe SQL, invalid semantic identity,
@@ -21266,7 +21644,10 @@ export function validateBlockStudioSource(source, semanticLayer) {
21266
21644
  if (semanticLayer) {
21267
21645
  const semanticCompose = composeSemanticBlockSql(source, semanticLayer);
21268
21646
  semanticRefs = semanticCompose.semanticRefs;
21269
- diagnostics.push(...semanticCompose.diagnostics);
21647
+ // Synchronous validation owns authored shape, references, safety, and
21648
+ // parameter contracts. A native compile miss is not an authoring error:
21649
+ // dbt Cloud or MetricFlow may compile the exact same source successfully.
21650
+ diagnostics.push(...semanticCompose.diagnostics.map(staticSemanticAuthoringDiagnostic));
21270
21651
  executableSql = semanticCompose.sql;
21271
21652
  }
21272
21653
  else {
@@ -21352,10 +21733,11 @@ export function validateBlockStudioSource(source, semanticLayer) {
21352
21733
  });
21353
21734
  }
21354
21735
  }
21736
+ const presentedDiagnostics = presentBlockStudioDiagnostics(diagnostics, semanticRefs);
21355
21737
  return {
21356
- valid: diagnostics.every((diagnostic) => diagnostic.severity !== 'error'),
21357
- saveable: blockStudioValidationAllowsDraftSave({ diagnostics }),
21358
- diagnostics,
21738
+ valid: presentedDiagnostics.every((diagnostic) => diagnostic.severity !== 'error'),
21739
+ saveable: blockStudioValidationAllowsDraftSave({ diagnostics: presentedDiagnostics }),
21740
+ diagnostics: presentedDiagnostics,
21359
21741
  semanticRefs,
21360
21742
  chartConfig: chartConfig ?? undefined,
21361
21743
  executableSql,
@@ -22339,22 +22721,88 @@ function evaluateBlockStudioTests(tests, rows, columns, startedAt) {
22339
22721
  function buildBlockStudioCertificationChecklist(input) {
22340
22722
  const parsed = parseBlockSourceMetadata(input.source);
22341
22723
  const sql = extractBlockStudioSql(input.source) ?? '';
22342
- const blockers = new Set();
22343
- for (const diagnostic of input.validation.diagnostics) {
22344
- if (diagnostic.severity === 'error')
22345
- blockers.add(diagnostic.message);
22346
- }
22347
- for (const error of input.certificationErrors)
22348
- blockers.add(`${error.rule}: ${error.message}`);
22349
- for (const blocker of input.extraBlockers ?? [])
22350
- blockers.add(blocker);
22351
- if (!input.previewSucceeded)
22352
- blockers.add('Block has not run successfully');
22724
+ const issues = input.validation.diagnostics
22725
+ .filter((diagnostic) => diagnostic.severity === 'error');
22726
+ for (const error of input.certificationErrors) {
22727
+ const combined = `${error.rule}: ${error.message}`;
22728
+ const lower = combined.toLowerCase();
22729
+ if (lower.includes('missing owner')) {
22730
+ issues.push({
22731
+ severity: 'error',
22732
+ code: 'owner_missing',
22733
+ title: 'Add an owner',
22734
+ field: 'Metadata',
22735
+ message: 'Owner is required before certification.',
22736
+ correction: 'Enter the accountable team or person in Metadata, save the draft, and certify again.',
22737
+ action: 'edit_source',
22738
+ technicalDetails: combined,
22739
+ });
22740
+ }
22741
+ else if (lower.includes('tests pass')) {
22742
+ issues.push({
22743
+ severity: 'error',
22744
+ code: 'tests_failed',
22745
+ title: 'A block test failed',
22746
+ field: 'Tests',
22747
+ message: 'One or more declared tests did not pass against the latest preview result.',
22748
+ correction: 'Review the failed assertion and result, correct the query or expectation, then run certification again.',
22749
+ action: 'edit_source',
22750
+ technicalDetails: combined,
22751
+ });
22752
+ }
22753
+ else {
22754
+ issues.push({
22755
+ severity: 'error',
22756
+ code: `certification_${error.rule.toLowerCase().replace(/[^a-z0-9]+/g, '_')}`,
22757
+ title: 'Certification requirement is not ready',
22758
+ field: 'Metadata',
22759
+ message: compactSemanticRuntimeFailure(combined),
22760
+ correction: 'Update the named block field, save the draft, and certify again.',
22761
+ action: 'edit_source',
22762
+ technicalDetails: combined,
22763
+ });
22764
+ }
22765
+ }
22766
+ for (const blocker of input.extraBlockers ?? []) {
22767
+ issues.push({
22768
+ severity: 'error',
22769
+ code: 'preview_failed',
22770
+ title: 'Preview did not finish',
22771
+ field: 'Run',
22772
+ message: compactBlockStudioRuntimeFailure(blocker),
22773
+ correction: 'Correct the reported query or runtime problem, then run the block again before certification.',
22774
+ technicalDetails: blocker,
22775
+ action: 'run_again',
22776
+ });
22777
+ }
22778
+ if (!input.previewSucceeded && (input.extraBlockers?.length ?? 0) === 0) {
22779
+ issues.push({
22780
+ severity: 'error',
22781
+ code: 'preview_required',
22782
+ title: 'Run the block',
22783
+ field: 'Run',
22784
+ message: 'The block needs a successful preview result before certification.',
22785
+ correction: 'Run the block, resolve any preview error, then certify again.',
22786
+ action: 'run_again',
22787
+ });
22788
+ }
22353
22789
  // Only a REAL test failure blocks. "No tests" and "no chart" are advisory review
22354
22790
  // items the AI fills at draft time — never hard gates (owner is the gate). This is
22355
22791
  // what keeps "build a DQL" from demanding ~13 fields per block at 300-query scale.
22356
- if (input.testResults && input.testResults.failed > 0)
22357
- blockers.add('Tests must pass before certification');
22792
+ if (input.testResults && input.testResults.failed > 0 && !issues.some((issue) => issue.code === 'tests_failed')) {
22793
+ issues.push({
22794
+ severity: 'error',
22795
+ code: 'tests_failed',
22796
+ title: 'A block test failed',
22797
+ field: 'Tests',
22798
+ message: 'One or more declared tests did not pass against the latest preview result.',
22799
+ correction: 'Review the failed assertion and result, correct the query or expectation, then run certification again.',
22800
+ action: 'edit_source',
22801
+ });
22802
+ }
22803
+ const uniqueIssues = issues.filter((issue, index, all) => all.findIndex((candidate) => (candidate.code === issue.code
22804
+ && candidate.message === issue.message
22805
+ && (candidate.references ?? []).join('|') === (issue.references ?? []).join('|'))) === index);
22358
22806
  return {
22359
22807
  // The metadata tab is green once the one required field (owner) is present.
22360
22808
  // Description/domain are AI-filled + editable and surface as review items, not gates.
@@ -22365,7 +22813,8 @@ function buildBlockStudioCertificationChecklist(input) {
22365
22813
  chart: Boolean(input.validation.chartConfig?.chart),
22366
22814
  lineage: extractSqlTablesLight(sql).length > 0 || input.validation.semanticRefs.metrics.length > 0,
22367
22815
  aiReviewed: true,
22368
- blockers: Array.from(blockers),
22816
+ blockers: uniqueIssues.map((issue) => issue.message),
22817
+ issues: uniqueIssues,
22369
22818
  checkedAt: new Date().toISOString(),
22370
22819
  };
22371
22820
  }