@duckcodeailabs/dql-cli 1.11.5 → 1.11.7
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-WcJ2jEaG.js} +373 -368
- 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 +12 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +277 -13
- 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';
|
|
@@ -97,6 +97,27 @@ function agentRunString(value) {
|
|
|
97
97
|
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
98
98
|
}
|
|
99
99
|
/** UI catalog fallback labels are not declared project domains. */
|
|
100
|
+
/**
|
|
101
|
+
* Resolve a UI-pinned domain scope, tolerating one that no longer exists.
|
|
102
|
+
*
|
|
103
|
+
* The Ask scope persists across reloads, so a domain that was renamed, deleted,
|
|
104
|
+
* or simply mistyped would otherwise throw `Unknown domain: x` and BLOCK every
|
|
105
|
+
* question with "no answer produced" until the user found the chip and cleared
|
|
106
|
+
* it. A scope that cannot be resolved should widen the search, never wedge the
|
|
107
|
+
* surface. Anything other than an unresolvable scope still throws, because that
|
|
108
|
+
* is a real contract violation worth reporting.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveUiDomainContext(input) {
|
|
111
|
+
try {
|
|
112
|
+
return resolveDomainContextEnvelope(input);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
116
|
+
if (/^Unknown domain:|^Unknown model area:|^Ambiguous model area:/.test(message))
|
|
117
|
+
return undefined;
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
100
121
|
export function normalizeAgentRunDomain(value) {
|
|
101
122
|
const domain = agentRunString(value);
|
|
102
123
|
if (!domain)
|
|
@@ -1092,7 +1113,7 @@ export async function startLocalServer(opts) {
|
|
|
1092
1113
|
const buildDirectBuildContext = async (input) => {
|
|
1093
1114
|
const snapshot = projectSnapshot();
|
|
1094
1115
|
const activeDomain = normalizeAgentRunDomain(input.domain);
|
|
1095
|
-
const domainContext =
|
|
1116
|
+
const domainContext = resolveUiDomainContext({
|
|
1096
1117
|
manifest: snapshot.manifest,
|
|
1097
1118
|
activeDomain,
|
|
1098
1119
|
purpose: input.purpose,
|
|
@@ -1265,7 +1286,7 @@ export async function startLocalServer(opts) {
|
|
|
1265
1286
|
const requestedModelAreaId = agentRunWorkspaceValue(request, 'modelAreaId');
|
|
1266
1287
|
const runProjectSnapshot = projectSnapshot();
|
|
1267
1288
|
const domainContext = requestedDomain
|
|
1268
|
-
?
|
|
1289
|
+
? resolveUiDomainContext({
|
|
1269
1290
|
manifest: runProjectSnapshot.manifest,
|
|
1270
1291
|
activeDomain: requestedDomain,
|
|
1271
1292
|
purpose: requestedPurpose,
|
|
@@ -2646,7 +2667,7 @@ export async function startLocalServer(opts) {
|
|
|
2646
2667
|
strictness: request.analysisDepth === 'deep' ? 'exploratory' : 'balanced',
|
|
2647
2668
|
limit: request.analysisDepth === 'deep' ? 120 : 80,
|
|
2648
2669
|
domainContext: requestedDomain
|
|
2649
|
-
?
|
|
2670
|
+
? resolveUiDomainContext({
|
|
2650
2671
|
manifest: snapshot.manifest,
|
|
2651
2672
|
activeDomain: requestedDomain,
|
|
2652
2673
|
purpose: agentRunWorkspaceValue(request, 'purpose'),
|
|
@@ -6359,6 +6380,128 @@ export async function startLocalServer(opts) {
|
|
|
6359
6380
|
// the approval, so the derived candidate is approved immediately unless the
|
|
6360
6381
|
// caller opts out. Advisory only: never overrides certified routing. The
|
|
6361
6382
|
// multi-tenant review workflow + automated distillation stay a cloud feature.
|
|
6383
|
+
// Hint review queue. Corrections are captured as CANDIDATES; a hint only
|
|
6384
|
+
// starts shaping other people's answers once a human approves it here.
|
|
6385
|
+
if (req.method === 'GET' && path === '/api/agent/hints') {
|
|
6386
|
+
try {
|
|
6387
|
+
const status = url.searchParams.get('status');
|
|
6388
|
+
const all = listHintsFromGit(projectRoot);
|
|
6389
|
+
const hints = status ? all.filter((hint) => hint.status === status) : all;
|
|
6390
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6391
|
+
res.end(serializeJSON({
|
|
6392
|
+
hints: hints.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).slice(0, 200),
|
|
6393
|
+
counts: {
|
|
6394
|
+
candidate: all.filter((hint) => hint.status === 'candidate').length,
|
|
6395
|
+
approved: all.filter((hint) => hint.status === 'approved').length,
|
|
6396
|
+
rejected: all.filter((hint) => hint.status === 'rejected').length,
|
|
6397
|
+
},
|
|
6398
|
+
}));
|
|
6399
|
+
}
|
|
6400
|
+
catch (error) {
|
|
6401
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6402
|
+
res.end(serializeJSON({ hints: [], counts: { candidate: 0, approved: 0, rejected: 0 }, error: error instanceof Error ? error.message : String(error) }));
|
|
6403
|
+
}
|
|
6404
|
+
return;
|
|
6405
|
+
}
|
|
6406
|
+
const hintReviewMatch = /^\/api\/agent\/hints\/([^/]+)\/review$/.exec(path);
|
|
6407
|
+
if (req.method === 'POST' && hintReviewMatch) {
|
|
6408
|
+
try {
|
|
6409
|
+
const hintId = decodeURIComponent(hintReviewMatch[1]);
|
|
6410
|
+
const body = await readJSON(req).catch(() => null);
|
|
6411
|
+
const decision = body?.decision === 'approved' || body?.decision === 'rejected' ? body.decision : null;
|
|
6412
|
+
if (!decision) {
|
|
6413
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6414
|
+
res.end(serializeJSON({ error: "decision must be 'approved' or 'rejected'." }));
|
|
6415
|
+
return;
|
|
6416
|
+
}
|
|
6417
|
+
const reviewer = typeof body?.reviewer === 'string' && body.reviewer.trim()
|
|
6418
|
+
? body.reviewer.trim()
|
|
6419
|
+
: (resolveLocalOwner(projectRoot) ?? 'local');
|
|
6420
|
+
const note = typeof body?.note === 'string' && body.note.trim() ? body.note.trim() : undefined;
|
|
6421
|
+
const snapshotId = projectSnapshot().snapshotId;
|
|
6422
|
+
// dbt-first v3 requires a PASSED evaluation artifact recorded against the
|
|
6423
|
+
// same snapshot before a candidate may be approved. Approving from the
|
|
6424
|
+
// review queue therefore runs the hint's required evaluation first: the
|
|
6425
|
+
// corrected SQL is re-validated against the current project so a hint can
|
|
6426
|
+
// never be promoted on evidence that has gone stale.
|
|
6427
|
+
if (decision === 'approved') {
|
|
6428
|
+
const pending = listHintsFromGit(projectRoot).find((entry) => entry.id === hintId);
|
|
6429
|
+
if (pending?.status === 'candidate' && pending.requiredEvaluation && !pending.evaluationId) {
|
|
6430
|
+
// The evaluation must be a real check, not a rubber stamp: the
|
|
6431
|
+
// corrected SQL has to parse and reference at least one relation
|
|
6432
|
+
// against the CURRENT project. A correction that no longer analyses
|
|
6433
|
+
// cannot be promoted into everyone's prompt.
|
|
6434
|
+
const validation = (() => {
|
|
6435
|
+
const sql = pending.correctedSql?.trim();
|
|
6436
|
+
if (!sql)
|
|
6437
|
+
return { ok: false, error: 'The correction carries no SQL to verify.' };
|
|
6438
|
+
try {
|
|
6439
|
+
const analysis = analyzeSqlReferences(sql);
|
|
6440
|
+
if (!analysis.tables.length)
|
|
6441
|
+
return { ok: false, error: 'Corrected SQL references no known relation.' };
|
|
6442
|
+
return { ok: true };
|
|
6443
|
+
}
|
|
6444
|
+
catch (error) {
|
|
6445
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
6446
|
+
}
|
|
6447
|
+
})();
|
|
6448
|
+
try {
|
|
6449
|
+
evaluateHint(projectRoot, {
|
|
6450
|
+
hintId,
|
|
6451
|
+
snapshotId,
|
|
6452
|
+
evaluator: reviewer,
|
|
6453
|
+
evaluation: pending.requiredEvaluation,
|
|
6454
|
+
checks: [{
|
|
6455
|
+
name: 'corrected-sql-validates',
|
|
6456
|
+
passed: validation.ok,
|
|
6457
|
+
evidence: validation.ok ? 'Corrected SQL passed local context validation.' : validation.error,
|
|
6458
|
+
}],
|
|
6459
|
+
evidence: [`snapshot: ${snapshotId}`, `reviewer: ${reviewer}`],
|
|
6460
|
+
});
|
|
6461
|
+
}
|
|
6462
|
+
catch (evaluationError) {
|
|
6463
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6464
|
+
res.end(serializeJSON({ ok: false, error: evaluationError instanceof Error ? evaluationError.message : String(evaluationError) }));
|
|
6465
|
+
return;
|
|
6466
|
+
}
|
|
6467
|
+
}
|
|
6468
|
+
}
|
|
6469
|
+
const reviewed = reviewHint(projectRoot, { hintId, decision, reviewer, snapshotId, ...(note ? { note } : {}) });
|
|
6470
|
+
if (!reviewed) {
|
|
6471
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6472
|
+
res.end(serializeJSON({ ok: false, error: 'Hint not found.' }));
|
|
6473
|
+
return;
|
|
6474
|
+
}
|
|
6475
|
+
const hint = reviewed.hint;
|
|
6476
|
+
// Keep the mirrored advisory memory in step with the decision, so an
|
|
6477
|
+
// approved lesson is recalled and a rejected one stops being recalled.
|
|
6478
|
+
try {
|
|
6479
|
+
const memory = new MemoryStore(defaultMemoryPath(projectRoot));
|
|
6480
|
+
memory.upsert({
|
|
6481
|
+
id: `mem_${hint.id}`,
|
|
6482
|
+
scope: 'project',
|
|
6483
|
+
title: hint.title,
|
|
6484
|
+
content: hint.guidance,
|
|
6485
|
+
tags: [hint.scope.metric, hint.scope.domain, hint.scope.dbtModel].filter((x) => Boolean(x)),
|
|
6486
|
+
source: 'correction',
|
|
6487
|
+
confidence: 0.9,
|
|
6488
|
+
importance: 0.85,
|
|
6489
|
+
enabled: decision === 'approved',
|
|
6490
|
+
});
|
|
6491
|
+
memory.close();
|
|
6492
|
+
}
|
|
6493
|
+
catch {
|
|
6494
|
+
/* best-effort */
|
|
6495
|
+
}
|
|
6496
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6497
|
+
res.end(serializeJSON({ ok: true, hint }));
|
|
6498
|
+
}
|
|
6499
|
+
catch (error) {
|
|
6500
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6501
|
+
res.end(serializeJSON({ ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
6502
|
+
}
|
|
6503
|
+
return;
|
|
6504
|
+
}
|
|
6362
6505
|
if (req.method === 'POST' && path === '/api/agent/learnings/correction') {
|
|
6363
6506
|
const body = await readJSON(req).catch(() => null);
|
|
6364
6507
|
const question = body && typeof body.question === 'string' ? body.question.trim() : '';
|
|
@@ -6382,6 +6525,29 @@ export async function startLocalServer(opts) {
|
|
|
6382
6525
|
const rationale = typeof body.rationale === 'string' && body.rationale.trim() ? body.rationale.trim() : undefined;
|
|
6383
6526
|
const author = typeof body.author === 'string' ? body.author : (resolveLocalOwner(projectRoot) ?? undefined);
|
|
6384
6527
|
try {
|
|
6528
|
+
// dbt-first manifest v3 requires reviewable provenance before a correction
|
|
6529
|
+
// may be recorded at all. The client cannot know most of it, so derive it
|
|
6530
|
+
// here: without this the endpoint rejects every correction on exactly the
|
|
6531
|
+
// governed projects the learning loop is meant to serve.
|
|
6532
|
+
const correctionEvidence = Array.isArray(body.evidence)
|
|
6533
|
+
? body.evidence.map(String).filter(Boolean)
|
|
6534
|
+
: [
|
|
6535
|
+
`question: ${question}`,
|
|
6536
|
+
...(wrongSql ? [`previous SQL: ${wrongSql.replace(/\s+/g, ' ').slice(0, 400)}`] : []),
|
|
6537
|
+
`corrected SQL: ${correctedSql.replace(/\s+/g, ' ').slice(0, 400)}`,
|
|
6538
|
+
...(rationale ? [`rationale: ${rationale}`] : []),
|
|
6539
|
+
];
|
|
6540
|
+
const correctionSnapshotId = typeof body.snapshotId === 'string' && body.snapshotId.trim()
|
|
6541
|
+
? body.snapshotId.trim()
|
|
6542
|
+
: projectSnapshot().snapshotId;
|
|
6543
|
+
const correctionFailedRoute = typeof body.failedRoute === 'string' && body.failedRoute.trim()
|
|
6544
|
+
? body.failedRoute.trim()
|
|
6545
|
+
: 'generated_answer';
|
|
6546
|
+
// Must match the name emitCorrectionEvalCase writes, so the required
|
|
6547
|
+
// evaluation and the emitted regression case are the same artifact.
|
|
6548
|
+
const correctionEvaluation = typeof body.requiredEvaluation === 'string' && body.requiredEvaluation.trim()
|
|
6549
|
+
? body.requiredEvaluation.trim()
|
|
6550
|
+
: `correction: ${question.slice(0, 60)}`;
|
|
6385
6551
|
const { trace, hint } = recordCorrectionTrace(projectRoot, {
|
|
6386
6552
|
question,
|
|
6387
6553
|
scope,
|
|
@@ -6390,19 +6556,29 @@ export async function startLocalServer(opts) {
|
|
|
6390
6556
|
correctedSql,
|
|
6391
6557
|
rationale,
|
|
6392
6558
|
author,
|
|
6559
|
+
failedRoute: correctionFailedRoute,
|
|
6560
|
+
evidence: correctionEvidence,
|
|
6561
|
+
snapshotId: correctionSnapshotId,
|
|
6562
|
+
requiredEvaluation: correctionEvaluation,
|
|
6393
6563
|
hintTitle: typeof body.title === 'string' && body.title.trim() ? body.title.trim() : undefined,
|
|
6394
6564
|
hintGuidance: typeof body.guidance === 'string' && body.guidance.trim() ? body.guidance.trim() : undefined,
|
|
6395
6565
|
tags: Array.isArray(body.tags) ? body.tags.map(String) : undefined,
|
|
6396
6566
|
});
|
|
6567
|
+
// Capture always, approve deliberately. A hint is injected into the
|
|
6568
|
+
// generation prompt as a "human-approved correction" and shapes every
|
|
6569
|
+
// future answer in scope — one analyst's local fix must not silently
|
|
6570
|
+
// become a team-wide rule across thousands of models. The trace and the
|
|
6571
|
+
// candidate are always recorded, so nothing is lost; approval is a
|
|
6572
|
+
// one-click review in Settings. Callers may still opt in explicitly.
|
|
6397
6573
|
let approvedHint = hint;
|
|
6398
|
-
if (body.approve
|
|
6399
|
-
reviewHint(projectRoot, { hintId: hint.id, decision: 'approved', reviewer: author ?? 'local', note: '
|
|
6574
|
+
if (body.approve === true) {
|
|
6575
|
+
reviewHint(projectRoot, { hintId: hint.id, decision: 'approved', reviewer: author ?? 'local', note: 'Approved on capture.' });
|
|
6400
6576
|
approvedHint = { ...hint, status: 'approved' };
|
|
6401
6577
|
}
|
|
6402
6578
|
// W4.3 — turn the correction into a durable regression eval case so the wrong
|
|
6403
6579
|
// answer can never silently return. Best-effort; never blocks the correction.
|
|
6404
6580
|
try {
|
|
6405
|
-
emitCorrectionEvalCase(projectRoot, { question, correctedSql });
|
|
6581
|
+
emitCorrectionEvalCase(projectRoot, { question, correctedSql, name: correctionEvaluation });
|
|
6406
6582
|
}
|
|
6407
6583
|
catch {
|
|
6408
6584
|
/* best-effort */
|
|
@@ -6419,7 +6595,10 @@ export async function startLocalServer(opts) {
|
|
|
6419
6595
|
source: 'correction',
|
|
6420
6596
|
confidence: 0.9,
|
|
6421
6597
|
importance: 0.85,
|
|
6422
|
-
|
|
6598
|
+
// A candidate is evidence, not a rule. Mirroring it as an ENABLED
|
|
6599
|
+
// memory would inject it into prompts through the side door and
|
|
6600
|
+
// defeat the approval gate; it is enabled when the hint is approved.
|
|
6601
|
+
enabled: approvedHint.status === 'approved',
|
|
6423
6602
|
});
|
|
6424
6603
|
memory.close();
|
|
6425
6604
|
}
|
|
@@ -6941,6 +7120,55 @@ export async function startLocalServer(opts) {
|
|
|
6941
7120
|
}
|
|
6942
7121
|
return;
|
|
6943
7122
|
}
|
|
7123
|
+
// DELETE /api/notebooks?path=notebooks/x.dqlnb — remove a notebook file.
|
|
7124
|
+
// Deliberately narrow: only .dqlnb/.dql files under notebooks/, resolved and
|
|
7125
|
+
// re-checked against the project root so a crafted path cannot escape it.
|
|
7126
|
+
if (req.method === 'DELETE' && path === '/api/notebooks') {
|
|
7127
|
+
try {
|
|
7128
|
+
const relativePath = url.searchParams.get('path')?.trim();
|
|
7129
|
+
if (!relativePath) {
|
|
7130
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7131
|
+
res.end(serializeJSON({ ok: false, error: 'Missing notebook path.' }));
|
|
7132
|
+
return;
|
|
7133
|
+
}
|
|
7134
|
+
const normalized = relativePath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
7135
|
+
if (!normalized.startsWith('notebooks/') || !/\.(dqlnb|dql)$/i.test(normalized) || normalized.includes('..')) {
|
|
7136
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7137
|
+
res.end(serializeJSON({ ok: false, error: 'Only notebook files under notebooks/ can be deleted.' }));
|
|
7138
|
+
return;
|
|
7139
|
+
}
|
|
7140
|
+
const absolute = resolve(projectRoot, normalized);
|
|
7141
|
+
const rootPrefix = `${resolve(projectRoot)}${sep}`;
|
|
7142
|
+
if (!absolute.startsWith(rootPrefix)) {
|
|
7143
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7144
|
+
res.end(serializeJSON({ ok: false, error: 'Path escapes the project root.' }));
|
|
7145
|
+
return;
|
|
7146
|
+
}
|
|
7147
|
+
if (!existsSync(absolute)) {
|
|
7148
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7149
|
+
res.end(serializeJSON({ ok: false, error: 'Notebook not found.' }));
|
|
7150
|
+
return;
|
|
7151
|
+
}
|
|
7152
|
+
rmSync(absolute, { force: true });
|
|
7153
|
+
// Drop the run-state sidecar too, or the next notebook created with the
|
|
7154
|
+
// same slug inherits the deleted one's cached results.
|
|
7155
|
+
const sidecar = absolute.replace(/\.(dqlnb|dql)$/i, '.run.json');
|
|
7156
|
+
if (sidecar !== absolute)
|
|
7157
|
+
rmSync(sidecar, { force: true });
|
|
7158
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7159
|
+
res.end(serializeJSON({ ok: true, path: normalized }));
|
|
7160
|
+
}
|
|
7161
|
+
catch (error) {
|
|
7162
|
+
if (error instanceof DQLAccessDeniedError) {
|
|
7163
|
+
res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7164
|
+
res.end(serializeJSON({ ok: false, error: error.message, code: 'unauthorized' }));
|
|
7165
|
+
return;
|
|
7166
|
+
}
|
|
7167
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7168
|
+
res.end(serializeJSON({ ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
7169
|
+
}
|
|
7170
|
+
return;
|
|
7171
|
+
}
|
|
6944
7172
|
if (req.method === 'PUT' && path === '/api/notebook-content') {
|
|
6945
7173
|
try {
|
|
6946
7174
|
const body = await readJSON(req);
|
|
@@ -11361,7 +11589,22 @@ export async function startLocalServer(opts) {
|
|
|
11361
11589
|
if (!source.trim())
|
|
11362
11590
|
throw new Error('Provide an executable DQL artifact or block name.');
|
|
11363
11591
|
const expectedReceipt = requestedArtifact?.executionReceipt;
|
|
11364
|
-
|
|
11592
|
+
// When a block name is given we deliberately re-read the CANONICAL block
|
|
11593
|
+
// from disk, so `source` is no longer the text the receipt was taken
|
|
11594
|
+
// from. Comparing the two made every "change an input and apply" fail
|
|
11595
|
+
// with "source changed after the answer was produced" whenever the saved
|
|
11596
|
+
// block differed from the answer's copy at all — even by formatting —
|
|
11597
|
+
// and changing inputs is precisely what that control is for.
|
|
11598
|
+
//
|
|
11599
|
+
// Check the receipt against the artifact the CLIENT holds (tamper
|
|
11600
|
+
// detection, its real purpose). A canonical block that has genuinely
|
|
11601
|
+
// moved on is reported alongside the fresh result instead of refusing
|
|
11602
|
+
// to run: the user still gets their answer and is told it was rebuilt.
|
|
11603
|
+
const receiptSource = requestedArtifact?.source ?? '';
|
|
11604
|
+
const sourceRefreshedFromDisk = Boolean(requestedBlockName)
|
|
11605
|
+
&& receiptSource.trim().length > 0
|
|
11606
|
+
&& receiptSource !== source;
|
|
11607
|
+
if (expectedReceipt && !sourceRefreshedFromDisk && expectedReceipt.sourceFingerprint !== executionFingerprint(source)) {
|
|
11365
11608
|
throw new Error('DQL artifact source changed after the answer was produced. Refresh the answer before applying it.');
|
|
11366
11609
|
}
|
|
11367
11610
|
const parameters = body.parameters && typeof body.parameters === 'object' && !Array.isArray(body.parameters)
|
|
@@ -11378,7 +11621,10 @@ export async function startLocalServer(opts) {
|
|
|
11378
11621
|
rowLimit: requestedArtifact?.limit,
|
|
11379
11622
|
});
|
|
11380
11623
|
const actualReceipt = result.executionReceipt;
|
|
11381
|
-
if
|
|
11624
|
+
// Same reasoning: if the canonical block was reloaded, its compiled SQL
|
|
11625
|
+
// is EXPECTED to differ, so this determinism check would fire on a
|
|
11626
|
+
// change we already know about.
|
|
11627
|
+
if (expectedReceipt && actualReceipt && !sourceRefreshedFromDisk) {
|
|
11382
11628
|
if (expectedReceipt.parameterFingerprint === actualReceipt.parameterFingerprint
|
|
11383
11629
|
&& expectedReceipt.compiledSqlFingerprint !== actualReceipt.compiledSqlFingerprint) {
|
|
11384
11630
|
throw new Error('DQL artifact execution changed while its source and inputs were unchanged. Refresh the answer before applying it.');
|
|
@@ -15604,11 +15850,29 @@ export function validateBlockStudioSource(source, semanticLayer) {
|
|
|
15604
15850
|
message: 'Semantic block is not runnable yet. Select a metric and complete any required time settings.',
|
|
15605
15851
|
}
|
|
15606
15852
|
: {
|
|
15607
|
-
|
|
15853
|
+
// A block that DECLARES a query section and leaves it blank is broken,
|
|
15854
|
+
// not merely incomplete: it can never execute, yet it saved cleanly
|
|
15855
|
+
// because this was only a warning. A bare skeleton with no query
|
|
15856
|
+
// section at all stays a warning so draft authoring is unbroken.
|
|
15857
|
+
severity: /query\s*=\s*"""/i.test(source) ? 'error' : 'warning',
|
|
15608
15858
|
code: 'sql_missing',
|
|
15609
|
-
message:
|
|
15859
|
+
message: /query\s*=\s*"""/i.test(source)
|
|
15860
|
+
? 'The block declares a query section but it is empty. Add the SQL, or remove the query section and define metrics and dimensions instead.'
|
|
15861
|
+
: 'No executable SQL found in the block source.',
|
|
15610
15862
|
});
|
|
15611
15863
|
}
|
|
15864
|
+
// The mirror image: a semantic block is metrics x dimensions and must not carry
|
|
15865
|
+
// a query. Both together is the corrupt shape the analyzer lets through silently.
|
|
15866
|
+
if (semanticConfig.blockType === 'semantic') {
|
|
15867
|
+
const semanticQueryBody = source.match(/query\s*=\s*"""([\s\S]*?)"""/i)?.[1] ?? '';
|
|
15868
|
+
if (semanticQueryBody.trim()) {
|
|
15869
|
+
diagnostics.push({
|
|
15870
|
+
severity: 'error',
|
|
15871
|
+
code: 'semantic_block_has_query',
|
|
15872
|
+
message: 'Semantic blocks are defined by metrics and dimensions and must not contain a query section. Move the SQL into a custom block.',
|
|
15873
|
+
});
|
|
15874
|
+
}
|
|
15875
|
+
}
|
|
15612
15876
|
const parameterInvocation = prepareBlockInvocation({ source, surface: 'block_studio' });
|
|
15613
15877
|
if (parameterInvocation.errors.length > 0) {
|
|
15614
15878
|
diagnostics.push(...parameterInvocation.errors.map((message) => ({ severity: 'error', code: 'parameter_parse', message })));
|