@duckcodeailabs/dql-cli 1.11.4 → 1.11.6

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.
@@ -5,7 +5,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, s
5
5
  import { createRequire } from "node:module";
6
6
  import { homedir } from "node:os";
7
7
  import { fileURLToPath } from "node:url";
8
- import { dirname, extname, join, normalize, relative, resolve, } from "node:path";
8
+ import { dirname, extname, join, normalize, relative, resolve, sep, } from "node:path";
9
9
  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';
@@ -19,7 +19,7 @@ import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from '
19
19
  import { resolveRetrievalHealthStatus } from './retrieval-health.js';
20
20
  import { createDqlAgentProviderRunner, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
21
21
  import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
22
- import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, recallRelevantTurns, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, defaultConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, propose, proposePlan, recordCorrectionTrace, emitCorrectionEvalCase, mineJoinPatterns, reviewHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, } from '@duckcodeailabs/dql-agent';
22
+ import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, recallRelevantTurns, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, defaultConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, propose, proposePlan, recordCorrectionTrace, listHintsFromGit, evaluateHint, emitCorrectionEvalCase, mineJoinPatterns, reviewHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, } from '@duckcodeailabs/dql-agent';
23
23
  import { gatherProposeEnrichment } from './propose-enrich.js';
24
24
  import { handleAppsApi, proposeAppAiBuild, recommendVisualization } from './apps-api.js';
25
25
  import { getActiveProvider, getEffectiveProviderConfig, isProviderSettingsId, listProviderSettings, saveProviderSettings, } from './settings/provider-settings.js';
@@ -2003,7 +2003,13 @@ export async function startLocalServer(opts) {
2003
2003
  : 'Semantic compilation details', governedAnswer, undefined, needsClarification ? 'not_applicable' : 'review_required')]
2004
2004
  : governedAnswer.dqlArtifact && !isProviderError && !isGroundingGap && !isModelDeclined && !isPolicyBlocked
2005
2005
  ? [agentRunArtifact('dql_block_draft', 'DQL draft (review required)', governedAnswer.dqlArtifact, undefined, 'review_required')]
2006
- : [])
2006
+ // Every refusal still owes the user an account of itself. Emitting no
2007
+ // artifact leaves the answer as a bare sentence with a Copy button:
2008
+ // the inspector can only be opened FROM an artifact, so "How it was
2009
+ // answered", the DQL draft and the compiled SQL all become
2010
+ // unreachable exactly when the user most needs to see why it stopped
2011
+ // and carry the query into a notebook.
2012
+ : [agentRunArtifact('answer', 'No answer was accepted', governedAnswer, undefined, needsClarification ? 'not_applicable' : 'blocked')])
2007
2013
  : [agentRunArtifact('answer', isCertified ? 'Certified answer' : isSemantic ? 'Governed semantic answer' : isExploratory ? 'Exploratory DBT-grounded answer' : 'Review-required answer', governedAnswer, governedAnswer.sourceCertifiedBlock ?? governedAnswer.block?.name, isCertified ? 'certified' : isSemantic ? 'governed' : 'review_required')],
2008
2014
  evaluations: [
2009
2015
  agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to governed answer.', {
@@ -6353,6 +6359,128 @@ export async function startLocalServer(opts) {
6353
6359
  // the approval, so the derived candidate is approved immediately unless the
6354
6360
  // caller opts out. Advisory only: never overrides certified routing. The
6355
6361
  // multi-tenant review workflow + automated distillation stay a cloud feature.
6362
+ // Hint review queue. Corrections are captured as CANDIDATES; a hint only
6363
+ // starts shaping other people's answers once a human approves it here.
6364
+ if (req.method === 'GET' && path === '/api/agent/hints') {
6365
+ try {
6366
+ const status = url.searchParams.get('status');
6367
+ const all = listHintsFromGit(projectRoot);
6368
+ const hints = status ? all.filter((hint) => hint.status === status) : all;
6369
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
6370
+ res.end(serializeJSON({
6371
+ hints: hints.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).slice(0, 200),
6372
+ counts: {
6373
+ candidate: all.filter((hint) => hint.status === 'candidate').length,
6374
+ approved: all.filter((hint) => hint.status === 'approved').length,
6375
+ rejected: all.filter((hint) => hint.status === 'rejected').length,
6376
+ },
6377
+ }));
6378
+ }
6379
+ catch (error) {
6380
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
6381
+ res.end(serializeJSON({ hints: [], counts: { candidate: 0, approved: 0, rejected: 0 }, error: error instanceof Error ? error.message : String(error) }));
6382
+ }
6383
+ return;
6384
+ }
6385
+ const hintReviewMatch = /^\/api\/agent\/hints\/([^/]+)\/review$/.exec(path);
6386
+ if (req.method === 'POST' && hintReviewMatch) {
6387
+ try {
6388
+ const hintId = decodeURIComponent(hintReviewMatch[1]);
6389
+ const body = await readJSON(req).catch(() => null);
6390
+ const decision = body?.decision === 'approved' || body?.decision === 'rejected' ? body.decision : null;
6391
+ if (!decision) {
6392
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
6393
+ res.end(serializeJSON({ error: "decision must be 'approved' or 'rejected'." }));
6394
+ return;
6395
+ }
6396
+ const reviewer = typeof body?.reviewer === 'string' && body.reviewer.trim()
6397
+ ? body.reviewer.trim()
6398
+ : (resolveLocalOwner(projectRoot) ?? 'local');
6399
+ const note = typeof body?.note === 'string' && body.note.trim() ? body.note.trim() : undefined;
6400
+ const snapshotId = projectSnapshot().snapshotId;
6401
+ // dbt-first v3 requires a PASSED evaluation artifact recorded against the
6402
+ // same snapshot before a candidate may be approved. Approving from the
6403
+ // review queue therefore runs the hint's required evaluation first: the
6404
+ // corrected SQL is re-validated against the current project so a hint can
6405
+ // never be promoted on evidence that has gone stale.
6406
+ if (decision === 'approved') {
6407
+ const pending = listHintsFromGit(projectRoot).find((entry) => entry.id === hintId);
6408
+ if (pending?.status === 'candidate' && pending.requiredEvaluation && !pending.evaluationId) {
6409
+ // The evaluation must be a real check, not a rubber stamp: the
6410
+ // corrected SQL has to parse and reference at least one relation
6411
+ // against the CURRENT project. A correction that no longer analyses
6412
+ // cannot be promoted into everyone's prompt.
6413
+ const validation = (() => {
6414
+ const sql = pending.correctedSql?.trim();
6415
+ if (!sql)
6416
+ return { ok: false, error: 'The correction carries no SQL to verify.' };
6417
+ try {
6418
+ const analysis = analyzeSqlReferences(sql);
6419
+ if (!analysis.tables.length)
6420
+ return { ok: false, error: 'Corrected SQL references no known relation.' };
6421
+ return { ok: true };
6422
+ }
6423
+ catch (error) {
6424
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
6425
+ }
6426
+ })();
6427
+ try {
6428
+ evaluateHint(projectRoot, {
6429
+ hintId,
6430
+ snapshotId,
6431
+ evaluator: reviewer,
6432
+ evaluation: pending.requiredEvaluation,
6433
+ checks: [{
6434
+ name: 'corrected-sql-validates',
6435
+ passed: validation.ok,
6436
+ evidence: validation.ok ? 'Corrected SQL passed local context validation.' : validation.error,
6437
+ }],
6438
+ evidence: [`snapshot: ${snapshotId}`, `reviewer: ${reviewer}`],
6439
+ });
6440
+ }
6441
+ catch (evaluationError) {
6442
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
6443
+ res.end(serializeJSON({ ok: false, error: evaluationError instanceof Error ? evaluationError.message : String(evaluationError) }));
6444
+ return;
6445
+ }
6446
+ }
6447
+ }
6448
+ const reviewed = reviewHint(projectRoot, { hintId, decision, reviewer, snapshotId, ...(note ? { note } : {}) });
6449
+ if (!reviewed) {
6450
+ res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
6451
+ res.end(serializeJSON({ ok: false, error: 'Hint not found.' }));
6452
+ return;
6453
+ }
6454
+ const hint = reviewed.hint;
6455
+ // Keep the mirrored advisory memory in step with the decision, so an
6456
+ // approved lesson is recalled and a rejected one stops being recalled.
6457
+ try {
6458
+ const memory = new MemoryStore(defaultMemoryPath(projectRoot));
6459
+ memory.upsert({
6460
+ id: `mem_${hint.id}`,
6461
+ scope: 'project',
6462
+ title: hint.title,
6463
+ content: hint.guidance,
6464
+ tags: [hint.scope.metric, hint.scope.domain, hint.scope.dbtModel].filter((x) => Boolean(x)),
6465
+ source: 'correction',
6466
+ confidence: 0.9,
6467
+ importance: 0.85,
6468
+ enabled: decision === 'approved',
6469
+ });
6470
+ memory.close();
6471
+ }
6472
+ catch {
6473
+ /* best-effort */
6474
+ }
6475
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
6476
+ res.end(serializeJSON({ ok: true, hint }));
6477
+ }
6478
+ catch (error) {
6479
+ res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
6480
+ res.end(serializeJSON({ ok: false, error: error instanceof Error ? error.message : String(error) }));
6481
+ }
6482
+ return;
6483
+ }
6356
6484
  if (req.method === 'POST' && path === '/api/agent/learnings/correction') {
6357
6485
  const body = await readJSON(req).catch(() => null);
6358
6486
  const question = body && typeof body.question === 'string' ? body.question.trim() : '';
@@ -6376,6 +6504,29 @@ export async function startLocalServer(opts) {
6376
6504
  const rationale = typeof body.rationale === 'string' && body.rationale.trim() ? body.rationale.trim() : undefined;
6377
6505
  const author = typeof body.author === 'string' ? body.author : (resolveLocalOwner(projectRoot) ?? undefined);
6378
6506
  try {
6507
+ // dbt-first manifest v3 requires reviewable provenance before a correction
6508
+ // may be recorded at all. The client cannot know most of it, so derive it
6509
+ // here: without this the endpoint rejects every correction on exactly the
6510
+ // governed projects the learning loop is meant to serve.
6511
+ const correctionEvidence = Array.isArray(body.evidence)
6512
+ ? body.evidence.map(String).filter(Boolean)
6513
+ : [
6514
+ `question: ${question}`,
6515
+ ...(wrongSql ? [`previous SQL: ${wrongSql.replace(/\s+/g, ' ').slice(0, 400)}`] : []),
6516
+ `corrected SQL: ${correctedSql.replace(/\s+/g, ' ').slice(0, 400)}`,
6517
+ ...(rationale ? [`rationale: ${rationale}`] : []),
6518
+ ];
6519
+ const correctionSnapshotId = typeof body.snapshotId === 'string' && body.snapshotId.trim()
6520
+ ? body.snapshotId.trim()
6521
+ : projectSnapshot().snapshotId;
6522
+ const correctionFailedRoute = typeof body.failedRoute === 'string' && body.failedRoute.trim()
6523
+ ? body.failedRoute.trim()
6524
+ : 'generated_answer';
6525
+ // Must match the name emitCorrectionEvalCase writes, so the required
6526
+ // evaluation and the emitted regression case are the same artifact.
6527
+ const correctionEvaluation = typeof body.requiredEvaluation === 'string' && body.requiredEvaluation.trim()
6528
+ ? body.requiredEvaluation.trim()
6529
+ : `correction: ${question.slice(0, 60)}`;
6379
6530
  const { trace, hint } = recordCorrectionTrace(projectRoot, {
6380
6531
  question,
6381
6532
  scope,
@@ -6384,19 +6535,29 @@ export async function startLocalServer(opts) {
6384
6535
  correctedSql,
6385
6536
  rationale,
6386
6537
  author,
6538
+ failedRoute: correctionFailedRoute,
6539
+ evidence: correctionEvidence,
6540
+ snapshotId: correctionSnapshotId,
6541
+ requiredEvaluation: correctionEvaluation,
6387
6542
  hintTitle: typeof body.title === 'string' && body.title.trim() ? body.title.trim() : undefined,
6388
6543
  hintGuidance: typeof body.guidance === 'string' && body.guidance.trim() ? body.guidance.trim() : undefined,
6389
6544
  tags: Array.isArray(body.tags) ? body.tags.map(String) : undefined,
6390
6545
  });
6546
+ // Capture always, approve deliberately. A hint is injected into the
6547
+ // generation prompt as a "human-approved correction" and shapes every
6548
+ // future answer in scope — one analyst's local fix must not silently
6549
+ // become a team-wide rule across thousands of models. The trace and the
6550
+ // candidate are always recorded, so nothing is lost; approval is a
6551
+ // one-click review in Settings. Callers may still opt in explicitly.
6391
6552
  let approvedHint = hint;
6392
- if (body.approve !== false) {
6393
- reviewHint(projectRoot, { hintId: hint.id, decision: 'approved', reviewer: author ?? 'local', note: 'Self-approved (OSS single-user).' });
6553
+ if (body.approve === true) {
6554
+ reviewHint(projectRoot, { hintId: hint.id, decision: 'approved', reviewer: author ?? 'local', note: 'Approved on capture.' });
6394
6555
  approvedHint = { ...hint, status: 'approved' };
6395
6556
  }
6396
6557
  // W4.3 — turn the correction into a durable regression eval case so the wrong
6397
6558
  // answer can never silently return. Best-effort; never blocks the correction.
6398
6559
  try {
6399
- emitCorrectionEvalCase(projectRoot, { question, correctedSql });
6560
+ emitCorrectionEvalCase(projectRoot, { question, correctedSql, name: correctionEvaluation });
6400
6561
  }
6401
6562
  catch {
6402
6563
  /* best-effort */
@@ -6413,7 +6574,10 @@ export async function startLocalServer(opts) {
6413
6574
  source: 'correction',
6414
6575
  confidence: 0.9,
6415
6576
  importance: 0.85,
6416
- enabled: true,
6577
+ // A candidate is evidence, not a rule. Mirroring it as an ENABLED
6578
+ // memory would inject it into prompts through the side door and
6579
+ // defeat the approval gate; it is enabled when the hint is approved.
6580
+ enabled: approvedHint.status === 'approved',
6417
6581
  });
6418
6582
  memory.close();
6419
6583
  }
@@ -6935,6 +7099,55 @@ export async function startLocalServer(opts) {
6935
7099
  }
6936
7100
  return;
6937
7101
  }
7102
+ // DELETE /api/notebooks?path=notebooks/x.dqlnb — remove a notebook file.
7103
+ // Deliberately narrow: only .dqlnb/.dql files under notebooks/, resolved and
7104
+ // re-checked against the project root so a crafted path cannot escape it.
7105
+ if (req.method === 'DELETE' && path === '/api/notebooks') {
7106
+ try {
7107
+ const relativePath = url.searchParams.get('path')?.trim();
7108
+ if (!relativePath) {
7109
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
7110
+ res.end(serializeJSON({ ok: false, error: 'Missing notebook path.' }));
7111
+ return;
7112
+ }
7113
+ const normalized = relativePath.replace(/\\/g, '/').replace(/^\.\//, '');
7114
+ if (!normalized.startsWith('notebooks/') || !/\.(dqlnb|dql)$/i.test(normalized) || normalized.includes('..')) {
7115
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
7116
+ res.end(serializeJSON({ ok: false, error: 'Only notebook files under notebooks/ can be deleted.' }));
7117
+ return;
7118
+ }
7119
+ const absolute = resolve(projectRoot, normalized);
7120
+ const rootPrefix = `${resolve(projectRoot)}${sep}`;
7121
+ if (!absolute.startsWith(rootPrefix)) {
7122
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
7123
+ res.end(serializeJSON({ ok: false, error: 'Path escapes the project root.' }));
7124
+ return;
7125
+ }
7126
+ if (!existsSync(absolute)) {
7127
+ res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
7128
+ res.end(serializeJSON({ ok: false, error: 'Notebook not found.' }));
7129
+ return;
7130
+ }
7131
+ rmSync(absolute, { force: true });
7132
+ // Drop the run-state sidecar too, or the next notebook created with the
7133
+ // same slug inherits the deleted one's cached results.
7134
+ const sidecar = absolute.replace(/\.(dqlnb|dql)$/i, '.run.json');
7135
+ if (sidecar !== absolute)
7136
+ rmSync(sidecar, { force: true });
7137
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
7138
+ res.end(serializeJSON({ ok: true, path: normalized }));
7139
+ }
7140
+ catch (error) {
7141
+ if (error instanceof DQLAccessDeniedError) {
7142
+ res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
7143
+ res.end(serializeJSON({ ok: false, error: error.message, code: 'unauthorized' }));
7144
+ return;
7145
+ }
7146
+ res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
7147
+ res.end(serializeJSON({ ok: false, error: error instanceof Error ? error.message : String(error) }));
7148
+ }
7149
+ return;
7150
+ }
6938
7151
  if (req.method === 'PUT' && path === '/api/notebook-content') {
6939
7152
  try {
6940
7153
  const body = await readJSON(req);
@@ -15598,10 +15811,28 @@ export function validateBlockStudioSource(source, semanticLayer) {
15598
15811
  message: 'Semantic block is not runnable yet. Select a metric and complete any required time settings.',
15599
15812
  }
15600
15813
  : {
15601
- severity: 'warning',
15814
+ // A block that DECLARES a query section and leaves it blank is broken,
15815
+ // not merely incomplete: it can never execute, yet it saved cleanly
15816
+ // because this was only a warning. A bare skeleton with no query
15817
+ // section at all stays a warning so draft authoring is unbroken.
15818
+ severity: /query\s*=\s*"""/i.test(source) ? 'error' : 'warning',
15602
15819
  code: 'sql_missing',
15603
- message: 'No executable SQL found in the block source.',
15820
+ message: /query\s*=\s*"""/i.test(source)
15821
+ ? 'The block declares a query section but it is empty. Add the SQL, or remove the query section and define metrics and dimensions instead.'
15822
+ : 'No executable SQL found in the block source.',
15823
+ });
15824
+ }
15825
+ // The mirror image: a semantic block is metrics x dimensions and must not carry
15826
+ // a query. Both together is the corrupt shape the analyzer lets through silently.
15827
+ if (semanticConfig.blockType === 'semantic') {
15828
+ const semanticQueryBody = source.match(/query\s*=\s*"""([\s\S]*?)"""/i)?.[1] ?? '';
15829
+ if (semanticQueryBody.trim()) {
15830
+ diagnostics.push({
15831
+ severity: 'error',
15832
+ code: 'semantic_block_has_query',
15833
+ message: 'Semantic blocks are defined by metrics and dimensions and must not contain a query section. Move the SQL into a custom block.',
15604
15834
  });
15835
+ }
15605
15836
  }
15606
15837
  const parameterInvocation = prepareBlockInvocation({ source, surface: 'block_studio' });
15607
15838
  if (parameterInvocation.errors.length > 0) {