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