@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.
- package/dist/assets/dql-notebook/assets/{index-CGOoV-5I.js → index-CiyV9kwM.js} +363 -358
- 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 +240 -9
- 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';
|
|
@@ -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
|
|
6393
|
-
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.' });
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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) {
|