@duckcodeailabs/dql-cli 1.13.2 → 1.13.4

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 (70) hide show
  1. package/dist/apps-api.d.ts +26 -3
  2. package/dist/apps-api.d.ts.map +1 -1
  3. package/dist/apps-api.js +1045 -175
  4. package/dist/apps-api.js.map +1 -1
  5. package/dist/assets/dql-notebook/assets/{AgentLogPage-BlUE3qsX.js → AgentLogPage-DYmHO6_i.js} +1 -1
  6. package/dist/assets/dql-notebook/assets/{AiBuildDialog-O88iK6UW.js → AiBuildDialog-BfhaOd1h.js} +1 -1
  7. package/dist/assets/dql-notebook/assets/{AiBuildResult-D-6G4Sh3.js → AiBuildResult-DLKFAe6R.js} +3 -3
  8. package/dist/assets/dql-notebook/assets/{AiSidePanel-MRSGtQDV.js → AiSidePanel-C6Z3URGr.js} +1 -1
  9. package/dist/assets/dql-notebook/assets/{AnalyticsHome-CcRVO6zN.js → AnalyticsHome-uG3xCP_s.js} +1 -1
  10. package/dist/assets/dql-notebook/assets/AppsView-DfieNjkh.js +5595 -0
  11. package/dist/assets/dql-notebook/assets/BlockStudio-90J_wvWL.js +55 -0
  12. package/dist/assets/dql-notebook/assets/{BusinessArtifactView-itEIUrYy.js → BusinessArtifactView-_zSMae5q.js} +1 -1
  13. package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-CbAqOpNc.js → DbtFirstModelingPage-DoXiPssu.js} +4 -4
  14. package/dist/assets/dql-notebook/assets/{GitPage-BxOqRYu2.js → GitPage-CUfAhS67.js} +3 -3
  15. package/dist/assets/dql-notebook/assets/{GlobalAiRail-Tfkx76CI.js → GlobalAiRail-ovtL7nQu.js} +1 -1
  16. package/dist/assets/dql-notebook/assets/{GovernedContextPage-CeaDbepP.js → GovernedContextPage-CaMiKAzA.js} +3 -3
  17. package/dist/assets/dql-notebook/assets/{HelpDocsPage-D1lAryYv.js → HelpDocsPage-CEGUx28b.js} +1 -1
  18. package/dist/assets/dql-notebook/assets/{HomePage-GB6se6xW.js → HomePage-2v3SRf8V.js} +1 -1
  19. package/dist/assets/dql-notebook/assets/{LineageDAG-zCyN-hdg.js → LineageDAG-AaqLEiBN.js} +1 -1
  20. package/dist/assets/dql-notebook/assets/{LineageDetailView-Cg0TgG2Y.js → LineageDetailView-CFc8pm91.js} +1 -1
  21. package/dist/assets/dql-notebook/assets/{LineageDrawer-DwMcenDf.js → LineageDrawer-DOkFjPq-.js} +1 -1
  22. package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-DUZx3ksU.js → LineagePathBreadcrumb-BrDwCuMs.js} +3 -3
  23. package/dist/assets/dql-notebook/assets/{MiniLineageGraph-DT_ycl6s.js → MiniLineageGraph-CiHtQs3y.js} +1 -1
  24. package/dist/assets/dql-notebook/assets/{NewBlockModal-qdhzeJPF.js → NewBlockModal-DAgJZbws.js} +1 -1
  25. package/dist/assets/dql-notebook/assets/{NewNotebookModal-BnaDzs1A.js → NewNotebookModal-xjG7nZ3O.js} +1 -1
  26. package/dist/assets/dql-notebook/assets/{NotebookEditor-CouPFzdo.js → NotebookEditor-CXZaKfYT.js} +1 -1
  27. package/dist/assets/dql-notebook/assets/{ReadinessPage-DB_KbH9Y.js → ReadinessPage-MuJBFwwg.js} +1 -1
  28. package/dist/assets/dql-notebook/assets/{SetupOnboarding-CamQq_N2.js → SetupOnboarding-vKFtKtjh.js} +1 -1
  29. package/dist/assets/dql-notebook/assets/{SkillsPage--mYkOAK2.js → SkillsPage-DCFVOV5f.js} +1 -1
  30. package/dist/assets/dql-notebook/assets/{TrustBadge-ClEYiYeA.js → TrustBadge-TcwXFFHK.js} +1 -1
  31. package/dist/assets/dql-notebook/assets/{UnifiedAgentRunPanel-DBfsmXes.js → UnifiedAgentRunPanel-Cu-YgIe4.js} +1 -1
  32. package/dist/assets/dql-notebook/assets/{answer-to-notebook-Cg3UyGwh.js → answer-to-notebook-DCFWlHB7.js} +1 -1
  33. package/dist/assets/dql-notebook/assets/{arrow-left-DnYPGqlr.js → arrow-left-BLnlJAtp.js} +1 -1
  34. package/dist/assets/dql-notebook/assets/{arrow-right-Ct_YGrmU.js → arrow-right-BmUAb1aW.js} +1 -1
  35. package/dist/assets/dql-notebook/assets/{book-open-text-DULlwbj6.js → book-open-text-bdSChSag.js} +1 -1
  36. package/dist/assets/dql-notebook/assets/{circle-x-ByYxoiS9.js → circle-x-Cc6dEaf4.js} +1 -1
  37. package/dist/assets/dql-notebook/assets/{dagre.esm-BjYSO53F.js → dagre.esm-CPebQKQ9.js} +1 -1
  38. package/dist/assets/dql-notebook/assets/{external-link-DprWh9Yf.js → external-link-rmXvubf3.js} +1 -1
  39. package/dist/assets/dql-notebook/assets/{grip-vertical-CrEEYkXN.js → grip-vertical-Sg4zd5-Q.js} +1 -1
  40. package/dist/assets/dql-notebook/assets/index-mQd_pC-P.js +635 -0
  41. package/dist/assets/dql-notebook/assets/{link-2-BkW-rkEC.js → link-2-BLaPoFxW.js} +1 -1
  42. package/dist/assets/dql-notebook/assets/{list-tree-BipBLmIE.js → list-tree-j0s4asFK.js} +1 -1
  43. package/dist/assets/dql-notebook/assets/{minimize-2-BhXsglrO.js → minimize-2-Cl4pef4s.js} +1 -1
  44. package/dist/assets/dql-notebook/assets/{panel-right-open-BoCSilYB.js → panel-right-open-Clz-FtWi.js} +1 -1
  45. package/dist/assets/dql-notebook/assets/{play-mdoZawPu.js → play-DFdazMg7.js} +1 -1
  46. package/dist/assets/dql-notebook/assets/{rotate-ccw-ELiDRUmj.js → rotate-ccw-C3tTaR7s.js} +1 -1
  47. package/dist/assets/dql-notebook/assets/{semantic-fields-DVNRLfeU.js → semantic-fields-DIAhTlfy.js} +1 -1
  48. package/dist/assets/dql-notebook/assets/{sliders-horizontal-GeUzpmKz.js → sliders-horizontal-BUiu2tfY.js} +1 -1
  49. package/dist/assets/dql-notebook/assets/{star-D63O81Wp.js → star--Yq77aH4.js} +1 -1
  50. package/dist/assets/dql-notebook/assets/{triangle-alert-BWrCxQVE.js → triangle-alert-D5Ng-xZO.js} +1 -1
  51. package/dist/assets/dql-notebook/assets/{upload-CI-ldXhC.js → upload-L8BKQAxP.js} +1 -1
  52. package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-Cv7Yq2hK.js → usePersistedAgentThreadId-DBu97YaM.js} +1 -1
  53. package/dist/assets/dql-notebook/assets/{user-round-Do21aFGo.js → user-round-BK5N1cJW.js} +1 -1
  54. package/dist/assets/dql-notebook/assets/{wand-sparkles-DT7u7F_x.js → wand-sparkles-D8XDQD9z.js} +1 -1
  55. package/dist/assets/dql-notebook/assets/{workflow-DHh2mMbn.js → workflow-Bl9zwG0q.js} +1 -1
  56. package/dist/assets/dql-notebook/assets/{wrench-7tYr_GKb.js → wrench-C-zLCMbW.js} +1 -1
  57. package/dist/assets/dql-notebook/assets/{x-CcYceFMz.js → x-stiJ-sP7.js} +1 -1
  58. package/dist/assets/dql-notebook/index.html +1 -1
  59. package/dist/local-operation-coordinator.d.ts.map +1 -1
  60. package/dist/local-operation-coordinator.js +16 -2
  61. package/dist/local-operation-coordinator.js.map +1 -1
  62. package/dist/local-runtime.d.ts +55 -19
  63. package/dist/local-runtime.d.ts.map +1 -1
  64. package/dist/local-runtime.js +1098 -581
  65. package/dist/local-runtime.js.map +1 -1
  66. package/dist/package.json +10 -10
  67. package/package.json +10 -10
  68. package/dist/assets/dql-notebook/assets/AppsView-BTkGC-wl.js +0 -5588
  69. package/dist/assets/dql-notebook/assets/BlockStudio-BrTNaDpb.js +0 -55
  70. package/dist/assets/dql-notebook/assets/index-9w93YPF9.js +0 -635
@@ -48,6 +48,7 @@ import { buildSemanticTargetBinding } from './semantic-execution/target-binding.
48
48
  import { discoverWarehouseMetadataScopes, normalizeConnectionMetadataScope, syncWarehouseMetadata, warehouseMetadataStatus, } from './warehouse-metadata.js';
49
49
  import { NotebookDatasetWorkspace, } from "./notebook-datasets.js";
50
50
  import { prepareBlockInvocation } from './block-invocation.js';
51
+ export const APP_SOURCE_REUSABLE_TAG = 'app-source';
51
52
  const NOTEBOOK_EXECUTE_PREVIEW_ROW_LIMIT = 500;
52
53
  const NOTEBOOK_FAVICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#6d5dfc"/><path d="M9 9h14v14H9z" fill="none" stroke="#fff" stroke-width="2"/><path d="M13 13h6M13 17h6M13 21h4" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>';
53
54
  export function resolveProjectSemanticConfig(projectConfig, projectRoot) {
@@ -968,6 +969,36 @@ export async function startLocalServer(opts) {
968
969
  // rather than treating compiled semantic SQL as unrelated generic SQL.
969
970
  const compiledSemanticQueries = new Map();
970
971
  const dashboardRunEvidence = new Map();
972
+ const loadAppBuildPreviewEvidence = (runId) => {
973
+ const memoryEvidence = dashboardRunEvidence.get(runId);
974
+ if (memoryEvidence && memoryEvidence.expiresAt >= Date.now())
975
+ return memoryEvidence;
976
+ if (memoryEvidence)
977
+ dashboardRunEvidence.delete(runId);
978
+ const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
979
+ try {
980
+ const persisted = storage.getAppPreviewEvidence(runId);
981
+ if (!persisted)
982
+ return null;
983
+ return {
984
+ appId: persisted.draftId,
985
+ dashboardId: persisted.dashboardId,
986
+ snapshotId: persisted.snapshotId,
987
+ filterFingerprint: persisted.filterFingerprint,
988
+ resultFingerprint: persisted.resultFingerprint,
989
+ personaFingerprint: persisted.personaFingerprint,
990
+ successfulTileIds: persisted.successfulTileIds,
991
+ semanticApprovalEligibleTileIds: persisted.semanticApprovalEligibleTileIds,
992
+ // Persisted receipts are bound again by draft revision, source
993
+ // fingerprints, filters and snapshot during preflight. They therefore
994
+ // survive a process restart without making result rows durable.
995
+ expiresAt: Number.POSITIVE_INFINITY,
996
+ };
997
+ }
998
+ finally {
999
+ storage.close();
1000
+ }
1001
+ };
971
1002
  const dbtNodeDetailCache = new Map();
972
1003
  const onboardingJobs = new Map();
973
1004
  const metricFlowInstaller = new ManagedMetricFlowInstaller(projectRoot);
@@ -5653,7 +5684,10 @@ export async function startLocalServer(opts) {
5653
5684
  parameters: invocation.values,
5654
5685
  })
5655
5686
  : null;
5656
- const validation = validateBlockStudioSource(source, semanticLayer);
5687
+ const staticValidation = validateBlockStudioSource(source, semanticLayer);
5688
+ const validation = semanticCompose
5689
+ ? reconcileBlockStudioRuntimeValidation(staticValidation, semanticCompose)
5690
+ : staticValidation;
5657
5691
  const executableSql = semanticCompose?.sql ?? validation.executableSql;
5658
5692
  if (!executableSql) {
5659
5693
  const message = semanticCompose?.diagnostics.find((item) => item.severity === 'error')?.message
@@ -5675,6 +5709,7 @@ export async function startLocalServer(opts) {
5675
5709
  chartConfig: plan?.chartConfig ?? validation.chartConfig ?? null,
5676
5710
  invocation,
5677
5711
  tests: plan?.tests ?? [],
5712
+ validation,
5678
5713
  };
5679
5714
  };
5680
5715
  const runBlockStudioTestSummary = async (source, targetConnection, executedPreview) => {
@@ -5775,16 +5810,32 @@ export async function startLocalServer(opts) {
5775
5810
  };
5776
5811
  };
5777
5812
  const certifyBlockStudioSource = async (source, blockPath, options = {}) => {
5778
- const validation = validateBlockStudioSource(source, semanticLayer);
5813
+ options.reportProgress?.({
5814
+ phase: 'validating',
5815
+ progress: 10,
5816
+ message: 'Validating the saved DQL source.',
5817
+ });
5818
+ let validation = validateBlockStudioSource(source, semanticLayer);
5779
5819
  let preview = null;
5780
5820
  let testResults = null;
5781
5821
  const blockers = [];
5822
+ options.reportProgress?.({
5823
+ phase: 'previewing',
5824
+ progress: 30,
5825
+ message: 'Compiling and previewing the saved block.',
5826
+ });
5782
5827
  try {
5783
5828
  preview = await runBlockStudioPreviewSource(source);
5829
+ validation = preview.validation;
5784
5830
  }
5785
5831
  catch (error) {
5786
5832
  blockers.push(error instanceof Error ? error.message : String(error));
5787
5833
  }
5834
+ options.reportProgress?.({
5835
+ phase: 'testing',
5836
+ progress: 60,
5837
+ message: 'Evaluating declared block tests.',
5838
+ });
5788
5839
  try {
5789
5840
  testResults = preview
5790
5841
  ? await runBlockStudioTestSummary(source, undefined, preview)
@@ -5800,6 +5851,11 @@ export async function startLocalServer(opts) {
5800
5851
  runAt: new Date(),
5801
5852
  };
5802
5853
  }
5854
+ options.reportProgress?.({
5855
+ phase: 'assessing',
5856
+ progress: 75,
5857
+ message: 'Assessing certification rules and evidence.',
5858
+ });
5803
5859
  const parsed = parseBlockSourceMetadata(source);
5804
5860
  const record = {
5805
5861
  id: parsed.name || 'local',
@@ -9272,633 +9328,659 @@ export async function startLocalServer(opts) {
9272
9328
  ? body.tileId.trim()
9273
9329
  : undefined;
9274
9330
  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
- });
9331
+ const localApps = { current: null };
9332
+ const itemsToRun = loaded.dashboard.layout.items.filter((item) => !requestedTileId || item.i === requestedTileId);
9333
+ const tileOrder = new Map(itemsToRun.map((item, index) => [item.i, index]));
9334
+ let nextTileIndex = 0;
9335
+ // App preview is a multi-source execution, so isolate each tile's
9336
+ // failure and run a small fixed pool. Four avoids serial 4k-catalog
9337
+ // behavior without flooding the configured warehouse.
9338
+ const runTileWorker = async () => {
9339
+ while (nextTileIndex < itemsToRun.length) {
9340
+ const item = itemsToRun[nextTileIndex++];
9341
+ if (!item)
9301
9342
  continue;
9302
- }
9303
- let pin = localApps.getAiPin(item.aiPin.id);
9304
- if (!pin) {
9343
+ if (item.text) {
9305
9344
  tiles.push({
9306
9345
  tileId: item.i,
9307
- status: 'unresolved',
9308
- tileType: 'aiPin',
9309
- error: `AI pin "${item.aiPin.id}" could not be found`,
9346
+ status: 'ok',
9347
+ tileType: 'text',
9348
+ title: item.title,
9349
+ viz: item.viz,
9350
+ text: item.text,
9310
9351
  });
9311
9352
  continue;
9312
9353
  }
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) {
9354
+ if (item.aiPin) {
9320
9355
  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;
9356
+ localApps.current ??= new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
9331
9357
  }
9332
9358
  catch (err) {
9333
9359
  tiles.push({
9334
9360
  tileId: item.i,
9335
9361
  status: 'error',
9336
9362
  tileType: 'aiPin',
9337
- title: item.title ?? pin.title,
9338
9363
  error: err instanceof Error ? err.message : String(err),
9339
- trustState: pinArtifact.trustState ?? 'review_required',
9340
9364
  });
9341
9365
  continue;
9342
9366
  }
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;
9367
+ let pin = localApps.current.getAiPin(item.aiPin.id);
9368
+ if (!pin) {
9369
+ tiles.push({
9370
+ tileId: item.i,
9371
+ status: 'unresolved',
9372
+ tileType: 'aiPin',
9373
+ error: `AI pin "${item.aiPin.id}" could not be found`,
9374
+ });
9375
+ continue;
9351
9376
  }
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)
9377
+ const pinPlan = pin.analysisPlan && typeof pin.analysisPlan === 'object' && !Array.isArray(pin.analysisPlan)
9378
+ ? pin.analysisPlan
9394
9379
  : 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.');
9380
+ const pinArtifact = normalizeDqlArtifactReference(pinPlan?.dqlArtifact);
9381
+ let renderedPinResult = pin.result;
9382
+ let renderedPinChartConfig = pin.chartConfig;
9383
+ if (pinArtifact) {
9384
+ try {
9385
+ const boundParameters = dashboardTileParameterValues({
9386
+ item,
9387
+ dashboardValues: dashboardVariables,
9388
+ requestValues: variables,
9389
+ });
9390
+ const executed = await executeDqlArtifactSourceForAgent(pinArtifact.source, { name: pinArtifact.name ?? pin.title, path: pinArtifact.sourcePath }, { question: pin.question, parameters: { ...(pinArtifact.parameterValues ?? {}), ...boundParameters } });
9391
+ renderedPinResult = executed;
9392
+ renderedPinChartConfig = executed.chartConfig && typeof executed.chartConfig === 'object'
9393
+ ? executed.chartConfig
9394
+ : renderedPinChartConfig;
9395
+ }
9396
+ catch (err) {
9397
+ tiles.push({
9398
+ tileId: item.i,
9399
+ status: 'error',
9400
+ tileType: 'aiPin',
9401
+ title: item.title ?? pin.title,
9402
+ error: err instanceof Error ? err.message : String(err),
9403
+ trustState: pinArtifact.trustState ?? 'review_required',
9404
+ });
9405
+ continue;
9406
+ }
9402
9407
  }
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.');
9408
+ else if (pin.refreshCadence === 'daily' && pin.sql && isAiPinRefreshDue(pin.lastRefreshedAt)) {
9409
+ try {
9410
+ const refreshed = await executeLocalSqlForStoredResult(pin.sql);
9411
+ pin = localApps.current.updateAiPinResult(pin.id, refreshed) ?? pin;
9412
+ }
9413
+ catch (err) {
9414
+ pin = localApps.current.updateAiPinResult(pin.id, pin.result, err instanceof Error ? err.message : String(err)) ?? pin;
9415
+ }
9407
9416
  }
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,
9417
+ tiles.push({
9418
+ tileId: item.i,
9419
+ status: 'ok',
9420
+ tileType: 'aiPin',
9421
+ title: item.title ?? pin.title,
9422
+ viz: item.viz,
9423
+ chartConfig: mergeDashboardChartConfig(renderedPinChartConfig, item),
9424
+ result: renderedPinResult,
9425
+ aiPin: pin,
9426
+ citation: {
9427
+ kind: 'ai_pin',
9428
+ name: pin.title,
9429
+ },
9430
+ artifact: {
9431
+ version: 1,
9432
+ sourceKind: 'ai_pin',
9433
+ name: pinArtifact?.name ?? pin.title,
9434
+ ...(pinArtifact?.sourcePath ? { sourcePath: pinArtifact.sourcePath } : {}),
9435
+ ...(pinArtifact?.source ? { dql: pinArtifact.source.slice(0, 40_000) } : {}),
9436
+ ...(pinArtifact?.compiledSql || pin.sql ? { sql: (pinArtifact?.compiledSql ?? pin.sql).slice(0, 40_000) } : {}),
9437
+ trustState: pinArtifact?.trustState === 'certified' || pin.certification === 'certified'
9438
+ ? 'certified'
9439
+ : 'review_required',
9440
+ explanation: [
9441
+ 'This result comes from a saved App insight.',
9442
+ pinArtifact ? 'The DQL artifact was executed again with the current App parameters.' : 'The saved bounded result is displayed with its original trust state.',
9443
+ ],
9444
+ executionTarget: executionTargetDescriptor(body),
9445
+ },
9420
9446
  });
9421
- let result;
9422
- let repair;
9423
- let repairedSql;
9424
- let repairedDql;
9447
+ continue;
9448
+ }
9449
+ if (item.draftAnalysis) {
9450
+ let draftSource;
9451
+ let draftAppRelativePath;
9452
+ let draftParameters = {};
9453
+ let draftTargetConnection;
9454
+ let draftRepairPlan;
9425
9455
  try {
9426
- result = await executeDqlArtifactSourceForAgent(draftSource, { name: item.title ?? item.i, path: draftAppRelativePath }, { question: item.title, parameters: draftParameters });
9456
+ const localDraftPath = loaded.draftArtifactsDir
9457
+ ? join(loaded.draftArtifactsDir, item.draftAnalysis.ref)
9458
+ : undefined;
9459
+ const draftRoot = localDraftPath && existsSync(localDraftPath)
9460
+ ? loaded.draftArtifactsDir
9461
+ : loaded.appDir;
9462
+ const draftPath = join(draftRoot, item.draftAnalysis.ref);
9463
+ draftAppRelativePath = relative(draftRoot, draftPath);
9464
+ if (draftAppRelativePath.startsWith('..') || draftAppRelativePath.startsWith('/')) {
9465
+ throw new Error('App-scoped analysis reference escapes the App directory.');
9466
+ }
9467
+ draftSource = readFileSync(draftPath, 'utf-8');
9468
+ const fingerprint = `sha256:${createHash('sha256').update(draftSource).digest('hex')}`;
9469
+ if (fingerprint !== item.draftAnalysis.artifactFingerprint) {
9470
+ throw new Error('App-scoped analysis changed after approval. Review the draft again.');
9471
+ }
9472
+ draftParameters = dashboardTileParameterValues({
9473
+ item,
9474
+ dashboardValues: dashboardVariables,
9475
+ requestValues: variables,
9476
+ });
9477
+ draftTargetConnection = isConnectionConfig(body.connection)
9478
+ ? body.connection
9479
+ : connection ?? undefined;
9480
+ draftRepairPlan = buildExecutionPlan({ id: item.i, type: 'dql', source: draftSource, title: item.title ?? item.i }, {
9481
+ semanticLayer,
9482
+ driver: draftTargetConnection?.driver ?? 'duckdb',
9483
+ parameters: draftParameters,
9484
+ });
9485
+ let result;
9486
+ let repair;
9487
+ let repairedSql;
9488
+ let repairedDql;
9489
+ try {
9490
+ result = await executeDqlArtifactSourceForAgent(draftSource, { name: item.title ?? item.i, path: draftAppRelativePath }, { question: item.title, parameters: draftParameters });
9491
+ }
9492
+ catch (executionError) {
9493
+ const repaired = await repairFailedAppTileExecution({
9494
+ source: 'draft_analysis',
9495
+ question: item.title ?? item.i,
9496
+ sourceSql: draftRepairPlan?.sql,
9497
+ sourceDql: draftSource,
9498
+ artifactName: item.title ?? item.i,
9499
+ error: executionError,
9500
+ targetConnection: draftTargetConnection,
9501
+ targetConnectionName: projectConfig.defaultConnectionName,
9502
+ sqlParams: draftRepairPlan?.sqlParams,
9503
+ variables: { ...(draftRepairPlan?.variables ?? {}), ...draftParameters },
9504
+ });
9505
+ if (!repaired.ok) {
9506
+ tiles.push({
9507
+ tileId: item.i,
9508
+ status: 'error',
9509
+ tileType: 'draftAnalysis',
9510
+ title: item.title ?? item.i,
9511
+ error: executionError instanceof Error ? executionError.message : String(executionError),
9512
+ trustState: 'review_required',
9513
+ repair: repaired.repair,
9514
+ artifact: {
9515
+ version: 1,
9516
+ sourceKind: 'draft_analysis',
9517
+ name: item.title ?? item.i,
9518
+ sourcePath: draftAppRelativePath,
9519
+ dql: draftSource.slice(0, 40_000),
9520
+ ...(draftRepairPlan?.sql ? { sql: draftRepairPlan.sql.slice(0, 40_000) } : {}),
9521
+ trustState: 'review_required',
9522
+ explanation: ['This App-scoped analysis remains review-required.', 'Automatic repair could not produce a safe executable result.'],
9523
+ executionTarget: executionTargetDescriptor(body),
9524
+ },
9525
+ });
9526
+ continue;
9527
+ }
9528
+ result = repaired.result;
9529
+ repair = repaired.repair;
9530
+ repairedSql = repaired.repairedSql;
9531
+ repairedDql = repaired.repairedDql;
9532
+ }
9533
+ tiles.push({
9534
+ tileId: item.i,
9535
+ status: 'ok',
9536
+ tileType: 'draftAnalysis',
9537
+ title: item.title ?? item.i,
9538
+ viz: item.viz,
9539
+ chartConfig: mergeDashboardChartConfig(result.chartConfig, item),
9540
+ result,
9541
+ trustState: 'review_required',
9542
+ citation: { kind: repair ? 'app_draft_analysis_repaired' : 'app_draft_analysis', name: item.title ?? item.i, path: draftAppRelativePath },
9543
+ ...(repair ? { repair } : {}),
9544
+ artifact: {
9545
+ version: 1,
9546
+ sourceKind: 'draft_analysis',
9547
+ name: item.title ?? item.i,
9548
+ sourcePath: draftAppRelativePath,
9549
+ dql: (repairedDql ?? draftSource).slice(0, 40_000),
9550
+ ...(repairedSql ?? draftRepairPlan?.sql ? { sql: (repairedSql ?? draftRepairPlan.sql).slice(0, 40_000) } : {}),
9551
+ trustState: 'review_required',
9552
+ explanation: [
9553
+ 'This is private App analysis and remains review-required.',
9554
+ 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.',
9555
+ ],
9556
+ executionTarget: executionTargetDescriptor(body),
9557
+ },
9558
+ });
9427
9559
  }
9428
- catch (executionError) {
9560
+ catch (err) {
9429
9561
  const repaired = await repairFailedAppTileExecution({
9430
9562
  source: 'draft_analysis',
9431
9563
  question: item.title ?? item.i,
9432
9564
  sourceSql: draftRepairPlan?.sql,
9433
9565
  sourceDql: draftSource,
9434
9566
  artifactName: item.title ?? item.i,
9435
- error: executionError,
9567
+ error: err,
9436
9568
  targetConnection: draftTargetConnection,
9437
9569
  targetConnectionName: projectConfig.defaultConnectionName,
9438
9570
  sqlParams: draftRepairPlan?.sqlParams,
9439
9571
  variables: { ...(draftRepairPlan?.variables ?? {}), ...draftParameters },
9440
9572
  });
9441
- if (!repaired.ok) {
9573
+ if (repaired.ok && draftSource) {
9442
9574
  tiles.push({
9443
9575
  tileId: item.i,
9444
- status: 'error',
9576
+ status: 'ok',
9445
9577
  tileType: 'draftAnalysis',
9446
9578
  title: item.title ?? item.i,
9447
- error: executionError instanceof Error ? executionError.message : String(executionError),
9579
+ viz: item.viz,
9580
+ chartConfig: mergeDashboardChartConfig(repaired.result.chartConfig, item),
9581
+ result: repaired.result,
9448
9582
  trustState: 'review_required',
9583
+ citation: { kind: 'app_draft_analysis_repaired', name: item.title ?? item.i, path: draftAppRelativePath },
9449
9584
  repair: repaired.repair,
9450
9585
  artifact: {
9451
9586
  version: 1,
9452
9587
  sourceKind: 'draft_analysis',
9453
9588
  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) } : {}),
9589
+ ...(draftAppRelativePath ? { sourcePath: draftAppRelativePath } : {}),
9590
+ dql: (repaired.repairedDql ?? draftSource).slice(0, 40_000),
9591
+ sql: repaired.repairedSql.slice(0, 40_000),
9457
9592
  trustState: 'review_required',
9458
- explanation: ['This App-scoped analysis remains review-required.', 'Automatic repair could not produce a safe executable result.'],
9593
+ explanation: ['A bounded DQL repair executed on the original data target. The saved App analysis was not changed.'],
9459
9594
  executionTarget: executionTargetDescriptor(body),
9460
9595
  },
9461
9596
  });
9462
9597
  continue;
9463
9598
  }
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) } : {}),
9599
+ tiles.push({
9600
+ tileId: item.i,
9601
+ status: 'error',
9602
+ tileType: 'draftAnalysis',
9603
+ title: item.title ?? item.i,
9604
+ error: err instanceof Error ? err.message : String(err),
9487
9605
  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
- });
9606
+ repair: repaired.repair,
9607
+ ...(draftSource ? {
9608
+ artifact: {
9609
+ version: 1,
9610
+ sourceKind: 'draft_analysis',
9611
+ name: item.title ?? item.i,
9612
+ ...(draftAppRelativePath ? { sourcePath: draftAppRelativePath } : {}),
9613
+ dql: draftSource.slice(0, 40_000),
9614
+ ...(draftRepairPlan?.sql ? { sql: draftRepairPlan.sql.slice(0, 40_000) } : {}),
9615
+ trustState: 'review_required',
9616
+ explanation: ['Automatic repair could not produce a safe executable result. The saved App analysis was not changed.'],
9617
+ executionTarget: executionTargetDescriptor(body),
9618
+ },
9619
+ } : {}),
9620
+ });
9621
+ }
9622
+ continue;
9495
9623
  }
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) {
9624
+ if (item.semantic) {
9625
+ try {
9626
+ if (!semanticLayer)
9627
+ throw new Error('Semantic layer is not available for this governed query.');
9628
+ const knownMetrics = new Set(semanticLayer.listMetrics().map((metric) => metric.name));
9629
+ const missingMetrics = item.semantic.metrics.filter((metric) => !knownMetrics.has(metric));
9630
+ if (missingMetrics.length)
9631
+ throw new Error(`Semantic definition changed; missing metric(s): ${missingMetrics.join(', ')}`);
9632
+ const knownModels = new Set(semanticLayer.listSemanticModels().map((model) => model.name));
9633
+ const missingModels = item.semantic.semanticModelRefs.filter((model) => !knownModels.has(model));
9634
+ if (missingModels.length)
9635
+ throw new Error(`Semantic definition changed; missing model(s): ${missingModels.join(', ')}`);
9636
+ const targetConnection = requireActiveConnection(isConnectionConfig(body.connection) ? body.connection : connection);
9637
+ const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer, projectRoot);
9638
+ const activeFilters = dashboardSemanticFiltersForTile(loaded.dashboard, item, dashboardVariables);
9639
+ const staticFilters = (item.semantic.filters ?? []).map((filter) => ({
9640
+ dimension: filter.field,
9641
+ operator: filter.operator,
9642
+ values: Array.isArray(filter.value) ? filter.value.map(String) : [String(filter.value)],
9643
+ }));
9644
+ const composed = await composeRuntimeSemanticQuery({
9645
+ metrics: item.semantic.metrics,
9646
+ dimensions: item.semantic.dimensions ?? [],
9647
+ filters: [...staticFilters, ...activeFilters],
9648
+ ...(item.semantic.timeDimension ? { timeDimension: { name: item.semantic.timeDimension, granularity: 'month' } } : {}),
9649
+ orderBy: item.semantic.orderBy?.map((order) => ({ name: order.field, direction: order.direction })),
9650
+ limit: item.semantic.limit,
9651
+ engine: item.semantic.provider,
9652
+ }, semanticLayer, {
9653
+ projectRoot,
9654
+ projectConfig,
9655
+ detectedProvider: semanticDetectedProvider,
9656
+ driver: targetConnection.driver,
9657
+ tableMapping,
9658
+ });
9659
+ if (!composed)
9660
+ throw new Error('The governed semantic query could not be composed.');
9661
+ const prepared = prepareLocalExecution(composed.sql, targetConnection, projectRoot, projectConfig);
9662
+ let normalizedResult;
9663
+ let repair;
9664
+ let executedSql = composed.sql;
9665
+ try {
9666
+ const result = await executor.executeQuery(prepared.sql, [], {}, prepared.connection);
9667
+ normalizedResult = normalizeQueryResult(result);
9668
+ }
9669
+ catch (executionError) {
9670
+ const repaired = await repairFailedAppTileExecution({
9671
+ source: 'semantic_query',
9672
+ question: item.title ?? item.semantic.id,
9673
+ sourceSql: composed.sql,
9674
+ error: executionError,
9675
+ targetConnection,
9676
+ });
9677
+ if (!repaired.ok) {
9678
+ tiles.push({
9679
+ tileId: item.i,
9680
+ status: 'error',
9681
+ tileType: 'semantic',
9682
+ title: item.title ?? item.semantic.id,
9683
+ error: executionError instanceof Error ? executionError.message : String(executionError),
9684
+ trustState: 'review_required',
9685
+ repair: repaired.repair,
9686
+ artifact: {
9687
+ version: 1,
9688
+ sourceKind: 'semantic_query',
9689
+ name: item.title ?? item.semantic.id,
9690
+ sql: composed.sql.slice(0, 40_000),
9691
+ trustState: 'review_required',
9692
+ explanation: ['Automatic repair could not execute the governed semantic query safely.'],
9693
+ executionTarget: executionTargetDescriptor(body),
9694
+ },
9695
+ });
9696
+ continue;
9697
+ }
9698
+ normalizedResult = normalizeQueryResult(repaired.result);
9699
+ repair = repaired.repair;
9700
+ executedSql = repaired.repairedSql;
9701
+ }
9510
9702
  tiles.push({
9511
9703
  tileId: item.i,
9512
9704
  status: 'ok',
9513
- tileType: 'draftAnalysis',
9514
- title: item.title ?? item.i,
9705
+ tileType: 'semantic',
9706
+ title: item.title ?? item.semantic.id,
9515
9707
  viz: item.viz,
9516
- chartConfig: mergeDashboardChartConfig(repaired.result.chartConfig, item),
9517
- result: repaired.result,
9708
+ result: normalizedResult,
9518
9709
  trustState: 'review_required',
9519
- citation: { kind: 'app_draft_analysis_repaired', name: item.title ?? item.i, path: draftAppRelativePath },
9520
- repair: repaired.repair,
9710
+ citation: { kind: repair ? 'semantic_query_repaired' : 'semantic_query', name: item.semantic.id },
9711
+ ...(repair ? { repair } : {}),
9521
9712
  artifact: {
9522
9713
  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),
9714
+ sourceKind: 'semantic_query',
9715
+ name: item.title ?? item.semantic.id,
9716
+ sql: executedSql.slice(0, 40_000),
9528
9717
  trustState: 'review_required',
9529
- explanation: ['A bounded DQL repair executed on the original data target. The saved App analysis was not changed.'],
9718
+ explanation: [
9719
+ `Metrics: ${item.semantic.metrics.join(', ') || 'none'}.`,
9720
+ `Dimensions: ${item.semantic.dimensions?.join(', ') || 'none'}.`,
9721
+ 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.',
9722
+ ],
9530
9723
  executionTarget: executionTargetDescriptor(body),
9531
9724
  },
9532
9725
  });
9533
- continue;
9534
9726
  }
9727
+ catch (err) {
9728
+ tiles.push({
9729
+ tileId: item.i,
9730
+ status: 'error',
9731
+ tileType: 'semantic',
9732
+ title: item.title ?? item.semantic.id,
9733
+ error: err instanceof Error ? err.message : String(err),
9734
+ trustState: 'review_required',
9735
+ });
9736
+ }
9737
+ continue;
9738
+ }
9739
+ const block = resolveDashboardItemBlock(item, manifest);
9740
+ if (!block) {
9535
9741
  tiles.push({
9536
9742
  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
- } : {}),
9743
+ status: 'unresolved',
9744
+ blockRef: item.block ? (isBlockIdRef(item.block) ? item.block.blockId : item.block.ref) : '(missing source)',
9745
+ error: 'Block reference could not be resolved',
9556
9746
  });
9747
+ continue;
9557
9748
  }
9558
- continue;
9559
- }
9560
- if (item.semantic) {
9749
+ let blockSource;
9750
+ let blockTargetConnection;
9751
+ let blockPlan;
9752
+ let blockFilterApplication;
9753
+ let blockInvocation;
9561
9754
  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,
9755
+ assertAppAccess({
9756
+ app: loaded.app,
9757
+ domain: block.domain ?? loaded.dashboard.metadata.domain ?? loaded.app.domain,
9758
+ level: 'execute',
9594
9759
  });
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);
9760
+ const absBlockPath = join(projectRoot, block.filePath);
9761
+ blockSource = readFileSync(absBlockPath, 'utf-8');
9762
+ const currentSourceRevision = `sha256:${createHash('sha256').update(blockSource).digest('hex')}`;
9763
+ if (item.sourceRevision && item.sourceRevision !== currentSourceRevision) {
9764
+ tiles.push({
9765
+ tileId: item.i,
9766
+ status: 'error',
9767
+ tileType: 'block',
9768
+ blockId: block.name,
9769
+ blockPath: block.filePath,
9770
+ certificationStatus: block.status ?? null,
9771
+ title: item.title ?? block.name,
9772
+ trustState: 'review_required',
9773
+ error: 'This App source changed after it was selected. Refresh the source binding and rerun preview.',
9774
+ });
9775
+ continue;
9604
9776
  }
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,
9777
+ blockTargetConnection = requireActiveConnection(isConnectionConfig(body.connection) ? body.connection : connection);
9778
+ const tableMapping = await resolveSemanticTableMapping(executor, blockTargetConnection, semanticLayer, projectRoot);
9779
+ const boundParameters = dashboardTileParameterValues({
9780
+ item,
9781
+ dashboardValues: dashboardVariables,
9782
+ requestValues: variables,
9783
+ });
9784
+ blockInvocation = prepareBlockInvocation({
9785
+ block: block.name,
9786
+ source: blockSource,
9787
+ parameters: { ...dashboardVariables, ...boundParameters },
9788
+ parameterSources: Object.fromEntries(Object.keys(boundParameters).map((name) => [name, 'surface'])),
9789
+ surface: 'app',
9790
+ });
9791
+ if (blockInvocation.errors.length || blockInvocation.unresolvedParameters.length) {
9792
+ tiles.push({
9793
+ tileId: item.i,
9794
+ status: 'unresolved',
9795
+ tileType: 'block',
9796
+ blockId: block.name,
9797
+ title: item.title ?? block.name,
9798
+ error: blockInvocation.errors[0] ?? `Needs values for: ${blockInvocation.unresolvedParameters.join(', ')}`,
9799
+ invocation: {
9800
+ resolvedParameters: blockInvocation.resolvedParameters,
9801
+ unresolvedParameters: blockInvocation.unresolvedParameters,
9802
+ auditId: blockInvocation.auditId,
9803
+ },
9612
9804
  });
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;
9805
+ continue;
9637
9806
  }
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
- },
9807
+ const semanticCompose = semanticLayer
9808
+ ? await composeSemanticBlockSqlForRuntime(blockSource, semanticLayer, {
9809
+ driver: blockTargetConnection.driver,
9810
+ tableMapping,
9811
+ projectRoot,
9812
+ projectConfig,
9813
+ detectedProvider: semanticDetectedProvider,
9814
+ parameters: blockInvocation.values,
9815
+ })
9816
+ : null;
9817
+ blockPlan = buildExecutionPlan({ id: item.i, type: 'dql', source: blockSource, title: item.title ?? block.name }, {
9818
+ semanticLayer,
9819
+ driver: blockTargetConnection.driver,
9820
+ tableMapping,
9821
+ parameters: blockInvocation.values,
9822
+ semanticSql: semanticCompose?.sql ?? undefined,
9661
9823
  });
9662
- }
9663
- catch (err) {
9664
- tiles.push({
9824
+ if (!blockPlan && !semanticCompose?.sql) {
9825
+ tiles.push({
9826
+ tileId: item.i,
9827
+ status: 'error',
9828
+ blockId: block.name,
9829
+ error: semanticCompose?.diagnostics.find((diagnostic) => diagnostic.severity === 'error')?.message ?? 'Block produced no executable plan',
9830
+ });
9831
+ continue;
9832
+ }
9833
+ blockFilterApplication = applyDashboardFiltersToBlockExecution({
9834
+ sql: semanticCompose?.sql ?? blockPlan.sql,
9835
+ sqlParams: blockPlan?.sqlParams ?? [],
9836
+ variables: { ...(blockPlan?.variables ?? {}), ...dashboardVariables },
9837
+ block,
9838
+ dashboard: loaded.dashboard,
9665
9839
  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',
9840
+ tileFilterBindings: item.filterBindings,
9671
9841
  });
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) {
9842
+ const prepared = prepareLocalExecution(blockFilterApplication.sql, blockTargetConnection, projectRoot, projectConfig);
9843
+ const result = await executor.executeQuery(prepared.sql, blockFilterApplication.sqlParams, runtimeVariables({ ...blockFilterApplication.variables, ...blockInvocation.values }), prepared.connection);
9713
9844
  tiles.push({
9714
9845
  tileId: item.i,
9715
- status: 'unresolved',
9716
- tileType: 'block',
9846
+ status: 'ok',
9717
9847
  blockId: block.name,
9848
+ blockPath: block.filePath,
9849
+ certificationStatus: block.status ?? null,
9718
9850
  title: item.title ?? block.name,
9719
- error: blockInvocation.errors[0] ?? `Needs values for: ${blockInvocation.unresolvedParameters.join(', ')}`,
9851
+ viz: item.viz,
9852
+ chartConfig: mergeDashboardChartConfig(blockPlan?.chartConfig, item),
9853
+ result: normalizeQueryResult(result),
9854
+ filters: {
9855
+ applied: blockFilterApplication.appliedFilters,
9856
+ skipped: blockFilterApplication.skippedFilters,
9857
+ },
9720
9858
  invocation: {
9721
9859
  resolvedParameters: blockInvocation.resolvedParameters,
9722
9860
  unresolvedParameters: blockInvocation.unresolvedParameters,
9723
9861
  auditId: blockInvocation.auditId,
9724
9862
  },
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
9863
  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 ?? {}),
9864
+ citation: {
9865
+ kind: 'block',
9866
+ name: block.name,
9867
+ path: block.filePath,
9868
+ },
9869
+ artifact: {
9870
+ version: 1,
9871
+ sourceKind: block.status === 'certified' ? 'certified_block' : 'review_block',
9872
+ name: block.name,
9873
+ sourcePath: block.filePath,
9874
+ dql: blockSource.slice(0, 40_000),
9875
+ sql: blockFilterApplication.sql.slice(0, 40_000),
9876
+ trustState: block.status === 'certified' ? 'certified' : 'review_required',
9877
+ explanation: [
9878
+ block.status === 'certified' ? 'This tile executed a certified project block.' : 'This project block is not certified and remains review-required.',
9879
+ 'Dashboard filters and typed parameters were bound before read-only execution.',
9880
+ ],
9881
+ executionTarget: executionTargetDescriptor(body),
9830
9882
  },
9831
9883
  });
9832
- if (repaired.ok) {
9884
+ }
9885
+ catch (err) {
9886
+ if (err instanceof DQLAccessDeniedError) {
9833
9887
  tiles.push({
9834
9888
  tileId: item.i,
9835
- status: 'ok',
9836
- tileType: 'block',
9889
+ status: 'unauthorized',
9837
9890
  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,
9891
+ error: err.message,
9892
+ });
9893
+ }
9894
+ else {
9895
+ const repaired = await repairFailedAppTileExecution({
9896
+ source: block.status === 'certified' ? 'certified_block' : 'review_block',
9897
+ question: item.title ?? block.name,
9898
+ sourceSql: blockFilterApplication?.sql ?? blockPlan?.sql,
9899
+ sourceDql: blockSource,
9900
+ artifactName: block.name,
9901
+ error: err,
9902
+ targetConnection: blockTargetConnection,
9903
+ targetConnectionName: projectConfig.defaultConnectionName,
9904
+ sqlParams: blockFilterApplication?.sqlParams ?? blockPlan?.sqlParams,
9905
+ variables: {
9906
+ ...(blockPlan?.variables ?? {}),
9907
+ ...(blockFilterApplication?.variables ?? {}),
9908
+ ...(blockInvocation?.values ?? {}),
9909
+ },
9910
+ });
9911
+ if (repaired.ok) {
9912
+ tiles.push({
9913
+ tileId: item.i,
9914
+ status: 'ok',
9915
+ tileType: 'block',
9916
+ blockId: block.name,
9917
+ blockPath: block.filePath,
9918
+ certificationStatus: block.status ?? null,
9919
+ title: item.title ?? block.name,
9920
+ viz: item.viz,
9921
+ chartConfig: mergeDashboardChartConfig(blockPlan?.chartConfig, item),
9922
+ result: normalizeQueryResult(repaired.result),
9923
+ ...(blockFilterApplication ? {
9924
+ filters: {
9925
+ applied: blockFilterApplication.appliedFilters,
9926
+ skipped: blockFilterApplication.skippedFilters,
9927
+ },
9928
+ } : {}),
9929
+ ...(blockInvocation ? {
9930
+ invocation: {
9931
+ resolvedParameters: blockInvocation.resolvedParameters,
9932
+ unresolvedParameters: blockInvocation.unresolvedParameters,
9933
+ auditId: blockInvocation.auditId,
9934
+ },
9935
+ } : {}),
9936
+ trustState: 'review_required',
9937
+ repair: repaired.repair,
9938
+ citation: { kind: 'block_repaired', name: block.name, path: block.filePath },
9939
+ artifact: {
9940
+ version: 1,
9941
+ sourceKind: block.status === 'certified' ? 'certified_block' : 'review_block',
9942
+ name: block.name,
9943
+ sourcePath: block.filePath,
9944
+ ...(repaired.repairedDql ?? blockSource ? { dql: (repaired.repairedDql ?? blockSource).slice(0, 40_000) } : {}),
9945
+ sql: repaired.repairedSql.slice(0, 40_000),
9946
+ trustState: 'review_required',
9947
+ explanation: [
9948
+ 'The saved certified block was not changed.',
9949
+ 'A bounded repair executed on the original data target; this repaired result requires human review.',
9950
+ ],
9951
+ executionTarget: executionTargetDescriptor(body),
9848
9952
  },
9849
- } : {}),
9850
- ...(blockInvocation ? {
9851
- invocation: {
9852
- resolvedParameters: blockInvocation.resolvedParameters,
9853
- unresolvedParameters: blockInvocation.unresolvedParameters,
9854
- auditId: blockInvocation.auditId,
9953
+ });
9954
+ continue;
9955
+ }
9956
+ tiles.push({
9957
+ tileId: item.i,
9958
+ status: 'error',
9959
+ tileType: 'block',
9960
+ blockId: block.name,
9961
+ error: err instanceof Error ? err.message : String(err),
9962
+ repair: repaired.repair,
9963
+ ...(blockSource ? {
9964
+ artifact: {
9965
+ version: 1,
9966
+ sourceKind: block.status === 'certified' ? 'certified_block' : 'review_block',
9967
+ name: block.name,
9968
+ sourcePath: block.filePath,
9969
+ dql: blockSource.slice(0, 40_000),
9970
+ ...(blockFilterApplication?.sql ?? blockPlan?.sql ? { sql: (blockFilterApplication?.sql ?? blockPlan.sql).slice(0, 40_000) } : {}),
9971
+ trustState: 'review_required',
9972
+ explanation: ['Automatic repair could not produce a safe executable result. The saved block was not changed.'],
9973
+ executionTarget: executionTargetDescriptor(body),
9855
9974
  },
9856
9975
  } : {}),
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
9976
  });
9875
- continue;
9876
9977
  }
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
9978
  }
9899
9979
  }
9900
- }
9901
- localApps?.close();
9980
+ };
9981
+ await Promise.all(Array.from({ length: Math.min(4, itemsToRun.length) }, () => runTileWorker()));
9982
+ tiles.sort((left, right) => (tileOrder.get(left.tileId) ?? 0) - (tileOrder.get(right.tileId) ?? 0));
9983
+ localApps.current?.close();
9902
9984
  // Which of each tile's result columns a viewer may safely filter on.
9903
9985
  // Decided here, from a real dialect parse of the SQL that actually ran,
9904
9986
  // for the same reason Ask does it server-side: an aggregate output needs
@@ -9920,7 +10002,7 @@ export async function startLocalServer(opts) {
9920
10002
  try {
9921
10003
  const dashboardItem = loaded.dashboard.layout.items.find((item) => item.i === tile.tileId);
9922
10004
  const block = dashboardItem ? resolveDashboardItemBlock(dashboardItem, manifest) : null;
9923
- const declaredDimensions = block?.status === 'certified' ? block.dimensions ?? [] : [];
10005
+ const declaredDimensions = block?.dimensions ?? [];
9924
10006
  const filterable = dashboardFilterableResultColumns(executedSql, columns, declaredDimensions, runDialect);
9925
10007
  return filterable.length > 0 ? { ...tile, filterableColumns: filterable } : tile;
9926
10008
  }
@@ -9951,7 +10033,7 @@ export async function startLocalServer(opts) {
9951
10033
  eligibleTileIds: loaded.dashboard.story?.eligibleTileIds,
9952
10034
  driverTileIds: loaded.dashboard.story?.driverTileIds,
9953
10035
  });
9954
- dashboardRunEvidence.set(runId, {
10036
+ const runEvidence = {
9955
10037
  appId,
9956
10038
  dashboardId,
9957
10039
  snapshotId: snapshot.snapshotId,
@@ -9965,7 +10047,28 @@ export async function startLocalServer(opts) {
9965
10047
  facts: storyResult.facts,
9966
10048
  story: storyResult.story,
9967
10049
  expiresAt: Date.now() + 15 * 60_000,
9968
- });
10050
+ };
10051
+ dashboardRunEvidence.set(runId, runEvidence);
10052
+ if (runSurface === 'app-builds') {
10053
+ const receiptStorage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
10054
+ try {
10055
+ receiptStorage.saveAppPreviewEvidence({
10056
+ runId,
10057
+ draftId: appId,
10058
+ dashboardId,
10059
+ snapshotId: snapshot.snapshotId,
10060
+ filterFingerprint,
10061
+ resultFingerprint,
10062
+ personaFingerprint,
10063
+ successfulTileIds: runEvidence.successfulTileIds,
10064
+ semanticApprovalEligibleTileIds: runEvidence.semanticApprovalEligibleTileIds,
10065
+ createdAt: new Date().toISOString(),
10066
+ });
10067
+ }
10068
+ finally {
10069
+ receiptStorage.close();
10070
+ }
10071
+ }
9969
10072
  for (const [id, evidence] of dashboardRunEvidence)
9970
10073
  if (evidence.expiresAt < Date.now())
9971
10074
  dashboardRunEvidence.delete(id);
@@ -10019,6 +10122,17 @@ export async function startLocalServer(opts) {
10019
10122
  // questions. Throws when no provider is configured — propose degrades
10020
10123
  // gracefully by listing the gap instead.
10021
10124
  generateGovernedAnswer: (question, appContext) => generateAppBuildAnswerWithRepair(question, appContext),
10125
+ // App Builder owns its orchestration and structured state. It shares
10126
+ // only the configured provider adapter with Ask/Notebook.
10127
+ planAppBuild: async ({ system, user }) => {
10128
+ const provider = await createBlockStudioAssistProvider(projectRoot);
10129
+ if (!provider)
10130
+ return undefined;
10131
+ return provider.generate([
10132
+ { role: 'system', content: system },
10133
+ { role: 'user', content: user },
10134
+ ], { maxTokens: 2_400, temperature: 0.1 });
10135
+ },
10022
10136
  queueOperation: (input, task) => {
10023
10137
  const operation = operationCoordinator.create(input);
10024
10138
  void operationCoordinator.run(operation.id, task);
@@ -10043,9 +10157,9 @@ export async function startLocalServer(opts) {
10043
10157
  return { ok: true, snapshotId: evidence.snapshotId, resultFingerprint: evidence.resultFingerprint };
10044
10158
  },
10045
10159
  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.' };
10160
+ const evidence = loadAppBuildPreviewEvidence(runId);
10161
+ if (!evidence)
10162
+ return { ok: false, error: 'The App preview receipt is unavailable. Run the local draft again.' };
10049
10163
  if (evidence.appId !== draftId || evidence.dashboardId !== dashboardId)
10050
10164
  return { ok: false, error: 'The App preview receipt does not match this local draft page.' };
10051
10165
  const successful = new Set(evidence.successfulTileIds);
@@ -11596,7 +11710,7 @@ export async function startLocalServer(opts) {
11596
11710
  return;
11597
11711
  }
11598
11712
  }
11599
- const draftSource = setBlockStudioStatusInSource(source, 'draft');
11713
+ const draftSource = markBlockStudioSourceReusable(setBlockStudioStatusInSource(source, 'draft'));
11600
11714
  const parsed = parseBlockSourceMetadata(draftSource);
11601
11715
  const name = typeof suppliedMetadata.name === 'string' && suppliedMetadata.name.trim()
11602
11716
  ? suppliedMetadata.name.trim()
@@ -11606,22 +11720,38 @@ export async function startLocalServer(opts) {
11606
11720
  res.end(serializeJSON({ error: 'Block name is required.' }));
11607
11721
  return;
11608
11722
  }
11609
- const canonicalPath = currentPath && !isDraftBlockPath(currentPath) ? currentPath : undefined;
11723
+ const existingCanonicalDraft = Boolean(currentPath && !isDraftBlockPath(currentPath));
11610
11724
  const stableSuffix = createHash('sha256')
11611
11725
  .update(currentPath ?? `${name}:${draftSource}`)
11612
11726
  .digest('hex')
11613
11727
  .slice(0, 16);
11614
- const draftPath = saveBlockStudioDraftArtifacts(projectRoot, {
11615
- currentPath: currentPath && isDraftBlockPath(currentPath) ? currentPath : undefined,
11728
+ const draftSaveOptions = {
11616
11729
  source: draftSource,
11617
11730
  name,
11618
11731
  domain: typeof suppliedMetadata.domain === 'string' ? suppliedMetadata.domain : parsed.domain,
11619
11732
  description: typeof suppliedMetadata.description === 'string' ? suppliedMetadata.description : parsed.description,
11620
11733
  owner: typeof suppliedMetadata.owner === 'string' ? suppliedMetadata.owner : parsed.owner,
11621
- tags: Array.isArray(suppliedMetadata.tags) ? suppliedMetadata.tags.map(String) : parsed.tags,
11734
+ tags: Array.from(new Set([
11735
+ ...(Array.isArray(suppliedMetadata.tags) ? suppliedMetadata.tags.map(String) : parsed.tags),
11736
+ APP_SOURCE_REUSABLE_TAG,
11737
+ ])),
11622
11738
  lineage: Array.isArray(suppliedMetadata.lineage) ? suppliedMetadata.lineage.map(String) : undefined,
11623
- stableSuffix,
11624
- });
11739
+ };
11740
+ // A saved draft already has a durable identity, even when it lives in
11741
+ // the canonical domain blocks directory. Reuse that identity for the
11742
+ // certification attempt. Creating a second `_drafts` artifact here
11743
+ // made a failed gate duplicate the block in the library.
11744
+ const draftPath = existingCanonicalDraft
11745
+ ? saveBlockStudioArtifacts(projectRoot, {
11746
+ currentPath: currentPath,
11747
+ ...draftSaveOptions,
11748
+ })
11749
+ : saveBlockStudioDraftArtifacts(projectRoot, {
11750
+ currentPath: currentPath && isDraftBlockPath(currentPath) ? currentPath : undefined,
11751
+ ...draftSaveOptions,
11752
+ stableSuffix,
11753
+ });
11754
+ const canonicalPath = existingCanonicalDraft ? draftPath : undefined;
11625
11755
  const operation = operationCoordinator.create({
11626
11756
  type: 'block_certification',
11627
11757
  scope: `block:${draftPath}`,
@@ -11641,22 +11771,37 @@ export async function startLocalServer(opts) {
11641
11771
  }
11642
11772
  }
11643
11773
  operationCoordinator.run(operation.id, async (signal, report) => {
11644
- report({ phase: 'validating', progress: 15, message: 'Validating and running the saved draft once.' });
11645
- const result = await certifyBlockStudioSource(draftSource, draftPath, { enterprise: body.enterprise === true });
11774
+ const result = await certifyBlockStudioSource(draftSource, draftPath, {
11775
+ enterprise: body.enterprise === true,
11776
+ reportProgress: (progress) => {
11777
+ if (signal.aborted)
11778
+ throw Object.assign(new Error('Certification cancelled.'), { code: 'OPERATION_CANCELLED' });
11779
+ report(progress);
11780
+ },
11781
+ });
11646
11782
  const blockers = Array.from(new Set(result.checklist.blockers));
11647
11783
  if (signal.aborted)
11648
11784
  throw Object.assign(new Error('Certification cancelled.'), { code: 'OPERATION_CANCELLED' });
11649
11785
  if (!result.certification.certified || blockers.length > 0) {
11786
+ if (result.preview)
11787
+ writeBlockStudioRunSummary(projectRoot, draftPath, draftSource, result.preview.result);
11788
+ const openedDraft = openBlockStudioDocument(projectRoot, draftPath, semanticLayer);
11650
11789
  return {
11651
11790
  outcome: 'draft_saved_with_blockers',
11652
11791
  oldPath: currentPath ?? draftPath,
11653
11792
  draftPath,
11654
11793
  blockers,
11655
11794
  checklist: result.checklist,
11656
- block: openBlockStudioDocument(projectRoot, draftPath, semanticLayer),
11795
+ runSummary: result.preview ? {
11796
+ rowCount: result.preview.result.rowCount ?? result.preview.result.rows.length,
11797
+ executionTime: result.preview.result.executionTime,
11798
+ columns: result.preview.result.columns,
11799
+ ranAt: new Date().toISOString(),
11800
+ } : undefined,
11801
+ block: { ...openedDraft, validation: result.validation },
11657
11802
  };
11658
11803
  }
11659
- report({ phase: 'publishing', progress: 80, message: 'Publishing the certified block.' });
11804
+ report({ phase: 'publishing', progress: 82, message: 'Publishing the certified block.' });
11660
11805
  const certifiedSource = setBlockStudioStatusInSource(draftSource, 'certified');
11661
11806
  const certifiedMetadata = parseBlockSourceMetadata(certifiedSource);
11662
11807
  const savedPath = saveBlockStudioArtifacts(projectRoot, {
@@ -11698,14 +11843,25 @@ export async function startLocalServer(opts) {
11698
11843
  catch {
11699
11844
  /* best-effort: learning capture must never block certification */
11700
11845
  }
11846
+ report({ phase: 'indexing', progress: 92, message: 'Queueing the certified source index refresh.' });
11701
11847
  const refreshOperation = scheduleProjectRefresh('block-studio-certification');
11848
+ report({ phase: 'indexing', progress: 96, message: 'Certified source saved; index refresh queued.' });
11849
+ if (result.preview)
11850
+ writeBlockStudioRunSummary(projectRoot, savedPath, certifiedSource, result.preview.result);
11851
+ const openedCertified = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
11702
11852
  return {
11703
11853
  outcome: 'certified',
11704
11854
  oldPath: currentPath ?? draftPath,
11705
11855
  draftPath,
11706
11856
  newPath: savedPath,
11707
- block: openBlockStudioDocument(projectRoot, savedPath, semanticLayer),
11857
+ block: { ...openedCertified, validation: result.validation },
11708
11858
  checklist: result.checklist,
11859
+ runSummary: result.preview ? {
11860
+ rowCount: result.preview.result.rowCount ?? result.preview.result.rows.length,
11861
+ executionTime: result.preview.result.executionTime,
11862
+ columns: result.preview.result.columns,
11863
+ ranAt: new Date().toISOString(),
11864
+ } : undefined,
11709
11865
  indexRefresh: { status: 'queued', operationId: refreshOperation.id },
11710
11866
  };
11711
11867
  });
@@ -11723,6 +11879,32 @@ export async function startLocalServer(opts) {
11723
11879
  }
11724
11880
  return;
11725
11881
  }
11882
+ if (req.method === 'POST' && path === '/api/block-studio/certification-check') {
11883
+ try {
11884
+ const body = await readJSON(req);
11885
+ const source = typeof body.source === 'string' ? body.source : '';
11886
+ if (!source.trim()) {
11887
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
11888
+ res.end(serializeJSON({ error: 'source is required' }));
11889
+ return;
11890
+ }
11891
+ // Read-only certification assessment for repair/review workflows. It
11892
+ // deliberately does not write source, receipts, learning, or indexes.
11893
+ const result = await certifyBlockStudioSource(source, null, { enterprise: body.enterprise === true });
11894
+ const blockers = Array.from(new Set(result.checklist.blockers));
11895
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
11896
+ res.end(serializeJSON({
11897
+ ok: result.certification.certified && blockers.length === 0,
11898
+ ...result,
11899
+ blockers,
11900
+ }));
11901
+ }
11902
+ catch (error) {
11903
+ res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
11904
+ res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
11905
+ }
11906
+ return;
11907
+ }
11726
11908
  if (req.method === 'POST' && path === '/api/block-studio/certify') {
11727
11909
  try {
11728
11910
  const body = await readJSON(req);
@@ -11740,6 +11922,8 @@ export async function startLocalServer(opts) {
11740
11922
  const result = await certifyBlockStudioSource(source, blockPath, { enterprise: body.enterprise === true });
11741
11923
  const blockers = Array.from(new Set(result.checklist.blockers));
11742
11924
  if (!result.certification.certified || blockers.length > 0) {
11925
+ if (blockPath && result.preview)
11926
+ writeBlockStudioRunSummary(projectRoot, blockPath, source, result.preview.result);
11743
11927
  res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
11744
11928
  res.end(serializeJSON({ ok: false, ...result, blockers }));
11745
11929
  return;
@@ -11759,10 +11943,14 @@ export async function startLocalServer(opts) {
11759
11943
  owner: parsed.owner,
11760
11944
  tags: parsed.tags,
11761
11945
  });
11946
+ if (result.preview)
11947
+ writeBlockStudioRunSummary(projectRoot, savedPath, certifiedSource, result.preview.result);
11762
11948
  certifiedPayload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
11763
11949
  }
11764
11950
  else {
11765
11951
  setBlockStudioStatus(projectRoot, normalizedBlockPath, 'certified');
11952
+ if (result.preview)
11953
+ writeBlockStudioRunSummary(projectRoot, normalizedBlockPath, certifiedSource, result.preview.result);
11766
11954
  certifiedPayload = openBlockStudioDocument(projectRoot, normalizedBlockPath, semanticLayer);
11767
11955
  }
11768
11956
  }
@@ -11802,7 +11990,7 @@ export async function startLocalServer(opts) {
11802
11990
  source: certifiedPayload?.source ?? certifiedSource,
11803
11991
  metadata: certifiedPayload?.metadata,
11804
11992
  companionPath: certifiedPayload?.companionPath ?? null,
11805
- validation: certifiedPayload?.validation ?? result.validation,
11993
+ validation: result.validation,
11806
11994
  lineageRefresh: { status: 'queued', operationId: refreshOperation.id },
11807
11995
  }));
11808
11996
  }
@@ -12337,13 +12525,14 @@ export async function startLocalServer(opts) {
12337
12525
  }));
12338
12526
  return;
12339
12527
  }
12528
+ const reusableSource = markBlockStudioSourceReusable(readiness.candidate.dqlSource);
12340
12529
  const savedPath = saveBlockStudioArtifacts(projectRoot, {
12341
- source: readiness.candidate.dqlSource,
12530
+ source: reusableSource,
12342
12531
  name: readiness.candidate.name,
12343
12532
  domain: readiness.candidate.domain,
12344
12533
  description: readiness.candidate.description,
12345
12534
  owner: readiness.candidate.owner,
12346
- tags: readiness.candidate.tags,
12535
+ tags: Array.from(new Set([...readiness.candidate.tags, APP_SOURCE_REUSABLE_TAG])),
12347
12536
  lineage: readiness.candidate.lineage.sourceTables,
12348
12537
  importMeta: {
12349
12538
  importId,
@@ -12352,7 +12541,13 @@ export async function startLocalServer(opts) {
12352
12541
  sourcePath: readiness.candidate.sourcePath,
12353
12542
  },
12354
12543
  });
12355
- const next = { ...readiness.candidate, reviewStatus: 'saved', savedPath };
12544
+ const next = {
12545
+ ...readiness.candidate,
12546
+ dqlSource: reusableSource,
12547
+ tags: Array.from(new Set([...readiness.candidate.tags, APP_SOURCE_REUSABLE_TAG])),
12548
+ reviewStatus: 'saved',
12549
+ savedPath,
12550
+ };
12356
12551
  writeBlockStudioImportCandidate(projectRoot, importId, next);
12357
12552
  await refreshLocalMetadataCatalog(projectRoot);
12358
12553
  const payload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
@@ -12568,15 +12763,26 @@ export async function startLocalServer(opts) {
12568
12763
  ? body.parameters
12569
12764
  : {};
12570
12765
  const preview = await runBlockStudioPreviewSource(source, targetConnection, parameters);
12766
+ const blockPath = typeof body.path === 'string' ? body.path : null;
12767
+ if (blockPath)
12768
+ writeBlockStudioRunSummary(projectRoot, blockPath, source, preview.result);
12571
12769
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
12572
12770
  res.end(serializeJSON(preview));
12573
12771
  }
12574
12772
  catch (error) {
12575
12773
  const raw = error instanceof Error ? error.message : String(error);
12774
+ const friendlyMessage = compactBlockStudioRuntimeFailure(raw);
12576
12775
  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) }));
12776
+ // Keep the result surface concise. Raw compiler output remains available
12777
+ // for support and AI repair, but is never the primary end-user message.
12778
+ res.end(serializeJSON({
12779
+ error: raw,
12780
+ friendlyMessage,
12781
+ code: 'BLOCK_PREVIEW_FAILED',
12782
+ recoverable: true,
12783
+ details: { technicalDetails: raw },
12784
+ nextActions: ['Review the named field or metric.', 'Fix the draft and run it again.'],
12785
+ }));
12580
12786
  }
12581
12787
  return;
12582
12788
  }
@@ -12584,7 +12790,7 @@ export async function startLocalServer(opts) {
12584
12790
  try {
12585
12791
  const body = await readJSON(req);
12586
12792
  const source = typeof body.source === 'string'
12587
- ? sanitizeAgentBlockDraftSource(body.source)
12793
+ ? markBlockStudioSourceReusable(sanitizeAgentBlockDraftSource(body.source))
12588
12794
  : '';
12589
12795
  const name = typeof body.name === 'string' ? body.name.trim() : '';
12590
12796
  if (!source.trim() || !name) {
@@ -12597,7 +12803,10 @@ export async function startLocalServer(opts) {
12597
12803
  name,
12598
12804
  domain: typeof body.domain === 'string' ? body.domain : undefined,
12599
12805
  description: typeof body.description === 'string' ? body.description : undefined,
12600
- tags: Array.isArray(body.tags) ? body.tags.map(String) : ['ai-generated', 'review-required'],
12806
+ tags: Array.from(new Set([
12807
+ ...(Array.isArray(body.tags) ? body.tags.map(String) : ['ai-generated', 'review-required']),
12808
+ APP_SOURCE_REUSABLE_TAG,
12809
+ ])),
12601
12810
  stableSuffix: typeof body.runId === 'string' ? body.runId : undefined,
12602
12811
  });
12603
12812
  const payload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
@@ -12613,7 +12822,9 @@ export async function startLocalServer(opts) {
12613
12822
  if (req.method === 'POST' && path === '/api/block-studio/save') {
12614
12823
  try {
12615
12824
  const body = await readJSON(req);
12616
- const source = typeof body.source === 'string' ? body.source : '';
12825
+ const source = typeof body.source === 'string'
12826
+ ? markBlockStudioSourceReusable(body.source)
12827
+ : '';
12617
12828
  const metadata = body.metadata && typeof body.metadata === 'object'
12618
12829
  ? body.metadata
12619
12830
  : {};
@@ -12650,7 +12861,10 @@ export async function startLocalServer(opts) {
12650
12861
  folderPath: metadata.folderPath,
12651
12862
  description: metadata.description,
12652
12863
  owner: metadata.owner,
12653
- tags: Array.isArray(metadata.tags) ? metadata.tags.map(String) : [],
12864
+ tags: Array.from(new Set([
12865
+ ...(Array.isArray(metadata.tags) ? metadata.tags.map(String) : []),
12866
+ APP_SOURCE_REUSABLE_TAG,
12867
+ ])),
12654
12868
  lineage: Array.isArray(metadata.lineage) ? metadata.lineage.map(String) : undefined,
12655
12869
  importMeta: metadata.sourceKind || metadata.sourcePath || metadata.importId || metadata.candidateId
12656
12870
  ? {
@@ -16240,7 +16454,8 @@ function resolveDashboardItemBlock(item, manifest) {
16240
16454
  return manifest.blocks[item.block.blockId] ?? null;
16241
16455
  }
16242
16456
  const normalizedRef = normalize(item.block.ref).replaceAll('\\', '/');
16243
- return Object.values(manifest.blocks).find((b) => normalize(b.filePath).replaceAll('\\', '/') === normalizedRef) ?? null;
16457
+ return (manifest.blockDeclarations ?? Object.values(manifest.blocks))
16458
+ .find((b) => normalize(b.filePath).replaceAll('\\', '/') === normalizedRef) ?? null;
16244
16459
  }
16245
16460
  function mergeDashboardChartConfig(base, item) {
16246
16461
  const options = item.viz.options ?? {};
@@ -20708,7 +20923,62 @@ export function openBlockStudioDocument(projectRoot, relativePath, semanticLayer
20708
20923
  metadata,
20709
20924
  companionPath: companionPath && existsSync(join(projectRoot, companionPath)) ? companionPath : null,
20710
20925
  validation: validateBlockStudioSource(source, semanticLayer),
20926
+ lastRun: readBlockStudioRunSummary(projectRoot, normalizedPath, source) ?? undefined,
20927
+ };
20928
+ }
20929
+ function blockStudioExecutionFingerprint(source) {
20930
+ return createHash('sha256')
20931
+ .update(setBlockStudioStatusInSource(source, 'draft').trim())
20932
+ .digest('hex');
20933
+ }
20934
+ function blockStudioRunSummaryPath(projectRoot, blockPath) {
20935
+ const normalizedPath = normalize(blockPath).replace(/^\/+/, '').replaceAll('\\', '/');
20936
+ const pathKey = createHash('sha256').update(normalizedPath).digest('hex').slice(0, 32);
20937
+ return join(projectRoot, '.dql', 'runs', 'block-studio', `${pathKey}.json`);
20938
+ }
20939
+ function writeBlockStudioRunSummary(projectRoot, blockPath, source, result) {
20940
+ const normalizedPath = normalize(blockPath).replace(/^\/+/, '').replaceAll('\\', '/');
20941
+ if (!isBlockStudioBlockPath(normalizedPath))
20942
+ return;
20943
+ const receiptPath = blockStudioRunSummaryPath(projectRoot, normalizedPath);
20944
+ mkdirSync(dirname(receiptPath), { recursive: true });
20945
+ const receipt = {
20946
+ version: 1,
20947
+ blockPath: normalizedPath,
20948
+ sourceFingerprint: blockStudioExecutionFingerprint(source),
20949
+ rowCount: result.rowCount ?? result.rows.length,
20950
+ executionTime: result.executionTime,
20951
+ columns: result.columns,
20952
+ ranAt: new Date().toISOString(),
20711
20953
  };
20954
+ const tempPath = `${receiptPath}.${process.pid}.tmp`;
20955
+ writeFileSync(tempPath, JSON.stringify(receipt, null, 2) + '\n', 'utf-8');
20956
+ renameSync(tempPath, receiptPath);
20957
+ }
20958
+ function readBlockStudioRunSummary(projectRoot, blockPath, source) {
20959
+ try {
20960
+ const normalizedPath = normalize(blockPath).replace(/^\/+/, '').replaceAll('\\', '/');
20961
+ const receiptPath = blockStudioRunSummaryPath(projectRoot, normalizedPath);
20962
+ if (!existsSync(receiptPath))
20963
+ return null;
20964
+ const receipt = JSON.parse(readFileSync(receiptPath, 'utf-8'));
20965
+ if (receipt.version !== 1
20966
+ || receipt.blockPath !== normalizedPath
20967
+ || receipt.sourceFingerprint !== blockStudioExecutionFingerprint(source)
20968
+ || !Number.isFinite(receipt.rowCount)
20969
+ || !Array.isArray(receipt.columns)
20970
+ || typeof receipt.ranAt !== 'string')
20971
+ return null;
20972
+ return {
20973
+ rowCount: Number(receipt.rowCount),
20974
+ executionTime: typeof receipt.executionTime === 'number' ? receipt.executionTime : undefined,
20975
+ columns: receipt.columns.filter((column) => typeof column === 'string'),
20976
+ ranAt: receipt.ranAt,
20977
+ };
20978
+ }
20979
+ catch {
20980
+ return null;
20981
+ }
20712
20982
  }
20713
20983
  const BLOCK_STUDIO_DRAFT_RUNTIME_DIAGNOSTICS = new Set([
20714
20984
  'metricflow_unavailable',
@@ -20716,6 +20986,161 @@ const BLOCK_STUDIO_DRAFT_RUNTIME_DIAGNOSTICS = new Set([
20716
20986
  'semantic_layer_missing',
20717
20987
  'semantic_runtime_required',
20718
20988
  ]);
20989
+ const BLOCK_STUDIO_RUNTIME_COMPILE_DIAGNOSTICS = new Set([
20990
+ 'metricflow_unavailable',
20991
+ 'semantic_compose_failed',
20992
+ 'semantic_layer_missing',
20993
+ 'semantic_runtime_check_required',
20994
+ 'semantic_runtime_required',
20995
+ ]);
20996
+ function staticSemanticAuthoringDiagnostic(diagnostic) {
20997
+ if (!BLOCK_STUDIO_RUNTIME_COMPILE_DIAGNOSTICS.has(diagnostic.code ?? ''))
20998
+ return diagnostic;
20999
+ return {
21000
+ severity: 'warning',
21001
+ code: 'semantic_runtime_check_required',
21002
+ title: 'Run required for semantic validation',
21003
+ field: 'Runtime',
21004
+ message: 'The block structure and references are valid, but only the configured semantic runtime can prove this metric combination.',
21005
+ correction: 'Run the block to validate compilation and data access before certification.',
21006
+ technicalDetails: diagnostic.message,
21007
+ action: 'run_again',
21008
+ };
21009
+ }
21010
+ function presentBlockStudioDiagnostic(diagnostic, semanticRefs) {
21011
+ if (diagnostic.title || diagnostic.correction)
21012
+ return diagnostic;
21013
+ const raw = diagnostic.message.trim();
21014
+ const locationMatch = /(?:line\s+)?(\d+)[: ,]+(?:column\s+)?(\d+)/i.exec(raw);
21015
+ const technicalDetails = raw.length > 300 || raw.includes('\n') ? raw : undefined;
21016
+ const base = {
21017
+ ...diagnostic,
21018
+ ...(technicalDetails ? { technicalDetails } : {}),
21019
+ ...(locationMatch ? { location: { line: Number(locationMatch[1]), column: Number(locationMatch[2]) } } : {}),
21020
+ };
21021
+ switch (diagnostic.code) {
21022
+ case 'semantic_compose_failed':
21023
+ return {
21024
+ ...base,
21025
+ title: 'Selected metrics cannot be compiled',
21026
+ field: 'Metrics',
21027
+ references: semanticRefs.metrics,
21028
+ message: semanticRefs.metrics.length > 0
21029
+ ? `${semanticRefs.metrics.length} selected metric${semanticRefs.metrics.length === 1 ? '' : 's'} could not be compiled by the configured semantic runtime.`
21030
+ : 'The selected metrics and dimensions do not share a governed query path.',
21031
+ 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.',
21032
+ technicalDetails: raw,
21033
+ action: 'review_metrics',
21034
+ };
21035
+ case 'metricflow_unavailable':
21036
+ case 'semantic_runtime_required':
21037
+ case 'semantic_layer_missing':
21038
+ return {
21039
+ ...base,
21040
+ title: 'Semantic runtime is not ready',
21041
+ field: 'Runtime',
21042
+ references: semanticRefs.metrics,
21043
+ message: 'This semantic block needs a configured dbt Cloud, local MetricFlow, or DQL semantic runtime before it can run or be certified.',
21044
+ correction: 'Open semantic runtime settings, connect the project runtime, then run the block again.',
21045
+ technicalDetails: raw,
21046
+ action: 'configure_runtime',
21047
+ };
21048
+ case 'semantic_metric_missing':
21049
+ return {
21050
+ ...base,
21051
+ title: 'Choose at least one metric',
21052
+ field: 'Metrics',
21053
+ message: 'This semantic block does not select a metric yet.',
21054
+ correction: 'Add one or more governed metrics in the visual builder or DQL source.',
21055
+ action: 'review_metrics',
21056
+ };
21057
+ case 'semantic_ref': {
21058
+ const reference = raw.split(':').slice(1).join(':').trim();
21059
+ return {
21060
+ ...base,
21061
+ title: 'Semantic reference was not found',
21062
+ field: 'Metrics and dimensions',
21063
+ references: reference ? [reference] : [...semanticRefs.metrics, ...semanticRefs.dimensions],
21064
+ message: reference ? `“${reference}” is not available in the current semantic snapshot.` : compactSemanticRuntimeFailure(raw),
21065
+ correction: 'Choose a current metric or dimension from the semantic catalog, then run validation again.',
21066
+ action: 'review_metrics',
21067
+ };
21068
+ }
21069
+ case 'semantic_granularity_missing':
21070
+ return {
21071
+ ...base,
21072
+ title: 'Choose a time grain',
21073
+ field: 'Time',
21074
+ message: 'A time dimension is selected without a granularity.',
21075
+ correction: 'Choose day, week, month, quarter, or year for the selected time dimension.',
21076
+ action: 'review_metrics',
21077
+ };
21078
+ case 'parameter_parse':
21079
+ return {
21080
+ ...base,
21081
+ title: 'Run input is invalid',
21082
+ field: 'Parameters',
21083
+ message: compactSemanticRuntimeFailure(raw),
21084
+ correction: 'Correct the named parameter, default value, or binding, then run validation again.',
21085
+ action: 'edit_parameters',
21086
+ };
21087
+ case 'syntax':
21088
+ case 'semantic_shape':
21089
+ case 'semantic_block_has_query':
21090
+ case 'sql_missing':
21091
+ case 'sql_read_only':
21092
+ return {
21093
+ ...base,
21094
+ title: diagnostic.code === 'sql_read_only' ? 'Only read-only SQL is allowed' : 'DQL source needs attention',
21095
+ field: 'DQL source',
21096
+ message: compactSemanticRuntimeFailure(raw),
21097
+ correction: diagnostic.code === 'sql_read_only'
21098
+ ? 'Replace write or DDL statements with one read-only query.'
21099
+ : 'Open DQL source, correct the highlighted declaration, then validate again.',
21100
+ action: 'edit_source',
21101
+ };
21102
+ default:
21103
+ return base;
21104
+ }
21105
+ }
21106
+ export function compactBlockStudioRuntimeFailure(failure) {
21107
+ const semanticPrefix = 'Could not compose SQL for semantic block metrics.';
21108
+ const semanticIndex = failure.indexOf(semanticPrefix);
21109
+ if (semanticIndex >= 0) {
21110
+ const remainder = failure.slice(semanticIndex + semanticPrefix.length);
21111
+ const references = Array.from(remainder.matchAll(/(?:^|\s)([A-Za-z_][\w.:/-]*)\s*:/g), (match) => match[1]);
21112
+ const uniqueReferences = Array.from(new Set(references));
21113
+ if (uniqueReferences.length > 0) {
21114
+ 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.`;
21115
+ }
21116
+ return 'The selected metrics and dimensions do not share a governed query path. Review the semantic relationships, then run again.';
21117
+ }
21118
+ return compactSemanticRuntimeFailure(failure);
21119
+ }
21120
+ function presentBlockStudioDiagnostics(diagnostics, semanticRefs) {
21121
+ return diagnostics.map((diagnostic) => presentBlockStudioDiagnostic(diagnostic, semanticRefs));
21122
+ }
21123
+ /**
21124
+ * Certification and preview must use the same compiler verdict. A successful
21125
+ * dbt/MetricFlow compile supersedes the native compiler's capability warning;
21126
+ * syntax, unsafe SQL, unknown references, and parameter errors remain intact.
21127
+ */
21128
+ export function reconcileBlockStudioRuntimeValidation(validation, runtime) {
21129
+ const diagnostics = [
21130
+ ...validation.diagnostics.filter((diagnostic) => !BLOCK_STUDIO_RUNTIME_COMPILE_DIAGNOSTICS.has(diagnostic.code ?? '')),
21131
+ ...runtime.diagnostics,
21132
+ ].filter((diagnostic, index, all) => all.findIndex((candidate) => (candidate.severity === diagnostic.severity
21133
+ && candidate.code === diagnostic.code
21134
+ && candidate.message === diagnostic.message)) === index);
21135
+ const presented = presentBlockStudioDiagnostics(diagnostics, validation.semanticRefs);
21136
+ return {
21137
+ ...validation,
21138
+ valid: presented.every((diagnostic) => diagnostic.severity !== 'error'),
21139
+ saveable: blockStudioValidationAllowsDraftSave({ diagnostics: presented }),
21140
+ diagnostics: presented,
21141
+ executableSql: runtime.sql ?? validation.executableSql,
21142
+ };
21143
+ }
20719
21144
  /**
20720
21145
  * Draft persistence is stricter than loose text storage but does not require a
20721
21146
  * configured execution runtime. Syntax, unsafe SQL, invalid semantic identity,
@@ -21266,7 +21691,10 @@ export function validateBlockStudioSource(source, semanticLayer) {
21266
21691
  if (semanticLayer) {
21267
21692
  const semanticCompose = composeSemanticBlockSql(source, semanticLayer);
21268
21693
  semanticRefs = semanticCompose.semanticRefs;
21269
- diagnostics.push(...semanticCompose.diagnostics);
21694
+ // Synchronous validation owns authored shape, references, safety, and
21695
+ // parameter contracts. A native compile miss is not an authoring error:
21696
+ // dbt Cloud or MetricFlow may compile the exact same source successfully.
21697
+ diagnostics.push(...semanticCompose.diagnostics.map(staticSemanticAuthoringDiagnostic));
21270
21698
  executableSql = semanticCompose.sql;
21271
21699
  }
21272
21700
  else {
@@ -21352,10 +21780,11 @@ export function validateBlockStudioSource(source, semanticLayer) {
21352
21780
  });
21353
21781
  }
21354
21782
  }
21783
+ const presentedDiagnostics = presentBlockStudioDiagnostics(diagnostics, semanticRefs);
21355
21784
  return {
21356
- valid: diagnostics.every((diagnostic) => diagnostic.severity !== 'error'),
21357
- saveable: blockStudioValidationAllowsDraftSave({ diagnostics }),
21358
- diagnostics,
21785
+ valid: presentedDiagnostics.every((diagnostic) => diagnostic.severity !== 'error'),
21786
+ saveable: blockStudioValidationAllowsDraftSave({ diagnostics: presentedDiagnostics }),
21787
+ diagnostics: presentedDiagnostics,
21359
21788
  semanticRefs,
21360
21789
  chartConfig: chartConfig ?? undefined,
21361
21790
  executableSql,
@@ -21363,14 +21792,18 @@ export function validateBlockStudioSource(source, semanticLayer) {
21363
21792
  };
21364
21793
  }
21365
21794
  export async function createDqlArtifactGenerationSessionForProject(projectRoot, options, semanticLayer) {
21366
- const dqlArtifact = normalizeDqlArtifactReference(options.dqlArtifact);
21367
- if (!dqlArtifact)
21795
+ const normalizedDqlArtifact = normalizeDqlArtifactReference(options.dqlArtifact);
21796
+ if (!normalizedDqlArtifact)
21368
21797
  throw new Error('A DQL artifact with source is required for DQL draft promotion.');
21798
+ const dqlArtifact = {
21799
+ ...normalizedDqlArtifact,
21800
+ source: markBlockStudioSourceReusable(normalizedDqlArtifact.source),
21801
+ };
21369
21802
  const question = notebookResearchString(options.question) ?? dqlArtifact.name ?? 'Generated DQL artifact';
21370
21803
  const slug = deriveGeneratedDraftSlug(dqlArtifact.name ?? question);
21371
21804
  const domain = notebookResearchString(options.domain) ?? 'misc';
21372
21805
  const owner = notebookResearchString(options.owner) ?? '';
21373
- const tags = Array.from(new Set(['notebook-research', 'review-required', 'dql-artifact', ...(Array.isArray(options.tags) ? options.tags : [])]
21806
+ const tags = Array.from(new Set(['notebook-research', 'review-required', 'dql-artifact', APP_SOURCE_REUSABLE_TAG, ...(Array.isArray(options.tags) ? options.tags : [])]
21374
21807
  .map((tag) => notebookResearchString(tag))
21375
21808
  .filter((tag) => Boolean(tag))));
21376
21809
  const proposedContractId = `${domain}.Unknown.${slug}`;
@@ -22264,6 +22697,23 @@ export function parseBlockSourceMetadata(source) {
22264
22697
  metricsRef: extractStringArray('metrics'),
22265
22698
  };
22266
22699
  }
22700
+ /**
22701
+ * Persist the explicit App-source reuse decision in canonical DQL. Generated
22702
+ * execution stays transient; only Block Studio save/promotion routes call this
22703
+ * helper. The marker is a normal parser-supported tag, so refresh/reindex can
22704
+ * make the participation decision without relying on filenames or prose.
22705
+ */
22706
+ export function markBlockStudioSourceReusable(source) {
22707
+ const parsed = parseBlockSourceMetadata(source);
22708
+ if (parsed.tags.some((tag) => tag.trim().toLowerCase() === APP_SOURCE_REUSABLE_TAG))
22709
+ return source;
22710
+ const tags = Array.from(new Set([...parsed.tags, APP_SOURCE_REUSABLE_TAG]));
22711
+ const rendered = tags.map((tag) => JSON.stringify(tag)).join(', ');
22712
+ if (/\btags\s*=\s*\[[\s\S]*?\]/i.test(source)) {
22713
+ return source.replace(/\btags\s*=\s*\[[\s\S]*?\]/i, `tags = [${rendered}]`);
22714
+ }
22715
+ return source.replace(/(\bblock\s+"[^"]+"\s*\{)/i, `$1\n tags = [${rendered}]`);
22716
+ }
22267
22717
  function compareBlockStudioValues(actual, operator, expected) {
22268
22718
  const expectedValue = normalizeBlockStudioExpected(expected);
22269
22719
  if (operator === '==' || operator === '=')
@@ -22339,22 +22789,88 @@ function evaluateBlockStudioTests(tests, rows, columns, startedAt) {
22339
22789
  function buildBlockStudioCertificationChecklist(input) {
22340
22790
  const parsed = parseBlockSourceMetadata(input.source);
22341
22791
  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');
22792
+ const issues = input.validation.diagnostics
22793
+ .filter((diagnostic) => diagnostic.severity === 'error');
22794
+ for (const error of input.certificationErrors) {
22795
+ const combined = `${error.rule}: ${error.message}`;
22796
+ const lower = combined.toLowerCase();
22797
+ if (lower.includes('missing owner')) {
22798
+ issues.push({
22799
+ severity: 'error',
22800
+ code: 'owner_missing',
22801
+ title: 'Add an owner',
22802
+ field: 'Metadata',
22803
+ message: 'Owner is required before certification.',
22804
+ correction: 'Enter the accountable team or person in Metadata, save the draft, and certify again.',
22805
+ action: 'edit_source',
22806
+ technicalDetails: combined,
22807
+ });
22808
+ }
22809
+ else if (lower.includes('tests pass')) {
22810
+ issues.push({
22811
+ severity: 'error',
22812
+ code: 'tests_failed',
22813
+ title: 'A block test failed',
22814
+ field: 'Tests',
22815
+ message: 'One or more declared tests did not pass against the latest preview result.',
22816
+ correction: 'Review the failed assertion and result, correct the query or expectation, then run certification again.',
22817
+ action: 'edit_source',
22818
+ technicalDetails: combined,
22819
+ });
22820
+ }
22821
+ else {
22822
+ issues.push({
22823
+ severity: 'error',
22824
+ code: `certification_${error.rule.toLowerCase().replace(/[^a-z0-9]+/g, '_')}`,
22825
+ title: 'Certification requirement is not ready',
22826
+ field: 'Metadata',
22827
+ message: compactSemanticRuntimeFailure(combined),
22828
+ correction: 'Update the named block field, save the draft, and certify again.',
22829
+ action: 'edit_source',
22830
+ technicalDetails: combined,
22831
+ });
22832
+ }
22833
+ }
22834
+ for (const blocker of input.extraBlockers ?? []) {
22835
+ issues.push({
22836
+ severity: 'error',
22837
+ code: 'preview_failed',
22838
+ title: 'Preview did not finish',
22839
+ field: 'Run',
22840
+ message: compactBlockStudioRuntimeFailure(blocker),
22841
+ correction: 'Correct the reported query or runtime problem, then run the block again before certification.',
22842
+ technicalDetails: blocker,
22843
+ action: 'run_again',
22844
+ });
22845
+ }
22846
+ if (!input.previewSucceeded && (input.extraBlockers?.length ?? 0) === 0) {
22847
+ issues.push({
22848
+ severity: 'error',
22849
+ code: 'preview_required',
22850
+ title: 'Run the block',
22851
+ field: 'Run',
22852
+ message: 'The block needs a successful preview result before certification.',
22853
+ correction: 'Run the block, resolve any preview error, then certify again.',
22854
+ action: 'run_again',
22855
+ });
22856
+ }
22353
22857
  // Only a REAL test failure blocks. "No tests" and "no chart" are advisory review
22354
22858
  // items the AI fills at draft time — never hard gates (owner is the gate). This is
22355
22859
  // 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');
22860
+ if (input.testResults && input.testResults.failed > 0 && !issues.some((issue) => issue.code === 'tests_failed')) {
22861
+ issues.push({
22862
+ severity: 'error',
22863
+ code: 'tests_failed',
22864
+ title: 'A block test failed',
22865
+ field: 'Tests',
22866
+ message: 'One or more declared tests did not pass against the latest preview result.',
22867
+ correction: 'Review the failed assertion and result, correct the query or expectation, then run certification again.',
22868
+ action: 'edit_source',
22869
+ });
22870
+ }
22871
+ const uniqueIssues = issues.filter((issue, index, all) => all.findIndex((candidate) => (candidate.code === issue.code
22872
+ && candidate.message === issue.message
22873
+ && (candidate.references ?? []).join('|') === (issue.references ?? []).join('|'))) === index);
22358
22874
  return {
22359
22875
  // The metadata tab is green once the one required field (owner) is present.
22360
22876
  // Description/domain are AI-filled + editable and surface as review items, not gates.
@@ -22365,7 +22881,8 @@ function buildBlockStudioCertificationChecklist(input) {
22365
22881
  chart: Boolean(input.validation.chartConfig?.chart),
22366
22882
  lineage: extractSqlTablesLight(sql).length > 0 || input.validation.semanticRefs.metrics.length > 0,
22367
22883
  aiReviewed: true,
22368
- blockers: Array.from(blockers),
22884
+ blockers: uniqueIssues.map((issue) => issue.message),
22885
+ issues: uniqueIssues,
22369
22886
  checkedAt: new Date().toISOString(),
22370
22887
  };
22371
22888
  }