@duckcodeailabs/dql-cli 1.11.0 → 1.11.2
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/assets/dql-notebook/assets/{index-BohL6W-H.js → index-DotkS-Xi.js} +382 -360
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/block-studio-import.js +2 -1
- package/dist/block-studio-import.js.map +1 -1
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +11 -9
- package/dist/commands/new.js.map +1 -1
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +4 -1
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/local-runtime.d.ts +11 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +263 -166
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- package/package.json +10 -10
package/dist/local-runtime.js
CHANGED
|
@@ -10,7 +10,7 @@ import { Readable } from "node:stream";
|
|
|
10
10
|
import { buildMixedSourceWarehouseFallbackSql, findMentionedNotebookDataset, planMixedSourceNotebookSql, planMixedSourceSql, } from "./mixed-source-sql.js";
|
|
11
11
|
import { resolveNpmInvocation } from './npm-runtime.js';
|
|
12
12
|
import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, hasStandaloneSemanticRef, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
|
|
13
|
-
import { loadSemanticLayerFromDir, normalizeDqlArtifactReference, serializeMetricDefinitionToYaml, resolveSemanticLayerAsync, resolveRepoSource, getDialect, Parser, NodeKind, blockParameterDefinitions, buildLineageGraph, buildManifest, collectInputFiles, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryBusiness360, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, domainFolderSlug, previewModelingChange, applyModelingChange, previewDbtSourcePatch, applyDbtSourcePatch, loadDomainPackageRegistry, loadDbtNodeAuthoringDetail, normalizeAnalyticalFailureV1, relationshipValidationProofFingerprint, discoverDbtDomains, renderDomainDeclaration, ProjectSnapshotService, analyzeSqlReferences, semanticDimensionReference, } from '@duckcodeailabs/dql-core';
|
|
13
|
+
import { loadSemanticLayerFromDir, normalizeDqlArtifactReference, serializeMetricDefinitionToYaml, resolveSemanticLayerAsync, resolveRepoSource, getDialect, Parser, NodeKind, blockParameterDefinitions, buildLineageGraph, buildManifest, collectInputFiles, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryBusiness360, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, domainFolderSlug, previewModelingChange, applyModelingChange, previewDbtSourcePatch, applyDbtSourcePatch, loadDomainPackageRegistry, loadDbtNodeAuthoringDetail, normalizeAnalyticalFailureV1, relationshipValidationProofFingerprint, renderSemanticBlockSource, discoverDbtDomains, renderDomainDeclaration, ProjectSnapshotService, analyzeSqlReferences, semanticDimensionReference, } from '@duckcodeailabs/dql-core';
|
|
14
14
|
import { load as loadYaml } from 'js-yaml';
|
|
15
15
|
import { listBlockTemplates } from './block-templates.js';
|
|
16
16
|
import { getRunner as getLLMRunner } from './llm/index.js';
|
|
@@ -96,6 +96,16 @@ function agentRunRecord(value) {
|
|
|
96
96
|
function agentRunString(value) {
|
|
97
97
|
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
98
98
|
}
|
|
99
|
+
/** UI catalog fallback labels are not declared project domains. */
|
|
100
|
+
export function normalizeAgentRunDomain(value) {
|
|
101
|
+
const domain = agentRunString(value);
|
|
102
|
+
if (!domain)
|
|
103
|
+
return undefined;
|
|
104
|
+
const normalized = domain.toLowerCase();
|
|
105
|
+
return normalized === 'uncategorized' || normalized === '_uncategorized'
|
|
106
|
+
? undefined
|
|
107
|
+
: domain;
|
|
108
|
+
}
|
|
99
109
|
function parseAgentRunRequestedMode(value) {
|
|
100
110
|
return typeof value === 'string' && AGENT_RUN_REQUESTED_MODES.has(value)
|
|
101
111
|
? value
|
|
@@ -1081,12 +1091,13 @@ export async function startLocalServer(opts) {
|
|
|
1081
1091
|
*/
|
|
1082
1092
|
const buildDirectBuildContext = async (input) => {
|
|
1083
1093
|
const snapshot = projectSnapshot();
|
|
1094
|
+
const activeDomain = normalizeAgentRunDomain(input.domain);
|
|
1084
1095
|
const domainContext = resolveDomainContextEnvelope({
|
|
1085
1096
|
manifest: snapshot.manifest,
|
|
1086
|
-
activeDomain
|
|
1097
|
+
activeDomain,
|
|
1087
1098
|
purpose: input.purpose,
|
|
1088
1099
|
modelAreaId: input.modelAreaId,
|
|
1089
|
-
source:
|
|
1100
|
+
source: activeDomain ? 'explicit_ui' : 'inferred',
|
|
1090
1101
|
snapshotId: snapshot.snapshotId,
|
|
1091
1102
|
});
|
|
1092
1103
|
const contextPack = await buildLocalContextPack(projectRoot, {
|
|
@@ -1095,7 +1106,7 @@ export async function startLocalServer(opts) {
|
|
|
1095
1106
|
surface: input.surface,
|
|
1096
1107
|
selectedContext: {
|
|
1097
1108
|
activeSurface: input.surface,
|
|
1098
|
-
domain:
|
|
1109
|
+
domain: activeDomain,
|
|
1099
1110
|
purpose: input.purpose,
|
|
1100
1111
|
modelAreaId: input.modelAreaId,
|
|
1101
1112
|
...input.selectedContext,
|
|
@@ -1249,7 +1260,7 @@ export async function startLocalServer(opts) {
|
|
|
1249
1260
|
const semanticTableMapping = semanticLayer && semanticConnection
|
|
1250
1261
|
? await resolveSemanticTableMapping(executor, semanticConnection, semanticLayer)
|
|
1251
1262
|
: undefined;
|
|
1252
|
-
const requestedDomain = agentRunWorkspaceValue(request, 'domain');
|
|
1263
|
+
const requestedDomain = normalizeAgentRunDomain(agentRunWorkspaceValue(request, 'domain'));
|
|
1253
1264
|
const requestedPurpose = agentRunWorkspaceValue(request, 'purpose');
|
|
1254
1265
|
const requestedModelAreaId = agentRunWorkspaceValue(request, 'modelAreaId');
|
|
1255
1266
|
const runProjectSnapshot = projectSnapshot();
|
|
@@ -1800,6 +1811,20 @@ export async function startLocalServer(opts) {
|
|
|
1800
1811
|
status: 'blocked',
|
|
1801
1812
|
trustState: 'blocked',
|
|
1802
1813
|
stopReason: 'blocked',
|
|
1814
|
+
artifacts: [agentRunArtifact('answer', 'AI answer provider failed', {
|
|
1815
|
+
kind: 'no_answer',
|
|
1816
|
+
refusalCode: 'provider_error',
|
|
1817
|
+
text: message,
|
|
1818
|
+
answer: message,
|
|
1819
|
+
...(routeDecision?.resolvedAnalyticalPlan
|
|
1820
|
+
? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
|
|
1821
|
+
: {}),
|
|
1822
|
+
providerFailure: {
|
|
1823
|
+
code: 'AI_PROVIDER_FAILURE',
|
|
1824
|
+
message,
|
|
1825
|
+
recoverable: true,
|
|
1826
|
+
},
|
|
1827
|
+
}, undefined, 'blocked')],
|
|
1803
1828
|
evaluations: [
|
|
1804
1829
|
agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to governed answer.'),
|
|
1805
1830
|
agentRunEvaluation('ai-provider', 'AI provider', false, 'blocking', message, { originalErrorType: error instanceof Error ? error.name : typeof error }),
|
|
@@ -1825,12 +1850,12 @@ export async function startLocalServer(opts) {
|
|
|
1825
1850
|
&& !isExploratory;
|
|
1826
1851
|
const isProviderError = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'provider_error';
|
|
1827
1852
|
const isAnalyticalFailure = Boolean(governedAnswer.analyticalFailure);
|
|
1828
|
-
const isTerminalFailure = isProviderError || isExecutionFailure || isAnalyticalFailure;
|
|
1829
1853
|
// AGT-004: a rejected attribution/export/proof policy is a deliberate
|
|
1830
1854
|
// governance boundary, not an ambiguous user question and not a repairable
|
|
1831
1855
|
// retrieval miss. Keep the precise policy detail visible, but never burn two
|
|
1832
1856
|
// more provider calls retrying the same incompatible candidate.
|
|
1833
1857
|
const isPolicyBlocked = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'policy_blocked';
|
|
1858
|
+
const isTerminalFailure = isProviderError || isExecutionFailure || isAnalyticalFailure || isPolicyBlocked;
|
|
1834
1859
|
// The model tried to compose a governed query and declined despite having usable
|
|
1835
1860
|
// context (e.g. it wasn't confident about a multi-table join). The answer loop
|
|
1836
1861
|
// has already spent its one evidence-aware repair. Keep this terminal and
|
|
@@ -1922,7 +1947,16 @@ export async function startLocalServer(opts) {
|
|
|
1922
1947
|
: [{ id: 'review-analytical-failure', label: 'Review the plan, DQL, SQL, and safe repair actions', route: 'blocked' }]
|
|
1923
1948
|
: isProviderError
|
|
1924
1949
|
? [{ id: 'retry-after-provider', label: 'Retry after fixing the AI provider', route: 'generated_answer' }]
|
|
1925
|
-
:
|
|
1950
|
+
: isPolicyBlocked
|
|
1951
|
+
? [
|
|
1952
|
+
...(governedAnswer.dqlArtifact
|
|
1953
|
+
? [{ id: 'edit_dql', label: 'Open DQL to repair', route: 'dql_block_draft', artifactKind: 'dql_block_draft' }]
|
|
1954
|
+
: []),
|
|
1955
|
+
...(sql
|
|
1956
|
+
? [{ id: 'open_sql_notebook', label: 'Open SQL in Notebook', route: 'sql_cell', artifactKind: 'sql_cell' }]
|
|
1957
|
+
: []),
|
|
1958
|
+
]
|
|
1959
|
+
: [{ id: 'retry-after-connection', label: 'Retry after fixing the database connection', route: 'generated_answer' }];
|
|
1926
1960
|
const nextActions = needsClarification
|
|
1927
1961
|
? [{ id: 'clarify', label: 'Clarify question', route: 'generated_answer' }]
|
|
1928
1962
|
: isTerminalFailure
|
|
@@ -1944,7 +1978,11 @@ export async function startLocalServer(opts) {
|
|
|
1944
1978
|
answerRefusalCode: governedAnswer.kind === 'no_answer' ? governedAnswer.refusalCode : undefined,
|
|
1945
1979
|
answerTier: governedAnswer.route?.tier,
|
|
1946
1980
|
summary: isTerminalFailure
|
|
1947
|
-
?
|
|
1981
|
+
? governedAnswer.analyticalFailure?.message
|
|
1982
|
+
?? governedAnswer.executionError
|
|
1983
|
+
?? governedAnswer.answer
|
|
1984
|
+
?? governedAnswer.text
|
|
1985
|
+
?? 'The governed query could not be executed.'
|
|
1948
1986
|
: governedAnswer.route?.label ?? (isCertified ? 'Answered from certified DQL context.' : isExploratory ? 'Exploratory DBT-grounded analysis requires review.' : 'Answered with review-required generated analysis.'),
|
|
1949
1987
|
answer: synthesizedAnswer ?? governedAnswer.answer ?? governedAnswer.text,
|
|
1950
1988
|
...(governedAnswer.clarificationOptions?.length
|
|
@@ -2373,87 +2411,88 @@ export async function startLocalServer(opts) {
|
|
|
2373
2411
|
],
|
|
2374
2412
|
};
|
|
2375
2413
|
},
|
|
2376
|
-
dql_block_draft: async ({ request, routeDecision, attempt, repairHint }) => {
|
|
2414
|
+
dql_block_draft: async ({ request, routeDecision, attempt, repairHint, emit, runId, maxRepairAttempts, }) => {
|
|
2377
2415
|
const contextRecord = agentRunRecord(request.conversationContext);
|
|
2378
2416
|
const carriedDqlArtifact = normalizeDqlArtifactReference(contextRecord?.dqlArtifact)
|
|
2379
2417
|
?? normalizeDqlArtifactReference(request.workspaceContext?.dqlArtifact);
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
tags: ['agent-run', 'review-required'],
|
|
2390
|
-
contextPackId: agentRunString(contextRecord?.contextPackId),
|
|
2391
|
-
routeIntent: agentRunString(contextRecord?.route),
|
|
2392
|
-
sourceBlock: carriedDqlArtifact.sourcePath,
|
|
2393
|
-
}, semanticLayer);
|
|
2394
|
-
const candidate = session.candidates[0];
|
|
2395
|
-
const validation = candidate?.validation && typeof candidate.validation === 'object'
|
|
2396
|
-
? candidate.validation
|
|
2397
|
-
: undefined;
|
|
2398
|
-
const ready = validation?.valid !== false;
|
|
2399
|
-
const payload = {
|
|
2400
|
-
target: 'block',
|
|
2401
|
-
name: candidate?.name ?? carriedDqlArtifact.name ?? 'DQL block draft',
|
|
2402
|
-
path: candidate?.draftSave?.path ?? candidate?.savedPath,
|
|
2403
|
-
importId: session.id,
|
|
2404
|
-
candidateId: candidate?.id,
|
|
2405
|
-
source: candidate?.dqlSource,
|
|
2406
|
-
dqlSource: candidate?.dqlSource,
|
|
2407
|
-
dqlArtifact: carriedDqlArtifact,
|
|
2408
|
-
certifierVerdict: {
|
|
2409
|
-
ready,
|
|
2410
|
-
diagnostics: validation?.diagnostics ?? [],
|
|
2411
|
-
},
|
|
2412
|
-
generation: session.generation,
|
|
2413
|
-
};
|
|
2414
|
-
return {
|
|
2415
|
-
summary: ready
|
|
2416
|
-
? 'Saved the generated DQL artifact as a review-required block draft.'
|
|
2417
|
-
: 'Saved the generated DQL artifact as a block draft with review blockers or warnings.',
|
|
2418
|
-
artifacts: [agentRunArtifact('dql_block_draft', payload.name, payload, payload.path)],
|
|
2418
|
+
// UI-016 / E2E-015: a cold Block AI request uses the same governed answer
|
|
2419
|
+
// pipeline as Ask and Notebook. Generation is write-free. The returned
|
|
2420
|
+
// ownerless artifact is persisted only when the user explicitly chooses
|
|
2421
|
+
// Add to Block Studio.
|
|
2422
|
+
const governedResult = carriedDqlArtifact && (attempt ?? 0) === 0
|
|
2423
|
+
? {
|
|
2424
|
+
summary: 'Reused the governed answer artifact as a transient Block AI draft.',
|
|
2425
|
+
answer: 'Prepared the existing governed DQL as an ownerless review draft. No file was changed.',
|
|
2426
|
+
artifacts: [agentRunArtifact('answer', carriedDqlArtifact.name ?? 'Governed DQL answer', { dqlArtifact: carriedDqlArtifact }, undefined, carriedDqlArtifact.trustState)],
|
|
2419
2427
|
evaluations: [
|
|
2420
|
-
agentRunEvaluation('
|
|
2421
|
-
agentRunEvaluation('dql-artifact-reuse', 'Used generated DQL artifact', true, 'info', 'Persisted the existing DQL artifact without regenerating it from SQL.', { importId: session.id, path: payload.path }),
|
|
2422
|
-
agentRunEvaluation('certification-boundary', 'Certification boundary', ready, 'warning', ready
|
|
2423
|
-
? 'The draft has no automatic validation blockers, but certification still requires human review.'
|
|
2424
|
-
: 'The draft has validation blockers that must be resolved before certification review.', payload),
|
|
2425
|
-
],
|
|
2426
|
-
nextActions: [
|
|
2427
|
-
{ id: 'open-review', label: 'Open review checklist', artifactKind: 'dql_block_draft' },
|
|
2428
|
-
{ id: 'build-app', label: 'Build app from block', route: 'app_build', artifactKind: 'app_draft' },
|
|
2428
|
+
agentRunEvaluation('dql-artifact-reuse', 'Reused governed DQL artifact', true, 'info', 'Reused the exact answer artifact without regenerating SQL or writing a block file.'),
|
|
2429
2429
|
],
|
|
2430
|
-
}
|
|
2430
|
+
}
|
|
2431
|
+
: await answerRunExecutor({
|
|
2432
|
+
request,
|
|
2433
|
+
route: 'generated_answer',
|
|
2434
|
+
routeDecision,
|
|
2435
|
+
attempt,
|
|
2436
|
+
repairHint,
|
|
2437
|
+
emit,
|
|
2438
|
+
runId,
|
|
2439
|
+
maxRepairAttempts,
|
|
2440
|
+
});
|
|
2441
|
+
if (governedResult.status === 'blocked' || governedResult.status === 'needs_clarification') {
|
|
2442
|
+
return governedResult;
|
|
2443
|
+
}
|
|
2444
|
+
const governedPayload = (governedResult.artifacts ?? [])
|
|
2445
|
+
.map((artifact) => agentRunRecord(artifact.payload))
|
|
2446
|
+
.find((payload) => Boolean(normalizeDqlArtifactReference(payload?.dqlArtifact)
|
|
2447
|
+
?? normalizeDqlArtifactReference(payload)));
|
|
2448
|
+
const directDqlArtifact = normalizeDqlArtifactReference(governedPayload);
|
|
2449
|
+
const governedAnswer = governedPayload
|
|
2450
|
+
? directDqlArtifact
|
|
2451
|
+
? { dqlArtifact: directDqlArtifact }
|
|
2452
|
+
: governedPayload
|
|
2453
|
+
: undefined;
|
|
2454
|
+
const dqlArtifact = governedAnswer
|
|
2455
|
+
? ownerlessReviewDqlArtifactFromAnswer(governedAnswer, request.question)
|
|
2456
|
+
: undefined;
|
|
2457
|
+
if (!dqlArtifact) {
|
|
2458
|
+
return governedResult;
|
|
2431
2459
|
}
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2460
|
+
const sql = dqlArtifact.compiledSql
|
|
2461
|
+
?? agentRunString(governedPayload?.proposedSql)
|
|
2462
|
+
?? agentRunString(governedPayload?.sql);
|
|
2463
|
+
const result = {
|
|
2464
|
+
target: 'block',
|
|
2465
|
+
name: dqlArtifact.name ?? deriveGeneratedDraftSlug(request.question),
|
|
2466
|
+
source: dqlArtifact.source,
|
|
2467
|
+
dqlSource: dqlArtifact.source,
|
|
2468
|
+
dqlArtifact,
|
|
2469
|
+
...(sql ? { sql, sqlPreview: sql } : {}),
|
|
2470
|
+
...(governedPayload?.result ? { result: governedPayload.result } : {}),
|
|
2471
|
+
description: request.question,
|
|
2472
|
+
dimensions: dqlArtifact.dimensions ?? [],
|
|
2473
|
+
outputs: [...(dqlArtifact.dimensions ?? []), ...(dqlArtifact.metrics ?? [])],
|
|
2474
|
+
certifierVerdict: {
|
|
2475
|
+
ready: true,
|
|
2476
|
+
blocking: [],
|
|
2477
|
+
warnings: [
|
|
2478
|
+
'Ownership is intentionally absent from the AI draft and is assigned by a human during promotion.',
|
|
2479
|
+
'Review the generated DQL, compiled SQL, grain, and result before promotion.',
|
|
2480
|
+
],
|
|
2481
|
+
},
|
|
2482
|
+
};
|
|
2443
2483
|
return {
|
|
2444
|
-
summary:
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2484
|
+
summary: 'Prepared an ownerless, review-required DQL block draft. No file was changed.',
|
|
2485
|
+
answer: governedResult.answer,
|
|
2486
|
+
status: 'needs_review',
|
|
2487
|
+
trustState: 'review_required',
|
|
2488
|
+
stopReason: 'human_review_required',
|
|
2489
|
+
artifacts: [agentRunArtifact('dql_block_draft', result.name, result, undefined, 'review_required')],
|
|
2448
2490
|
evaluations: [
|
|
2449
|
-
|
|
2450
|
-
agentRunEvaluation('
|
|
2451
|
-
? 'The draft has no automatic certifier blockers, but certification still requires human review.'
|
|
2452
|
-
: 'The draft has certifier blockers that must be resolved before certification review.', result),
|
|
2491
|
+
...(governedResult.evaluations ?? []),
|
|
2492
|
+
agentRunEvaluation('block-commit-boundary', 'Explicit Block Studio commit', true, 'warning', 'Generation produced an ownerless transient DQL artifact. Add to Block Studio is the only action that writes the review draft.', result),
|
|
2453
2493
|
],
|
|
2454
2494
|
nextActions: [
|
|
2455
|
-
{ id: '
|
|
2456
|
-
{ id: 'build-app', label: 'Build app from block', route: 'app_build', artifactKind: 'app_draft' },
|
|
2495
|
+
{ id: 'add-to-block-studio', label: 'Add to Block Studio', artifactKind: 'dql_block_draft' },
|
|
2457
2496
|
],
|
|
2458
2497
|
};
|
|
2459
2498
|
},
|
|
@@ -2566,7 +2605,7 @@ export async function startLocalServer(opts) {
|
|
|
2566
2605
|
if (pending)
|
|
2567
2606
|
return pending;
|
|
2568
2607
|
const snapshot = projectSnapshot();
|
|
2569
|
-
const requestedDomain = agentRunWorkspaceValue(request, 'domain');
|
|
2608
|
+
const requestedDomain = normalizeAgentRunDomain(agentRunWorkspaceValue(request, 'domain'));
|
|
2570
2609
|
// CTX-003: resolve result entities/values before evidence retrieval. The
|
|
2571
2610
|
// router, planner, and answer loop must rank the same typed follow-up; doing
|
|
2572
2611
|
// this only inside the provider adapter allowed stale catalog matches to win
|
|
@@ -15177,7 +15216,7 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
|
|
|
15177
15216
|
diagnostics.push({
|
|
15178
15217
|
severity: 'error',
|
|
15179
15218
|
code: 'semantic_metric_missing',
|
|
15180
|
-
message: 'Semantic block is missing a metric. Add
|
|
15219
|
+
message: 'Semantic block is missing a metric. Add metrics = ["metric_name"].',
|
|
15181
15220
|
});
|
|
15182
15221
|
return { sql: null, diagnostics, semanticRefs };
|
|
15183
15222
|
}
|
|
@@ -15203,12 +15242,9 @@ function composeSemanticBlockSql(source, semanticLayer, options) {
|
|
|
15203
15242
|
if (diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
|
|
15204
15243
|
return { sql: null, diagnostics, semanticRefs };
|
|
15205
15244
|
}
|
|
15206
|
-
//
|
|
15207
|
-
//
|
|
15208
|
-
//
|
|
15209
|
-
// keeps metric-bound blocks runnable offline (the full MetricFlow engine may be
|
|
15210
|
-
// unavailable). We still validated the metric exists above. Returning the query
|
|
15211
|
-
// as the composed SQL lets the normal {{ ref() }} resolution + execution apply.
|
|
15245
|
+
// Legacy import-adapter compatibility: older semantic blocks may carry a
|
|
15246
|
+
// pre-compiled `query`. Current writers keep compiled SQL outside semantic DQL,
|
|
15247
|
+
// but existing files remain executable while they are migrated by the formatter.
|
|
15212
15248
|
const precompiledQuery = extractBlockStudioSql(source);
|
|
15213
15249
|
if (precompiledQuery) {
|
|
15214
15250
|
return { sql: precompiledQuery, diagnostics, semanticRefs };
|
|
@@ -15890,7 +15926,96 @@ function saveDqlGenerationDraftForProject(projectRoot, importId, candidate) {
|
|
|
15890
15926
|
}
|
|
15891
15927
|
/** AI may propose block logic, but ownership is assigned by a human at promotion. */
|
|
15892
15928
|
export function sanitizeAgentBlockDraftSource(source) {
|
|
15893
|
-
return source.replace(
|
|
15929
|
+
return source.replace(/^\s*owner\s*=\s*.+(?:\r?\n|$)/gmi, '');
|
|
15930
|
+
}
|
|
15931
|
+
/**
|
|
15932
|
+
* UI-016 / CONTRACT-002 / AGT-021 — derive the transient Block AI artifact
|
|
15933
|
+
* from the exact governed answer artifact. Semantic drafts are re-rendered
|
|
15934
|
+
* through the one canonical metrics/dimensions writer; custom SQL drafts retain
|
|
15935
|
+
* their query body. Both become ownerless, review-required drafts and never
|
|
15936
|
+
* inherit a saved/certified identity from their answer source.
|
|
15937
|
+
*/
|
|
15938
|
+
export function ownerlessReviewDqlArtifactFromAnswer(answer, question) {
|
|
15939
|
+
const artifact = normalizeDqlArtifactReference(answer.result?.dqlArtifact)
|
|
15940
|
+
?? normalizeDqlArtifactReference(answer.dqlArtifact);
|
|
15941
|
+
if (!artifact?.source)
|
|
15942
|
+
return undefined;
|
|
15943
|
+
const parsed = parseSemanticBlockConfig(artifact.source);
|
|
15944
|
+
const metrics = Array.from(new Set([
|
|
15945
|
+
...(artifact.metrics ?? []),
|
|
15946
|
+
...parsed.metrics,
|
|
15947
|
+
...(parsed.metric ? [parsed.metric] : []),
|
|
15948
|
+
].map((metric) => metric.trim()).filter(Boolean)));
|
|
15949
|
+
const dimensions = Array.from(new Set([
|
|
15950
|
+
...(artifact.dimensions ?? []),
|
|
15951
|
+
...parsed.dimensions,
|
|
15952
|
+
].map((dimension) => dimension.trim()).filter(Boolean)));
|
|
15953
|
+
const name = artifact.name
|
|
15954
|
+
?? artifact.source.match(/\bblock\s+"([^"]+)"/i)?.[1]
|
|
15955
|
+
?? deriveGeneratedDraftSlug(question);
|
|
15956
|
+
const compiledSql = artifact.compiledSql
|
|
15957
|
+
?? answer.result?.sql
|
|
15958
|
+
?? answer.proposedSql
|
|
15959
|
+
?? answer.sql;
|
|
15960
|
+
if (artifact.kind === 'semantic_block' || parsed.blockType === 'semantic' || metrics.length > 0) {
|
|
15961
|
+
const source = renderSemanticBlockSource({
|
|
15962
|
+
name,
|
|
15963
|
+
status: 'draft',
|
|
15964
|
+
domain: parseBlockStudioStringField(artifact.source, 'domain'),
|
|
15965
|
+
description: question,
|
|
15966
|
+
tags: ['ai-generated', 'review-required'],
|
|
15967
|
+
metrics,
|
|
15968
|
+
dimensions,
|
|
15969
|
+
timeDimension: artifact.timeDimension ?? (parsed.timeDimension && parsed.granularity
|
|
15970
|
+
? { name: parsed.timeDimension, granularity: parsed.granularity }
|
|
15971
|
+
: undefined),
|
|
15972
|
+
requestedFilters: (artifact.filters ?? parsed.filters).flatMap((filter) => filter.values.length > 0
|
|
15973
|
+
? filter.values.map((value) => `${filter.dimension}=${value}`)
|
|
15974
|
+
: [filter.dimension]),
|
|
15975
|
+
orderBy: artifact.orderBy,
|
|
15976
|
+
limit: artifact.limit ?? parsed.limit,
|
|
15977
|
+
parameters: artifact.parameters?.map((parameter) => ({
|
|
15978
|
+
name: parameter.name,
|
|
15979
|
+
type: parameter.type,
|
|
15980
|
+
default: parameter.default,
|
|
15981
|
+
policy: parameter.policy,
|
|
15982
|
+
binding: parameter.binding?.kind === 'semantic_filter'
|
|
15983
|
+
? parameter.binding.field
|
|
15984
|
+
: parameter.binding?.kind === 'limit'
|
|
15985
|
+
? 'limit'
|
|
15986
|
+
: undefined,
|
|
15987
|
+
})),
|
|
15988
|
+
visualization: metrics.length > 1 ? { chart: 'table' } : undefined,
|
|
15989
|
+
});
|
|
15990
|
+
return {
|
|
15991
|
+
...artifact,
|
|
15992
|
+
kind: 'semantic_block',
|
|
15993
|
+
name,
|
|
15994
|
+
source,
|
|
15995
|
+
sourcePath: undefined,
|
|
15996
|
+
metrics,
|
|
15997
|
+
dimensions,
|
|
15998
|
+
persistence: 'transient',
|
|
15999
|
+
trustState: 'review_required',
|
|
16000
|
+
...(compiledSql ? { compiledSql } : {}),
|
|
16001
|
+
};
|
|
16002
|
+
}
|
|
16003
|
+
let source = sanitizeAgentBlockDraftSource(artifact.source);
|
|
16004
|
+
if (/\bstatus\s*=\s*"[^"]*"/i.test(source)) {
|
|
16005
|
+
source = source.replace(/\bstatus\s*=\s*"[^"]*"/i, 'status = "draft"');
|
|
16006
|
+
}
|
|
16007
|
+
else {
|
|
16008
|
+
source = source.replace(/(\bblock\s+"[^"]+"\s*\{)/i, '$1\n status = "draft"');
|
|
16009
|
+
}
|
|
16010
|
+
return {
|
|
16011
|
+
...artifact,
|
|
16012
|
+
name,
|
|
16013
|
+
source,
|
|
16014
|
+
sourcePath: undefined,
|
|
16015
|
+
persistence: 'transient',
|
|
16016
|
+
trustState: 'review_required',
|
|
16017
|
+
...(compiledSql ? { compiledSql } : {}),
|
|
16018
|
+
};
|
|
15894
16019
|
}
|
|
15895
16020
|
export function saveBlockStudioArtifacts(projectRoot, options) {
|
|
15896
16021
|
const slug = options.name.toLowerCase().replace(/[^a-z0-9_]+/g, '-').replace(/^[-_]+|[-_]+$/g, '') || 'block';
|
|
@@ -16656,29 +16781,18 @@ function buildSemanticImportCandidateSource(candidate, patch, semanticLayer) {
|
|
|
16656
16781
|
const allowedFilters = (patch.allowedFilters ?? [])
|
|
16657
16782
|
.map((filter) => compatibleByName.get(normalizedTerm(filter))?.name)
|
|
16658
16783
|
.filter((filter) => Boolean(filter));
|
|
16659
|
-
|
|
16660
|
-
|
|
16661
|
-
|
|
16662
|
-
:
|
|
16663
|
-
|
|
16664
|
-
|
|
16665
|
-
|
|
16666
|
-
|
|
16667
|
-
|
|
16668
|
-
|
|
16669
|
-
|
|
16670
|
-
|
|
16671
|
-
metricLine,
|
|
16672
|
-
dimensionNames.length > 0 ? ` dimensions = [${dimensionNames.map(quote).join(', ')}]` : '',
|
|
16673
|
-
allowedFilters.length > 0 ? ` requested_filters = [${allowedFilters.map(quote).join(', ')}]` : '',
|
|
16674
|
-
'',
|
|
16675
|
-
' visualization {',
|
|
16676
|
-
` chart = "${dimensionNames.length > 0 ? 'bar' : 'kpi'}"`,
|
|
16677
|
-
' }',
|
|
16678
|
-
'}',
|
|
16679
|
-
'',
|
|
16680
|
-
];
|
|
16681
|
-
return lines.filter((line, index) => line !== '' || (index > 0 && lines[index - 1] !== '')).join('\n');
|
|
16784
|
+
return renderSemanticBlockSource({
|
|
16785
|
+
name: candidate.name,
|
|
16786
|
+
status: 'draft',
|
|
16787
|
+
domain: candidate.domain,
|
|
16788
|
+
description: candidate.description,
|
|
16789
|
+
owner: candidate.owner,
|
|
16790
|
+
tags: candidate.tags,
|
|
16791
|
+
metrics: metricNames,
|
|
16792
|
+
dimensions: dimensionNames,
|
|
16793
|
+
requestedFilters: allowedFilters,
|
|
16794
|
+
visualization: { chart: dimensionNames.length > 0 ? 'bar' : 'kpi' },
|
|
16795
|
+
});
|
|
16682
16796
|
}
|
|
16683
16797
|
function inferDqlGenerationTerms(evidence) {
|
|
16684
16798
|
return Array.from(new Set(evidence
|
|
@@ -17950,44 +18064,18 @@ export function createSemanticBuilderBlock(projectRoot, options) {
|
|
|
17950
18064
|
};
|
|
17951
18065
|
}
|
|
17952
18066
|
function buildSemanticBlockContent(options) {
|
|
17953
|
-
|
|
17954
|
-
|
|
17955
|
-
|
|
17956
|
-
|
|
17957
|
-
|
|
17958
|
-
|
|
17959
|
-
|
|
17960
|
-
|
|
17961
|
-
|
|
17962
|
-
|
|
17963
|
-
|
|
17964
|
-
}
|
|
17965
|
-
if (options.metrics.length === 0) {
|
|
17966
|
-
lines.push(' metric = ""');
|
|
17967
|
-
}
|
|
17968
|
-
else if (options.metrics.length === 1) {
|
|
17969
|
-
lines.push(` metric = "${escapeDqlString(options.metrics[0])}"`);
|
|
17970
|
-
}
|
|
17971
|
-
else {
|
|
17972
|
-
lines.push(` metrics = [${options.metrics.map((metric) => `"${escapeDqlString(metric)}"`).join(', ')}]`);
|
|
17973
|
-
}
|
|
17974
|
-
if (options.dimensions.length > 0) {
|
|
17975
|
-
lines.push(` dimensions = [${options.dimensions.map((dimension) => `"${escapeDqlString(dimension)}"`).join(', ')}]`);
|
|
17976
|
-
}
|
|
17977
|
-
else {
|
|
17978
|
-
lines.push(' dimensions = []');
|
|
17979
|
-
}
|
|
17980
|
-
if (options.timeDimension) {
|
|
17981
|
-
lines.push(` time_dimension = "${escapeDqlString(options.timeDimension.name)}"`);
|
|
17982
|
-
lines.push(` granularity = "${escapeDqlString(options.timeDimension.granularity)}"`);
|
|
17983
|
-
}
|
|
17984
|
-
const visualization = buildVisualizationBlock(options.chart ?? 'table', options.dimensions, options.timeDimension, options.metrics);
|
|
17985
|
-
if (visualization) {
|
|
17986
|
-
lines.push('');
|
|
17987
|
-
lines.push(...visualization);
|
|
17988
|
-
}
|
|
17989
|
-
lines.push('}');
|
|
17990
|
-
return lines.join('\n') + '\n';
|
|
18067
|
+
return renderSemanticBlockSource({
|
|
18068
|
+
name: options.name,
|
|
18069
|
+
status: 'draft',
|
|
18070
|
+
domain: options.domain,
|
|
18071
|
+
description: options.description,
|
|
18072
|
+
owner: options.owner,
|
|
18073
|
+
tags: options.tags,
|
|
18074
|
+
metrics: options.metrics,
|
|
18075
|
+
dimensions: options.dimensions,
|
|
18076
|
+
timeDimension: options.timeDimension,
|
|
18077
|
+
visualization: semanticBlockVisualization(options.chart ?? 'table', options.dimensions, options.timeDimension, options.metrics),
|
|
18078
|
+
});
|
|
17991
18079
|
}
|
|
17992
18080
|
function buildCustomSemanticBlockContent(options) {
|
|
17993
18081
|
const lines = [
|
|
@@ -18039,6 +18127,21 @@ function buildVisualizationBlock(chart, dimensions, timeDimension, metrics) {
|
|
|
18039
18127
|
' }',
|
|
18040
18128
|
];
|
|
18041
18129
|
}
|
|
18130
|
+
function semanticBlockVisualization(chart, dimensions, timeDimension, metrics) {
|
|
18131
|
+
if (metrics.length > 1)
|
|
18132
|
+
return { chart: 'table' };
|
|
18133
|
+
const x = timeDimension ? `${timeDimension.name}_${timeDimension.granularity}` : dimensions[0];
|
|
18134
|
+
const y = metrics[0];
|
|
18135
|
+
if (chart === 'table')
|
|
18136
|
+
return { chart: 'table' };
|
|
18137
|
+
if (chart === 'kpi')
|
|
18138
|
+
return { chart: 'kpi', ...(y ? { y } : {}) };
|
|
18139
|
+
return {
|
|
18140
|
+
chart: x ? chart : 'table',
|
|
18141
|
+
...(x ? { x } : {}),
|
|
18142
|
+
...(x && y ? { y } : {}),
|
|
18143
|
+
};
|
|
18144
|
+
}
|
|
18042
18145
|
function writeBlockCompanionFile(projectRoot, options) {
|
|
18043
18146
|
const extractedRefs = extractSemanticReferenceNames(options.content);
|
|
18044
18147
|
const semanticMetrics = Array.from(new Set([...(options.semanticMetrics ?? []), ...extractedRefs.metrics]));
|
|
@@ -18227,23 +18330,17 @@ function buildBlankBlockContent(options) {
|
|
|
18227
18330
|
return lines.join('\n') + '\n';
|
|
18228
18331
|
}
|
|
18229
18332
|
function buildBlankSemanticBlockContent(options) {
|
|
18230
|
-
|
|
18231
|
-
|
|
18232
|
-
|
|
18233
|
-
|
|
18234
|
-
|
|
18235
|
-
|
|
18236
|
-
|
|
18237
|
-
|
|
18238
|
-
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
' visualization {',
|
|
18242
|
-
' chart = "table"',
|
|
18243
|
-
' }',
|
|
18244
|
-
'}',
|
|
18245
|
-
];
|
|
18246
|
-
return lines.join('\n') + '\n';
|
|
18333
|
+
return renderSemanticBlockSource({
|
|
18334
|
+
name: options.name,
|
|
18335
|
+
status: 'draft',
|
|
18336
|
+
domain: options.domain,
|
|
18337
|
+
description: options.description?.trim() || options.name,
|
|
18338
|
+
owner: options.owner,
|
|
18339
|
+
tags: options.tags,
|
|
18340
|
+
metrics: [],
|
|
18341
|
+
dimensions: [],
|
|
18342
|
+
visualization: { chart: 'table' },
|
|
18343
|
+
});
|
|
18247
18344
|
}
|
|
18248
18345
|
function parseYamlScalar(value) {
|
|
18249
18346
|
const trimmed = value.trim();
|