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