@duckcodeailabs/dql-cli 1.11.5 → 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.
- package/dist/assets/dql-notebook/assets/{index-CNP_7Iui.js → index-CiyV9kwM.js} +362 -357
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/block-studio-import.d.ts.map +1 -1
- package/dist/block-studio-import.js +7 -1
- package/dist/block-studio-import.js.map +1 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +233 -8
- 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
|
@@ -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';
|
|
@@ -6359,6 +6359,128 @@ export async function startLocalServer(opts) {
|
|
|
6359
6359
|
// the approval, so the derived candidate is approved immediately unless the
|
|
6360
6360
|
// caller opts out. Advisory only: never overrides certified routing. The
|
|
6361
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
|
+
}
|
|
6362
6484
|
if (req.method === 'POST' && path === '/api/agent/learnings/correction') {
|
|
6363
6485
|
const body = await readJSON(req).catch(() => null);
|
|
6364
6486
|
const question = body && typeof body.question === 'string' ? body.question.trim() : '';
|
|
@@ -6382,6 +6504,29 @@ export async function startLocalServer(opts) {
|
|
|
6382
6504
|
const rationale = typeof body.rationale === 'string' && body.rationale.trim() ? body.rationale.trim() : undefined;
|
|
6383
6505
|
const author = typeof body.author === 'string' ? body.author : (resolveLocalOwner(projectRoot) ?? undefined);
|
|
6384
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)}`;
|
|
6385
6530
|
const { trace, hint } = recordCorrectionTrace(projectRoot, {
|
|
6386
6531
|
question,
|
|
6387
6532
|
scope,
|
|
@@ -6390,19 +6535,29 @@ export async function startLocalServer(opts) {
|
|
|
6390
6535
|
correctedSql,
|
|
6391
6536
|
rationale,
|
|
6392
6537
|
author,
|
|
6538
|
+
failedRoute: correctionFailedRoute,
|
|
6539
|
+
evidence: correctionEvidence,
|
|
6540
|
+
snapshotId: correctionSnapshotId,
|
|
6541
|
+
requiredEvaluation: correctionEvaluation,
|
|
6393
6542
|
hintTitle: typeof body.title === 'string' && body.title.trim() ? body.title.trim() : undefined,
|
|
6394
6543
|
hintGuidance: typeof body.guidance === 'string' && body.guidance.trim() ? body.guidance.trim() : undefined,
|
|
6395
6544
|
tags: Array.isArray(body.tags) ? body.tags.map(String) : undefined,
|
|
6396
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.
|
|
6397
6552
|
let approvedHint = hint;
|
|
6398
|
-
if (body.approve
|
|
6399
|
-
reviewHint(projectRoot, { hintId: hint.id, decision: 'approved', reviewer: author ?? 'local', note: '
|
|
6553
|
+
if (body.approve === true) {
|
|
6554
|
+
reviewHint(projectRoot, { hintId: hint.id, decision: 'approved', reviewer: author ?? 'local', note: 'Approved on capture.' });
|
|
6400
6555
|
approvedHint = { ...hint, status: 'approved' };
|
|
6401
6556
|
}
|
|
6402
6557
|
// W4.3 — turn the correction into a durable regression eval case so the wrong
|
|
6403
6558
|
// answer can never silently return. Best-effort; never blocks the correction.
|
|
6404
6559
|
try {
|
|
6405
|
-
emitCorrectionEvalCase(projectRoot, { question, correctedSql });
|
|
6560
|
+
emitCorrectionEvalCase(projectRoot, { question, correctedSql, name: correctionEvaluation });
|
|
6406
6561
|
}
|
|
6407
6562
|
catch {
|
|
6408
6563
|
/* best-effort */
|
|
@@ -6419,7 +6574,10 @@ export async function startLocalServer(opts) {
|
|
|
6419
6574
|
source: 'correction',
|
|
6420
6575
|
confidence: 0.9,
|
|
6421
6576
|
importance: 0.85,
|
|
6422
|
-
|
|
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',
|
|
6423
6581
|
});
|
|
6424
6582
|
memory.close();
|
|
6425
6583
|
}
|
|
@@ -6941,6 +7099,55 @@ export async function startLocalServer(opts) {
|
|
|
6941
7099
|
}
|
|
6942
7100
|
return;
|
|
6943
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
|
+
}
|
|
6944
7151
|
if (req.method === 'PUT' && path === '/api/notebook-content') {
|
|
6945
7152
|
try {
|
|
6946
7153
|
const body = await readJSON(req);
|
|
@@ -15604,10 +15811,28 @@ export function validateBlockStudioSource(source, semanticLayer) {
|
|
|
15604
15811
|
message: 'Semantic block is not runnable yet. Select a metric and complete any required time settings.',
|
|
15605
15812
|
}
|
|
15606
15813
|
: {
|
|
15607
|
-
|
|
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',
|
|
15608
15819
|
code: 'sql_missing',
|
|
15609
|
-
message:
|
|
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.',
|
|
15610
15834
|
});
|
|
15835
|
+
}
|
|
15611
15836
|
}
|
|
15612
15837
|
const parameterInvocation = prepareBlockInvocation({ source, surface: 'block_studio' });
|
|
15613
15838
|
if (parameterInvocation.errors.length > 0) {
|