@duckcodeailabs/dql-cli 1.12.11 → 1.13.0
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/apps-api.d.ts +151 -3
- package/dist/apps-api.d.ts.map +1 -1
- package/dist/apps-api.js +675 -97
- package/dist/apps-api.js.map +1 -1
- package/dist/assets/dql-notebook/assets/AgentAnswerCard-Cp0eh6zU.js +5 -0
- package/dist/assets/dql-notebook/assets/{AgentLogPage-hzHqpD3-.js → AgentLogPage-CBGEoGmU.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildDialog-BQRWoxp9.js → AiBuildDialog-CwAD3tXE.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildResult-D9t_4sQl.js → AiBuildResult-CyqTuQe5.js} +1 -1
- package/dist/assets/dql-notebook/assets/AiSidePanel-DjB8PWHG.js +6 -0
- package/dist/assets/dql-notebook/assets/AnalyticsHome-B7BkvA-R.js +6 -0
- package/dist/assets/dql-notebook/assets/AppsView-CUrqtEGC.js +4583 -0
- package/dist/assets/dql-notebook/assets/BlockStudio-Bl0aGQJR.js +60 -0
- package/dist/assets/dql-notebook/assets/{BusinessArtifactView-CnTbFVGT.js → BusinessArtifactView-C4SZVNW-.js} +1 -1
- package/dist/assets/dql-notebook/assets/DbtFirstModelingPage-B0uxitnx.js +22 -0
- package/dist/assets/dql-notebook/assets/GitPage-BfVD4tVq.js +7 -0
- package/dist/assets/dql-notebook/assets/GlobalAiRail-DUwdy3fB.js +1 -0
- package/dist/assets/dql-notebook/assets/GovernedContextPage-D9f26ZFb.js +7 -0
- package/dist/assets/dql-notebook/assets/{HelpDocsPage-CL0vbSER.js → HelpDocsPage-BDPNVot1.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HomePage-DzAucwBW.js → HomePage-DiOnRhEG.js} +1 -1
- package/dist/assets/dql-notebook/assets/LineageDAG-BiUFX2KL.js +1 -0
- package/dist/assets/dql-notebook/assets/{LineageDetailView-DdVH-nz6.js → LineageDetailView-CfbSvlPJ.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDrawer-BvrVZNKY.js → LineageDrawer-CgTScaGB.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-CSut-zvm.js → LineagePathBreadcrumb-D7ZA7k68.js} +3 -3
- package/dist/assets/dql-notebook/assets/{MiniLineageGraph-zg_1wW3M.js → MiniLineageGraph-GkD4y17x.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewBlockModal-9cDletl-.js → NewBlockModal-B9TfRjt7.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewNotebookModal-V6de8L4d.js → NewNotebookModal-BbO9NKUl.js} +2 -2
- package/dist/assets/dql-notebook/assets/NotebookEditor-CuMpvXR9.js +200 -0
- package/dist/assets/dql-notebook/assets/ReadinessPage-BROJ9YBM.js +1 -0
- package/dist/assets/dql-notebook/assets/{SetupOnboarding-Bg-MjbC9.js → SetupOnboarding-BCcZlpuQ.js} +2 -2
- package/dist/assets/dql-notebook/assets/{TrustBadge-DGuHB8kP.js → TrustBadge-vQQh8d0D.js} +1 -1
- package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-Dj4AXAhT.js +83 -0
- package/dist/assets/dql-notebook/assets/{answer-to-notebook-BDzY4MuQ.js → answer-to-notebook-DuqCcDDl.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-left-IjlnAihE.js → arrow-left-CMe1kkOL.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-right-BI4cXgOY.js → arrow-right-DvB_DONr.js} +1 -1
- package/dist/assets/dql-notebook/assets/{book-open-text-i6xhxu_I.js → book-open-text-CJ3NAvrs.js} +1 -1
- package/dist/assets/dql-notebook/assets/{bot-C6DE7Q6m.js → bot-CV8DYRrt.js} +1 -1
- package/dist/assets/dql-notebook/assets/{chevron-up-DSEILnDs.js → chevron-up-HxU1l85K.js} +1 -1
- package/dist/assets/dql-notebook/assets/{circle-x-BoAorVSm.js → circle-x-BCgtha1W.js} +1 -1
- package/dist/assets/dql-notebook/assets/{dagre.esm-CycymVj6.js → dagre.esm-GKp7qoUL.js} +1 -1
- package/dist/assets/dql-notebook/assets/{external-link-ZwE5iHsX.js → external-link-C-PXokWV.js} +1 -1
- package/dist/assets/dql-notebook/assets/{grip-vertical-Dnp2st8l.js → grip-vertical-B4EHbGXW.js} +1 -1
- package/dist/assets/dql-notebook/assets/index-1mIC_XX4.js +634 -0
- package/dist/assets/dql-notebook/assets/{link-2-CTKe6Snr.js → link-2-BLx-goet.js} +1 -1
- package/dist/assets/dql-notebook/assets/{list-tree-Ced8e_S1.js → list-tree-BRFpEmvV.js} +1 -1
- package/dist/assets/dql-notebook/assets/{maximize-2-Cojc6867.js → maximize-2-CCd5uRIZ.js} +1 -1
- package/dist/assets/dql-notebook/assets/{minimize-2-CBIAE0XH.js → minimize-2-CDwSCGlO.js} +1 -1
- package/dist/assets/dql-notebook/assets/{panel-right-open-po3bWSeL.js → panel-right-open-CunA1xO7.js} +1 -1
- package/dist/assets/dql-notebook/assets/{pencil-Cb45c6WU.js → pencil-D_I14Wyk.js} +1 -1
- package/dist/assets/dql-notebook/assets/{rotate-ccw-B9v1LTJ0.js → rotate-ccw-DOrfTZk8.js} +1 -1
- package/dist/assets/dql-notebook/assets/{semantic-fields-B6ii0an0.js → semantic-fields-AfQvjt5R.js} +1 -1
- package/dist/assets/dql-notebook/assets/{star-CO4tfaDq.js → star-DDhv4e8M.js} +1 -1
- package/dist/assets/dql-notebook/assets/{triangle-alert-BSdIu2El.js → triangle-alert-2ZXx8wAu.js} +1 -1
- package/dist/assets/dql-notebook/assets/usePersistedAgentThreadId-3rzkZrqc.js +18 -0
- package/dist/assets/dql-notebook/assets/{user-round-BfrV8U-W.js → user-round-BA074kiQ.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wand-sparkles-BgTxZNBc.js → wand-sparkles-CEYHLt9b.js} +1 -1
- package/dist/assets/dql-notebook/assets/{workflow--2ycp0yy.js → workflow-CZtyrcR-.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wrench-LMCrpa9_.js → wrench-Ong23-wu.js} +1 -1
- package/dist/assets/dql-notebook/assets/{x-CeNgQwGm.js → x-CSo1M3tu.js} +1 -1
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/llm/providers/dql-agent-provider.d.ts +4 -3
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +13 -10
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/local-operation-coordinator.d.ts +59 -0
- package/dist/local-operation-coordinator.d.ts.map +1 -0
- package/dist/local-operation-coordinator.js +256 -0
- package/dist/local-operation-coordinator.js.map +1 -0
- package/dist/local-runtime.d.ts +49 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +1590 -323
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +12 -11
- package/dist/project-refresh-coordinator.d.ts +19 -0
- package/dist/project-refresh-coordinator.d.ts.map +1 -0
- package/dist/project-refresh-coordinator.js +134 -0
- package/dist/project-refresh-coordinator.js.map +1 -0
- package/dist/project-refresh-worker.d.ts +13 -0
- package/dist/project-refresh-worker.d.ts.map +1 -0
- package/dist/project-refresh-worker.js +58 -0
- package/dist/project-refresh-worker.js.map +1 -0
- package/package.json +13 -11
- package/dist/assets/dql-notebook/assets/AgentAnswerCard-CpL2kDCV.js +0 -5
- package/dist/assets/dql-notebook/assets/AiSidePanel-BCQB9rVF.js +0 -6
- package/dist/assets/dql-notebook/assets/AnalyticsHome-Ba4Ve_Gd.js +0 -6
- package/dist/assets/dql-notebook/assets/AppsView-CdAT9BOq.js +0 -4466
- package/dist/assets/dql-notebook/assets/BlockStudio-Brg4fwn5.js +0 -60
- package/dist/assets/dql-notebook/assets/DbtFirstModelingPage-CQe2FUZq.js +0 -22
- package/dist/assets/dql-notebook/assets/GitPage-DHyDBChc.js +0 -7
- package/dist/assets/dql-notebook/assets/GlobalAiRail-Cys4n_rq.js +0 -1
- package/dist/assets/dql-notebook/assets/GovernedContextPage-DyKoDSZ2.js +0 -7
- package/dist/assets/dql-notebook/assets/LineageDAG-QHqd_AM5.js +0 -1
- package/dist/assets/dql-notebook/assets/NotebookEditor-EZ0pfWVs.js +0 -200
- package/dist/assets/dql-notebook/assets/ReadinessPage-BaGvjyfd.js +0 -1
- package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-BlWAVEwO.js +0 -83
- package/dist/assets/dql-notebook/assets/index-BTuliKhK.js +0 -634
- package/dist/assets/dql-notebook/assets/usePersistedAgentThreadId-CkVFrrjk.js +0 -18
package/dist/local-runtime.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { execFileSync, execSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { gzip } from "node:zlib";
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, watch, writeFileSync, } from "node:fs";
|
|
6
|
+
import { mkdir as mkdirAsync, rename as renameAsync, rm as rmAsync, writeFile as writeFileAsync, } from 'node:fs/promises';
|
|
6
7
|
import { createRequire } from "node:module";
|
|
7
8
|
import { homedir } from "node:os";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
10
|
import { dirname, extname, join, normalize, relative, resolve, sep, } from "node:path";
|
|
10
11
|
import { Readable } from "node:stream";
|
|
12
|
+
import { LocalOperationCoordinator, } from './local-operation-coordinator.js';
|
|
13
|
+
import { ProjectRefreshCoordinator } from './project-refresh-coordinator.js';
|
|
11
14
|
import { buildMixedSourceWarehouseFallbackSql, findMentionedNotebookDataset, planMixedSourceNotebookSql, planMixedSourceSql, } from "./mixed-source-sql.js";
|
|
12
15
|
import { resolveNpmInvocation } from './npm-runtime.js';
|
|
13
16
|
import { ensureDqlGitignore, isGovernedSourceFile, isLegacyBroadDqlIgnore, } from './git-contract.js';
|
|
@@ -24,7 +27,7 @@ import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.j
|
|
|
24
27
|
import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, prepareConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, normalizeWarehouseSqlFailure, parseProposal, propose, proposePlan, recordGovernedCorrection, HintStore, defaultHintIndexPath, ensureHintIndexFresh, listHintsFromGit, getHintEvaluationFromGit, getCorrectionTraceFromGit, inspectGovernedHint, editGovernedHintCandidate, reopenGovernedHint, retireHint, supersedeHint, hintsConflict, mineJoinPatterns, reviewGovernedHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, internalRelationIdsInSql, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, projectEmbeddingProvider, isHashedEmbeddingProvider, clearProjectEmbeddingCache, upgradeVectorIndexForProject, openMetadataCatalog, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, isTrustedConversationTurn, resolveInternalRelationIds, analyticalError, tagAnalyticalError, withAnalyticalErrorOrigin, withAnalyticalErrorOriginSync, } from '@duckcodeailabs/dql-agent';
|
|
25
28
|
import { addSqlResultFilter, filterableResultColumns, replaceBlockStudioSql } from './sql-result-filter.js';
|
|
26
29
|
import { gatherProposeEnrichment } from './propose-enrich.js';
|
|
27
|
-
import { handleAppsApi, proposeAppAiBuild, recommendVisualization } from './apps-api.js';
|
|
30
|
+
import { handleAppsApi, proposeAppAiBuild, recommendVisualization, } from './apps-api.js';
|
|
28
31
|
import { getActiveProvider, getEffectiveProviderConfig, isProviderSettingsId, listProviderSettings, saveProviderSettings, } from './settings/provider-settings.js';
|
|
29
32
|
import { ClaudeCodeCliProvider, CodexCliProvider } from './providers/subscription-cli.js';
|
|
30
33
|
import { ClaudeOAuthManager, ClaudeOAuthProvider, claudeOAuthConnected, CLAUDE_OAUTH_MODELS, CLAUDE_OAUTH_DEFAULT_MODEL, } from './providers/oauth/claude-oauth.js';
|
|
@@ -85,6 +88,63 @@ function hasDbtSemanticArtifacts(projectRoot, dbtProjectDir, configuredManifestP
|
|
|
85
88
|
return false;
|
|
86
89
|
}
|
|
87
90
|
const AGENT_RUN_REQUESTED_MODES = new Set(['auto', 'ask', 'research', 'sql', 'block', 'app']);
|
|
91
|
+
/** AGT-007 / AGT-022: App-specific context adapter into the uniform AgentRun. */
|
|
92
|
+
export function buildAppCopilotResearchAgentRequest(input) {
|
|
93
|
+
const memoOnly = input.mode === 'memo_only';
|
|
94
|
+
const appResearch = {
|
|
95
|
+
mode: 'app_research',
|
|
96
|
+
generationMode: input.mode ?? 'sql_and_memo',
|
|
97
|
+
intent: input.intent,
|
|
98
|
+
appId: input.appId,
|
|
99
|
+
dashboardId: input.dashboardId,
|
|
100
|
+
sourceTileId: input.sourceTileId,
|
|
101
|
+
sourceBlockId: input.sourceBlockId,
|
|
102
|
+
title: input.title,
|
|
103
|
+
instruction: [
|
|
104
|
+
'Answer as an app-scoped analyst. Start from certified block/result context, active filters, dbt/semantic metadata, and schema evidence.',
|
|
105
|
+
memoOnly
|
|
106
|
+
? 'Write the stakeholder memo from the supplied selected result, preview rows, generated SQL, metrics, drivers, filters, and caveats. Do not generate replacement SQL unless the user explicitly asks to fix SQL.'
|
|
107
|
+
: 'Generate review-required read-only SQL only when the certified result does not exactly answer the requested analysis grain. Execute only through the bounded generated SQL preview path.',
|
|
108
|
+
'In the natural-language answer, write a stakeholder analysis memo, not a generic chat answer. Use Markdown headings chosen for the question, usually Executive answer, Key numbers, Drivers or Business readout, Caveats, and Next action.',
|
|
109
|
+
'Use concrete numbers from the selected result or preview when available. If baseline, segment, grain, lineage, or source proof is missing, say that explicitly instead of inventing a driver story.',
|
|
110
|
+
'Keep raw SQL in proposedSql/sql. Do not put SQL code fences or implementation trace in the memo body.',
|
|
111
|
+
].join(' '),
|
|
112
|
+
generatedSql: input.generatedSql,
|
|
113
|
+
metrics: input.metrics,
|
|
114
|
+
drivers: input.drivers,
|
|
115
|
+
resultPreviews: input.resultPreviews,
|
|
116
|
+
summaryHint: input.summaryHint,
|
|
117
|
+
recommendationHint: input.recommendationHint,
|
|
118
|
+
sqlError: input.sqlError,
|
|
119
|
+
sqlErrorKind: input.sqlErrorKind,
|
|
120
|
+
hasReportEvidence: input.hasReportEvidence,
|
|
121
|
+
context: input.context,
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
question: memoOnly
|
|
125
|
+
? [
|
|
126
|
+
`Research question: ${input.question}`,
|
|
127
|
+
'Write the business memo now using only the evidence envelope supplied as upstream context.',
|
|
128
|
+
'Return Markdown sections. Keep SQL, query text, implementation trace, and raw routing details out of the memo body.',
|
|
129
|
+
'If the evidence is insufficient, state the gap and the next reviewer action instead of filling the story with generic text.',
|
|
130
|
+
].join('\n')
|
|
131
|
+
: input.question,
|
|
132
|
+
requestedMode: 'research',
|
|
133
|
+
audience: 'stakeholder',
|
|
134
|
+
selectedObject: {
|
|
135
|
+
kind: 'app',
|
|
136
|
+
id: input.appId,
|
|
137
|
+
title: input.title,
|
|
138
|
+
},
|
|
139
|
+
analysisDepth: 'deep',
|
|
140
|
+
workspaceContext: {
|
|
141
|
+
surface: 'app_copilot',
|
|
142
|
+
appId: input.appId,
|
|
143
|
+
dashboardId: input.dashboardId,
|
|
144
|
+
appResearch,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
88
148
|
const AGENT_RUN_SELECTED_OBJECT_KINDS = new Set([
|
|
89
149
|
'notebook',
|
|
90
150
|
'cell',
|
|
@@ -228,6 +288,7 @@ export function parseAgentRunRequestBody(body) {
|
|
|
228
288
|
request: {
|
|
229
289
|
question,
|
|
230
290
|
selectedEvidenceId: agentRunString(record.selectedEvidenceId),
|
|
291
|
+
clarificationSourceQuestion: agentRunString(record.clarificationSourceQuestion),
|
|
231
292
|
requestedMode,
|
|
232
293
|
audience,
|
|
233
294
|
intent: agentRunString(record.intent),
|
|
@@ -324,7 +385,16 @@ export function agentRunDeadlineMs(request, env = process.env, activeProviderId)
|
|
|
324
385
|
if (request.requestedMode === 'research' || effectiveDepth === 'deep') {
|
|
325
386
|
return scale(researchDeadline);
|
|
326
387
|
}
|
|
327
|
-
|
|
388
|
+
// A structured clarification chip displays a compact metric/path label, but
|
|
389
|
+
// the engine restores and executes the original analytical question. Budget
|
|
390
|
+
// from that same original question as well. Otherwise a complex ranking or
|
|
391
|
+
// cross-model request can incorrectly inherit the 45s scalar-lookup budget
|
|
392
|
+
// merely because the selected label is short, then time out after the user
|
|
393
|
+
// already supplied the exact governed meaning.
|
|
394
|
+
const deadlineQuestion = request.selectedEvidenceId
|
|
395
|
+
? request.clarificationSourceQuestion?.trim() || request.question
|
|
396
|
+
: request.question;
|
|
397
|
+
const plan = buildAnalysisQuestionPlan(deadlineQuestion);
|
|
328
398
|
if (plan.needsResearchWorkspace || isAnalyticalBuildQuestion(plan)) {
|
|
329
399
|
return scale(researchDeadline);
|
|
330
400
|
}
|
|
@@ -418,6 +488,41 @@ export function repairableSqlFromAgentRun(run) {
|
|
|
418
488
|
}
|
|
419
489
|
return undefined;
|
|
420
490
|
}
|
|
491
|
+
/** Editable DQL retained on an immutable failed run, including its bound values. */
|
|
492
|
+
export function repairableDqlArtifactFromAgentRun(run) {
|
|
493
|
+
for (const artifact of run.artifacts) {
|
|
494
|
+
const payload = agentRunRecord(artifact.payload);
|
|
495
|
+
if (!payload)
|
|
496
|
+
continue;
|
|
497
|
+
const dqlArtifact = normalizeDqlArtifactReference(payload.dqlArtifact);
|
|
498
|
+
// Only an ownerless/custom SQL block is editable in place. Certified and
|
|
499
|
+
// semantic artifacts remain immutable; their retained compiled SQL may
|
|
500
|
+
// still produce a separate review-required repair result below.
|
|
501
|
+
if (dqlArtifact?.kind === 'sql_block'
|
|
502
|
+
&& dqlArtifact.source?.trim()
|
|
503
|
+
&& notebookDqlSourceAllowsBackgroundRepair(dqlArtifact.source)) {
|
|
504
|
+
return dqlArtifact;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Keep the parameter surface immutable while repairing malformed DQL. A parser
|
|
511
|
+
* cannot reliably read a broken wrapper, so collect both declared parameter
|
|
512
|
+
* names and SQL interpolation names with a deliberately small lexical pass.
|
|
513
|
+
*/
|
|
514
|
+
export function dqlRepairParameterContract(source) {
|
|
515
|
+
const names = new Set();
|
|
516
|
+
for (const match of source.matchAll(/\$\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}/g)) {
|
|
517
|
+
names.add(match[1]);
|
|
518
|
+
}
|
|
519
|
+
for (const match of source.matchAll(/\bparams\s*\{([\s\S]*?)\}/gi)) {
|
|
520
|
+
for (const declaration of match[1].matchAll(/(?:^|\n)\s*([A-Za-z_][A-Za-z0-9_]*)\s*:/g)) {
|
|
521
|
+
names.add(declaration[1]);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return [...names].sort((left, right) => left.localeCompare(right));
|
|
525
|
+
}
|
|
421
526
|
/** Structured producer failure retained on the source run. */
|
|
422
527
|
export function warehouseFailureFromAgentRun(run) {
|
|
423
528
|
for (const artifact of run.artifacts) {
|
|
@@ -429,10 +534,49 @@ export function warehouseFailureFromAgentRun(run) {
|
|
|
429
534
|
}
|
|
430
535
|
return undefined;
|
|
431
536
|
}
|
|
537
|
+
/**
|
|
538
|
+
* Retain planning and retrieval evidence for a successful derived repair while
|
|
539
|
+
* deliberately dropping the source failure and any result-bound facts. This
|
|
540
|
+
* is the presentation contract used by "How it was answered" and by grounded
|
|
541
|
+
* follow-ups after repair.
|
|
542
|
+
*/
|
|
543
|
+
export function repairPresentationContextFromAgentRun(run) {
|
|
544
|
+
const retainedKeys = [
|
|
545
|
+
'resolvedAnalyticalPlan',
|
|
546
|
+
'analyticalExecutionGraph',
|
|
547
|
+
'analyticalFreshnessObservation',
|
|
548
|
+
'governedCompilationReceipt',
|
|
549
|
+
'governedAnalyticalGraphCompilationReceipt',
|
|
550
|
+
'contextPack',
|
|
551
|
+
'contextPackId',
|
|
552
|
+
'cascade',
|
|
553
|
+
'citations',
|
|
554
|
+
'lineage',
|
|
555
|
+
];
|
|
556
|
+
const context = {};
|
|
557
|
+
for (const artifact of run.artifacts) {
|
|
558
|
+
const payload = agentRunRecord(artifact.payload);
|
|
559
|
+
if (!payload)
|
|
560
|
+
continue;
|
|
561
|
+
for (const key of retainedKeys) {
|
|
562
|
+
if (context[key] === undefined && payload[key] !== undefined)
|
|
563
|
+
context[key] = payload[key];
|
|
564
|
+
}
|
|
565
|
+
const semanticTrace = agentRunRecord(payload.semanticExecutionTrace);
|
|
566
|
+
if (context.semanticExecutionTrace === undefined && semanticTrace && !semanticTrace.failure) {
|
|
567
|
+
context.semanticExecutionTrace = semanticTrace;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return context;
|
|
571
|
+
}
|
|
432
572
|
/** Permission, authentication, policy and unsafe-query failures are never AI-repaired. */
|
|
433
573
|
export function analyticalFailureAllowsDeterministicRetry(failure) {
|
|
434
574
|
return !failure || !['permission', 'authentication', 'unsafe', 'cancelled'].includes(failure.category);
|
|
435
575
|
}
|
|
576
|
+
/** Structured policy/access refusals stay terminal even when retained SQL exists. */
|
|
577
|
+
export function analyticalFailureAllowsAppRepair(failure) {
|
|
578
|
+
return !failure || !['PERMISSION_DENIED', 'POLICY_DENIED'].includes(failure.code);
|
|
579
|
+
}
|
|
436
580
|
function businessNarrativeGaps(warnings) {
|
|
437
581
|
const businessRelevant = (warnings ?? []).filter((warning) => !(/missing requested output column/i.test(warning)
|
|
438
582
|
|| /\bquery plan\b/i.test(warning)
|
|
@@ -1011,11 +1155,11 @@ export async function startLocalServer(opts) {
|
|
|
1011
1155
|
const resolveNamedConnection = (name) => {
|
|
1012
1156
|
if (!name)
|
|
1013
1157
|
return null;
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1158
|
+
// `GET /api/connections` exposes the active legacy/default connection as
|
|
1159
|
+
// `default` so every execution surface can present one uniform selector.
|
|
1160
|
+
// Resolve that selector through the same project contract used by metadata
|
|
1161
|
+
// APIs instead of looking only in the newer named `connections` map.
|
|
1162
|
+
return projectConnectionById(projectRoot, projectConfig, name);
|
|
1019
1163
|
};
|
|
1020
1164
|
const resolveExecutionConnection = async (body) => {
|
|
1021
1165
|
const target = body.executionTarget && typeof body.executionTarget === "object"
|
|
@@ -1104,6 +1248,35 @@ export async function startLocalServer(opts) {
|
|
|
1104
1248
|
// unchanged. Explicit refresh, save, and certification paths still request
|
|
1105
1249
|
// the default forced rebuild below.
|
|
1106
1250
|
await refreshLocalMetadataCatalog(projectRoot, undefined, undefined, false);
|
|
1251
|
+
// PERF-001 / API-001: project mutations acknowledge after their source file
|
|
1252
|
+
// is durable. Rebuildable manifest, lineage, metadata, and KG projections run
|
|
1253
|
+
// in one coalesced worker lane and remain observable across page navigation.
|
|
1254
|
+
const operationCoordinator = new LocalOperationCoordinator(join(projectRoot, '.dql', 'cache', 'operations.sqlite'));
|
|
1255
|
+
const operationSseClients = new Set();
|
|
1256
|
+
const operationIdempotency = new Map();
|
|
1257
|
+
const projectRefreshCoordinator = new ProjectRefreshCoordinator(operationCoordinator, () => {
|
|
1258
|
+
projectSnapshots.invalidate();
|
|
1259
|
+
dbtNodeDetailCache.clear();
|
|
1260
|
+
invalidateAgentProjectState(projectRoot);
|
|
1261
|
+
_lineageCache.delete(projectRoot);
|
|
1262
|
+
});
|
|
1263
|
+
const scheduleProjectRefresh = (reason, writeManifest = true) => (projectRefreshCoordinator.schedule({
|
|
1264
|
+
projectRoot,
|
|
1265
|
+
dbtManifestPath: resolveDbtManifestPath(projectRoot, projectConfig) ?? undefined,
|
|
1266
|
+
writeManifest,
|
|
1267
|
+
reason,
|
|
1268
|
+
}));
|
|
1269
|
+
const unsubscribeOperationEvents = operationCoordinator.subscribe((operation) => {
|
|
1270
|
+
const payload = serializeJSON(operation);
|
|
1271
|
+
for (const client of operationSseClients) {
|
|
1272
|
+
try {
|
|
1273
|
+
client.write(`event: operation\ndata: ${payload}\n\n`);
|
|
1274
|
+
}
|
|
1275
|
+
catch {
|
|
1276
|
+
operationSseClients.delete(client);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1107
1280
|
const startupDbtManifestPath = resolveDbtManifestPath(projectRoot, projectConfig);
|
|
1108
1281
|
if (startupDbtManifestPath && !isAgentProjectIndexReady(projectRoot)) {
|
|
1109
1282
|
try {
|
|
@@ -1185,8 +1358,8 @@ export async function startLocalServer(opts) {
|
|
|
1185
1358
|
catch { /* non-fatal */ }
|
|
1186
1359
|
}
|
|
1187
1360
|
}
|
|
1188
|
-
const executeLocalSqlForStoredResult = async (sql) => {
|
|
1189
|
-
const activeConnection = requireActiveConnection();
|
|
1361
|
+
const executeLocalSqlForStoredResult = async (sql, executionConnection) => {
|
|
1362
|
+
const activeConnection = requireActiveConnection(executionConnection);
|
|
1190
1363
|
const semantic = prepareSemanticSql(sql, semanticLayer);
|
|
1191
1364
|
if (semantic.unresolvedRefs.length > 0) {
|
|
1192
1365
|
throw new Error(`Unknown semantic reference${semantic.unresolvedRefs.length > 1 ? 's' : ''}: ${semantic.unresolvedRefs.join(', ')}`);
|
|
@@ -2453,6 +2626,23 @@ export async function startLocalServer(opts) {
|
|
|
2453
2626
|
const needsClarification = Boolean(plan.followUp);
|
|
2454
2627
|
const notebookPath = agentRunNotebookPath(request, runId);
|
|
2455
2628
|
const researchIntent = agentRunResearchIntent(request);
|
|
2629
|
+
const researchSource = agentRunRecord(request.workspaceContext?.researchSource);
|
|
2630
|
+
const researchExecutionConnection = !needsClarification && request.executionTarget
|
|
2631
|
+
? await resolveAgentRunExecutionConnection(request)
|
|
2632
|
+
: undefined;
|
|
2633
|
+
const researchExecutionConnectionName = request.executionTarget?.target === 'connection'
|
|
2634
|
+
? request.executionTarget.connectionName
|
|
2635
|
+
: undefined;
|
|
2636
|
+
const researchContextEnvelope = {
|
|
2637
|
+
surface: 'unified_agent_run',
|
|
2638
|
+
agentRunId: runId,
|
|
2639
|
+
routeDecision,
|
|
2640
|
+
selectedObject: request.selectedObject,
|
|
2641
|
+
executionTarget: request.executionTarget,
|
|
2642
|
+
workspaceContext: request.workspaceContext,
|
|
2643
|
+
researchSource,
|
|
2644
|
+
plan,
|
|
2645
|
+
};
|
|
2456
2646
|
let researchRun;
|
|
2457
2647
|
let researchWorkspaceError;
|
|
2458
2648
|
if (!needsClarification) {
|
|
@@ -2474,14 +2664,7 @@ export async function startLocalServer(opts) {
|
|
|
2474
2664
|
intent: researchIntent,
|
|
2475
2665
|
domain: agentRunWorkspaceValue(request, 'domain'),
|
|
2476
2666
|
owner: agentRunWorkspaceValue(request, 'owner'),
|
|
2477
|
-
context:
|
|
2478
|
-
surface: 'unified_agent_run',
|
|
2479
|
-
agentRunId: runId,
|
|
2480
|
-
routeDecision,
|
|
2481
|
-
selectedObject: request.selectedObject,
|
|
2482
|
-
workspaceContext: request.workspaceContext,
|
|
2483
|
-
plan,
|
|
2484
|
-
},
|
|
2667
|
+
context: researchContextEnvelope,
|
|
2485
2668
|
});
|
|
2486
2669
|
emit({
|
|
2487
2670
|
type: 'artifact.created',
|
|
@@ -2496,14 +2679,13 @@ export async function startLocalServer(opts) {
|
|
|
2496
2679
|
sourceCellFingerprint,
|
|
2497
2680
|
question: request.question,
|
|
2498
2681
|
intent: researchIntent,
|
|
2499
|
-
context:
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
},
|
|
2682
|
+
context: researchContextEnvelope,
|
|
2683
|
+
executionConnection: researchExecutionConnection,
|
|
2684
|
+
executionConnectionName: researchExecutionConnectionName,
|
|
2685
|
+
signal: request.signal,
|
|
2686
|
+
baselineSql: agentRunString(researchSource?.sql),
|
|
2687
|
+
baselineDqlArtifact: researchSource?.dqlArtifact,
|
|
2688
|
+
baselineRunId: agentRunString(researchSource?.runId),
|
|
2507
2689
|
});
|
|
2508
2690
|
researchRun = withNotebookResearchChecklist(executed);
|
|
2509
2691
|
}
|
|
@@ -2816,7 +2998,7 @@ export async function startLocalServer(opts) {
|
|
|
2816
2998
|
message: `Generating review-required SQL for an uncovered question: ${question}`,
|
|
2817
2999
|
route: 'app_build',
|
|
2818
3000
|
});
|
|
2819
|
-
return
|
|
3001
|
+
return generateAppBuildAnswerWithRepair(question);
|
|
2820
3002
|
},
|
|
2821
3003
|
});
|
|
2822
3004
|
}
|
|
@@ -3716,13 +3898,15 @@ export async function startLocalServer(opts) {
|
|
|
3716
3898
|
* evidence. The refusal can say the relation is not visible to this
|
|
3717
3899
|
* connection, rather than only that DQL had not looked at it.
|
|
3718
3900
|
*/
|
|
3719
|
-
const probeNamedRelationsForAgent = async (relations, probeConnection) => {
|
|
3901
|
+
const probeNamedRelationsForAgent = async (relations, probeConnection, verifyExecutionSchema = false) => {
|
|
3720
3902
|
const probeTarget = probeConnection ?? connection;
|
|
3721
3903
|
if (!probeTarget)
|
|
3722
3904
|
return undefined;
|
|
3723
|
-
//
|
|
3724
|
-
//
|
|
3725
|
-
|
|
3905
|
+
// Named relation discovery remains behind the operator's live-grounding
|
|
3906
|
+
// switch. Executable schema verification is different: it is a bounded,
|
|
3907
|
+
// read-only metadata point lookup for relations already selected from dbt,
|
|
3908
|
+
// and never reads row values or mutates the active target.
|
|
3909
|
+
if (!verifyExecutionSchema && resolveAgentRuntimeValueGrounding(projectConfig).mode !== 'safe_automatic')
|
|
3726
3910
|
return undefined;
|
|
3727
3911
|
const sql = buildNamedRelationProbeSql(relations);
|
|
3728
3912
|
if (!sql)
|
|
@@ -3775,12 +3959,29 @@ export async function startLocalServer(opts) {
|
|
|
3775
3959
|
&& activeRuntimeSnapshot.status !== 'stale');
|
|
3776
3960
|
const valueGrounding = resolveAgentRuntimeValueGrounding(projectConfig);
|
|
3777
3961
|
if (catalogContext.length > 0) {
|
|
3778
|
-
if (!schemaConnection
|
|
3962
|
+
if (!schemaConnection)
|
|
3779
3963
|
return catalogContext;
|
|
3780
|
-
//
|
|
3781
|
-
//
|
|
3782
|
-
//
|
|
3783
|
-
|
|
3964
|
+
// dbt metadata is the semantic authority, but generated SQL must use the
|
|
3965
|
+
// physical columns visible on THIS execution target. Verify only the few
|
|
3966
|
+
// question-ranked relations with a bounded information_schema point lookup;
|
|
3967
|
+
// this is read-only and never mutates DuckDB, Snowflake, or any other target.
|
|
3968
|
+
let executionSchema = catalogContext;
|
|
3969
|
+
try {
|
|
3970
|
+
const live = await probeNamedRelationsForAgent(catalogContext.slice(0, 4).map((table) => table.relation), schemaConnection, true);
|
|
3971
|
+
if (live?.schemaContext?.length) {
|
|
3972
|
+
executionSchema = reconcileAgentSchemaContextWithLive(catalogContext, live.schemaContext);
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
catch {
|
|
3976
|
+
// Live verification is additive. Existing metadata remains available if
|
|
3977
|
+
// the connection cannot expose information_schema to this user.
|
|
3978
|
+
}
|
|
3979
|
+
// The immutable dbt/DQL snapshot owns schema discovery. Live verification
|
|
3980
|
+
// only confirms those named relations; row values remain governed by the
|
|
3981
|
+
// separate allowlisted grounding policy below.
|
|
3982
|
+
if (valueGrounding.mode !== 'safe_automatic')
|
|
3983
|
+
return executionSchema;
|
|
3984
|
+
const enriched = await enrichAgentSchemaContextWithValueMatches(question, executionSchema, executor, schemaConnection, valueGrounding.searchSafeColumns);
|
|
3784
3985
|
return enriched;
|
|
3785
3986
|
}
|
|
3786
3987
|
// An activated, scope-qualified warehouse generation is the complete warm
|
|
@@ -3912,83 +4113,365 @@ export async function startLocalServer(opts) {
|
|
|
3912
4113
|
}
|
|
3913
4114
|
return { result, repairedSql, repairMode, firstFailure, rewrites };
|
|
3914
4115
|
};
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
4116
|
+
/**
|
|
4117
|
+
* Repair a DQL wrapper that cannot compile far enough to expose executable
|
|
4118
|
+
* SQL. This is the DQL-side half of the same bounded repair contract used by
|
|
4119
|
+
* Ask and Notebook: one provider call, one block, identical parameters, one
|
|
4120
|
+
* read-only execution, and the exact target selected by the failed run.
|
|
4121
|
+
*/
|
|
4122
|
+
const executeBoundedDqlRepair = async (input) => {
|
|
4123
|
+
const provider = await createBlockStudioAssistProvider(projectRoot);
|
|
4124
|
+
if (!provider) {
|
|
4125
|
+
throw boundedRepairError('Configure an available AI provider in Settings to repair this DQL cell.', 409);
|
|
3921
4126
|
}
|
|
3922
|
-
let
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
context: input.context,
|
|
3954
|
-
};
|
|
3955
|
-
const controller = new AbortController();
|
|
3956
|
-
await runner.run({
|
|
3957
|
-
provider: resolvedProvider,
|
|
3958
|
-
messages: [{
|
|
4127
|
+
let schemaContext;
|
|
4128
|
+
try {
|
|
4129
|
+
// Include source identifiers in retrieval so a terse cell title still
|
|
4130
|
+
// resolves the physical dbt relations and live target columns it uses.
|
|
4131
|
+
schemaContext = await getSchemaContextForAgent(`${input.question}\n${input.source.slice(0, 6_000)}`, undefined, input.targetConnection, input.targetConnectionName);
|
|
4132
|
+
}
|
|
4133
|
+
catch (error) {
|
|
4134
|
+
const failure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
|
|
4135
|
+
throw boundedRepairError(failure.redactedMessage, 422, failure);
|
|
4136
|
+
}
|
|
4137
|
+
const boundedSchema = schemaContext.slice(0, 16).map((table) => ({
|
|
4138
|
+
relation: table.relation,
|
|
4139
|
+
columns: table.columns.slice(0, 40).map((column) => ({ name: column.name, type: column.type })),
|
|
4140
|
+
}));
|
|
4141
|
+
let raw;
|
|
4142
|
+
try {
|
|
4143
|
+
raw = await provider.generate([
|
|
4144
|
+
{
|
|
4145
|
+
role: 'system',
|
|
4146
|
+
content: [
|
|
4147
|
+
'Repair one malformed executable DQL custom block and its embedded read-only SQL.',
|
|
4148
|
+
'Return only one JSON object with exactly one key named dql. Do not use markdown.',
|
|
4149
|
+
'The DQL shape is: block "Name" { type = "custom" params { name: type = default } query = """SELECT ...""" visualization { ... } tests { ... } }.',
|
|
4150
|
+
'Preserve the block name, business fields, visualization, tests, requested grain, filters, output columns, and aggregation semantics.',
|
|
4151
|
+
'Preserve every declared parameter and every ${parameter} interpolation exactly. Do not add or remove parameters.',
|
|
4152
|
+
'Use only relations and columns present in the supplied schema context.',
|
|
4153
|
+
'Never emit internal relation identities such as source::, model::, seed::, or snapshot::.',
|
|
4154
|
+
'Return exactly one custom block. Never certify it, bind a saved block, add writes, DDL, or multiple SQL statements.',
|
|
4155
|
+
].join(' '),
|
|
4156
|
+
},
|
|
4157
|
+
{
|
|
3959
4158
|
role: 'user',
|
|
3960
|
-
content:
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
}
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
4159
|
+
content: JSON.stringify({
|
|
4160
|
+
question: input.question,
|
|
4161
|
+
driver: input.targetConnection.driver,
|
|
4162
|
+
failure: input.failure.slice(0, 2_000),
|
|
4163
|
+
dql: input.source.slice(0, 40_000),
|
|
4164
|
+
parameters: input.parameters ?? {},
|
|
4165
|
+
schema: boundedSchema,
|
|
4166
|
+
}),
|
|
4167
|
+
},
|
|
4168
|
+
], { maxTokens: 2400, temperature: 0 });
|
|
4169
|
+
}
|
|
4170
|
+
catch {
|
|
4171
|
+
throw boundedRepairError('The AI provider could not complete this DQL repair. Check Provider Settings and retry.', 502);
|
|
4172
|
+
}
|
|
4173
|
+
let repairedSource;
|
|
4174
|
+
try {
|
|
4175
|
+
const parsed = parseFirstJsonObject(raw);
|
|
4176
|
+
repairedSource = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
4177
|
+
&& typeof parsed.dql === 'string'
|
|
4178
|
+
? String(parsed.dql).trim()
|
|
4179
|
+
: undefined;
|
|
4180
|
+
}
|
|
4181
|
+
catch {
|
|
4182
|
+
repairedSource = undefined;
|
|
4183
|
+
}
|
|
4184
|
+
if (!repairedSource) {
|
|
4185
|
+
throw boundedRepairError('The AI repair did not return a complete DQL block.', 422);
|
|
4186
|
+
}
|
|
4187
|
+
if (repairedSource.length > 50_000) {
|
|
4188
|
+
throw boundedRepairError('The repaired DQL exceeded the safe editable-cell size limit.', 422);
|
|
4189
|
+
}
|
|
4190
|
+
if (/^\s*@block\(/i.test(repairedSource) || /\btype\s*=\s*"semantic"/i.test(repairedSource)) {
|
|
4191
|
+
throw boundedRepairError('The repair changed this editable custom block into a governed reference or semantic block.', 422);
|
|
4192
|
+
}
|
|
4193
|
+
if (!/\bstatus\s*=\s*"certified"/i.test(input.source)
|
|
4194
|
+
&& /\bstatus\s*=\s*"certified"/i.test(repairedSource)) {
|
|
4195
|
+
throw boundedRepairError('The repair attempted to certify an AI-edited block, so DQL stopped it.', 422);
|
|
4196
|
+
}
|
|
4197
|
+
const beforeParameters = dqlRepairParameterContract(input.source);
|
|
4198
|
+
const afterParameters = dqlRepairParameterContract(repairedSource);
|
|
4199
|
+
if (beforeParameters.join('|') !== afterParameters.join('|')) {
|
|
4200
|
+
throw boundedRepairError('The repair changed this block\'s parameter contract, so DQL stopped it before execution.', 422);
|
|
4201
|
+
}
|
|
4202
|
+
let invocation;
|
|
4203
|
+
let plan;
|
|
4204
|
+
try {
|
|
4205
|
+
const program = new Parser(repairedSource, '<bounded-dql-repair>').parse();
|
|
4206
|
+
const blocks = program.statements.filter((statement) => statement.kind === NodeKind.BlockDecl);
|
|
4207
|
+
if (blocks.length !== 1 || program.statements.length !== 1) {
|
|
4208
|
+
throw new Error('The repaired source must contain exactly one DQL block.');
|
|
4209
|
+
}
|
|
4210
|
+
const block = blocks[0];
|
|
4211
|
+
if (block.kind !== NodeKind.BlockDecl || block.blockType !== 'custom') {
|
|
4212
|
+
throw new Error('The repaired source must remain a custom DQL block.');
|
|
4213
|
+
}
|
|
4214
|
+
invocation = prepareBlockInvocation({
|
|
4215
|
+
source: repairedSource,
|
|
4216
|
+
parameters: input.parameters,
|
|
4217
|
+
question: input.question,
|
|
4218
|
+
surface: 'notebook',
|
|
4219
|
+
});
|
|
4220
|
+
if (invocation.errors.length)
|
|
4221
|
+
throw new Error(invocation.errors.join(' '));
|
|
4222
|
+
if (invocation.unresolvedParameters.length) {
|
|
4223
|
+
throw new Error(`Provide required parameters before retrying: ${invocation.unresolvedParameters.join(', ')}.`);
|
|
4224
|
+
}
|
|
4225
|
+
plan = buildExecutionPlan({
|
|
4226
|
+
id: 'bounded-dql-repair',
|
|
4227
|
+
type: 'dql',
|
|
4228
|
+
source: repairedSource,
|
|
4229
|
+
title: input.artifactName,
|
|
4230
|
+
}, {
|
|
4231
|
+
semanticLayer,
|
|
4232
|
+
driver: input.targetConnection.driver,
|
|
4233
|
+
parameters: invocation.values,
|
|
4234
|
+
});
|
|
4235
|
+
if (!plan?.sql?.trim())
|
|
4236
|
+
throw new Error('The repaired DQL did not compile to SQL.');
|
|
4237
|
+
}
|
|
4238
|
+
catch (error) {
|
|
4239
|
+
throw boundedRepairError(`The repaired DQL did not compile safely: ${error instanceof Error ? error.message : String(error)}`, 422);
|
|
4240
|
+
}
|
|
4241
|
+
const seed = {
|
|
4242
|
+
kind: 'sql_block',
|
|
4243
|
+
source: repairedSource,
|
|
4244
|
+
name: input.artifactName ?? plan.title,
|
|
4245
|
+
persistence: 'transient',
|
|
4246
|
+
trustState: 'review_required',
|
|
4247
|
+
compiledSql: plan.sql,
|
|
4248
|
+
...(invocation.parameters.length ? { parameters: invocation.parameters } : {}),
|
|
4249
|
+
...(Object.keys(invocation.values).length ? { parameterValues: invocation.values } : {}),
|
|
4250
|
+
};
|
|
4251
|
+
let result;
|
|
4252
|
+
try {
|
|
4253
|
+
result = await executeGeneratedSqlDirect(input.question, plan.sql, seed, input.targetConnection, { sqlParams: plan.sqlParams, variables: { ...plan.variables, ...invocation.values } });
|
|
4254
|
+
}
|
|
4255
|
+
catch (error) {
|
|
4256
|
+
const failure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
|
|
4257
|
+
throw boundedRepairError(failure.redactedMessage, 422, failure);
|
|
4258
|
+
}
|
|
4259
|
+
result = {
|
|
4260
|
+
...result,
|
|
4261
|
+
dqlArtifact: {
|
|
4262
|
+
...(result.dqlArtifact ?? seed),
|
|
4263
|
+
source: repairedSource,
|
|
4264
|
+
compiledSql: result.sql ?? plan.sql,
|
|
4265
|
+
trustState: 'review_required',
|
|
4266
|
+
persistence: 'transient',
|
|
4267
|
+
...(result.executionReceipt ? { executionReceipt: result.executionReceipt } : {}),
|
|
3972
4268
|
},
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
4269
|
+
};
|
|
4270
|
+
return {
|
|
4271
|
+
result,
|
|
4272
|
+
repairedSql: plan.sql,
|
|
4273
|
+
repairedSource,
|
|
4274
|
+
repairMode: 'ai',
|
|
4275
|
+
rewrites: [],
|
|
4276
|
+
sqlParams: plan.sqlParams,
|
|
4277
|
+
variables: { ...plan.variables, ...invocation.values },
|
|
4278
|
+
};
|
|
4279
|
+
};
|
|
4280
|
+
/**
|
|
4281
|
+
* App proposal parity with Ask/Notebook repair. The first governed attempt is
|
|
4282
|
+
* retained unchanged. Only an execution failure with retained read-only SQL
|
|
4283
|
+
* may spend the shared one-attempt repair budget, on the same connection.
|
|
4284
|
+
* The repaired candidate is still exploratory and review-required.
|
|
4285
|
+
*/
|
|
4286
|
+
const generateAppBuildAnswerWithRepair = async (question, appContext) => {
|
|
4287
|
+
const request = {
|
|
4288
|
+
question,
|
|
4289
|
+
audience: 'stakeholder',
|
|
4290
|
+
workspaceContext: appContext
|
|
4291
|
+
? { surface: 'app_builder', appContext }
|
|
4292
|
+
: { surface: 'app_builder' },
|
|
4293
|
+
};
|
|
4294
|
+
const answer = await runGovernedAgentAnswerForRun(request);
|
|
4295
|
+
const sourceSql = answer.proposedSql?.trim() || answer.sql?.trim();
|
|
4296
|
+
const hasExecutionFailure = agentAnswerHasExecutionFailure(answer)
|
|
4297
|
+
|| Boolean(answer.warehouseFailure)
|
|
4298
|
+
|| Boolean(answer.analyticalFailure);
|
|
4299
|
+
if (answer.result || !sourceSql || !hasExecutionFailure)
|
|
4300
|
+
return answer;
|
|
4301
|
+
const attemptedAt = new Date().toISOString();
|
|
4302
|
+
const originalFailure = answer.warehouseFailure?.redactedMessage
|
|
4303
|
+
?? answer.executionError
|
|
4304
|
+
?? 'The generated query did not execute.';
|
|
4305
|
+
const originalSqlFingerprint = `sha256:${createHash('sha256').update(sourceSql).digest('hex')}`;
|
|
4306
|
+
const failedTrace = (message) => ({
|
|
4307
|
+
version: 1,
|
|
4308
|
+
status: 'failed',
|
|
4309
|
+
source: 'query_generator',
|
|
4310
|
+
attemptedAt,
|
|
4311
|
+
originalFailure,
|
|
4312
|
+
originalSqlFingerprint,
|
|
4313
|
+
approvalEligible: false,
|
|
4314
|
+
message,
|
|
4315
|
+
});
|
|
4316
|
+
if (!analyticalFailureAllowsDeterministicRetry(answer.warehouseFailure)
|
|
4317
|
+
|| !analyticalFailureAllowsAppRepair(answer.analyticalFailure)) {
|
|
4318
|
+
return {
|
|
4319
|
+
...answer,
|
|
4320
|
+
appBuilderRepair: failedTrace('This failure needs an access, policy, or connection decision and was not sent to AI repair.'),
|
|
4321
|
+
};
|
|
4322
|
+
}
|
|
4323
|
+
try {
|
|
4324
|
+
const targetConnection = await resolveAgentRunExecutionConnection(request);
|
|
4325
|
+
const targetConnectionName = request.executionTarget?.target === 'connection'
|
|
4326
|
+
? request.executionTarget.connectionName
|
|
4327
|
+
: request.executionTarget?.target === 'local'
|
|
4328
|
+
? 'local'
|
|
4329
|
+
: projectConfig.defaultConnectionName;
|
|
4330
|
+
const repaired = await executeBoundedSqlRepair({
|
|
4331
|
+
question,
|
|
4332
|
+
sourceSql,
|
|
4333
|
+
targetConnection,
|
|
4334
|
+
targetConnectionName,
|
|
4335
|
+
});
|
|
4336
|
+
const repair = {
|
|
4337
|
+
version: 1,
|
|
4338
|
+
status: 'repaired',
|
|
4339
|
+
source: 'query_generator',
|
|
4340
|
+
mode: repaired.repairMode,
|
|
4341
|
+
attemptedAt,
|
|
4342
|
+
originalFailure,
|
|
4343
|
+
originalSqlFingerprint,
|
|
4344
|
+
repairedSqlFingerprint: `sha256:${createHash('sha256').update(repaired.repairedSql).digest('hex')}`,
|
|
4345
|
+
approvalEligible: false,
|
|
4346
|
+
message: `${repaired.repairMode === 'ai' ? 'AI' : 'Deterministic'} repair executed successfully on the same data target. Review the repaired query before adding it to the private App draft.`,
|
|
4347
|
+
};
|
|
4348
|
+
return {
|
|
4349
|
+
...answer,
|
|
4350
|
+
result: repaired.result,
|
|
4351
|
+
proposedSql: repaired.repairedSql,
|
|
4352
|
+
sql: repaired.repairedSql,
|
|
4353
|
+
executionError: undefined,
|
|
4354
|
+
warehouseFailure: undefined,
|
|
4355
|
+
analyticalFailure: undefined,
|
|
4356
|
+
validationWarnings: [
|
|
4357
|
+
...(answer.validationWarnings ?? []),
|
|
4358
|
+
repair.message,
|
|
4359
|
+
],
|
|
4360
|
+
appBuilderRepair: repair,
|
|
4361
|
+
};
|
|
4362
|
+
}
|
|
4363
|
+
catch (error) {
|
|
4364
|
+
return {
|
|
4365
|
+
...answer,
|
|
4366
|
+
appBuilderRepair: failedTrace(error instanceof Error
|
|
4367
|
+
? `The bounded repair could not complete: ${error.message}`
|
|
4368
|
+
: 'The bounded repair could not complete.'),
|
|
4369
|
+
};
|
|
4370
|
+
}
|
|
4371
|
+
};
|
|
4372
|
+
const repairFailedAppTileExecution = async (input) => {
|
|
4373
|
+
const attemptedAt = new Date().toISOString();
|
|
4374
|
+
const driver = input.targetConnection?.driver ?? connection?.driver ?? 'duckdb';
|
|
4375
|
+
const failure = normalizeWarehouseSqlFailure(input.error, driver);
|
|
4376
|
+
const originalFailure = failure.redactedMessage;
|
|
4377
|
+
const originalSqlFingerprint = input.sourceSql
|
|
4378
|
+
? `sha256:${createHash('sha256').update(input.sourceSql).digest('hex')}`
|
|
4379
|
+
: undefined;
|
|
4380
|
+
const failed = (message) => ({
|
|
4381
|
+
ok: false,
|
|
4382
|
+
repair: {
|
|
4383
|
+
version: 1,
|
|
4384
|
+
status: 'failed',
|
|
4385
|
+
source: input.source,
|
|
4386
|
+
attemptedAt,
|
|
4387
|
+
originalFailure,
|
|
4388
|
+
...(originalSqlFingerprint ? { originalSqlFingerprint } : {}),
|
|
4389
|
+
approvalEligible: false,
|
|
4390
|
+
message,
|
|
4391
|
+
},
|
|
4392
|
+
});
|
|
4393
|
+
if (!input.targetConnection) {
|
|
4394
|
+
return failed('Configure the original data target before retrying this App tile.');
|
|
4395
|
+
}
|
|
4396
|
+
if (!input.sourceSql?.trim() && input.sourceDql?.trim()) {
|
|
4397
|
+
try {
|
|
4398
|
+
const repaired = await executeBoundedDqlRepair({
|
|
4399
|
+
question: input.question,
|
|
4400
|
+
source: input.sourceDql,
|
|
4401
|
+
failure: originalFailure,
|
|
4402
|
+
targetConnection: input.targetConnection,
|
|
4403
|
+
targetConnectionName: input.targetConnectionName,
|
|
4404
|
+
parameters: input.variables,
|
|
4405
|
+
artifactName: input.artifactName,
|
|
4406
|
+
});
|
|
4407
|
+
const repair = {
|
|
4408
|
+
version: 1,
|
|
4409
|
+
status: 'repaired',
|
|
4410
|
+
source: input.source,
|
|
4411
|
+
mode: repaired.repairMode,
|
|
4412
|
+
attemptedAt,
|
|
4413
|
+
originalFailure,
|
|
4414
|
+
repairedSqlFingerprint: `sha256:${createHash('sha256').update(repaired.repairedSql).digest('hex')}`,
|
|
4415
|
+
approvalEligible: false,
|
|
4416
|
+
message: `${repaired.repairMode === 'ai' ? 'AI' : 'Deterministic'} DQL repair executed successfully on the same data target. The App result remains review-required and the source file was not changed.`,
|
|
4417
|
+
};
|
|
4418
|
+
return {
|
|
4419
|
+
ok: true,
|
|
4420
|
+
result: repaired.result,
|
|
4421
|
+
repairedSql: repaired.repairedSql,
|
|
4422
|
+
repairedDql: repaired.repairedSource,
|
|
4423
|
+
repair,
|
|
4424
|
+
};
|
|
3982
4425
|
}
|
|
3983
|
-
|
|
3984
|
-
|
|
4426
|
+
catch (error) {
|
|
4427
|
+
return failed(error instanceof Error
|
|
4428
|
+
? `The bounded DQL repair could not complete: ${error.message}`
|
|
4429
|
+
: 'The bounded DQL repair could not complete.');
|
|
3985
4430
|
}
|
|
3986
|
-
}, controller.signal);
|
|
3987
|
-
if (!governedAnswer) {
|
|
3988
|
-
throw new Error(providerError ?? 'The AI provider did not return a governed answer.');
|
|
3989
4431
|
}
|
|
4432
|
+
if (!input.sourceSql?.trim()) {
|
|
4433
|
+
return failed('DQL retained the failure, but no read-only SQL or editable DQL was available for bounded repair.');
|
|
4434
|
+
}
|
|
4435
|
+
if (!analyticalFailureAllowsDeterministicRetry(failure)) {
|
|
4436
|
+
return failed('This failure needs an access, policy, or connection decision and was not sent to AI repair.');
|
|
4437
|
+
}
|
|
4438
|
+
try {
|
|
4439
|
+
const repaired = await executeBoundedSqlRepair({
|
|
4440
|
+
question: input.question,
|
|
4441
|
+
sourceSql: input.sourceSql,
|
|
4442
|
+
targetConnection: input.targetConnection,
|
|
4443
|
+
targetConnectionName: input.targetConnectionName,
|
|
4444
|
+
sqlParams: input.sqlParams,
|
|
4445
|
+
variables: input.variables,
|
|
4446
|
+
});
|
|
4447
|
+
const repair = {
|
|
4448
|
+
version: 1,
|
|
4449
|
+
status: 'repaired',
|
|
4450
|
+
source: input.source,
|
|
4451
|
+
mode: repaired.repairMode,
|
|
4452
|
+
attemptedAt,
|
|
4453
|
+
originalFailure,
|
|
4454
|
+
...(originalSqlFingerprint ? { originalSqlFingerprint } : {}),
|
|
4455
|
+
repairedSqlFingerprint: `sha256:${createHash('sha256').update(repaired.repairedSql).digest('hex')}`,
|
|
4456
|
+
approvalEligible: false,
|
|
4457
|
+
message: `${repaired.repairMode === 'ai' ? 'AI' : 'Deterministic'} repair executed successfully on the same data target. The result remains review-required.`,
|
|
4458
|
+
};
|
|
4459
|
+
return { ok: true, result: repaired.result, repairedSql: repaired.repairedSql, repair };
|
|
4460
|
+
}
|
|
4461
|
+
catch (error) {
|
|
4462
|
+
return failed(error instanceof Error
|
|
4463
|
+
? `The bounded repair could not complete: ${error.message}`
|
|
4464
|
+
: 'The bounded repair could not complete.');
|
|
4465
|
+
}
|
|
4466
|
+
};
|
|
4467
|
+
const generateInvestigationSqlForApp = async (input) => {
|
|
4468
|
+
const request = buildAppCopilotResearchAgentRequest(input);
|
|
4469
|
+
// AGT-007 / AGT-022: App Copilot keeps its server-owned App evidence
|
|
4470
|
+
// adapter, while routing the actual investigation through the uniform
|
|
4471
|
+
// snapshot, retrieval, trust, execution, and bounded-repair orchestration.
|
|
4472
|
+
const governedAnswer = await runGovernedAgentAnswerForRun(request, undefined, 'research');
|
|
3990
4473
|
return {
|
|
3991
|
-
sql:
|
|
4474
|
+
sql: input.mode === 'memo_only' ? undefined : governedAnswer.proposedSql ?? governedAnswer.sql,
|
|
3992
4475
|
answer: governedAnswer.answer ?? governedAnswer.text,
|
|
3993
4476
|
result: governedAnswer.result,
|
|
3994
4477
|
analysisPlan: governedAnswer.analysisPlan,
|
|
@@ -4106,6 +4589,9 @@ export async function startLocalServer(opts) {
|
|
|
4106
4589
|
let generatedSql = notebookResearchString(input.generatedSql) ?? run.generatedSql;
|
|
4107
4590
|
let reviewedSql = notebookResearchString(input.reviewedSql) ?? run.reviewedSql;
|
|
4108
4591
|
let dqlArtifact = normalizeDqlArtifactReference(input.dqlArtifact) ?? run.dqlArtifact;
|
|
4592
|
+
const baselineSql = notebookResearchString(input.baselineSql);
|
|
4593
|
+
const baselineDqlArtifact = normalizeDqlArtifactReference(input.baselineDqlArtifact);
|
|
4594
|
+
let usedBaselineFallback = false;
|
|
4109
4595
|
const startedAt = new Date().toISOString();
|
|
4110
4596
|
storage.updateRun(run.id, {
|
|
4111
4597
|
domain,
|
|
@@ -4156,7 +4642,7 @@ export async function startLocalServer(opts) {
|
|
|
4156
4642
|
generationWarnings.push('No AI provider is configured. Metadata context was saved as a research plan; paste SQL or configure an AI provider to generate candidate SQL.');
|
|
4157
4643
|
}
|
|
4158
4644
|
else {
|
|
4159
|
-
const
|
|
4645
|
+
const researchSignal = input.signal ?? new AbortController().signal;
|
|
4160
4646
|
try {
|
|
4161
4647
|
await runner.run({
|
|
4162
4648
|
provider: resolvedProvider,
|
|
@@ -4178,18 +4664,19 @@ export async function startLocalServer(opts) {
|
|
|
4178
4664
|
reasoningEffort: resolveRunReasoningEffort(projectRoot, resolvedProvider, 'research', false),
|
|
4179
4665
|
analysisDepth: 'deep',
|
|
4180
4666
|
projectRoot,
|
|
4181
|
-
executeCertifiedBlock: executeCertifiedBlockForAgent,
|
|
4182
|
-
executeGeneratedSql:
|
|
4183
|
-
getSchemaContext: getSchemaContextForAgent,
|
|
4667
|
+
executeCertifiedBlock: (node, invocation) => executeCertifiedBlockForAgent(node, invocation, input.executionConnection, input.executionConnectionName),
|
|
4668
|
+
executeGeneratedSql: (sql, artifact) => executeGeneratedArtifactForAgent(question, sql, artifact, input.executionConnection, input.executionConnectionName),
|
|
4669
|
+
getSchemaContext: (schemaQuestion, preparedContextPack) => getSchemaContextForAgent(schemaQuestion, preparedContextPack, input.executionConnection, input.executionConnectionName),
|
|
4184
4670
|
}, (turn) => {
|
|
4185
4671
|
if (turn.kind === 'tool_result' && turn.id === 'governed_answer') {
|
|
4186
4672
|
governedAnswer = turn.output;
|
|
4187
4673
|
}
|
|
4188
4674
|
if (turn.kind === 'error')
|
|
4189
4675
|
providerError = turn.message;
|
|
4190
|
-
},
|
|
4676
|
+
}, researchSignal);
|
|
4191
4677
|
}
|
|
4192
4678
|
catch (error) {
|
|
4679
|
+
rethrowIfCancelled(error, input.signal);
|
|
4193
4680
|
providerError = error instanceof Error ? error.message : String(error);
|
|
4194
4681
|
}
|
|
4195
4682
|
if (!governedAnswer) {
|
|
@@ -4204,6 +4691,25 @@ export async function startLocalServer(opts) {
|
|
|
4204
4691
|
}
|
|
4205
4692
|
}
|
|
4206
4693
|
}
|
|
4694
|
+
const deeperCompositionFailed = !governedAnswer
|
|
4695
|
+
|| governedAnswer.kind === 'no_answer'
|
|
4696
|
+
|| agentAnswerHasExecutionFailure(governedAnswer)
|
|
4697
|
+
|| Boolean(governedAnswer.analyticalFailure)
|
|
4698
|
+
|| (!governedAnswer.result && !generatedSql && !dqlArtifact);
|
|
4699
|
+
if (!reviewedSql && baselineSql && deeperCompositionFailed) {
|
|
4700
|
+
const failure = governedAnswer?.executionError
|
|
4701
|
+
?? governedAnswer?.analyticalFailure?.message
|
|
4702
|
+
?? governedAnswer?.answer
|
|
4703
|
+
?? governedAnswer?.text
|
|
4704
|
+
?? providerError;
|
|
4705
|
+
generatedSql = baselineSql;
|
|
4706
|
+
dqlArtifact = baselineDqlArtifact ?? dqlArtifact;
|
|
4707
|
+
usedBaselineFallback = true;
|
|
4708
|
+
generationWarnings.push([
|
|
4709
|
+
'The deeper composition did not produce a safer executable query, so DQL kept and revalidated the successful Ask baseline on the same data target.',
|
|
4710
|
+
failure ? `Research detail: ${failure}` : '',
|
|
4711
|
+
].filter(Boolean).join(' '));
|
|
4712
|
+
}
|
|
4207
4713
|
const sqlForPreview = reviewedSql || generatedSql;
|
|
4208
4714
|
const warnings = [
|
|
4209
4715
|
...(contextPack.warnings ?? []),
|
|
@@ -4212,14 +4718,14 @@ export async function startLocalServer(opts) {
|
|
|
4212
4718
|
].filter(Boolean);
|
|
4213
4719
|
let resultPreview;
|
|
4214
4720
|
let previewError;
|
|
4215
|
-
if (governedAnswer?.result?.rows && !reviewedSql) {
|
|
4721
|
+
if (!usedBaselineFallback && governedAnswer?.result?.rows && !reviewedSql) {
|
|
4216
4722
|
resultPreview = normalizeNotebookAgentResult(governedAnswer.result);
|
|
4217
4723
|
}
|
|
4218
4724
|
else if (sqlForPreview) {
|
|
4219
4725
|
try {
|
|
4220
4726
|
const previewSql = buildAgentPreviewSql(sqlForPreview);
|
|
4221
4727
|
const previewStart = Date.now();
|
|
4222
|
-
resultPreview = await executeLocalSqlForStoredResult(previewSql);
|
|
4728
|
+
resultPreview = await executeLocalSqlForStoredResult(previewSql, input.executionConnection);
|
|
4223
4729
|
recordNotebookQueryRun(projectRoot, {
|
|
4224
4730
|
notebookPath: run.notebookPath,
|
|
4225
4731
|
cellId: run.sourceCellId,
|
|
@@ -4259,8 +4765,10 @@ export async function startLocalServer(opts) {
|
|
|
4259
4765
|
: undefined;
|
|
4260
4766
|
const evidence = {
|
|
4261
4767
|
trustStatus: {
|
|
4262
|
-
label:
|
|
4263
|
-
|
|
4768
|
+
label: usedBaselineFallback
|
|
4769
|
+
? 'Successful Ask baseline · revalidated on the same target'
|
|
4770
|
+
: governedAnswer?.trustLabel
|
|
4771
|
+
?? (reviewedSql ? 'Reviewed notebook SQL' : generatedSql ? 'AI-generated research SQL' : dqlArtifact ? 'AI-generated DQL artifact' : 'Metadata-grounded research plan'),
|
|
4264
4772
|
reviewRequired: true,
|
|
4265
4773
|
},
|
|
4266
4774
|
contextPackId: contextPack.id,
|
|
@@ -4294,9 +4802,11 @@ export async function startLocalServer(opts) {
|
|
|
4294
4802
|
...(governedAnswer?.citations ?? []),
|
|
4295
4803
|
].slice(0, 40),
|
|
4296
4804
|
};
|
|
4297
|
-
const summary =
|
|
4298
|
-
|
|
4299
|
-
|
|
4805
|
+
const summary = usedBaselineFallback && resultPreview && !previewError
|
|
4806
|
+
? 'Research retained the successful Ask baseline and revalidated it on the same data target. No unverified replacement query was accepted; use the dossier to add the next breakdown or comparison.'
|
|
4807
|
+
: notebookResearchString(governedAnswer?.answer)
|
|
4808
|
+
?? notebookResearchString(governedAnswer?.text)
|
|
4809
|
+
?? notebookResearchSummary(question, resultPreview, previewError);
|
|
4300
4810
|
const recommendation = previewError
|
|
4301
4811
|
? 'Review the SQL, selected metadata, and connection context before rerunning.'
|
|
4302
4812
|
: dqlArtifact && !reviewedSql
|
|
@@ -4334,6 +4844,9 @@ export async function startLocalServer(opts) {
|
|
|
4334
4844
|
contextPackId: contextPack.id,
|
|
4335
4845
|
routeDecision,
|
|
4336
4846
|
warnings: [
|
|
4847
|
+
...(usedBaselineFallback && input.baselineRunId
|
|
4848
|
+
? [`Baseline Ask run: ${input.baselineRunId}`]
|
|
4849
|
+
: []),
|
|
4337
4850
|
...warnings,
|
|
4338
4851
|
...(display && !display.ok ? [display.error] : []),
|
|
4339
4852
|
...(display && display.ok ? display.warnings : []),
|
|
@@ -4344,6 +4857,7 @@ export async function startLocalServer(opts) {
|
|
|
4344
4857
|
}) ?? run;
|
|
4345
4858
|
}
|
|
4346
4859
|
catch (error) {
|
|
4860
|
+
rethrowIfCancelled(error, input.signal);
|
|
4347
4861
|
return storage.updateRun(run.id, {
|
|
4348
4862
|
domain,
|
|
4349
4863
|
owner,
|
|
@@ -4570,16 +5084,19 @@ export async function startLocalServer(opts) {
|
|
|
4570
5084
|
});
|
|
4571
5085
|
// SSE clients for /api/watch hot-reload
|
|
4572
5086
|
const sseClients = new Set();
|
|
5087
|
+
const projectWatchers = [];
|
|
5088
|
+
let runtimeClosing = false;
|
|
4573
5089
|
// Watch all Git-owned authoring roots. Domain Packages are recursive and are
|
|
4574
5090
|
// the canonical home for modeling, skills, blocks, notebooks, Apps, and evals.
|
|
4575
5091
|
if (projectRoot) {
|
|
4576
|
-
let sourceRefreshTimer;
|
|
4577
5092
|
for (const dir of ['domains', 'skills', 'tests', 'notebooks', 'workbooks', 'blocks', 'apps', 'dashboards', 'semantic-layer', 'data']) {
|
|
4578
5093
|
const watchDir = join(projectRoot, dir);
|
|
4579
5094
|
if (!existsSync(watchDir))
|
|
4580
5095
|
continue;
|
|
4581
5096
|
try {
|
|
4582
|
-
watch(watchDir, { persistent: false, recursive: true }, (eventType, filename) => {
|
|
5097
|
+
const watcher = watch(watchDir, { persistent: false, recursive: true }, (eventType, filename) => {
|
|
5098
|
+
if (runtimeClosing)
|
|
5099
|
+
return;
|
|
4583
5100
|
if (!filename)
|
|
4584
5101
|
return;
|
|
4585
5102
|
const path = `${dir}/${filename}`;
|
|
@@ -4594,11 +5111,7 @@ export async function startLocalServer(opts) {
|
|
|
4594
5111
|
}
|
|
4595
5112
|
if (['domains', 'skills', 'tests', 'blocks', 'apps', 'notebooks'].includes(dir)) {
|
|
4596
5113
|
invalidateAgentProjectState(projectRoot);
|
|
4597
|
-
|
|
4598
|
-
clearTimeout(sourceRefreshTimer);
|
|
4599
|
-
sourceRefreshTimer = setTimeout(() => {
|
|
4600
|
-
void refreshUnifiedProjectIndexes(projectRoot).catch(() => undefined);
|
|
4601
|
-
}, 250);
|
|
5114
|
+
scheduleProjectRefresh(`file:${path}`);
|
|
4602
5115
|
}
|
|
4603
5116
|
// Hot-reload semantic layer on change and notify frontend
|
|
4604
5117
|
if (dir === 'semantic-layer') {
|
|
@@ -4616,21 +5129,20 @@ export async function startLocalServer(opts) {
|
|
|
4616
5129
|
}).catch(() => { });
|
|
4617
5130
|
}
|
|
4618
5131
|
});
|
|
5132
|
+
projectWatchers.push(watcher);
|
|
4619
5133
|
}
|
|
4620
5134
|
catch { /* dir not watchable */ }
|
|
4621
5135
|
}
|
|
4622
5136
|
const configuredDbtManifest = resolveDbtManifestPath(projectRoot, projectConfig);
|
|
4623
5137
|
if (configuredDbtManifest && existsSync(dirname(configuredDbtManifest))) {
|
|
4624
5138
|
try {
|
|
4625
|
-
watch(dirname(configuredDbtManifest), { persistent: false }, (_eventType, filename) => {
|
|
5139
|
+
const watcher = watch(dirname(configuredDbtManifest), { persistent: false }, (_eventType, filename) => {
|
|
5140
|
+
if (runtimeClosing)
|
|
5141
|
+
return;
|
|
4626
5142
|
if (!filename || !['manifest.json', 'catalog.json', 'semantic_manifest.json', 'run_results.json'].includes(String(filename)))
|
|
4627
5143
|
return;
|
|
4628
5144
|
invalidateAgentProjectState(projectRoot);
|
|
4629
|
-
|
|
4630
|
-
clearTimeout(sourceRefreshTimer);
|
|
4631
|
-
sourceRefreshTimer = setTimeout(() => {
|
|
4632
|
-
void refreshUnifiedProjectIndexes(projectRoot).catch(() => undefined);
|
|
4633
|
-
}, 250);
|
|
5145
|
+
scheduleProjectRefresh(`dbt:${String(filename)}`);
|
|
4634
5146
|
const payload = JSON.stringify({ type: 'dbt-artifacts-changed', path: String(filename) });
|
|
4635
5147
|
for (const client of sseClients) {
|
|
4636
5148
|
try {
|
|
@@ -4652,6 +5164,7 @@ export async function startLocalServer(opts) {
|
|
|
4652
5164
|
}
|
|
4653
5165
|
}).catch(() => { });
|
|
4654
5166
|
});
|
|
5167
|
+
projectWatchers.push(watcher);
|
|
4655
5168
|
}
|
|
4656
5169
|
catch { /* dbt artifact directory not watchable */ }
|
|
4657
5170
|
}
|
|
@@ -4734,11 +5247,15 @@ export async function startLocalServer(opts) {
|
|
|
4734
5247
|
result: normalizeQueryResult(result),
|
|
4735
5248
|
chartConfig: plan?.chartConfig ?? validation.chartConfig ?? null,
|
|
4736
5249
|
invocation,
|
|
5250
|
+
tests: plan?.tests ?? [],
|
|
4737
5251
|
};
|
|
4738
5252
|
};
|
|
4739
|
-
const runBlockStudioTestSummary = async (source, targetConnection) => {
|
|
4740
|
-
const activeConnection = requireActiveConnection(targetConnection);
|
|
5253
|
+
const runBlockStudioTestSummary = async (source, targetConnection, executedPreview) => {
|
|
4741
5254
|
const start = Date.now();
|
|
5255
|
+
if (executedPreview) {
|
|
5256
|
+
return evaluateBlockStudioTests(executedPreview.tests, executedPreview.result.rows, executedPreview.result.columns, start);
|
|
5257
|
+
}
|
|
5258
|
+
const activeConnection = requireActiveConnection(targetConnection);
|
|
4742
5259
|
const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer, projectRoot);
|
|
4743
5260
|
// Run tests against the SAME SQL the preview runs: for a semantic block with a
|
|
4744
5261
|
// pre-compiled query, that's the query (not a recompiled metric), so the test's
|
|
@@ -4783,40 +5300,7 @@ export async function startLocalServer(opts) {
|
|
|
4783
5300
|
const columns = Array.isArray(rawResult?.columns)
|
|
4784
5301
|
? rawResult.columns.map((column) => typeof column === 'string' ? column : column?.name ?? String(column))
|
|
4785
5302
|
: [];
|
|
4786
|
-
|
|
4787
|
-
let passed = 0;
|
|
4788
|
-
let failed = 0;
|
|
4789
|
-
for (const test of tests) {
|
|
4790
|
-
const name = `assert ${test.field} ${test.operator} ${formatBlockStudioExpected(test.expected)}`;
|
|
4791
|
-
let actual;
|
|
4792
|
-
if (test.field === 'row_count') {
|
|
4793
|
-
actual = rows.length;
|
|
4794
|
-
}
|
|
4795
|
-
else if (!columns.includes(test.field)) {
|
|
4796
|
-
assertions.push({ name, passed: false, expected: test.expected, error: `Column '${test.field}' not found in results` });
|
|
4797
|
-
failed += 1;
|
|
4798
|
-
continue;
|
|
4799
|
-
}
|
|
4800
|
-
else {
|
|
4801
|
-
actual = rows[0]?.[test.field];
|
|
4802
|
-
}
|
|
4803
|
-
const ok = compareBlockStudioValues(actual, test.operator, test.expected);
|
|
4804
|
-
if (ok) {
|
|
4805
|
-
assertions.push({ name, passed: true, actual, expected: test.expected });
|
|
4806
|
-
passed += 1;
|
|
4807
|
-
}
|
|
4808
|
-
else {
|
|
4809
|
-
assertions.push({
|
|
4810
|
-
name,
|
|
4811
|
-
passed: false,
|
|
4812
|
-
actual,
|
|
4813
|
-
expected: test.expected,
|
|
4814
|
-
error: `${String(actual)} ${test.operator} ${formatBlockStudioExpected(test.expected)} is false`,
|
|
4815
|
-
});
|
|
4816
|
-
failed += 1;
|
|
4817
|
-
}
|
|
4818
|
-
}
|
|
4819
|
-
return { passed, failed, skipped: 0, duration: Date.now() - start, assertions, runAt: new Date() };
|
|
5303
|
+
return evaluateBlockStudioTests(tests, rows, columns, start);
|
|
4820
5304
|
};
|
|
4821
5305
|
const saveDqlGenerationDraft = (importId, candidate) => saveDqlGenerationDraftForProject(projectRoot, importId, candidate);
|
|
4822
5306
|
const createDqlGenerationSessionFromBody = async (body) => {
|
|
@@ -4875,7 +5359,9 @@ export async function startLocalServer(opts) {
|
|
|
4875
5359
|
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4876
5360
|
}
|
|
4877
5361
|
try {
|
|
4878
|
-
testResults =
|
|
5362
|
+
testResults = preview
|
|
5363
|
+
? await runBlockStudioTestSummary(source, undefined, preview)
|
|
5364
|
+
: await runBlockStudioTestSummary(source);
|
|
4879
5365
|
}
|
|
4880
5366
|
catch (error) {
|
|
4881
5367
|
testResults = {
|
|
@@ -5032,15 +5518,24 @@ export async function startLocalServer(opts) {
|
|
|
5032
5518
|
writeHead(status, { ...headers, ...validatorHeaders });
|
|
5033
5519
|
return end(raw);
|
|
5034
5520
|
}
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5521
|
+
gzip(raw, (error, gz) => {
|
|
5522
|
+
if (response.destroyed || response.writableEnded)
|
|
5523
|
+
return;
|
|
5524
|
+
if (error) {
|
|
5525
|
+
writeHead(status, { ...headers, ...validatorHeaders });
|
|
5526
|
+
end(raw);
|
|
5527
|
+
return;
|
|
5528
|
+
}
|
|
5529
|
+
writeHead(status, {
|
|
5530
|
+
...headers,
|
|
5531
|
+
...validatorHeaders,
|
|
5532
|
+
'Content-Encoding': 'gzip',
|
|
5533
|
+
'Content-Length': gz.byteLength,
|
|
5534
|
+
Vary: 'Accept-Encoding, Origin',
|
|
5535
|
+
});
|
|
5536
|
+
end(gz);
|
|
5042
5537
|
});
|
|
5043
|
-
return
|
|
5538
|
+
return response;
|
|
5044
5539
|
});
|
|
5045
5540
|
return rawResponse;
|
|
5046
5541
|
};
|
|
@@ -5059,7 +5554,7 @@ export async function startLocalServer(opts) {
|
|
|
5059
5554
|
res.setHeader('Access-Control-Allow-Origin', requestOrigin);
|
|
5060
5555
|
res.setHeader('Vary', 'Origin');
|
|
5061
5556
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
5062
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
5557
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Idempotency-Key');
|
|
5063
5558
|
if (!originAllowed && path.startsWith('/api/')) {
|
|
5064
5559
|
res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5065
5560
|
res.end(serializeJSON({ error: 'Origin is not allowed.' }));
|
|
@@ -5094,6 +5589,40 @@ export async function startLocalServer(opts) {
|
|
|
5094
5589
|
res.end(serializeJSON({ status: 'ok', version: runtimeVersion, versionStatus, retrievalHealth }));
|
|
5095
5590
|
return;
|
|
5096
5591
|
}
|
|
5592
|
+
if (req.method === 'GET' && path === '/api/operations/events') {
|
|
5593
|
+
res.writeHead(200, {
|
|
5594
|
+
'Content-Type': 'text/event-stream',
|
|
5595
|
+
'Cache-Control': 'no-cache',
|
|
5596
|
+
'Connection': 'keep-alive',
|
|
5597
|
+
'X-Accel-Buffering': 'no',
|
|
5598
|
+
});
|
|
5599
|
+
res.write(': connected\n\n');
|
|
5600
|
+
operationSseClients.add(res);
|
|
5601
|
+
for (const operation of operationCoordinator.list(20).reverse()) {
|
|
5602
|
+
res.write(`event: operation\ndata: ${serializeJSON(operation)}\n\n`);
|
|
5603
|
+
}
|
|
5604
|
+
req.on('close', () => operationSseClients.delete(res));
|
|
5605
|
+
return;
|
|
5606
|
+
}
|
|
5607
|
+
if (req.method === 'GET' && path === '/api/operations') {
|
|
5608
|
+
const limit = Number(url.searchParams.get('limit') ?? 50);
|
|
5609
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5610
|
+
res.end(serializeJSON({ operations: operationCoordinator.list(limit) }));
|
|
5611
|
+
return;
|
|
5612
|
+
}
|
|
5613
|
+
const operationMatch = path.match(/^\/api\/operations\/([^/]+)$/);
|
|
5614
|
+
if (operationMatch && req.method === 'GET') {
|
|
5615
|
+
const operation = operationCoordinator.get(decodeURIComponent(operationMatch[1]));
|
|
5616
|
+
res.writeHead(operation ? 200 : 404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5617
|
+
res.end(serializeJSON(operation ?? { error: 'Operation not found.' }));
|
|
5618
|
+
return;
|
|
5619
|
+
}
|
|
5620
|
+
if (operationMatch && req.method === 'DELETE') {
|
|
5621
|
+
const operation = operationCoordinator.cancel(decodeURIComponent(operationMatch[1]));
|
|
5622
|
+
res.writeHead(operation ? 200 : 404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5623
|
+
res.end(serializeJSON(operation ?? { error: 'Operation not found.' }));
|
|
5624
|
+
return;
|
|
5625
|
+
}
|
|
5097
5626
|
if (req.method === 'GET' && path === '/api/onboarding/launch') {
|
|
5098
5627
|
const requestId = apiRequestId('onboarding-launch');
|
|
5099
5628
|
const prefs = readUserPrefs(userPrefsPath);
|
|
@@ -6018,13 +6547,14 @@ export async function startLocalServer(opts) {
|
|
|
6018
6547
|
return;
|
|
6019
6548
|
}
|
|
6020
6549
|
const sourceSql = repairableSqlFromAgentRun(sourceRun);
|
|
6550
|
+
const sourceDqlArtifact = repairableDqlArtifactFromAgentRun(sourceRun);
|
|
6021
6551
|
const retainedFailure = warehouseFailureFromAgentRun(sourceRun);
|
|
6022
|
-
if (!sourceSql || !analyticalFailureAllowsDeterministicRetry(retainedFailure)) {
|
|
6552
|
+
if ((!sourceSql && !sourceDqlArtifact) || !analyticalFailureAllowsDeterministicRetry(retainedFailure)) {
|
|
6023
6553
|
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6024
6554
|
res.end(serializeJSON({
|
|
6025
|
-
error: sourceSql
|
|
6555
|
+
error: sourceSql || sourceDqlArtifact
|
|
6026
6556
|
? 'This failure requires authorized access or a policy change and cannot be repaired by AI.'
|
|
6027
|
-
: 'This run has no retained SQL statement to repair.',
|
|
6557
|
+
: 'This run has no retained SQL statement or editable DQL artifact to repair.',
|
|
6028
6558
|
...(retainedFailure ? { warehouseFailure: retainedFailure } : {}),
|
|
6029
6559
|
}));
|
|
6030
6560
|
return;
|
|
@@ -6050,12 +6580,27 @@ export async function startLocalServer(opts) {
|
|
|
6050
6580
|
: projectConfig.defaultConnectionName;
|
|
6051
6581
|
let repaired;
|
|
6052
6582
|
try {
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6583
|
+
// Prefer the editable DQL contract even when the failed run also
|
|
6584
|
+
// retained compiled SQL. This keeps parameters, visualization, tests,
|
|
6585
|
+
// and the corrected wrapper attached to the repaired Ask result.
|
|
6586
|
+
repaired = sourceDqlArtifact
|
|
6587
|
+
? await executeBoundedDqlRepair({
|
|
6588
|
+
question: sourceRun.question,
|
|
6589
|
+
source: sourceDqlArtifact.source,
|
|
6590
|
+
failure: retainedFailure?.redactedMessage
|
|
6591
|
+
?? sourceRun.diagnosticReceipt?.failure?.message
|
|
6592
|
+
?? sourceRun.summary,
|
|
6593
|
+
targetConnection,
|
|
6594
|
+
targetConnectionName,
|
|
6595
|
+
parameters: sourceDqlArtifact.parameterValues,
|
|
6596
|
+
artifactName: sourceDqlArtifact.name,
|
|
6597
|
+
})
|
|
6598
|
+
: await executeBoundedSqlRepair({
|
|
6599
|
+
question: sourceRun.question,
|
|
6600
|
+
sourceSql: sourceSql,
|
|
6601
|
+
targetConnection,
|
|
6602
|
+
targetConnectionName,
|
|
6603
|
+
});
|
|
6059
6604
|
}
|
|
6060
6605
|
catch (error) {
|
|
6061
6606
|
const repairError = error;
|
|
@@ -6078,6 +6623,8 @@ export async function startLocalServer(opts) {
|
|
|
6078
6623
|
...(preview ? { columnFormats: agentColumnDisplayFormats(projectRoot, preview.columns) } : {}),
|
|
6079
6624
|
});
|
|
6080
6625
|
const sourceFailure = analyticalFailedRunFromAgentRun(sourceRun)?.failure;
|
|
6626
|
+
const presentationContext = repairPresentationContextFromAgentRun(sourceRun);
|
|
6627
|
+
const executionResultFingerprint = result.executionReceipt?.resultFingerprint;
|
|
6081
6628
|
const artifact = agentRunArtifact('answer', 'Repaired analytical answer', {
|
|
6082
6629
|
kind: 'uncertified',
|
|
6083
6630
|
certification: 'analyst_review_required',
|
|
@@ -6085,13 +6632,31 @@ export async function startLocalServer(opts) {
|
|
|
6085
6632
|
trustLabel: 'AI-generated · review required',
|
|
6086
6633
|
answer: synthesis.text,
|
|
6087
6634
|
text: synthesis.text,
|
|
6635
|
+
...presentationContext,
|
|
6088
6636
|
result,
|
|
6089
6637
|
proposedSql: repairedSql,
|
|
6090
6638
|
sql: result.sql ?? repairedSql,
|
|
6091
6639
|
dqlArtifact: result.dqlArtifact,
|
|
6640
|
+
diagnosticReceipt: {
|
|
6641
|
+
version: 1,
|
|
6642
|
+
phase: 'run.completed',
|
|
6643
|
+
route: 'generated_answer',
|
|
6644
|
+
repair: {
|
|
6645
|
+
attempt: 1,
|
|
6646
|
+
mode: repairMode,
|
|
6647
|
+
sourceRunId: sourceRun.id,
|
|
6648
|
+
targetPreserved: true,
|
|
6649
|
+
readOnly: true,
|
|
6650
|
+
},
|
|
6651
|
+
execution: {
|
|
6652
|
+
rowCount: result.rowCount,
|
|
6653
|
+
...(executionResultFingerprint ? { resultFingerprint: executionResultFingerprint } : {}),
|
|
6654
|
+
},
|
|
6655
|
+
},
|
|
6092
6656
|
previousAttempt: {
|
|
6093
6657
|
sourceRunId: sourceRun.id,
|
|
6094
|
-
sql: sourceSql,
|
|
6658
|
+
...(sourceSql ? { sql: sourceSql } : {}),
|
|
6659
|
+
...(sourceDqlArtifact?.source ? { dql: sourceDqlArtifact.source } : {}),
|
|
6095
6660
|
failure: retainedFailure ?? firstFailure,
|
|
6096
6661
|
},
|
|
6097
6662
|
repair: { attempt: 1, mode: repairMode },
|
|
@@ -6274,39 +6839,83 @@ export async function startLocalServer(opts) {
|
|
|
6274
6839
|
const wantsStream = url.searchParams.get('stream') === '1' || url.searchParams.get('stream') === 'true';
|
|
6275
6840
|
const runId = parsed.request.runId;
|
|
6276
6841
|
const runController = new AbortController();
|
|
6277
|
-
parsed.request.signal = AbortSignal.any([
|
|
6278
|
-
runController.signal,
|
|
6279
|
-
AbortSignal.timeout(agentRunDeadlineMs(parsed.request, process.env, getActiveProvider(projectRoot))),
|
|
6280
|
-
]);
|
|
6281
6842
|
if (runId)
|
|
6282
6843
|
activeAgentRunControllers.set(runId, runController);
|
|
6844
|
+
let streamConnected = true;
|
|
6845
|
+
res.on('close', () => { streamConnected = false; });
|
|
6846
|
+
const writeStream = (event, data) => {
|
|
6847
|
+
if (!streamConnected || res.destroyed || res.writableEnded)
|
|
6848
|
+
return;
|
|
6849
|
+
try {
|
|
6850
|
+
writeAgentRunSse(res, event, data);
|
|
6851
|
+
}
|
|
6852
|
+
catch {
|
|
6853
|
+
streamConnected = false;
|
|
6854
|
+
}
|
|
6855
|
+
};
|
|
6856
|
+
if (wantsStream) {
|
|
6857
|
+
res.writeHead(200, {
|
|
6858
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
6859
|
+
'Cache-Control': 'no-cache',
|
|
6860
|
+
'Connection': 'keep-alive',
|
|
6861
|
+
'X-Accel-Buffering': 'no',
|
|
6862
|
+
});
|
|
6863
|
+
}
|
|
6864
|
+
const operation = runId ? operationCoordinator.create({
|
|
6865
|
+
type: 'agent_run',
|
|
6866
|
+
scope: `agent-run:${runId}`,
|
|
6867
|
+
resourceRevision: parsed.request.threadId,
|
|
6868
|
+
message: 'AI request accepted. You can change pages while it runs.',
|
|
6869
|
+
cancellable: true,
|
|
6870
|
+
}) : null;
|
|
6871
|
+
if (wantsStream)
|
|
6872
|
+
writeStream('agent-run-accepted', { runId, operationId: operation?.id });
|
|
6873
|
+
let completedRun;
|
|
6874
|
+
let runError;
|
|
6283
6875
|
try {
|
|
6876
|
+
const executeRun = async (operationSignal, report) => {
|
|
6877
|
+
parsed.request.signal = AbortSignal.any([
|
|
6878
|
+
runController.signal,
|
|
6879
|
+
...(operationSignal ? [operationSignal] : []),
|
|
6880
|
+
AbortSignal.timeout(agentRunDeadlineMs(parsed.request, process.env, getActiveProvider(projectRoot))),
|
|
6881
|
+
]);
|
|
6882
|
+
report?.({ phase: 'reasoning', progress: 8, message: 'Grounding the request and selecting the governed route.' });
|
|
6883
|
+
try {
|
|
6884
|
+
const run = await agentRunEngine.run(parsed.request, (event) => {
|
|
6885
|
+
if (wantsStream)
|
|
6886
|
+
writeStream('agent-run-event', event);
|
|
6887
|
+
report?.({ phase: event.type, progress: 45, message: event.message });
|
|
6888
|
+
}, (delta) => {
|
|
6889
|
+
if (wantsStream)
|
|
6890
|
+
writeStream('agent-run-answer-delta', { runId: parsed.request.runId, delta });
|
|
6891
|
+
});
|
|
6892
|
+
completedRun = run;
|
|
6893
|
+
recordConversationTurn(conversationStore, parsed.request.threadId, run);
|
|
6894
|
+
return { runId: run.id, threadId: parsed.request.threadId, status: run.status };
|
|
6895
|
+
}
|
|
6896
|
+
catch (error) {
|
|
6897
|
+
runError = error;
|
|
6898
|
+
throw error;
|
|
6899
|
+
}
|
|
6900
|
+
};
|
|
6901
|
+
if (operation)
|
|
6902
|
+
await operationCoordinator.run(operation.id, (signal, report) => executeRun(signal, report));
|
|
6903
|
+
else
|
|
6904
|
+
await executeRun();
|
|
6905
|
+
if (runError)
|
|
6906
|
+
throw runError;
|
|
6907
|
+
if (!completedRun)
|
|
6908
|
+
throw new Error('The AI request ended before producing a stored run.');
|
|
6284
6909
|
if (wantsStream) {
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
});
|
|
6291
|
-
const run = await agentRunEngine.run(parsed.request, (event) => {
|
|
6292
|
-
writeAgentRunSse(res, 'agent-run-event', event);
|
|
6293
|
-
}, (delta) => {
|
|
6294
|
-
writeAgentRunSse(res, 'agent-run-answer-delta', { runId: parsed.request.runId, delta });
|
|
6295
|
-
});
|
|
6296
|
-
recordConversationTurn(conversationStore, parsed.request.threadId, run);
|
|
6297
|
-
// The terminal frame carried the whole stored run — 2.4 MB on a real
|
|
6298
|
-
// question, of which the renderable part was ~3 KB. The rest was the
|
|
6299
|
-
// same artifacts repeated four times over. The client renders from
|
|
6300
|
-
// the projection and fetches the full record from the run-state
|
|
6301
|
-
// route only if something needs it.
|
|
6302
|
-
writeAgentRunSse(res, 'agent-run-complete', slimAgentRunForTransport(run));
|
|
6303
|
-
res.end();
|
|
6910
|
+
// The complete stored run stays in the run-state route; the live
|
|
6911
|
+
// stream ships only the renderable transport projection.
|
|
6912
|
+
writeStream('agent-run-complete', slimAgentRunForTransport(completedRun));
|
|
6913
|
+
if (streamConnected && !res.writableEnded)
|
|
6914
|
+
res.end();
|
|
6304
6915
|
return;
|
|
6305
6916
|
}
|
|
6306
|
-
const run = await agentRunEngine.run(parsed.request);
|
|
6307
|
-
recordConversationTurn(conversationStore, parsed.request.threadId, run);
|
|
6308
6917
|
res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6309
|
-
res.end(serializeJSON({ run }));
|
|
6918
|
+
res.end(serializeJSON({ run: completedRun }));
|
|
6310
6919
|
}
|
|
6311
6920
|
finally {
|
|
6312
6921
|
if (runId)
|
|
@@ -6316,8 +6925,10 @@ export async function startLocalServer(opts) {
|
|
|
6316
6925
|
catch (error) {
|
|
6317
6926
|
const message = error instanceof Error ? error.message : String(error);
|
|
6318
6927
|
if (res.headersSent) {
|
|
6319
|
-
|
|
6320
|
-
|
|
6928
|
+
if (!res.destroyed && !res.writableEnded) {
|
|
6929
|
+
writeAgentRunSse(res, 'agent-run-error', { error: message });
|
|
6930
|
+
res.end();
|
|
6931
|
+
}
|
|
6321
6932
|
}
|
|
6322
6933
|
else {
|
|
6323
6934
|
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -7989,9 +8600,14 @@ export async function startLocalServer(opts) {
|
|
|
7989
8600
|
? body.variables
|
|
7990
8601
|
: {};
|
|
7991
8602
|
const dashboardVariables = dashboardRuntimeVariables(loaded.dashboard, variables);
|
|
8603
|
+
const requestedTileId = typeof body.tileId === 'string' && body.tileId.trim()
|
|
8604
|
+
? body.tileId.trim()
|
|
8605
|
+
: undefined;
|
|
7992
8606
|
const tiles = [];
|
|
7993
8607
|
let localApps = null;
|
|
7994
8608
|
for (const item of loaded.dashboard.layout.items) {
|
|
8609
|
+
if (requestedTileId && item.i !== requestedTileId)
|
|
8610
|
+
continue;
|
|
7995
8611
|
if (item.text) {
|
|
7996
8612
|
tiles.push({
|
|
7997
8613
|
tileId: item.i,
|
|
@@ -8079,9 +8695,194 @@ export async function startLocalServer(opts) {
|
|
|
8079
8695
|
kind: 'ai_pin',
|
|
8080
8696
|
name: pin.title,
|
|
8081
8697
|
},
|
|
8698
|
+
artifact: {
|
|
8699
|
+
version: 1,
|
|
8700
|
+
sourceKind: 'ai_pin',
|
|
8701
|
+
name: pinArtifact?.name ?? pin.title,
|
|
8702
|
+
...(pinArtifact?.sourcePath ? { sourcePath: pinArtifact.sourcePath } : {}),
|
|
8703
|
+
...(pinArtifact?.source ? { dql: pinArtifact.source.slice(0, 40_000) } : {}),
|
|
8704
|
+
...(pinArtifact?.compiledSql || pin.sql ? { sql: (pinArtifact?.compiledSql ?? pin.sql).slice(0, 40_000) } : {}),
|
|
8705
|
+
trustState: pinArtifact?.trustState === 'certified' || pin.certification === 'certified'
|
|
8706
|
+
? 'certified'
|
|
8707
|
+
: 'review_required',
|
|
8708
|
+
explanation: [
|
|
8709
|
+
'This result comes from a saved App insight.',
|
|
8710
|
+
pinArtifact ? 'The DQL artifact was executed again with the current App parameters.' : 'The saved bounded result is displayed with its original trust state.',
|
|
8711
|
+
],
|
|
8712
|
+
executionTarget: executionTargetDescriptor(body),
|
|
8713
|
+
},
|
|
8082
8714
|
});
|
|
8083
8715
|
continue;
|
|
8084
8716
|
}
|
|
8717
|
+
if (item.draftAnalysis) {
|
|
8718
|
+
let draftSource;
|
|
8719
|
+
let draftAppRelativePath;
|
|
8720
|
+
let draftParameters = {};
|
|
8721
|
+
let draftTargetConnection;
|
|
8722
|
+
let draftRepairPlan;
|
|
8723
|
+
try {
|
|
8724
|
+
const draftPath = join(loaded.appDir, item.draftAnalysis.ref);
|
|
8725
|
+
draftAppRelativePath = relative(loaded.appDir, draftPath);
|
|
8726
|
+
if (draftAppRelativePath.startsWith('..') || draftAppRelativePath.startsWith('/')) {
|
|
8727
|
+
throw new Error('App-scoped analysis reference escapes the App directory.');
|
|
8728
|
+
}
|
|
8729
|
+
draftSource = readFileSync(draftPath, 'utf-8');
|
|
8730
|
+
const fingerprint = `sha256:${createHash('sha256').update(draftSource).digest('hex')}`;
|
|
8731
|
+
if (fingerprint !== item.draftAnalysis.artifactFingerprint) {
|
|
8732
|
+
throw new Error('App-scoped analysis changed after approval. Review the draft again.');
|
|
8733
|
+
}
|
|
8734
|
+
draftParameters = dashboardTileParameterValues({
|
|
8735
|
+
item,
|
|
8736
|
+
dashboardValues: dashboardVariables,
|
|
8737
|
+
requestValues: variables,
|
|
8738
|
+
});
|
|
8739
|
+
draftTargetConnection = isConnectionConfig(body.connection)
|
|
8740
|
+
? body.connection
|
|
8741
|
+
: connection ?? undefined;
|
|
8742
|
+
draftRepairPlan = buildExecutionPlan({ id: item.i, type: 'dql', source: draftSource, title: item.title ?? item.i }, {
|
|
8743
|
+
semanticLayer,
|
|
8744
|
+
driver: draftTargetConnection?.driver ?? 'duckdb',
|
|
8745
|
+
parameters: draftParameters,
|
|
8746
|
+
});
|
|
8747
|
+
let result;
|
|
8748
|
+
let repair;
|
|
8749
|
+
let repairedSql;
|
|
8750
|
+
let repairedDql;
|
|
8751
|
+
try {
|
|
8752
|
+
result = await executeDqlArtifactSourceForAgent(draftSource, { name: item.title ?? item.i, path: draftAppRelativePath }, { question: item.title, parameters: draftParameters });
|
|
8753
|
+
}
|
|
8754
|
+
catch (executionError) {
|
|
8755
|
+
const repaired = await repairFailedAppTileExecution({
|
|
8756
|
+
source: 'draft_analysis',
|
|
8757
|
+
question: item.title ?? item.i,
|
|
8758
|
+
sourceSql: draftRepairPlan?.sql,
|
|
8759
|
+
sourceDql: draftSource,
|
|
8760
|
+
artifactName: item.title ?? item.i,
|
|
8761
|
+
error: executionError,
|
|
8762
|
+
targetConnection: draftTargetConnection,
|
|
8763
|
+
targetConnectionName: projectConfig.defaultConnectionName,
|
|
8764
|
+
sqlParams: draftRepairPlan?.sqlParams,
|
|
8765
|
+
variables: { ...(draftRepairPlan?.variables ?? {}), ...draftParameters },
|
|
8766
|
+
});
|
|
8767
|
+
if (!repaired.ok) {
|
|
8768
|
+
tiles.push({
|
|
8769
|
+
tileId: item.i,
|
|
8770
|
+
status: 'error',
|
|
8771
|
+
tileType: 'draftAnalysis',
|
|
8772
|
+
title: item.title ?? item.i,
|
|
8773
|
+
error: executionError instanceof Error ? executionError.message : String(executionError),
|
|
8774
|
+
trustState: 'review_required',
|
|
8775
|
+
repair: repaired.repair,
|
|
8776
|
+
artifact: {
|
|
8777
|
+
version: 1,
|
|
8778
|
+
sourceKind: 'draft_analysis',
|
|
8779
|
+
name: item.title ?? item.i,
|
|
8780
|
+
sourcePath: draftAppRelativePath,
|
|
8781
|
+
dql: draftSource.slice(0, 40_000),
|
|
8782
|
+
...(draftRepairPlan?.sql ? { sql: draftRepairPlan.sql.slice(0, 40_000) } : {}),
|
|
8783
|
+
trustState: 'review_required',
|
|
8784
|
+
explanation: ['This App-scoped analysis remains review-required.', 'Automatic repair could not produce a safe executable result.'],
|
|
8785
|
+
executionTarget: executionTargetDescriptor(body),
|
|
8786
|
+
},
|
|
8787
|
+
});
|
|
8788
|
+
continue;
|
|
8789
|
+
}
|
|
8790
|
+
result = repaired.result;
|
|
8791
|
+
repair = repaired.repair;
|
|
8792
|
+
repairedSql = repaired.repairedSql;
|
|
8793
|
+
repairedDql = repaired.repairedDql;
|
|
8794
|
+
}
|
|
8795
|
+
tiles.push({
|
|
8796
|
+
tileId: item.i,
|
|
8797
|
+
status: 'ok',
|
|
8798
|
+
tileType: 'draftAnalysis',
|
|
8799
|
+
title: item.title ?? item.i,
|
|
8800
|
+
viz: item.viz,
|
|
8801
|
+
chartConfig: mergeDashboardChartConfig(result.chartConfig, item),
|
|
8802
|
+
result,
|
|
8803
|
+
trustState: 'review_required',
|
|
8804
|
+
citation: { kind: repair ? 'app_draft_analysis_repaired' : 'app_draft_analysis', name: item.title ?? item.i, path: draftAppRelativePath },
|
|
8805
|
+
...(repair ? { repair } : {}),
|
|
8806
|
+
artifact: {
|
|
8807
|
+
version: 1,
|
|
8808
|
+
sourceKind: 'draft_analysis',
|
|
8809
|
+
name: item.title ?? item.i,
|
|
8810
|
+
sourcePath: draftAppRelativePath,
|
|
8811
|
+
dql: (repairedDql ?? draftSource).slice(0, 40_000),
|
|
8812
|
+
...(repairedSql ?? draftRepairPlan?.sql ? { sql: (repairedSql ?? draftRepairPlan.sql).slice(0, 40_000) } : {}),
|
|
8813
|
+
trustState: 'review_required',
|
|
8814
|
+
explanation: [
|
|
8815
|
+
'This is private App analysis and remains review-required.',
|
|
8816
|
+
repair ? 'A bounded repair ran on the original data target without changing the saved source file.' : 'The saved App-scoped DQL was executed with the current filters.',
|
|
8817
|
+
],
|
|
8818
|
+
executionTarget: executionTargetDescriptor(body),
|
|
8819
|
+
},
|
|
8820
|
+
});
|
|
8821
|
+
}
|
|
8822
|
+
catch (err) {
|
|
8823
|
+
const repaired = await repairFailedAppTileExecution({
|
|
8824
|
+
source: 'draft_analysis',
|
|
8825
|
+
question: item.title ?? item.i,
|
|
8826
|
+
sourceSql: draftRepairPlan?.sql,
|
|
8827
|
+
sourceDql: draftSource,
|
|
8828
|
+
artifactName: item.title ?? item.i,
|
|
8829
|
+
error: err,
|
|
8830
|
+
targetConnection: draftTargetConnection,
|
|
8831
|
+
targetConnectionName: projectConfig.defaultConnectionName,
|
|
8832
|
+
sqlParams: draftRepairPlan?.sqlParams,
|
|
8833
|
+
variables: { ...(draftRepairPlan?.variables ?? {}), ...draftParameters },
|
|
8834
|
+
});
|
|
8835
|
+
if (repaired.ok && draftSource) {
|
|
8836
|
+
tiles.push({
|
|
8837
|
+
tileId: item.i,
|
|
8838
|
+
status: 'ok',
|
|
8839
|
+
tileType: 'draftAnalysis',
|
|
8840
|
+
title: item.title ?? item.i,
|
|
8841
|
+
viz: item.viz,
|
|
8842
|
+
chartConfig: mergeDashboardChartConfig(repaired.result.chartConfig, item),
|
|
8843
|
+
result: repaired.result,
|
|
8844
|
+
trustState: 'review_required',
|
|
8845
|
+
citation: { kind: 'app_draft_analysis_repaired', name: item.title ?? item.i, path: draftAppRelativePath },
|
|
8846
|
+
repair: repaired.repair,
|
|
8847
|
+
artifact: {
|
|
8848
|
+
version: 1,
|
|
8849
|
+
sourceKind: 'draft_analysis',
|
|
8850
|
+
name: item.title ?? item.i,
|
|
8851
|
+
...(draftAppRelativePath ? { sourcePath: draftAppRelativePath } : {}),
|
|
8852
|
+
dql: (repaired.repairedDql ?? draftSource).slice(0, 40_000),
|
|
8853
|
+
sql: repaired.repairedSql.slice(0, 40_000),
|
|
8854
|
+
trustState: 'review_required',
|
|
8855
|
+
explanation: ['A bounded DQL repair executed on the original data target. The saved App analysis was not changed.'],
|
|
8856
|
+
executionTarget: executionTargetDescriptor(body),
|
|
8857
|
+
},
|
|
8858
|
+
});
|
|
8859
|
+
continue;
|
|
8860
|
+
}
|
|
8861
|
+
tiles.push({
|
|
8862
|
+
tileId: item.i,
|
|
8863
|
+
status: 'error',
|
|
8864
|
+
tileType: 'draftAnalysis',
|
|
8865
|
+
title: item.title ?? item.i,
|
|
8866
|
+
error: err instanceof Error ? err.message : String(err),
|
|
8867
|
+
trustState: 'review_required',
|
|
8868
|
+
repair: repaired.repair,
|
|
8869
|
+
...(draftSource ? {
|
|
8870
|
+
artifact: {
|
|
8871
|
+
version: 1,
|
|
8872
|
+
sourceKind: 'draft_analysis',
|
|
8873
|
+
name: item.title ?? item.i,
|
|
8874
|
+
...(draftAppRelativePath ? { sourcePath: draftAppRelativePath } : {}),
|
|
8875
|
+
dql: draftSource.slice(0, 40_000),
|
|
8876
|
+
...(draftRepairPlan?.sql ? { sql: draftRepairPlan.sql.slice(0, 40_000) } : {}),
|
|
8877
|
+
trustState: 'review_required',
|
|
8878
|
+
explanation: ['Automatic repair could not produce a safe executable result. The saved App analysis was not changed.'],
|
|
8879
|
+
executionTarget: executionTargetDescriptor(body),
|
|
8880
|
+
},
|
|
8881
|
+
} : {}),
|
|
8882
|
+
});
|
|
8883
|
+
}
|
|
8884
|
+
continue;
|
|
8885
|
+
}
|
|
8085
8886
|
if (item.semantic) {
|
|
8086
8887
|
try {
|
|
8087
8888
|
if (!semanticLayer)
|
|
@@ -8122,16 +8923,69 @@ export async function startLocalServer(opts) {
|
|
|
8122
8923
|
if (!composed)
|
|
8123
8924
|
throw new Error('The governed semantic query could not be composed.');
|
|
8124
8925
|
const prepared = prepareLocalExecution(composed.sql, targetConnection, projectRoot, projectConfig);
|
|
8125
|
-
|
|
8926
|
+
let normalizedResult;
|
|
8927
|
+
let repair;
|
|
8928
|
+
let executedSql = composed.sql;
|
|
8929
|
+
try {
|
|
8930
|
+
const result = await executor.executeQuery(prepared.sql, [], {}, prepared.connection);
|
|
8931
|
+
normalizedResult = normalizeQueryResult(result);
|
|
8932
|
+
}
|
|
8933
|
+
catch (executionError) {
|
|
8934
|
+
const repaired = await repairFailedAppTileExecution({
|
|
8935
|
+
source: 'semantic_query',
|
|
8936
|
+
question: item.title ?? item.semantic.id,
|
|
8937
|
+
sourceSql: composed.sql,
|
|
8938
|
+
error: executionError,
|
|
8939
|
+
targetConnection,
|
|
8940
|
+
});
|
|
8941
|
+
if (!repaired.ok) {
|
|
8942
|
+
tiles.push({
|
|
8943
|
+
tileId: item.i,
|
|
8944
|
+
status: 'error',
|
|
8945
|
+
tileType: 'semantic',
|
|
8946
|
+
title: item.title ?? item.semantic.id,
|
|
8947
|
+
error: executionError instanceof Error ? executionError.message : String(executionError),
|
|
8948
|
+
trustState: 'review_required',
|
|
8949
|
+
repair: repaired.repair,
|
|
8950
|
+
artifact: {
|
|
8951
|
+
version: 1,
|
|
8952
|
+
sourceKind: 'semantic_query',
|
|
8953
|
+
name: item.title ?? item.semantic.id,
|
|
8954
|
+
sql: composed.sql.slice(0, 40_000),
|
|
8955
|
+
trustState: 'review_required',
|
|
8956
|
+
explanation: ['Automatic repair could not execute the governed semantic query safely.'],
|
|
8957
|
+
executionTarget: executionTargetDescriptor(body),
|
|
8958
|
+
},
|
|
8959
|
+
});
|
|
8960
|
+
continue;
|
|
8961
|
+
}
|
|
8962
|
+
normalizedResult = normalizeQueryResult(repaired.result);
|
|
8963
|
+
repair = repaired.repair;
|
|
8964
|
+
executedSql = repaired.repairedSql;
|
|
8965
|
+
}
|
|
8126
8966
|
tiles.push({
|
|
8127
8967
|
tileId: item.i,
|
|
8128
8968
|
status: 'ok',
|
|
8129
8969
|
tileType: 'semantic',
|
|
8130
8970
|
title: item.title ?? item.semantic.id,
|
|
8131
8971
|
viz: item.viz,
|
|
8132
|
-
result:
|
|
8972
|
+
result: normalizedResult,
|
|
8133
8973
|
trustState: 'review_required',
|
|
8134
|
-
citation: { kind: 'semantic_query', name: item.semantic.id },
|
|
8974
|
+
citation: { kind: repair ? 'semantic_query_repaired' : 'semantic_query', name: item.semantic.id },
|
|
8975
|
+
...(repair ? { repair } : {}),
|
|
8976
|
+
artifact: {
|
|
8977
|
+
version: 1,
|
|
8978
|
+
sourceKind: 'semantic_query',
|
|
8979
|
+
name: item.title ?? item.semantic.id,
|
|
8980
|
+
sql: executedSql.slice(0, 40_000),
|
|
8981
|
+
trustState: 'review_required',
|
|
8982
|
+
explanation: [
|
|
8983
|
+
`Metrics: ${item.semantic.metrics.join(', ') || 'none'}.`,
|
|
8984
|
+
`Dimensions: ${item.semantic.dimensions?.join(', ') || 'none'}.`,
|
|
8985
|
+
repair ? 'The composed SQL needed bounded repair and therefore remains review-required.' : 'The governed semantic intent was composed for the active warehouse and remains review-required until approval.',
|
|
8986
|
+
],
|
|
8987
|
+
executionTarget: executionTargetDescriptor(body),
|
|
8988
|
+
},
|
|
8135
8989
|
});
|
|
8136
8990
|
}
|
|
8137
8991
|
catch (err) {
|
|
@@ -8156,6 +9010,11 @@ export async function startLocalServer(opts) {
|
|
|
8156
9010
|
});
|
|
8157
9011
|
continue;
|
|
8158
9012
|
}
|
|
9013
|
+
let blockSource;
|
|
9014
|
+
let blockTargetConnection;
|
|
9015
|
+
let blockPlan;
|
|
9016
|
+
let blockFilterApplication;
|
|
9017
|
+
let blockInvocation;
|
|
8159
9018
|
try {
|
|
8160
9019
|
assertAppAccess({
|
|
8161
9020
|
app: loaded.app,
|
|
@@ -8163,55 +9022,55 @@ export async function startLocalServer(opts) {
|
|
|
8163
9022
|
level: 'execute',
|
|
8164
9023
|
});
|
|
8165
9024
|
const absBlockPath = join(projectRoot, block.filePath);
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
const tableMapping = await resolveSemanticTableMapping(executor,
|
|
9025
|
+
blockSource = readFileSync(absBlockPath, 'utf-8');
|
|
9026
|
+
blockTargetConnection = requireActiveConnection(isConnectionConfig(body.connection) ? body.connection : connection);
|
|
9027
|
+
const tableMapping = await resolveSemanticTableMapping(executor, blockTargetConnection, semanticLayer, projectRoot);
|
|
8169
9028
|
const boundParameters = dashboardTileParameterValues({
|
|
8170
9029
|
item,
|
|
8171
9030
|
dashboardValues: dashboardVariables,
|
|
8172
9031
|
requestValues: variables,
|
|
8173
9032
|
});
|
|
8174
|
-
|
|
9033
|
+
blockInvocation = prepareBlockInvocation({
|
|
8175
9034
|
block: block.name,
|
|
8176
|
-
source,
|
|
9035
|
+
source: blockSource,
|
|
8177
9036
|
parameters: { ...dashboardVariables, ...boundParameters },
|
|
8178
9037
|
parameterSources: Object.fromEntries(Object.keys(boundParameters).map((name) => [name, 'surface'])),
|
|
8179
9038
|
surface: 'app',
|
|
8180
9039
|
});
|
|
8181
|
-
if (
|
|
9040
|
+
if (blockInvocation.errors.length || blockInvocation.unresolvedParameters.length) {
|
|
8182
9041
|
tiles.push({
|
|
8183
9042
|
tileId: item.i,
|
|
8184
9043
|
status: 'unresolved',
|
|
8185
9044
|
tileType: 'block',
|
|
8186
9045
|
blockId: block.name,
|
|
8187
9046
|
title: item.title ?? block.name,
|
|
8188
|
-
error:
|
|
9047
|
+
error: blockInvocation.errors[0] ?? `Needs values for: ${blockInvocation.unresolvedParameters.join(', ')}`,
|
|
8189
9048
|
invocation: {
|
|
8190
|
-
resolvedParameters:
|
|
8191
|
-
unresolvedParameters:
|
|
8192
|
-
auditId:
|
|
9049
|
+
resolvedParameters: blockInvocation.resolvedParameters,
|
|
9050
|
+
unresolvedParameters: blockInvocation.unresolvedParameters,
|
|
9051
|
+
auditId: blockInvocation.auditId,
|
|
8193
9052
|
},
|
|
8194
9053
|
});
|
|
8195
9054
|
continue;
|
|
8196
9055
|
}
|
|
8197
9056
|
const semanticCompose = semanticLayer
|
|
8198
|
-
? await composeSemanticBlockSqlForRuntime(
|
|
8199
|
-
driver:
|
|
9057
|
+
? await composeSemanticBlockSqlForRuntime(blockSource, semanticLayer, {
|
|
9058
|
+
driver: blockTargetConnection.driver,
|
|
8200
9059
|
tableMapping,
|
|
8201
9060
|
projectRoot,
|
|
8202
9061
|
projectConfig,
|
|
8203
9062
|
detectedProvider: semanticDetectedProvider,
|
|
8204
|
-
parameters:
|
|
9063
|
+
parameters: blockInvocation.values,
|
|
8205
9064
|
})
|
|
8206
9065
|
: null;
|
|
8207
|
-
|
|
9066
|
+
blockPlan = buildExecutionPlan({ id: item.i, type: 'dql', source: blockSource, title: item.title ?? block.name }, {
|
|
8208
9067
|
semanticLayer,
|
|
8209
|
-
driver:
|
|
9068
|
+
driver: blockTargetConnection.driver,
|
|
8210
9069
|
tableMapping,
|
|
8211
|
-
parameters:
|
|
9070
|
+
parameters: blockInvocation.values,
|
|
8212
9071
|
semanticSql: semanticCompose?.sql ?? undefined,
|
|
8213
9072
|
});
|
|
8214
|
-
if (!
|
|
9073
|
+
if (!blockPlan && !semanticCompose?.sql) {
|
|
8215
9074
|
tiles.push({
|
|
8216
9075
|
tileId: item.i,
|
|
8217
9076
|
status: 'error',
|
|
@@ -8220,15 +9079,15 @@ export async function startLocalServer(opts) {
|
|
|
8220
9079
|
});
|
|
8221
9080
|
continue;
|
|
8222
9081
|
}
|
|
8223
|
-
|
|
8224
|
-
sql: semanticCompose?.sql ??
|
|
8225
|
-
sqlParams:
|
|
8226
|
-
variables: { ...(
|
|
9082
|
+
blockFilterApplication = applyDashboardFiltersToBlockExecution({
|
|
9083
|
+
sql: semanticCompose?.sql ?? blockPlan.sql,
|
|
9084
|
+
sqlParams: blockPlan?.sqlParams ?? [],
|
|
9085
|
+
variables: { ...(blockPlan?.variables ?? {}), ...dashboardVariables },
|
|
8227
9086
|
block,
|
|
8228
9087
|
dashboard: loaded.dashboard,
|
|
8229
9088
|
});
|
|
8230
|
-
const prepared = prepareLocalExecution(
|
|
8231
|
-
const result = await executor.executeQuery(prepared.sql,
|
|
9089
|
+
const prepared = prepareLocalExecution(blockFilterApplication.sql, blockTargetConnection, projectRoot, projectConfig);
|
|
9090
|
+
const result = await executor.executeQuery(prepared.sql, blockFilterApplication.sqlParams, runtimeVariables({ ...blockFilterApplication.variables, ...blockInvocation.values }), prepared.connection);
|
|
8232
9091
|
tiles.push({
|
|
8233
9092
|
tileId: item.i,
|
|
8234
9093
|
status: 'ok',
|
|
@@ -8237,22 +9096,37 @@ export async function startLocalServer(opts) {
|
|
|
8237
9096
|
certificationStatus: block.status ?? null,
|
|
8238
9097
|
title: item.title ?? block.name,
|
|
8239
9098
|
viz: item.viz,
|
|
8240
|
-
chartConfig: mergeDashboardChartConfig(
|
|
9099
|
+
chartConfig: mergeDashboardChartConfig(blockPlan?.chartConfig, item),
|
|
8241
9100
|
result: normalizeQueryResult(result),
|
|
8242
9101
|
filters: {
|
|
8243
|
-
applied:
|
|
8244
|
-
skipped:
|
|
9102
|
+
applied: blockFilterApplication.appliedFilters,
|
|
9103
|
+
skipped: blockFilterApplication.skippedFilters,
|
|
8245
9104
|
},
|
|
8246
9105
|
invocation: {
|
|
8247
|
-
resolvedParameters:
|
|
8248
|
-
unresolvedParameters:
|
|
8249
|
-
auditId:
|
|
9106
|
+
resolvedParameters: blockInvocation.resolvedParameters,
|
|
9107
|
+
unresolvedParameters: blockInvocation.unresolvedParameters,
|
|
9108
|
+
auditId: blockInvocation.auditId,
|
|
8250
9109
|
},
|
|
9110
|
+
trustState: block.status === 'certified' ? 'certified' : 'review_required',
|
|
8251
9111
|
citation: {
|
|
8252
9112
|
kind: 'block',
|
|
8253
9113
|
name: block.name,
|
|
8254
9114
|
path: block.filePath,
|
|
8255
9115
|
},
|
|
9116
|
+
artifact: {
|
|
9117
|
+
version: 1,
|
|
9118
|
+
sourceKind: 'certified_block',
|
|
9119
|
+
name: block.name,
|
|
9120
|
+
sourcePath: block.filePath,
|
|
9121
|
+
dql: blockSource.slice(0, 40_000),
|
|
9122
|
+
sql: blockFilterApplication.sql.slice(0, 40_000),
|
|
9123
|
+
trustState: block.status === 'certified' ? 'certified' : 'review_required',
|
|
9124
|
+
explanation: [
|
|
9125
|
+
block.status === 'certified' ? 'This tile executed a certified project block.' : 'This project block is not certified and remains review-required.',
|
|
9126
|
+
'Dashboard filters and typed parameters were bound before read-only execution.',
|
|
9127
|
+
],
|
|
9128
|
+
executionTarget: executionTargetDescriptor(body),
|
|
9129
|
+
},
|
|
8256
9130
|
});
|
|
8257
9131
|
}
|
|
8258
9132
|
catch (err) {
|
|
@@ -8265,11 +9139,87 @@ export async function startLocalServer(opts) {
|
|
|
8265
9139
|
});
|
|
8266
9140
|
}
|
|
8267
9141
|
else {
|
|
9142
|
+
const repaired = await repairFailedAppTileExecution({
|
|
9143
|
+
source: 'certified_block',
|
|
9144
|
+
question: item.title ?? block.name,
|
|
9145
|
+
sourceSql: blockFilterApplication?.sql ?? blockPlan?.sql,
|
|
9146
|
+
sourceDql: blockSource,
|
|
9147
|
+
artifactName: block.name,
|
|
9148
|
+
error: err,
|
|
9149
|
+
targetConnection: blockTargetConnection,
|
|
9150
|
+
targetConnectionName: projectConfig.defaultConnectionName,
|
|
9151
|
+
sqlParams: blockFilterApplication?.sqlParams ?? blockPlan?.sqlParams,
|
|
9152
|
+
variables: {
|
|
9153
|
+
...(blockPlan?.variables ?? {}),
|
|
9154
|
+
...(blockFilterApplication?.variables ?? {}),
|
|
9155
|
+
...(blockInvocation?.values ?? {}),
|
|
9156
|
+
},
|
|
9157
|
+
});
|
|
9158
|
+
if (repaired.ok) {
|
|
9159
|
+
tiles.push({
|
|
9160
|
+
tileId: item.i,
|
|
9161
|
+
status: 'ok',
|
|
9162
|
+
tileType: 'block',
|
|
9163
|
+
blockId: block.name,
|
|
9164
|
+
blockPath: block.filePath,
|
|
9165
|
+
certificationStatus: block.status ?? null,
|
|
9166
|
+
title: item.title ?? block.name,
|
|
9167
|
+
viz: item.viz,
|
|
9168
|
+
chartConfig: mergeDashboardChartConfig(blockPlan?.chartConfig, item),
|
|
9169
|
+
result: normalizeQueryResult(repaired.result),
|
|
9170
|
+
...(blockFilterApplication ? {
|
|
9171
|
+
filters: {
|
|
9172
|
+
applied: blockFilterApplication.appliedFilters,
|
|
9173
|
+
skipped: blockFilterApplication.skippedFilters,
|
|
9174
|
+
},
|
|
9175
|
+
} : {}),
|
|
9176
|
+
...(blockInvocation ? {
|
|
9177
|
+
invocation: {
|
|
9178
|
+
resolvedParameters: blockInvocation.resolvedParameters,
|
|
9179
|
+
unresolvedParameters: blockInvocation.unresolvedParameters,
|
|
9180
|
+
auditId: blockInvocation.auditId,
|
|
9181
|
+
},
|
|
9182
|
+
} : {}),
|
|
9183
|
+
trustState: 'review_required',
|
|
9184
|
+
repair: repaired.repair,
|
|
9185
|
+
citation: { kind: 'block_repaired', name: block.name, path: block.filePath },
|
|
9186
|
+
artifact: {
|
|
9187
|
+
version: 1,
|
|
9188
|
+
sourceKind: 'certified_block',
|
|
9189
|
+
name: block.name,
|
|
9190
|
+
sourcePath: block.filePath,
|
|
9191
|
+
...(repaired.repairedDql ?? blockSource ? { dql: (repaired.repairedDql ?? blockSource).slice(0, 40_000) } : {}),
|
|
9192
|
+
sql: repaired.repairedSql.slice(0, 40_000),
|
|
9193
|
+
trustState: 'review_required',
|
|
9194
|
+
explanation: [
|
|
9195
|
+
'The saved certified block was not changed.',
|
|
9196
|
+
'A bounded repair executed on the original data target; this repaired result requires human review.',
|
|
9197
|
+
],
|
|
9198
|
+
executionTarget: executionTargetDescriptor(body),
|
|
9199
|
+
},
|
|
9200
|
+
});
|
|
9201
|
+
continue;
|
|
9202
|
+
}
|
|
8268
9203
|
tiles.push({
|
|
8269
9204
|
tileId: item.i,
|
|
8270
9205
|
status: 'error',
|
|
9206
|
+
tileType: 'block',
|
|
8271
9207
|
blockId: block.name,
|
|
8272
9208
|
error: err instanceof Error ? err.message : String(err),
|
|
9209
|
+
repair: repaired.repair,
|
|
9210
|
+
...(blockSource ? {
|
|
9211
|
+
artifact: {
|
|
9212
|
+
version: 1,
|
|
9213
|
+
sourceKind: 'certified_block',
|
|
9214
|
+
name: block.name,
|
|
9215
|
+
sourcePath: block.filePath,
|
|
9216
|
+
dql: blockSource.slice(0, 40_000),
|
|
9217
|
+
...(blockFilterApplication?.sql ?? blockPlan?.sql ? { sql: (blockFilterApplication?.sql ?? blockPlan.sql).slice(0, 40_000) } : {}),
|
|
9218
|
+
trustState: 'review_required',
|
|
9219
|
+
explanation: ['Automatic repair could not produce a safe executable result. The saved block was not changed.'],
|
|
9220
|
+
executionTarget: executionTargetDescriptor(body),
|
|
9221
|
+
},
|
|
9222
|
+
} : {}),
|
|
8273
9223
|
});
|
|
8274
9224
|
}
|
|
8275
9225
|
}
|
|
@@ -8304,6 +9254,10 @@ export async function startLocalServer(opts) {
|
|
|
8304
9254
|
filterFingerprint,
|
|
8305
9255
|
resultFingerprint,
|
|
8306
9256
|
personaFingerprint,
|
|
9257
|
+
successfulTileIds: tiles.filter((tile) => tile.status === 'ok').map((tile) => tile.tileId),
|
|
9258
|
+
semanticApprovalEligibleTileIds: tiles.filter((tile) => (tile.status === 'ok'
|
|
9259
|
+
&& tile.tileType === 'semantic'
|
|
9260
|
+
&& (!('repair' in tile) || tile.repair?.approvalEligible !== false))).map((tile) => tile.tileId),
|
|
8307
9261
|
facts: storyResult.facts,
|
|
8308
9262
|
story: storyResult.story,
|
|
8309
9263
|
expiresAt: Date.now() + 15 * 60_000,
|
|
@@ -8355,7 +9309,30 @@ export async function startLocalServer(opts) {
|
|
|
8355
9309
|
// Two-phase app build gap-fill: bounded governed answers for uncovered
|
|
8356
9310
|
// questions. Throws when no provider is configured — propose degrades
|
|
8357
9311
|
// gracefully by listing the gap instead.
|
|
8358
|
-
generateGovernedAnswer: (question) =>
|
|
9312
|
+
generateGovernedAnswer: (question, appContext) => generateAppBuildAnswerWithRepair(question, appContext),
|
|
9313
|
+
queueOperation: (input, task) => {
|
|
9314
|
+
const operation = operationCoordinator.create(input);
|
|
9315
|
+
void operationCoordinator.run(operation.id, task);
|
|
9316
|
+
return operation;
|
|
9317
|
+
},
|
|
9318
|
+
scheduleProjectRefresh: (reason) => { scheduleProjectRefresh(reason); },
|
|
9319
|
+
verifyDashboardRun: ({ runId, appId, dashboardId, tileIds }) => {
|
|
9320
|
+
const evidence = dashboardRunEvidence.get(runId);
|
|
9321
|
+
if (!evidence || evidence.expiresAt < Date.now())
|
|
9322
|
+
return { ok: false, error: 'The dashboard run receipt expired. Run the dashboard again.' };
|
|
9323
|
+
if (evidence.appId !== appId || evidence.dashboardId !== dashboardId)
|
|
9324
|
+
return { ok: false, error: 'The dashboard run receipt does not match this App page.' };
|
|
9325
|
+
const successful = new Set(evidence.successfulTileIds);
|
|
9326
|
+
const failed = tileIds.filter((tileId) => !successful.has(tileId));
|
|
9327
|
+
if (failed.length > 0)
|
|
9328
|
+
return { ok: false, error: `These semantic tiles did not complete successfully: ${failed.join(', ')}` };
|
|
9329
|
+
const approvalEligible = new Set(evidence.semanticApprovalEligibleTileIds);
|
|
9330
|
+
const repaired = tileIds.filter((tileId) => !approvalEligible.has(tileId));
|
|
9331
|
+
if (repaired.length > 0) {
|
|
9332
|
+
return { ok: false, error: `These semantic previews required SQL repair and cannot be approved as governed semantic results: ${repaired.join(', ')}. Review them as exploratory App analysis instead.` };
|
|
9333
|
+
}
|
|
9334
|
+
return { ok: true, snapshotId: evidence.snapshotId, resultFingerprint: evidence.resultFingerprint };
|
|
9335
|
+
},
|
|
8359
9336
|
// Story narration for commit — LLM-backed with deterministic fallback.
|
|
8360
9337
|
narrate: narrateForAgentRun,
|
|
8361
9338
|
});
|
|
@@ -8531,13 +9508,21 @@ export async function startLocalServer(opts) {
|
|
|
8531
9508
|
res.end(serializeJSON({ error: 'Invalid path' }));
|
|
8532
9509
|
return;
|
|
8533
9510
|
}
|
|
8534
|
-
|
|
9511
|
+
await mkdirAsync(dirname(absPath), { recursive: true });
|
|
8535
9512
|
const toWrite = absPath.endsWith('.dql')
|
|
8536
9513
|
? canonicalizeSafe(content)
|
|
8537
9514
|
: absPath.endsWith('.dqlnb')
|
|
8538
9515
|
? canonicalizeNotebookSafe(content)
|
|
8539
9516
|
: content;
|
|
8540
|
-
|
|
9517
|
+
const tempPath = `${absPath}.tmp-${process.pid}-${Date.now()}`;
|
|
9518
|
+
try {
|
|
9519
|
+
await writeFileAsync(tempPath, toWrite, 'utf-8');
|
|
9520
|
+
await renameAsync(tempPath, absPath);
|
|
9521
|
+
}
|
|
9522
|
+
catch (error) {
|
|
9523
|
+
await rmAsync(tempPath, { force: true }).catch(() => undefined);
|
|
9524
|
+
throw error;
|
|
9525
|
+
}
|
|
8541
9526
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8542
9527
|
res.end(serializeJSON({ ok: true }));
|
|
8543
9528
|
}
|
|
@@ -9844,6 +10829,179 @@ export async function startLocalServer(opts) {
|
|
|
9844
10829
|
}
|
|
9845
10830
|
return;
|
|
9846
10831
|
}
|
|
10832
|
+
if (req.method === 'POST' && path === '/api/block-studio/certifications') {
|
|
10833
|
+
try {
|
|
10834
|
+
const body = await readJSON(req);
|
|
10835
|
+
const source = typeof body.source === 'string' ? body.source : '';
|
|
10836
|
+
const suppliedMetadata = body.metadata && typeof body.metadata === 'object'
|
|
10837
|
+
? body.metadata
|
|
10838
|
+
: {};
|
|
10839
|
+
const currentPath = typeof body.path === 'string'
|
|
10840
|
+
? normalize(body.path).replace(/^\/+/, '').replaceAll('\\', '/')
|
|
10841
|
+
: undefined;
|
|
10842
|
+
const expectedSourceFingerprint = typeof body.expectedSourceFingerprint === 'string'
|
|
10843
|
+
? body.expectedSourceFingerprint
|
|
10844
|
+
: undefined;
|
|
10845
|
+
const idempotencyKey = typeof req.headers['idempotency-key'] === 'string'
|
|
10846
|
+
? req.headers['idempotency-key'].trim()
|
|
10847
|
+
: '';
|
|
10848
|
+
if (!source.trim()) {
|
|
10849
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
10850
|
+
res.end(serializeJSON({ error: 'source is required' }));
|
|
10851
|
+
return;
|
|
10852
|
+
}
|
|
10853
|
+
if (idempotencyKey) {
|
|
10854
|
+
const existingId = operationIdempotency.get(idempotencyKey);
|
|
10855
|
+
const existing = existingId ? operationCoordinator.get(existingId) : null;
|
|
10856
|
+
if (existing) {
|
|
10857
|
+
res.writeHead(202, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
10858
|
+
res.end(serializeJSON({ operation: existing, deduplicated: true }));
|
|
10859
|
+
return;
|
|
10860
|
+
}
|
|
10861
|
+
}
|
|
10862
|
+
if (currentPath && expectedSourceFingerprint) {
|
|
10863
|
+
const absoluteCurrentPath = safeJoin(projectRoot, currentPath);
|
|
10864
|
+
if (!absoluteCurrentPath || !existsSync(absoluteCurrentPath)) {
|
|
10865
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
10866
|
+
res.end(serializeJSON({ error: 'The block changed or moved. Reopen it before certifying.', code: 'SOURCE_CHANGED' }));
|
|
10867
|
+
return;
|
|
10868
|
+
}
|
|
10869
|
+
const actualFingerprint = createHash('sha256')
|
|
10870
|
+
.update(readFileSync(absoluteCurrentPath, 'utf-8'))
|
|
10871
|
+
.digest('hex');
|
|
10872
|
+
if (actualFingerprint !== expectedSourceFingerprint) {
|
|
10873
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
10874
|
+
res.end(serializeJSON({ error: 'The block changed after it was opened. Reopen it before certifying.', code: 'SOURCE_CHANGED' }));
|
|
10875
|
+
return;
|
|
10876
|
+
}
|
|
10877
|
+
}
|
|
10878
|
+
const draftSource = setBlockStudioStatusInSource(source, 'draft');
|
|
10879
|
+
const parsed = parseBlockSourceMetadata(draftSource);
|
|
10880
|
+
const name = typeof suppliedMetadata.name === 'string' && suppliedMetadata.name.trim()
|
|
10881
|
+
? suppliedMetadata.name.trim()
|
|
10882
|
+
: parsed.name;
|
|
10883
|
+
if (!name) {
|
|
10884
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
10885
|
+
res.end(serializeJSON({ error: 'Block name is required.' }));
|
|
10886
|
+
return;
|
|
10887
|
+
}
|
|
10888
|
+
const canonicalPath = currentPath && !isDraftBlockPath(currentPath) ? currentPath : undefined;
|
|
10889
|
+
const stableSuffix = createHash('sha256')
|
|
10890
|
+
.update(currentPath ?? `${name}:${draftSource}`)
|
|
10891
|
+
.digest('hex')
|
|
10892
|
+
.slice(0, 16);
|
|
10893
|
+
const draftPath = saveBlockStudioDraftArtifacts(projectRoot, {
|
|
10894
|
+
currentPath: currentPath && isDraftBlockPath(currentPath) ? currentPath : undefined,
|
|
10895
|
+
source: draftSource,
|
|
10896
|
+
name,
|
|
10897
|
+
domain: typeof suppliedMetadata.domain === 'string' ? suppliedMetadata.domain : parsed.domain,
|
|
10898
|
+
description: typeof suppliedMetadata.description === 'string' ? suppliedMetadata.description : parsed.description,
|
|
10899
|
+
owner: typeof suppliedMetadata.owner === 'string' ? suppliedMetadata.owner : parsed.owner,
|
|
10900
|
+
tags: Array.isArray(suppliedMetadata.tags) ? suppliedMetadata.tags.map(String) : parsed.tags,
|
|
10901
|
+
lineage: Array.isArray(suppliedMetadata.lineage) ? suppliedMetadata.lineage.map(String) : undefined,
|
|
10902
|
+
stableSuffix,
|
|
10903
|
+
});
|
|
10904
|
+
const operation = operationCoordinator.create({
|
|
10905
|
+
type: 'block_certification',
|
|
10906
|
+
scope: `block:${draftPath}`,
|
|
10907
|
+
resourceRevision: typeof body.clientRevision === 'string'
|
|
10908
|
+
? body.clientRevision
|
|
10909
|
+
: createHash('sha256').update(draftSource).digest('hex'),
|
|
10910
|
+
message: `Certification queued for ${name}.`,
|
|
10911
|
+
cancellable: true,
|
|
10912
|
+
});
|
|
10913
|
+
if (idempotencyKey) {
|
|
10914
|
+
operationIdempotency.set(idempotencyKey, operation.id);
|
|
10915
|
+
while (operationIdempotency.size > 100) {
|
|
10916
|
+
const oldest = operationIdempotency.keys().next().value;
|
|
10917
|
+
if (typeof oldest !== 'string')
|
|
10918
|
+
break;
|
|
10919
|
+
operationIdempotency.delete(oldest);
|
|
10920
|
+
}
|
|
10921
|
+
}
|
|
10922
|
+
operationCoordinator.run(operation.id, async (signal, report) => {
|
|
10923
|
+
report({ phase: 'validating', progress: 15, message: 'Validating and running the saved draft once.' });
|
|
10924
|
+
const result = await certifyBlockStudioSource(draftSource, draftPath, { enterprise: body.enterprise === true });
|
|
10925
|
+
const blockers = Array.from(new Set(result.checklist.blockers));
|
|
10926
|
+
if (signal.aborted)
|
|
10927
|
+
throw Object.assign(new Error('Certification cancelled.'), { code: 'OPERATION_CANCELLED' });
|
|
10928
|
+
if (!result.certification.certified || blockers.length > 0) {
|
|
10929
|
+
return {
|
|
10930
|
+
outcome: 'draft_saved_with_blockers',
|
|
10931
|
+
oldPath: currentPath ?? draftPath,
|
|
10932
|
+
draftPath,
|
|
10933
|
+
blockers,
|
|
10934
|
+
checklist: result.checklist,
|
|
10935
|
+
block: openBlockStudioDocument(projectRoot, draftPath, semanticLayer),
|
|
10936
|
+
};
|
|
10937
|
+
}
|
|
10938
|
+
report({ phase: 'publishing', progress: 80, message: 'Publishing the certified block.' });
|
|
10939
|
+
const certifiedSource = setBlockStudioStatusInSource(draftSource, 'certified');
|
|
10940
|
+
const certifiedMetadata = parseBlockSourceMetadata(certifiedSource);
|
|
10941
|
+
const savedPath = saveBlockStudioArtifacts(projectRoot, {
|
|
10942
|
+
currentPath: canonicalPath ?? draftPath,
|
|
10943
|
+
source: certifiedSource,
|
|
10944
|
+
name: certifiedMetadata.name || name,
|
|
10945
|
+
domain: certifiedMetadata.domain,
|
|
10946
|
+
description: certifiedMetadata.description,
|
|
10947
|
+
owner: certifiedMetadata.owner,
|
|
10948
|
+
tags: certifiedMetadata.tags,
|
|
10949
|
+
});
|
|
10950
|
+
if (canonicalPath && draftPath !== savedPath) {
|
|
10951
|
+
try {
|
|
10952
|
+
deleteBlockStudioArtifacts(projectRoot, draftPath);
|
|
10953
|
+
}
|
|
10954
|
+
catch { /* certified source is already durable */ }
|
|
10955
|
+
}
|
|
10956
|
+
// Shared learning remains the same deterministic, advisory write used
|
|
10957
|
+
// by the synchronous compatibility route. It never gates publication.
|
|
10958
|
+
try {
|
|
10959
|
+
const learnedOutputs = Array.isArray(certifiedMetadata.outputs)
|
|
10960
|
+
? certifiedMetadata.outputs.filter((output) => typeof output === 'string')
|
|
10961
|
+
: [];
|
|
10962
|
+
if (certifiedMetadata.name) {
|
|
10963
|
+
const memory = new MemoryStore(defaultMemoryPath(projectRoot));
|
|
10964
|
+
memory.upsert({
|
|
10965
|
+
id: `mem_certify_${certifiedMetadata.name}`,
|
|
10966
|
+
scope: 'project',
|
|
10967
|
+
title: `Certified block: ${certifiedMetadata.name}`,
|
|
10968
|
+
content: `Prefer the certified block "${certifiedMetadata.name}" for ${certifiedMetadata.description?.trim() || `questions in the ${certifiedMetadata.domain ?? 'analytics'} domain`}.${certifiedMetadata.grain ? ` Grain: ${certifiedMetadata.grain}.` : ''}${learnedOutputs.length ? ` Outputs: ${learnedOutputs.slice(0, 8).join(', ')}.` : ''} It is the trusted source — reuse it instead of generating new SQL.`,
|
|
10969
|
+
tags: [certifiedMetadata.domain, certifiedMetadata.name, ...learnedOutputs.slice(0, 4)].filter((value) => Boolean(value)),
|
|
10970
|
+
source: 'certify',
|
|
10971
|
+
confidence: 0.95,
|
|
10972
|
+
importance: 0.85,
|
|
10973
|
+
enabled: true,
|
|
10974
|
+
});
|
|
10975
|
+
}
|
|
10976
|
+
}
|
|
10977
|
+
catch {
|
|
10978
|
+
/* best-effort: learning capture must never block certification */
|
|
10979
|
+
}
|
|
10980
|
+
const refreshOperation = scheduleProjectRefresh('block-studio-certification');
|
|
10981
|
+
return {
|
|
10982
|
+
outcome: 'certified',
|
|
10983
|
+
oldPath: currentPath ?? draftPath,
|
|
10984
|
+
draftPath,
|
|
10985
|
+
newPath: savedPath,
|
|
10986
|
+
block: openBlockStudioDocument(projectRoot, savedPath, semanticLayer),
|
|
10987
|
+
checklist: result.checklist,
|
|
10988
|
+
indexRefresh: { status: 'queued', operationId: refreshOperation.id },
|
|
10989
|
+
};
|
|
10990
|
+
});
|
|
10991
|
+
res.writeHead(202, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
10992
|
+
res.end(serializeJSON({
|
|
10993
|
+
operation: operationCoordinator.get(operation.id) ?? operation,
|
|
10994
|
+
draft: openBlockStudioDocument(projectRoot, draftPath, semanticLayer),
|
|
10995
|
+
}));
|
|
10996
|
+
}
|
|
10997
|
+
catch (error) {
|
|
10998
|
+
const code = apiErrorCode(error, 'CERTIFICATION_REQUEST_FAILED');
|
|
10999
|
+
const status = code === 'SOURCE_CHANGED' || apiErrorMessage(error) === 'BLOCK_EXISTS' ? 409 : 500;
|
|
11000
|
+
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11001
|
+
res.end(serializeJSON({ error: apiErrorMessage(error), code }));
|
|
11002
|
+
}
|
|
11003
|
+
return;
|
|
11004
|
+
}
|
|
9847
11005
|
if (req.method === 'POST' && path === '/api/block-studio/certify') {
|
|
9848
11006
|
try {
|
|
9849
11007
|
const body = await readJSON(req);
|
|
@@ -9913,7 +11071,7 @@ export async function startLocalServer(opts) {
|
|
|
9913
11071
|
catch {
|
|
9914
11072
|
/* best-effort: learning capture must never block certification */
|
|
9915
11073
|
}
|
|
9916
|
-
|
|
11074
|
+
const refreshOperation = scheduleProjectRefresh('block-studio-certify');
|
|
9917
11075
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
9918
11076
|
res.end(serializeJSON({
|
|
9919
11077
|
ok: true,
|
|
@@ -9924,6 +11082,7 @@ export async function startLocalServer(opts) {
|
|
|
9924
11082
|
metadata: certifiedPayload?.metadata,
|
|
9925
11083
|
companionPath: certifiedPayload?.companionPath ?? null,
|
|
9926
11084
|
validation: certifiedPayload?.validation ?? result.validation,
|
|
11085
|
+
lineageRefresh: { status: 'queued', operationId: refreshOperation.id },
|
|
9927
11086
|
}));
|
|
9928
11087
|
}
|
|
9929
11088
|
catch (error) {
|
|
@@ -10773,25 +11932,13 @@ export async function startLocalServer(opts) {
|
|
|
10773
11932
|
stableSuffix: metadata.candidateId,
|
|
10774
11933
|
})
|
|
10775
11934
|
: saveBlockStudioArtifacts(projectRoot, saveOptions);
|
|
10776
|
-
|
|
10777
|
-
let lineageRefresh;
|
|
10778
|
-
try {
|
|
10779
|
-
compiledManifest = compileBlockStudioManifest(projectRoot, projectConfig);
|
|
10780
|
-
lineageRefresh = { status: 'ready', compiledAt: new Date().toISOString() };
|
|
10781
|
-
}
|
|
10782
|
-
catch (error) {
|
|
10783
|
-
// The block is already safely stored. Preserve it and report the
|
|
10784
|
-
// rebuild failure separately instead of turning a successful save
|
|
10785
|
-
// into a misleading 500 response.
|
|
10786
|
-
lineageRefresh = {
|
|
10787
|
-
status: 'failed',
|
|
10788
|
-
message: error instanceof Error ? error.message : String(error),
|
|
10789
|
-
};
|
|
10790
|
-
}
|
|
10791
|
-
await refreshLocalMetadataCatalog(projectRoot, compiledManifest, semanticLayer);
|
|
11935
|
+
const refreshOperation = scheduleProjectRefresh('block-studio-save');
|
|
10792
11936
|
const payload = {
|
|
10793
11937
|
...openBlockStudioDocument(projectRoot, savedPath, semanticLayer),
|
|
10794
|
-
lineageRefresh
|
|
11938
|
+
lineageRefresh: {
|
|
11939
|
+
status: 'queued',
|
|
11940
|
+
operationId: refreshOperation.id,
|
|
11941
|
+
},
|
|
10795
11942
|
};
|
|
10796
11943
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
10797
11944
|
res.end(serializeJSON(payload));
|
|
@@ -10811,21 +11958,13 @@ export async function startLocalServer(opts) {
|
|
|
10811
11958
|
try {
|
|
10812
11959
|
const blockPath = url.searchParams.get('path');
|
|
10813
11960
|
const deleted = deleteBlockStudioArtifacts(projectRoot, blockPath ?? '');
|
|
10814
|
-
|
|
10815
|
-
let lineageRefresh;
|
|
10816
|
-
try {
|
|
10817
|
-
compiledManifest = compileBlockStudioManifest(projectRoot, projectConfig);
|
|
10818
|
-
lineageRefresh = { status: 'ready', compiledAt: new Date().toISOString() };
|
|
10819
|
-
}
|
|
10820
|
-
catch (error) {
|
|
10821
|
-
lineageRefresh = {
|
|
10822
|
-
status: 'failed',
|
|
10823
|
-
message: error instanceof Error ? error.message : String(error),
|
|
10824
|
-
};
|
|
10825
|
-
}
|
|
10826
|
-
await refreshLocalMetadataCatalog(projectRoot, compiledManifest, semanticLayer);
|
|
11961
|
+
const refreshOperation = scheduleProjectRefresh('block-studio-delete');
|
|
10827
11962
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
10828
|
-
res.end(serializeJSON({
|
|
11963
|
+
res.end(serializeJSON({
|
|
11964
|
+
ok: true,
|
|
11965
|
+
...deleted,
|
|
11966
|
+
lineageRefresh: { status: 'queued', operationId: refreshOperation.id },
|
|
11967
|
+
}));
|
|
10829
11968
|
}
|
|
10830
11969
|
catch (error) {
|
|
10831
11970
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -12445,9 +13584,9 @@ export async function startLocalServer(opts) {
|
|
|
12445
13584
|
return;
|
|
12446
13585
|
}
|
|
12447
13586
|
const embeddedSourceSql = notebookCellRepairSql(cell);
|
|
12448
|
-
if (!embeddedSourceSql) {
|
|
13587
|
+
if (!embeddedSourceSql && (cell.type !== 'dql' || !notebookDqlSourceAllowsBackgroundRepair(cell.source))) {
|
|
12449
13588
|
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
12450
|
-
res.end(serializeJSON({ error: 'Background repair supports SQL cells and
|
|
13589
|
+
res.end(serializeJSON({ error: 'Background repair supports SQL cells and editable custom DQL blocks. Open governed references or semantic definitions in their owning editor.' }));
|
|
12451
13590
|
return;
|
|
12452
13591
|
}
|
|
12453
13592
|
if (cell.type === 'sql' && /\{\{[^}]+\}\}/.test(cell.source)) {
|
|
@@ -12480,31 +13619,33 @@ export async function startLocalServer(opts) {
|
|
|
12480
13619
|
const parameters = body.parameters && typeof body.parameters === 'object' && !Array.isArray(body.parameters)
|
|
12481
13620
|
? body.parameters
|
|
12482
13621
|
: {};
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
if (
|
|
12487
|
-
|
|
12488
|
-
|
|
12489
|
-
|
|
12490
|
-
|
|
13622
|
+
let invocation = null;
|
|
13623
|
+
let plan = null;
|
|
13624
|
+
let dqlCompilationError;
|
|
13625
|
+
if (cell.type === 'dql') {
|
|
13626
|
+
try {
|
|
13627
|
+
invocation = prepareBlockInvocation({ source: cell.source, parameters, surface: 'notebook' });
|
|
13628
|
+
if (invocation.errors.length)
|
|
13629
|
+
throw boundedRepairError(invocation.errors.join(' '), 409);
|
|
13630
|
+
if (invocation.unresolvedParameters.length) {
|
|
13631
|
+
throw boundedRepairError(`Provide required parameter${invocation.unresolvedParameters.length === 1 ? '' : 's'} before retrying: ${invocation.unresolvedParameters.join(', ')}.`, 409);
|
|
13632
|
+
}
|
|
13633
|
+
plan = buildExecutionPlan(cell, {
|
|
13634
|
+
semanticLayer,
|
|
13635
|
+
driver: targetConnection.driver,
|
|
13636
|
+
parameters: invocation.values,
|
|
13637
|
+
});
|
|
13638
|
+
}
|
|
13639
|
+
catch (error) {
|
|
13640
|
+
const repairError = error;
|
|
13641
|
+
if (repairError.status === 409)
|
|
13642
|
+
throw error;
|
|
13643
|
+
dqlCompilationError = repairError.message || String(error);
|
|
13644
|
+
}
|
|
12491
13645
|
}
|
|
12492
|
-
const plan = cell.type === 'dql'
|
|
12493
|
-
? buildExecutionPlan(cell, {
|
|
12494
|
-
semanticLayer,
|
|
12495
|
-
driver: targetConnection.driver,
|
|
12496
|
-
parameters: invocation?.values,
|
|
12497
|
-
})
|
|
12498
|
-
: null;
|
|
12499
13646
|
const attemptedSql = typeof body.attemptedSql === 'string' && body.attemptedSql.trim()
|
|
12500
13647
|
? body.attemptedSql.trim()
|
|
12501
13648
|
: embeddedSourceSql;
|
|
12502
|
-
const sourceSql = plan?.sql ?? attemptedSql;
|
|
12503
|
-
const sqlParams = plan?.sqlParams ?? [];
|
|
12504
|
-
const variables = {
|
|
12505
|
-
...(plan?.variables ?? {}),
|
|
12506
|
-
...(invocation?.values ?? {}),
|
|
12507
|
-
};
|
|
12508
13649
|
const executionTarget = executionTargetDescriptor(body);
|
|
12509
13650
|
const targetConnectionName = executionTarget.target === 'connection'
|
|
12510
13651
|
? executionTarget.connectionName
|
|
@@ -12515,24 +13656,48 @@ export async function startLocalServer(opts) {
|
|
|
12515
13656
|
`Repair notebook cell ${cell.title?.trim() || cell.id}.`,
|
|
12516
13657
|
submittedError ? `Failure: ${submittedError.slice(0, 1_000)}` : '',
|
|
12517
13658
|
].filter(Boolean).join(' ');
|
|
12518
|
-
|
|
12519
|
-
question,
|
|
12520
|
-
sourceSql,
|
|
12521
|
-
targetConnection,
|
|
12522
|
-
targetConnectionName,
|
|
12523
|
-
sqlParams,
|
|
12524
|
-
variables,
|
|
12525
|
-
});
|
|
13659
|
+
let repaired;
|
|
12526
13660
|
let repairedSource;
|
|
12527
|
-
if (cell.type === 'dql') {
|
|
12528
|
-
const
|
|
12529
|
-
|
|
12530
|
-
|
|
12531
|
-
:
|
|
12532
|
-
|
|
13661
|
+
if (cell.type === 'dql' && dqlCompilationError) {
|
|
13662
|
+
const repairedDql = await executeBoundedDqlRepair({
|
|
13663
|
+
question,
|
|
13664
|
+
source: cell.source,
|
|
13665
|
+
failure: [submittedError, dqlCompilationError].filter(Boolean).join('\n'),
|
|
13666
|
+
targetConnection,
|
|
13667
|
+
targetConnectionName,
|
|
13668
|
+
parameters,
|
|
13669
|
+
artifactName: cell.title,
|
|
13670
|
+
});
|
|
13671
|
+
repaired = repairedDql;
|
|
13672
|
+
repairedSource = repairedDql.repairedSource;
|
|
12533
13673
|
}
|
|
12534
13674
|
else {
|
|
12535
|
-
|
|
13675
|
+
const sourceSql = plan?.sql ?? attemptedSql;
|
|
13676
|
+
if (!sourceSql?.trim()) {
|
|
13677
|
+
throw boundedRepairError('DQL could not identify an embedded query to repair.', 422);
|
|
13678
|
+
}
|
|
13679
|
+
const sqlParams = plan?.sqlParams ?? [];
|
|
13680
|
+
repaired = await executeBoundedSqlRepair({
|
|
13681
|
+
question,
|
|
13682
|
+
sourceSql,
|
|
13683
|
+
targetConnection,
|
|
13684
|
+
targetConnectionName,
|
|
13685
|
+
sqlParams,
|
|
13686
|
+
variables: {
|
|
13687
|
+
...(plan?.variables ?? {}),
|
|
13688
|
+
...(invocation?.values ?? {}),
|
|
13689
|
+
},
|
|
13690
|
+
});
|
|
13691
|
+
if (cell.type === 'dql') {
|
|
13692
|
+
const originalQuery = notebookCellRepairSql(cell);
|
|
13693
|
+
const repairedQuery = repaired.repairMode === 'deterministic'
|
|
13694
|
+
? applyNotebookRepairRewrites(originalQuery, repaired.rewrites)
|
|
13695
|
+
: restoreNotebookDqlParameterInterpolations(repaired.repairedSql, sqlParams);
|
|
13696
|
+
repairedSource = replaceNotebookDqlQueryForRepair(cell.source, repairedQuery);
|
|
13697
|
+
}
|
|
13698
|
+
else {
|
|
13699
|
+
repairedSource = repaired.repairedSql;
|
|
13700
|
+
}
|
|
12536
13701
|
}
|
|
12537
13702
|
if (!repairedSource) {
|
|
12538
13703
|
throw boundedRepairError('DQL could not safely replace the embedded query in this cell.', 422);
|
|
@@ -12543,12 +13708,13 @@ export async function startLocalServer(opts) {
|
|
|
12543
13708
|
const validationPlan = buildExecutionPlan({ ...cell, source: repairedSource }, {
|
|
12544
13709
|
semanticLayer,
|
|
12545
13710
|
driver: targetConnection.driver,
|
|
12546
|
-
parameters: invocation?.values,
|
|
13711
|
+
parameters: invocation?.values ?? parameters,
|
|
12547
13712
|
});
|
|
12548
13713
|
if (!validationPlan?.sql) {
|
|
12549
13714
|
throw boundedRepairError('The repaired SQL ran, but the DQL wrapper no longer compiles.', 422);
|
|
12550
13715
|
}
|
|
12551
|
-
const beforeParameters = sqlParams
|
|
13716
|
+
const beforeParameters = (plan?.sqlParams ?? ('sqlParams' in repaired ? repaired.sqlParams : []))
|
|
13717
|
+
.map((parameter) => `${parameter.position}:${parameter.name}`);
|
|
12552
13718
|
const afterParameters = validationPlan.sqlParams.map((parameter) => `${parameter.position}:${parameter.name}`);
|
|
12553
13719
|
if (beforeParameters.join('|') !== afterParameters.join('|')) {
|
|
12554
13720
|
throw boundedRepairError('The repaired DQL changed the cell parameter contract, so the update was stopped.', 422);
|
|
@@ -13814,6 +14980,22 @@ table: ${table}${tagList}
|
|
|
13814
14980
|
})));
|
|
13815
14981
|
});
|
|
13816
14982
|
});
|
|
14983
|
+
server.once('close', () => {
|
|
14984
|
+
runtimeClosing = true;
|
|
14985
|
+
for (const watcher of projectWatchers)
|
|
14986
|
+
watcher.close();
|
|
14987
|
+
projectWatchers.length = 0;
|
|
14988
|
+
unsubscribeOperationEvents();
|
|
14989
|
+
projectRefreshCoordinator.close();
|
|
14990
|
+
for (const client of operationSseClients) {
|
|
14991
|
+
try {
|
|
14992
|
+
client.end();
|
|
14993
|
+
}
|
|
14994
|
+
catch { /* connection already closed */ }
|
|
14995
|
+
}
|
|
14996
|
+
operationSseClients.clear();
|
|
14997
|
+
operationCoordinator.close();
|
|
14998
|
+
});
|
|
13817
14999
|
opts.captureServer?.(server);
|
|
13818
15000
|
return new Promise((resolvePromise, reject) => {
|
|
13819
15001
|
let retriedWithRandomPort = false;
|
|
@@ -14255,7 +15437,7 @@ function loadAppDashboard(projectRoot, appId, dashboardId) {
|
|
|
14255
15437
|
for (const d of findDashboardsForApp(appDir)) {
|
|
14256
15438
|
const { document: dashboard } = loadDashboardDocument(d);
|
|
14257
15439
|
if (dashboard?.id === dashboardId)
|
|
14258
|
-
return { app, dashboard };
|
|
15440
|
+
return { app, dashboard, appDir };
|
|
14259
15441
|
}
|
|
14260
15442
|
}
|
|
14261
15443
|
return null;
|
|
@@ -17157,6 +18339,11 @@ export function notebookCellRepairSql(cell) {
|
|
|
17157
18339
|
const match = /\bquery\s*=\s*"""([\s\S]*?)"""/i.exec(cell.source);
|
|
17158
18340
|
return match?.[1]?.trim() || undefined;
|
|
17159
18341
|
}
|
|
18342
|
+
export function notebookDqlSourceAllowsBackgroundRepair(source) {
|
|
18343
|
+
if (/^\s*@block\(/i.test(source) || /\btype\s*=\s*"semantic"/i.test(source))
|
|
18344
|
+
return false;
|
|
18345
|
+
return /\bblock\b/i.test(source) && /\bquery\b/i.test(source);
|
|
18346
|
+
}
|
|
17160
18347
|
export function replaceNotebookDqlQueryForRepair(source, sql) {
|
|
17161
18348
|
const queryField = /\b(query\s*=\s*""")([\s\S]*?)(""")/i;
|
|
17162
18349
|
if (!queryField.test(source) || !sql.trim())
|
|
@@ -17618,6 +18805,7 @@ export function openBlockStudioDocument(projectRoot, relativePath, semanticLayer
|
|
|
17618
18805
|
owner: parsedMetadata.owner || companion?.owner || '',
|
|
17619
18806
|
tags: parsedMetadata.tags.length > 0 ? parsedMetadata.tags : companion?.tags ?? [],
|
|
17620
18807
|
reviewStatus: parsedMetadata.status || companion?.reviewStatus || 'draft',
|
|
18808
|
+
sourceFingerprint: createHash('sha256').update(source).digest('hex'),
|
|
17621
18809
|
};
|
|
17622
18810
|
return {
|
|
17623
18811
|
path: normalizedPath,
|
|
@@ -19182,6 +20370,45 @@ function formatBlockStudioExpected(expected) {
|
|
|
19182
20370
|
return String(normalized);
|
|
19183
20371
|
return JSON.stringify(normalized);
|
|
19184
20372
|
}
|
|
20373
|
+
function evaluateBlockStudioTests(tests, rows, columns, startedAt) {
|
|
20374
|
+
if (tests.length === 0) {
|
|
20375
|
+
return { passed: 0, failed: 0, skipped: 0, duration: Date.now() - startedAt, assertions: [], runAt: new Date() };
|
|
20376
|
+
}
|
|
20377
|
+
const assertions = [];
|
|
20378
|
+
let passed = 0;
|
|
20379
|
+
let failed = 0;
|
|
20380
|
+
for (const test of tests) {
|
|
20381
|
+
const name = `assert ${test.field} ${test.operator} ${formatBlockStudioExpected(test.expected)}`;
|
|
20382
|
+
let actual;
|
|
20383
|
+
if (test.field === 'row_count') {
|
|
20384
|
+
actual = rows.length;
|
|
20385
|
+
}
|
|
20386
|
+
else if (!columns.includes(test.field)) {
|
|
20387
|
+
assertions.push({ name, passed: false, expected: test.expected, error: `Column '${test.field}' not found in results` });
|
|
20388
|
+
failed += 1;
|
|
20389
|
+
continue;
|
|
20390
|
+
}
|
|
20391
|
+
else {
|
|
20392
|
+
actual = rows[0]?.[test.field];
|
|
20393
|
+
}
|
|
20394
|
+
const ok = compareBlockStudioValues(actual, test.operator, test.expected);
|
|
20395
|
+
if (ok) {
|
|
20396
|
+
assertions.push({ name, passed: true, actual, expected: test.expected });
|
|
20397
|
+
passed += 1;
|
|
20398
|
+
}
|
|
20399
|
+
else {
|
|
20400
|
+
assertions.push({
|
|
20401
|
+
name,
|
|
20402
|
+
passed: false,
|
|
20403
|
+
actual,
|
|
20404
|
+
expected: test.expected,
|
|
20405
|
+
error: `${String(actual)} ${test.operator} ${formatBlockStudioExpected(test.expected)} is false`,
|
|
20406
|
+
});
|
|
20407
|
+
failed += 1;
|
|
20408
|
+
}
|
|
20409
|
+
}
|
|
20410
|
+
return { passed, failed, skipped: 0, duration: Date.now() - startedAt, assertions, runAt: new Date() };
|
|
20411
|
+
}
|
|
19185
20412
|
function buildBlockStudioCertificationChecklist(input) {
|
|
19186
20413
|
const parsed = parseBlockSourceMetadata(input.source);
|
|
19187
20414
|
const sql = extractBlockStudioSql(input.source) ?? '';
|
|
@@ -24272,6 +25499,46 @@ function mergeAgentSchemaContexts(...contexts) {
|
|
|
24272
25499
|
}
|
|
24273
25500
|
return Array.from(byRelation.values()).slice(0, 16);
|
|
24274
25501
|
}
|
|
25502
|
+
function agentRelationMatchKey(relation) {
|
|
25503
|
+
return relation
|
|
25504
|
+
.replace(/["`\[\]]/g, '')
|
|
25505
|
+
.split('.')
|
|
25506
|
+
.filter(Boolean)
|
|
25507
|
+
.slice(-2)
|
|
25508
|
+
.join('.')
|
|
25509
|
+
.toLowerCase();
|
|
25510
|
+
}
|
|
25511
|
+
/**
|
|
25512
|
+
* Keep dbt descriptions and semantic meaning, but make a complete live point
|
|
25513
|
+
* lookup authoritative for executable column names on the selected connection.
|
|
25514
|
+
* No fuzzy aliases are invented: a stale `customer_name` declaration disappears
|
|
25515
|
+
* when the live relation exposes only `name`.
|
|
25516
|
+
*/
|
|
25517
|
+
export function reconcileAgentSchemaContextWithLive(catalog, live) {
|
|
25518
|
+
const liveByRelation = new Map(live
|
|
25519
|
+
.filter((table) => table.columns.length > 0)
|
|
25520
|
+
.map((table) => [agentRelationMatchKey(table.relation), table]));
|
|
25521
|
+
return catalog.map((table) => {
|
|
25522
|
+
const verified = liveByRelation.get(agentRelationMatchKey(table.relation));
|
|
25523
|
+
if (!verified)
|
|
25524
|
+
return table;
|
|
25525
|
+
const metadataColumns = new Map(table.columns.map((column) => [column.name.toLowerCase(), column]));
|
|
25526
|
+
return {
|
|
25527
|
+
...table,
|
|
25528
|
+
source: 'dbt metadata verified against live runtime schema',
|
|
25529
|
+
columnCompleteness: 'complete',
|
|
25530
|
+
columns: verified.columns.map((column) => {
|
|
25531
|
+
const metadata = metadataColumns.get(column.name.toLowerCase());
|
|
25532
|
+
return {
|
|
25533
|
+
...column,
|
|
25534
|
+
type: column.type ?? metadata?.type,
|
|
25535
|
+
description: metadata?.description,
|
|
25536
|
+
sampleValues: metadata?.sampleValues,
|
|
25537
|
+
};
|
|
25538
|
+
}),
|
|
25539
|
+
};
|
|
25540
|
+
});
|
|
25541
|
+
}
|
|
24275
25542
|
function mergeAgentSchemaSampleValues(snapshot, enriched) {
|
|
24276
25543
|
const samples = new Map();
|
|
24277
25544
|
for (const table of enriched) {
|