@duckcodeailabs/dql-cli 1.12.11 → 1.13.1
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/args.d.ts +2 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +3 -0
- package/dist/args.js.map +1 -1
- package/dist/assets/dql-notebook/assets/AgentAnswerCard-c8QUo6NH.js +5 -0
- package/dist/assets/dql-notebook/assets/{AgentLogPage-hzHqpD3-.js → AgentLogPage-D6fgEAEy.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildDialog-BQRWoxp9.js → AiBuildDialog-8miXFPse.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildResult-D9t_4sQl.js → AiBuildResult-DPJ_SI0R.js} +3 -3
- package/dist/assets/dql-notebook/assets/AiSidePanel-NVA6lZnn.js +6 -0
- package/dist/assets/dql-notebook/assets/AnalyticsHome-DgwnRW71.js +6 -0
- package/dist/assets/dql-notebook/assets/AppsView-B6LYUpgm.js +4578 -0
- package/dist/assets/dql-notebook/assets/BlockStudio--jNw9XUI.js +60 -0
- package/dist/assets/dql-notebook/assets/{BusinessArtifactView-CnTbFVGT.js → BusinessArtifactView-DtRMiaF4.js} +1 -1
- package/dist/assets/dql-notebook/assets/DbtFirstModelingPage-DhBsLCYr.js +22 -0
- package/dist/assets/dql-notebook/assets/GitPage-CazC-eTR.js +7 -0
- package/dist/assets/dql-notebook/assets/GlobalAiRail-1Tgweuu2.js +1 -0
- package/dist/assets/dql-notebook/assets/GovernedContextPage-C7qGrH3d.js +16 -0
- package/dist/assets/dql-notebook/assets/{HelpDocsPage-CL0vbSER.js → HelpDocsPage-DMHPsVqP.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HomePage-DzAucwBW.js → HomePage-DcSI4dnU.js} +1 -1
- package/dist/assets/dql-notebook/assets/LineageDAG-BJ6xz06u.js +1 -0
- package/dist/assets/dql-notebook/assets/{LineageDetailView-DdVH-nz6.js → LineageDetailView-8WrQm-MF.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDrawer-BvrVZNKY.js → LineageDrawer-hnqCW_t-.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-CSut-zvm.js → LineagePathBreadcrumb-B4LZTUsl.js} +3 -3
- package/dist/assets/dql-notebook/assets/{MiniLineageGraph-zg_1wW3M.js → MiniLineageGraph-CotnRuxa.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewBlockModal-9cDletl-.js → NewBlockModal-DHn-CEEW.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewNotebookModal-V6de8L4d.js → NewNotebookModal-BPGp-8oq.js} +2 -2
- package/dist/assets/dql-notebook/assets/NotebookEditor-B0OTHM6W.js +195 -0
- package/dist/assets/dql-notebook/assets/ReadinessPage-CoOIpxWq.js +1 -0
- package/dist/assets/dql-notebook/assets/{SetupOnboarding-Bg-MjbC9.js → SetupOnboarding-DxK_mFzf.js} +2 -2
- package/dist/assets/dql-notebook/assets/{TrustBadge-DGuHB8kP.js → TrustBadge-ZuD5N5Ww.js} +1 -1
- package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-duFXXNV_.js +88 -0
- package/dist/assets/dql-notebook/assets/{answer-to-notebook-BDzY4MuQ.js → answer-to-notebook-WAVAYjBv.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-left-IjlnAihE.js → arrow-left-L8odLbyN.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-right-BI4cXgOY.js → arrow-right-WIF1V9a9.js} +1 -1
- package/dist/assets/dql-notebook/assets/{book-open-text-i6xhxu_I.js → book-open-text-BpqWMa4m.js} +1 -1
- package/dist/assets/dql-notebook/assets/{bot-C6DE7Q6m.js → bot-CLR1V6dO.js} +1 -1
- package/dist/assets/dql-notebook/assets/{circle-x-BoAorVSm.js → circle-x-CQBOM0b0.js} +1 -1
- package/dist/assets/dql-notebook/assets/{dagre.esm-CycymVj6.js → dagre.esm-DlZi7NR3.js} +1 -1
- package/dist/assets/dql-notebook/assets/{external-link-ZwE5iHsX.js → external-link-DYqdQIGp.js} +1 -1
- package/dist/assets/dql-notebook/assets/{grip-vertical-Dnp2st8l.js → grip-vertical-HmUijbwN.js} +1 -1
- package/dist/assets/dql-notebook/assets/index-Cpx47jCH.js +634 -0
- package/dist/assets/dql-notebook/assets/{list-tree-Ced8e_S1.js → list-tree-DGKqCxmz.js} +1 -1
- package/dist/assets/dql-notebook/assets/{minimize-2-CBIAE0XH.js → minimize-2-C8dvqCxX.js} +1 -1
- package/dist/assets/dql-notebook/assets/{panel-right-open-po3bWSeL.js → panel-right-open-yuVkIo5n.js} +1 -1
- package/dist/assets/dql-notebook/assets/{rotate-ccw-B9v1LTJ0.js → rotate-ccw-Di0kRnrM.js} +1 -1
- package/dist/assets/dql-notebook/assets/{semantic-fields-B6ii0an0.js → semantic-fields-BMQV0li8.js} +1 -1
- package/dist/assets/dql-notebook/assets/sliders-horizontal-alebptlJ.js +6 -0
- package/dist/assets/dql-notebook/assets/{star-CO4tfaDq.js → star-CgCQbnXc.js} +1 -1
- package/dist/assets/dql-notebook/assets/{triangle-alert-BSdIu2El.js → triangle-alert-SyuSZ4ht.js} +1 -1
- package/dist/assets/dql-notebook/assets/upload-BPhfea_E.js +11 -0
- package/dist/assets/dql-notebook/assets/usePersistedAgentThreadId-Bnl9gWhj.js +28 -0
- package/dist/assets/dql-notebook/assets/{user-round-BfrV8U-W.js → user-round-J4PQHxo9.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wand-sparkles-BgTxZNBc.js → wand-sparkles-c4WDXTCk.js} +1 -1
- package/dist/assets/dql-notebook/assets/{workflow--2ycp0yy.js → workflow-NtRLaq7n.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wrench-LMCrpa9_.js → wrench-CS1f1Zv4.js} +1 -1
- package/dist/assets/dql-notebook/assets/{x-CeNgQwGm.js → x-CbKEfe5m.js} +1 -1
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/commands/model.d.ts.map +1 -1
- package/dist/commands/model.js +62 -1
- package/dist/commands/model.js.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +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 +59 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +2653 -342
- 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/chevron-up-DSEILnDs.js +0 -6
- package/dist/assets/dql-notebook/assets/index-BTuliKhK.js +0 -634
- package/dist/assets/dql-notebook/assets/link-2-CTKe6Snr.js +0 -6
- package/dist/assets/dql-notebook/assets/maximize-2-Cojc6867.js +0 -6
- package/dist/assets/dql-notebook/assets/pencil-Cb45c6WU.js +0 -6
- package/dist/assets/dql-notebook/assets/usePersistedAgentThreadId-CkVFrrjk.js +0 -18
package/dist/local-runtime.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
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
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, watch, writeFileSync, } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, 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';
|
|
14
17
|
import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, hasStandaloneSemanticRef, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
|
|
15
|
-
import { loadSemanticLayerFromDir, normalizeDqlArtifactReference, serializeMetricDefinitionToYaml, resolveSemanticLayerAsync, resolveRepoSource, getDialect, Parser, NodeKind, blockParameterDefinitions, buildLineageGraph, buildManifest, collectInputFiles, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryBusiness360, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, domainFolderSlug, previewModelingChange, applyModelingChange, previewDbtSourcePatch, applyDbtSourcePatch, loadDomainPackageRegistry, loadDbtNodeAuthoringDetail, normalizeAnalyticalFailureV1, relationshipValidationProofFingerprint, renderSemanticBlockSource, discoverDbtDomains, renderDomainDeclaration, ProjectSnapshotService, analyzeSqlReferences, semanticDimensionReference, } from '@duckcodeailabs/dql-core';
|
|
18
|
+
import { loadSemanticLayerFromDir, normalizeDqlArtifactReference, serializeMetricDefinitionToYaml, resolveSemanticLayerAsync, resolveRepoSource, getDialect, Parser, NodeKind, blockParameterDefinitions, buildLineageGraph, buildManifest, collectInputFiles, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryBusiness360, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, domainFolderSlug, previewModelingChange, previewModelingChanges, applyModelingChange, previewDbtSourcePatch, applyDbtSourcePatch, loadDomainPackageRegistry, loadDbtNodeAuthoringDetail, normalizeAnalyticalFailureV1, relationshipValidationProofFingerprint, renderSemanticBlockSource, discoverDbtDomains, renderDomainDeclaration, ProjectSnapshotService, analyzeSqlReferences, semanticDimensionReference, } from '@duckcodeailabs/dql-core';
|
|
16
19
|
import { load as loadYaml } from 'js-yaml';
|
|
17
20
|
import { listBlockTemplates } from './block-templates.js';
|
|
18
21
|
import { getRunner as getLLMRunner } from './llm/index.js';
|
|
@@ -21,10 +24,10 @@ import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from '
|
|
|
21
24
|
import { resolveRetrievalHealthStatus } from './retrieval-health.js';
|
|
22
25
|
import { createDqlAgentProviderRunner, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
|
|
23
26
|
import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
|
|
24
|
-
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';
|
|
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, previewSkillChange, buildContextAuthoringProposal, contextAuthoringDependencyClosure, FileContextAuthoringProposalStore, 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(', ')}`);
|
|
@@ -2239,7 +2412,12 @@ export async function startLocalServer(opts) {
|
|
|
2239
2412
|
: isTerminalFailure
|
|
2240
2413
|
? terminalFailureActions
|
|
2241
2414
|
: isGroundingGap
|
|
2242
|
-
?
|
|
2415
|
+
? governedAnswer.refusalCode === 'modeling_gap'
|
|
2416
|
+
? [
|
|
2417
|
+
{ id: 'fix-modeling-gap', label: 'Fix with Modeling AI', route: 'modeling_draft', artifactKind: 'modeling_change_proposal' },
|
|
2418
|
+
{ id: 'research-gap', label: 'Research missing metadata coverage', route: 'research', artifactKind: 'research_run' },
|
|
2419
|
+
]
|
|
2420
|
+
: [{ id: 'research-gap', label: 'Research missing metadata coverage', route: 'research', artifactKind: 'research_run' }]
|
|
2243
2421
|
: [
|
|
2244
2422
|
{ id: 'create-block', label: governedAnswer.dqlArtifact ? 'Review DQL draft' : 'Create DQL draft', route: 'dql_block_draft', artifactKind: 'dql_block_draft' },
|
|
2245
2423
|
{ id: 'research-gap', label: 'Research deeper', route: 'research' },
|
|
@@ -2412,11 +2590,175 @@ export async function startLocalServer(opts) {
|
|
|
2412
2590
|
nextActions,
|
|
2413
2591
|
};
|
|
2414
2592
|
};
|
|
2593
|
+
const contextProposalStore = new FileContextAuthoringProposalStore(projectRoot);
|
|
2594
|
+
const previewContextAuthoring = (input) => {
|
|
2595
|
+
const snapshot = projectSnapshot();
|
|
2596
|
+
if (input.expectedSnapshotId && input.expectedSnapshotId !== snapshot.snapshotId) {
|
|
2597
|
+
throw Object.assign(new Error('Project sources changed before the authoring proposal was previewed.'), { code: 'PROPOSAL_STALE' });
|
|
2598
|
+
}
|
|
2599
|
+
if (input.operations.length === 0)
|
|
2600
|
+
throw Object.assign(new Error('At least one authoring operation is required.'), { code: 'INVALID_REQUEST' });
|
|
2601
|
+
const diagnostics = [...(input.diagnostics ?? [])];
|
|
2602
|
+
const operations = input.operations.map((operation) => {
|
|
2603
|
+
if (operation.kind === 'skill_change') {
|
|
2604
|
+
const value = { ...operation.value };
|
|
2605
|
+
value.status = 'draft';
|
|
2606
|
+
if (value.sourceRefs?.some((reference) => !contextSourceReferenceExists(snapshot.manifest, reference))) {
|
|
2607
|
+
diagnostics.push({ code: 'UNKNOWN_GOVERNED_REFERENCE', severity: 'blocking', operationId: operation.id, message: 'The Skill proposal references a model or relationship that is not present in the current snapshot.' });
|
|
2608
|
+
}
|
|
2609
|
+
return { ...operation, value };
|
|
2610
|
+
}
|
|
2611
|
+
if (operation.kind !== 'modeling_change')
|
|
2612
|
+
return operation;
|
|
2613
|
+
if ((input.origin === 'ai' || input.origin === 'correction') && operation.change.operation.startsWith('remove_')) {
|
|
2614
|
+
diagnostics.push({ code: 'AI_DELETE_FORBIDDEN', severity: 'blocking', operationId: operation.id, message: 'Modeling AI can create or update context, but it cannot delete governed source.' });
|
|
2615
|
+
}
|
|
2616
|
+
if (operation.change.operation === 'upsert_relationship') {
|
|
2617
|
+
const current = operation.change.value;
|
|
2618
|
+
if (current.crossDomain && (input.origin === 'ai' || input.origin === 'yaml_import')) {
|
|
2619
|
+
diagnostics.push({ code: 'CROSS_DOMAIN_IMPORT_FORBIDDEN', severity: 'blocking', operationId: operation.id, message: 'Cross-domain relationships require the explicit export, contract, and import workflow.' });
|
|
2620
|
+
}
|
|
2621
|
+
if (input.origin === 'ai' || input.origin === 'correction' || input.origin === 'yaml_import') {
|
|
2622
|
+
return {
|
|
2623
|
+
...operation,
|
|
2624
|
+
change: {
|
|
2625
|
+
...operation.change,
|
|
2626
|
+
value: {
|
|
2627
|
+
...current,
|
|
2628
|
+
status: 'draft',
|
|
2629
|
+
fanout: current.fanout === 'unsafe' ? 'forbidden' : current.fanout,
|
|
2630
|
+
certifiedAgainst: undefined,
|
|
2631
|
+
validation: undefined,
|
|
2632
|
+
},
|
|
2633
|
+
},
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
if (operation.change.operation === 'upsert_entity'
|
|
2638
|
+
&& (input.origin === 'ai' || input.origin === 'yaml_import' || input.origin === 'dbt_discovery')
|
|
2639
|
+
&& !operation.change.value.areaId) {
|
|
2640
|
+
diagnostics.push({ code: 'MODEL_AREA_REQUIRED', severity: 'blocking', operationId: operation.id, message: 'Imported and AI-authored models must target one Model Area.' });
|
|
2641
|
+
}
|
|
2642
|
+
return operation;
|
|
2643
|
+
});
|
|
2644
|
+
const operationIds = new Set(operations.map((operation) => operation.id));
|
|
2645
|
+
for (const operation of operations) {
|
|
2646
|
+
for (const dependency of operation.dependsOn ?? []) {
|
|
2647
|
+
if (!operationIds.has(dependency))
|
|
2648
|
+
diagnostics.push({ code: 'DEPENDENCY_CONFLICT', severity: 'blocking', operationId: operation.id, message: `Required proposal operation is missing: ${dependency}` });
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
const patches = [];
|
|
2652
|
+
const modelingOperations = operations.filter((operation) => operation.kind === 'modeling_change');
|
|
2653
|
+
if (modelingOperations.length > 0) {
|
|
2654
|
+
const batch = previewModelingChanges(projectRoot, modelingOperations.map((operation) => operation.change));
|
|
2655
|
+
for (const patch of batch.patches) {
|
|
2656
|
+
const owner = modelingOperations.find((operation) => {
|
|
2657
|
+
const single = previewModelingChange(projectRoot, operation.change);
|
|
2658
|
+
return single.patches.some((candidate) => candidate.path === patch.path);
|
|
2659
|
+
})?.id ?? modelingOperations[0].id;
|
|
2660
|
+
patches.push({ ...patch, owner: 'dql', operationId: owner });
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
for (const operation of operations) {
|
|
2664
|
+
if (operation.kind === 'skill_change') {
|
|
2665
|
+
const preview = previewSkillChange(projectRoot, operation.value, operation.targetQualifiedId, operation.expectedSourceHash);
|
|
2666
|
+
for (const patch of preview.patches) {
|
|
2667
|
+
patches.push({
|
|
2668
|
+
path: relative(projectRoot, patch.path).replace(/\\/g, '/'),
|
|
2669
|
+
before: patch.before,
|
|
2670
|
+
after: patch.after,
|
|
2671
|
+
changed: patch.changed,
|
|
2672
|
+
owner: 'dql',
|
|
2673
|
+
operationId: operation.id,
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
else if (operation.kind === 'dbt_source_change') {
|
|
2678
|
+
const { dbtProjectDir, manifestPath } = onboardingDbtPaths({});
|
|
2679
|
+
const preview = previewDbtSourcePatch(dbtProjectDir, manifestPath, operation.change);
|
|
2680
|
+
patches.push({
|
|
2681
|
+
path: relative(projectRoot, resolve(dbtProjectDir, preview.patch.path)).replace(/\\/g, '/'),
|
|
2682
|
+
before: preview.patch.before,
|
|
2683
|
+
after: preview.patch.after,
|
|
2684
|
+
changed: preview.patch.changed,
|
|
2685
|
+
owner: 'dbt',
|
|
2686
|
+
operationId: operation.id,
|
|
2687
|
+
});
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
const duplicatePaths = patches.map((patch) => patch.path).filter((path, index, values) => values.indexOf(path) !== index);
|
|
2691
|
+
for (const path of [...new Set(duplicatePaths)])
|
|
2692
|
+
diagnostics.push({ code: 'OWNERSHIP_CONFLICT', severity: 'blocking', message: `More than one proposal operation owns ${path}. Repreview after resolving the conflict.` });
|
|
2693
|
+
const dependencyFingerprints = Object.fromEntries(patches.map((patch) => [
|
|
2694
|
+
patch.path,
|
|
2695
|
+
createHash('sha256').update(patch.before).digest('hex'),
|
|
2696
|
+
]));
|
|
2697
|
+
const proposal = buildContextAuthoringProposal({
|
|
2698
|
+
origin: input.origin,
|
|
2699
|
+
baseSnapshotId: snapshot.snapshotId,
|
|
2700
|
+
dependencyFingerprints,
|
|
2701
|
+
operations,
|
|
2702
|
+
patches,
|
|
2703
|
+
diagnostics,
|
|
2704
|
+
sourceRunId: input.sourceRunId,
|
|
2705
|
+
sourceArtifactId: input.sourceArtifactId,
|
|
2706
|
+
revision: input.revision,
|
|
2707
|
+
});
|
|
2708
|
+
contextProposalStore.save(proposal);
|
|
2709
|
+
return proposal;
|
|
2710
|
+
};
|
|
2711
|
+
const modelingAuthoringRunExecutor = async ({ runId, request }) => {
|
|
2712
|
+
const snapshot = projectSnapshot();
|
|
2713
|
+
const operations = buildMetadataBoundModelingOperations(projectRoot, snapshot.manifest, request);
|
|
2714
|
+
const proposal = previewContextAuthoring({
|
|
2715
|
+
origin: request.workspaceContext?.correction === true ? 'correction' : 'ai',
|
|
2716
|
+
operations,
|
|
2717
|
+
expectedSnapshotId: typeof request.workspaceContext?.snapshotId === 'string' ? request.workspaceContext.snapshotId : snapshot.snapshotId,
|
|
2718
|
+
sourceRunId: runId,
|
|
2719
|
+
revision: Number(request.workspaceContext?.revision ?? 1),
|
|
2720
|
+
});
|
|
2721
|
+
return {
|
|
2722
|
+
summary: `Prepared ${proposal.impact.modelingChanges} metadata-bound modeling change${proposal.impact.modelingChanges === 1 ? '' : 's'} for review.`,
|
|
2723
|
+
answer: proposal.diagnostics.some((item) => item.severity === 'blocking')
|
|
2724
|
+
? 'I prepared a modeling proposal, but the blocking items must be resolved before it can be saved.'
|
|
2725
|
+
: 'I prepared an evidence-bound modeling proposal. Review the exact source diff before saving it as a draft.',
|
|
2726
|
+
status: 'needs_review',
|
|
2727
|
+
trustState: 'review_required',
|
|
2728
|
+
stopReason: 'human_review_required',
|
|
2729
|
+
artifacts: [{ id: `${runId}:modeling-proposal`, kind: 'modeling_change_proposal', title: 'Modeling change proposal', trustState: 'review_required', ref: proposal.id, payload: proposal }],
|
|
2730
|
+
evaluations: proposal.diagnostics.map((diagnostic) => ({ id: `${proposal.id}:${diagnostic.code}`, label: diagnostic.code, passed: diagnostic.severity !== 'blocking', severity: diagnostic.severity, message: diagnostic.message })),
|
|
2731
|
+
nextActions: [{ id: 'review-modeling-proposal', label: 'Review in Modeling', artifactKind: 'modeling_change_proposal' }],
|
|
2732
|
+
};
|
|
2733
|
+
};
|
|
2734
|
+
const skillAuthoringRunExecutor = async ({ runId, request }) => {
|
|
2735
|
+
const snapshot = projectSnapshot();
|
|
2736
|
+
const operations = buildMetadataBoundSkillOperations(projectRoot, snapshot.manifest, request);
|
|
2737
|
+
const proposal = previewContextAuthoring({
|
|
2738
|
+
origin: request.workspaceContext?.correction === true ? 'correction' : 'ai',
|
|
2739
|
+
operations,
|
|
2740
|
+
expectedSnapshotId: typeof request.workspaceContext?.snapshotId === 'string' ? request.workspaceContext.snapshotId : snapshot.snapshotId,
|
|
2741
|
+
sourceRunId: runId,
|
|
2742
|
+
revision: Number(request.workspaceContext?.revision ?? 1),
|
|
2743
|
+
});
|
|
2744
|
+
return {
|
|
2745
|
+
summary: `Prepared ${proposal.impact.skillChanges} metadata-bound Skill change${proposal.impact.skillChanges === 1 ? '' : 's'} for review.`,
|
|
2746
|
+
answer: 'I prepared a scope-qualified Skill draft. It will not guide an agent unless you save it and later activate it explicitly.',
|
|
2747
|
+
status: 'needs_review',
|
|
2748
|
+
trustState: 'review_required',
|
|
2749
|
+
stopReason: 'human_review_required',
|
|
2750
|
+
artifacts: [{ id: `${runId}:skill-proposal`, kind: 'skill_change_proposal', title: 'Skill change proposal', trustState: 'review_required', ref: proposal.id, payload: proposal }],
|
|
2751
|
+
evaluations: proposal.diagnostics.map((diagnostic) => ({ id: `${proposal.id}:${diagnostic.code}`, label: diagnostic.code, passed: diagnostic.severity !== 'blocking', severity: diagnostic.severity, message: diagnostic.message })),
|
|
2752
|
+
nextActions: [{ id: 'review-skill-proposal', label: 'Review Skill draft', artifactKind: 'skill_change_proposal' }],
|
|
2753
|
+
};
|
|
2754
|
+
};
|
|
2415
2755
|
const agentRunExecutors = {
|
|
2416
2756
|
conversation: conversationRunExecutor,
|
|
2417
2757
|
certified_answer: answerRunExecutor,
|
|
2418
2758
|
semantic_answer: answerRunExecutor,
|
|
2419
2759
|
generated_answer: answerRunExecutor,
|
|
2760
|
+
modeling_draft: modelingAuthoringRunExecutor,
|
|
2761
|
+
skill_draft: skillAuthoringRunExecutor,
|
|
2420
2762
|
research: async (researchContext) => {
|
|
2421
2763
|
const { runId, request, routeDecision, emit } = researchContext;
|
|
2422
2764
|
const metrics = loadSemanticMetrics(projectRoot);
|
|
@@ -2453,6 +2795,23 @@ export async function startLocalServer(opts) {
|
|
|
2453
2795
|
const needsClarification = Boolean(plan.followUp);
|
|
2454
2796
|
const notebookPath = agentRunNotebookPath(request, runId);
|
|
2455
2797
|
const researchIntent = agentRunResearchIntent(request);
|
|
2798
|
+
const researchSource = agentRunRecord(request.workspaceContext?.researchSource);
|
|
2799
|
+
const researchExecutionConnection = !needsClarification && request.executionTarget
|
|
2800
|
+
? await resolveAgentRunExecutionConnection(request)
|
|
2801
|
+
: undefined;
|
|
2802
|
+
const researchExecutionConnectionName = request.executionTarget?.target === 'connection'
|
|
2803
|
+
? request.executionTarget.connectionName
|
|
2804
|
+
: undefined;
|
|
2805
|
+
const researchContextEnvelope = {
|
|
2806
|
+
surface: 'unified_agent_run',
|
|
2807
|
+
agentRunId: runId,
|
|
2808
|
+
routeDecision,
|
|
2809
|
+
selectedObject: request.selectedObject,
|
|
2810
|
+
executionTarget: request.executionTarget,
|
|
2811
|
+
workspaceContext: request.workspaceContext,
|
|
2812
|
+
researchSource,
|
|
2813
|
+
plan,
|
|
2814
|
+
};
|
|
2456
2815
|
let researchRun;
|
|
2457
2816
|
let researchWorkspaceError;
|
|
2458
2817
|
if (!needsClarification) {
|
|
@@ -2474,14 +2833,7 @@ export async function startLocalServer(opts) {
|
|
|
2474
2833
|
intent: researchIntent,
|
|
2475
2834
|
domain: agentRunWorkspaceValue(request, 'domain'),
|
|
2476
2835
|
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
|
-
},
|
|
2836
|
+
context: researchContextEnvelope,
|
|
2485
2837
|
});
|
|
2486
2838
|
emit({
|
|
2487
2839
|
type: 'artifact.created',
|
|
@@ -2496,14 +2848,13 @@ export async function startLocalServer(opts) {
|
|
|
2496
2848
|
sourceCellFingerprint,
|
|
2497
2849
|
question: request.question,
|
|
2498
2850
|
intent: researchIntent,
|
|
2499
|
-
context:
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
},
|
|
2851
|
+
context: researchContextEnvelope,
|
|
2852
|
+
executionConnection: researchExecutionConnection,
|
|
2853
|
+
executionConnectionName: researchExecutionConnectionName,
|
|
2854
|
+
signal: request.signal,
|
|
2855
|
+
baselineSql: agentRunString(researchSource?.sql),
|
|
2856
|
+
baselineDqlArtifact: researchSource?.dqlArtifact,
|
|
2857
|
+
baselineRunId: agentRunString(researchSource?.runId),
|
|
2507
2858
|
});
|
|
2508
2859
|
researchRun = withNotebookResearchChecklist(executed);
|
|
2509
2860
|
}
|
|
@@ -2816,7 +3167,7 @@ export async function startLocalServer(opts) {
|
|
|
2816
3167
|
message: `Generating review-required SQL for an uncovered question: ${question}`,
|
|
2817
3168
|
route: 'app_build',
|
|
2818
3169
|
});
|
|
2819
|
-
return
|
|
3170
|
+
return generateAppBuildAnswerWithRepair(question);
|
|
2820
3171
|
},
|
|
2821
3172
|
});
|
|
2822
3173
|
}
|
|
@@ -3716,13 +4067,15 @@ export async function startLocalServer(opts) {
|
|
|
3716
4067
|
* evidence. The refusal can say the relation is not visible to this
|
|
3717
4068
|
* connection, rather than only that DQL had not looked at it.
|
|
3718
4069
|
*/
|
|
3719
|
-
const probeNamedRelationsForAgent = async (relations, probeConnection) => {
|
|
4070
|
+
const probeNamedRelationsForAgent = async (relations, probeConnection, verifyExecutionSchema = false) => {
|
|
3720
4071
|
const probeTarget = probeConnection ?? connection;
|
|
3721
4072
|
if (!probeTarget)
|
|
3722
4073
|
return undefined;
|
|
3723
|
-
//
|
|
3724
|
-
//
|
|
3725
|
-
|
|
4074
|
+
// Named relation discovery remains behind the operator's live-grounding
|
|
4075
|
+
// switch. Executable schema verification is different: it is a bounded,
|
|
4076
|
+
// read-only metadata point lookup for relations already selected from dbt,
|
|
4077
|
+
// and never reads row values or mutates the active target.
|
|
4078
|
+
if (!verifyExecutionSchema && resolveAgentRuntimeValueGrounding(projectConfig).mode !== 'safe_automatic')
|
|
3726
4079
|
return undefined;
|
|
3727
4080
|
const sql = buildNamedRelationProbeSql(relations);
|
|
3728
4081
|
if (!sql)
|
|
@@ -3775,12 +4128,29 @@ export async function startLocalServer(opts) {
|
|
|
3775
4128
|
&& activeRuntimeSnapshot.status !== 'stale');
|
|
3776
4129
|
const valueGrounding = resolveAgentRuntimeValueGrounding(projectConfig);
|
|
3777
4130
|
if (catalogContext.length > 0) {
|
|
3778
|
-
if (!schemaConnection
|
|
4131
|
+
if (!schemaConnection)
|
|
3779
4132
|
return catalogContext;
|
|
3780
|
-
//
|
|
3781
|
-
//
|
|
3782
|
-
//
|
|
3783
|
-
|
|
4133
|
+
// dbt metadata is the semantic authority, but generated SQL must use the
|
|
4134
|
+
// physical columns visible on THIS execution target. Verify only the few
|
|
4135
|
+
// question-ranked relations with a bounded information_schema point lookup;
|
|
4136
|
+
// this is read-only and never mutates DuckDB, Snowflake, or any other target.
|
|
4137
|
+
let executionSchema = catalogContext;
|
|
4138
|
+
try {
|
|
4139
|
+
const live = await probeNamedRelationsForAgent(catalogContext.slice(0, 4).map((table) => table.relation), schemaConnection, true);
|
|
4140
|
+
if (live?.schemaContext?.length) {
|
|
4141
|
+
executionSchema = reconcileAgentSchemaContextWithLive(catalogContext, live.schemaContext);
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
catch {
|
|
4145
|
+
// Live verification is additive. Existing metadata remains available if
|
|
4146
|
+
// the connection cannot expose information_schema to this user.
|
|
4147
|
+
}
|
|
4148
|
+
// The immutable dbt/DQL snapshot owns schema discovery. Live verification
|
|
4149
|
+
// only confirms those named relations; row values remain governed by the
|
|
4150
|
+
// separate allowlisted grounding policy below.
|
|
4151
|
+
if (valueGrounding.mode !== 'safe_automatic')
|
|
4152
|
+
return executionSchema;
|
|
4153
|
+
const enriched = await enrichAgentSchemaContextWithValueMatches(question, executionSchema, executor, schemaConnection, valueGrounding.searchSafeColumns);
|
|
3784
4154
|
return enriched;
|
|
3785
4155
|
}
|
|
3786
4156
|
// An activated, scope-qualified warehouse generation is the complete warm
|
|
@@ -3912,83 +4282,365 @@ export async function startLocalServer(opts) {
|
|
|
3912
4282
|
}
|
|
3913
4283
|
return { result, repairedSql, repairMode, firstFailure, rewrites };
|
|
3914
4284
|
};
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
4285
|
+
/**
|
|
4286
|
+
* Repair a DQL wrapper that cannot compile far enough to expose executable
|
|
4287
|
+
* SQL. This is the DQL-side half of the same bounded repair contract used by
|
|
4288
|
+
* Ask and Notebook: one provider call, one block, identical parameters, one
|
|
4289
|
+
* read-only execution, and the exact target selected by the failed run.
|
|
4290
|
+
*/
|
|
4291
|
+
const executeBoundedDqlRepair = async (input) => {
|
|
4292
|
+
const provider = await createBlockStudioAssistProvider(projectRoot);
|
|
4293
|
+
if (!provider) {
|
|
4294
|
+
throw boundedRepairError('Configure an available AI provider in Settings to repair this DQL draft.', 409);
|
|
3921
4295
|
}
|
|
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: [{
|
|
4296
|
+
let schemaContext;
|
|
4297
|
+
try {
|
|
4298
|
+
// Include source identifiers in retrieval so a terse cell title still
|
|
4299
|
+
// resolves the physical dbt relations and live target columns it uses.
|
|
4300
|
+
schemaContext = await getSchemaContextForAgent(`${input.question}\n${input.source.slice(0, 6_000)}`, undefined, input.targetConnection, input.targetConnectionName);
|
|
4301
|
+
}
|
|
4302
|
+
catch (error) {
|
|
4303
|
+
const failure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
|
|
4304
|
+
throw boundedRepairError(failure.redactedMessage, 422, failure);
|
|
4305
|
+
}
|
|
4306
|
+
const boundedSchema = schemaContext.slice(0, 16).map((table) => ({
|
|
4307
|
+
relation: table.relation,
|
|
4308
|
+
columns: table.columns.slice(0, 40).map((column) => ({ name: column.name, type: column.type })),
|
|
4309
|
+
}));
|
|
4310
|
+
let raw;
|
|
4311
|
+
try {
|
|
4312
|
+
raw = await provider.generate([
|
|
4313
|
+
{
|
|
4314
|
+
role: 'system',
|
|
4315
|
+
content: [
|
|
4316
|
+
'Repair one malformed executable DQL custom block and its embedded read-only SQL.',
|
|
4317
|
+
'Return only one JSON object with exactly one key named dql. Do not use markdown.',
|
|
4318
|
+
'The DQL shape is: block "Name" { type = "custom" params { name: type = default } query = """SELECT ...""" visualization { ... } tests { ... } }.',
|
|
4319
|
+
'Preserve the block name, business fields, visualization, tests, requested grain, filters, output columns, and aggregation semantics.',
|
|
4320
|
+
'Preserve every declared parameter and every ${parameter} interpolation exactly. Do not add or remove parameters.',
|
|
4321
|
+
'Use only relations and columns present in the supplied schema context.',
|
|
4322
|
+
'Never emit internal relation identities such as source::, model::, seed::, or snapshot::.',
|
|
4323
|
+
'Return exactly one custom block. Never certify it, bind a saved block, add writes, DDL, or multiple SQL statements.',
|
|
4324
|
+
].join(' '),
|
|
4325
|
+
},
|
|
4326
|
+
{
|
|
3959
4327
|
role: 'user',
|
|
3960
|
-
content:
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
}
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
4328
|
+
content: JSON.stringify({
|
|
4329
|
+
question: input.question,
|
|
4330
|
+
driver: input.targetConnection.driver,
|
|
4331
|
+
failure: input.failure.slice(0, 2_000),
|
|
4332
|
+
dql: input.source.slice(0, 40_000),
|
|
4333
|
+
parameters: input.parameters ?? {},
|
|
4334
|
+
schema: boundedSchema,
|
|
4335
|
+
}),
|
|
4336
|
+
},
|
|
4337
|
+
], { maxTokens: 2400, temperature: 0 });
|
|
4338
|
+
}
|
|
4339
|
+
catch {
|
|
4340
|
+
throw boundedRepairError('The AI provider could not complete this DQL repair. Check Provider Settings and retry.', 502);
|
|
4341
|
+
}
|
|
4342
|
+
let repairedSource;
|
|
4343
|
+
try {
|
|
4344
|
+
const parsed = parseFirstJsonObject(raw);
|
|
4345
|
+
repairedSource = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
4346
|
+
&& typeof parsed.dql === 'string'
|
|
4347
|
+
? String(parsed.dql).trim()
|
|
4348
|
+
: undefined;
|
|
4349
|
+
}
|
|
4350
|
+
catch {
|
|
4351
|
+
repairedSource = undefined;
|
|
4352
|
+
}
|
|
4353
|
+
if (!repairedSource) {
|
|
4354
|
+
throw boundedRepairError('The AI repair did not return a complete DQL block.', 422);
|
|
4355
|
+
}
|
|
4356
|
+
if (repairedSource.length > 50_000) {
|
|
4357
|
+
throw boundedRepairError('The repaired DQL exceeded the safe editable-draft size limit.', 422);
|
|
4358
|
+
}
|
|
4359
|
+
if (/^\s*@block\(/i.test(repairedSource) || /\btype\s*=\s*"semantic"/i.test(repairedSource)) {
|
|
4360
|
+
throw boundedRepairError('The repair changed this editable custom block into a governed reference or semantic block.', 422);
|
|
4361
|
+
}
|
|
4362
|
+
if (!/\bstatus\s*=\s*"certified"/i.test(input.source)
|
|
4363
|
+
&& /\bstatus\s*=\s*"certified"/i.test(repairedSource)) {
|
|
4364
|
+
throw boundedRepairError('The repair attempted to certify an AI-edited block, so DQL stopped it.', 422);
|
|
4365
|
+
}
|
|
4366
|
+
const beforeParameters = dqlRepairParameterContract(input.source);
|
|
4367
|
+
const afterParameters = dqlRepairParameterContract(repairedSource);
|
|
4368
|
+
if (beforeParameters.join('|') !== afterParameters.join('|')) {
|
|
4369
|
+
throw boundedRepairError('The repair changed this block\'s parameter contract, so DQL stopped it before execution.', 422);
|
|
4370
|
+
}
|
|
4371
|
+
let invocation;
|
|
4372
|
+
let plan;
|
|
4373
|
+
try {
|
|
4374
|
+
const program = new Parser(repairedSource, '<bounded-dql-repair>').parse();
|
|
4375
|
+
const blocks = program.statements.filter((statement) => statement.kind === NodeKind.BlockDecl);
|
|
4376
|
+
if (blocks.length !== 1 || program.statements.length !== 1) {
|
|
4377
|
+
throw new Error('The repaired source must contain exactly one DQL block.');
|
|
4378
|
+
}
|
|
4379
|
+
const block = blocks[0];
|
|
4380
|
+
if (block.kind !== NodeKind.BlockDecl || block.blockType !== 'custom') {
|
|
4381
|
+
throw new Error('The repaired source must remain a custom DQL block.');
|
|
4382
|
+
}
|
|
4383
|
+
invocation = prepareBlockInvocation({
|
|
4384
|
+
source: repairedSource,
|
|
4385
|
+
parameters: input.parameters,
|
|
4386
|
+
question: input.question,
|
|
4387
|
+
surface: 'notebook',
|
|
4388
|
+
});
|
|
4389
|
+
if (invocation.errors.length)
|
|
4390
|
+
throw new Error(invocation.errors.join(' '));
|
|
4391
|
+
if (invocation.unresolvedParameters.length) {
|
|
4392
|
+
throw new Error(`Provide required parameters before retrying: ${invocation.unresolvedParameters.join(', ')}.`);
|
|
4393
|
+
}
|
|
4394
|
+
plan = buildExecutionPlan({
|
|
4395
|
+
id: 'bounded-dql-repair',
|
|
4396
|
+
type: 'dql',
|
|
4397
|
+
source: repairedSource,
|
|
4398
|
+
title: input.artifactName,
|
|
4399
|
+
}, {
|
|
4400
|
+
semanticLayer,
|
|
4401
|
+
driver: input.targetConnection.driver,
|
|
4402
|
+
parameters: invocation.values,
|
|
4403
|
+
});
|
|
4404
|
+
if (!plan?.sql?.trim())
|
|
4405
|
+
throw new Error('The repaired DQL did not compile to SQL.');
|
|
4406
|
+
}
|
|
4407
|
+
catch (error) {
|
|
4408
|
+
throw boundedRepairError(`The repaired DQL did not compile safely: ${error instanceof Error ? error.message : String(error)}`, 422);
|
|
4409
|
+
}
|
|
4410
|
+
const seed = {
|
|
4411
|
+
kind: 'sql_block',
|
|
4412
|
+
source: repairedSource,
|
|
4413
|
+
name: input.artifactName ?? plan.title,
|
|
4414
|
+
persistence: 'transient',
|
|
4415
|
+
trustState: 'review_required',
|
|
4416
|
+
compiledSql: plan.sql,
|
|
4417
|
+
...(invocation.parameters.length ? { parameters: invocation.parameters } : {}),
|
|
4418
|
+
...(Object.keys(invocation.values).length ? { parameterValues: invocation.values } : {}),
|
|
4419
|
+
};
|
|
4420
|
+
let result;
|
|
4421
|
+
try {
|
|
4422
|
+
result = await executeGeneratedSqlDirect(input.question, plan.sql, seed, input.targetConnection, { sqlParams: plan.sqlParams, variables: { ...plan.variables, ...invocation.values } });
|
|
4423
|
+
}
|
|
4424
|
+
catch (error) {
|
|
4425
|
+
const failure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
|
|
4426
|
+
throw boundedRepairError(failure.redactedMessage, 422, failure);
|
|
4427
|
+
}
|
|
4428
|
+
result = {
|
|
4429
|
+
...result,
|
|
4430
|
+
dqlArtifact: {
|
|
4431
|
+
...(result.dqlArtifact ?? seed),
|
|
4432
|
+
source: repairedSource,
|
|
4433
|
+
compiledSql: result.sql ?? plan.sql,
|
|
4434
|
+
trustState: 'review_required',
|
|
4435
|
+
persistence: 'transient',
|
|
4436
|
+
...(result.executionReceipt ? { executionReceipt: result.executionReceipt } : {}),
|
|
3972
4437
|
},
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
4438
|
+
};
|
|
4439
|
+
return {
|
|
4440
|
+
result,
|
|
4441
|
+
repairedSql: plan.sql,
|
|
4442
|
+
repairedSource,
|
|
4443
|
+
repairMode: 'ai',
|
|
4444
|
+
rewrites: [],
|
|
4445
|
+
sqlParams: plan.sqlParams,
|
|
4446
|
+
variables: { ...plan.variables, ...invocation.values },
|
|
4447
|
+
};
|
|
4448
|
+
};
|
|
4449
|
+
/**
|
|
4450
|
+
* App proposal parity with Ask/Notebook repair. The first governed attempt is
|
|
4451
|
+
* retained unchanged. Only an execution failure with retained read-only SQL
|
|
4452
|
+
* may spend the shared one-attempt repair budget, on the same connection.
|
|
4453
|
+
* The repaired candidate is still exploratory and review-required.
|
|
4454
|
+
*/
|
|
4455
|
+
const generateAppBuildAnswerWithRepair = async (question, appContext) => {
|
|
4456
|
+
const request = {
|
|
4457
|
+
question,
|
|
4458
|
+
audience: 'stakeholder',
|
|
4459
|
+
workspaceContext: appContext
|
|
4460
|
+
? { surface: 'app_builder', appContext }
|
|
4461
|
+
: { surface: 'app_builder' },
|
|
4462
|
+
};
|
|
4463
|
+
const answer = await runGovernedAgentAnswerForRun(request);
|
|
4464
|
+
const sourceSql = answer.proposedSql?.trim() || answer.sql?.trim();
|
|
4465
|
+
const hasExecutionFailure = agentAnswerHasExecutionFailure(answer)
|
|
4466
|
+
|| Boolean(answer.warehouseFailure)
|
|
4467
|
+
|| Boolean(answer.analyticalFailure);
|
|
4468
|
+
if (answer.result || !sourceSql || !hasExecutionFailure)
|
|
4469
|
+
return answer;
|
|
4470
|
+
const attemptedAt = new Date().toISOString();
|
|
4471
|
+
const originalFailure = answer.warehouseFailure?.redactedMessage
|
|
4472
|
+
?? answer.executionError
|
|
4473
|
+
?? 'The generated query did not execute.';
|
|
4474
|
+
const originalSqlFingerprint = `sha256:${createHash('sha256').update(sourceSql).digest('hex')}`;
|
|
4475
|
+
const failedTrace = (message) => ({
|
|
4476
|
+
version: 1,
|
|
4477
|
+
status: 'failed',
|
|
4478
|
+
source: 'query_generator',
|
|
4479
|
+
attemptedAt,
|
|
4480
|
+
originalFailure,
|
|
4481
|
+
originalSqlFingerprint,
|
|
4482
|
+
approvalEligible: false,
|
|
4483
|
+
message,
|
|
4484
|
+
});
|
|
4485
|
+
if (!analyticalFailureAllowsDeterministicRetry(answer.warehouseFailure)
|
|
4486
|
+
|| !analyticalFailureAllowsAppRepair(answer.analyticalFailure)) {
|
|
4487
|
+
return {
|
|
4488
|
+
...answer,
|
|
4489
|
+
appBuilderRepair: failedTrace('This failure needs an access, policy, or connection decision and was not sent to AI repair.'),
|
|
4490
|
+
};
|
|
4491
|
+
}
|
|
4492
|
+
try {
|
|
4493
|
+
const targetConnection = await resolveAgentRunExecutionConnection(request);
|
|
4494
|
+
const targetConnectionName = request.executionTarget?.target === 'connection'
|
|
4495
|
+
? request.executionTarget.connectionName
|
|
4496
|
+
: request.executionTarget?.target === 'local'
|
|
4497
|
+
? 'local'
|
|
4498
|
+
: projectConfig.defaultConnectionName;
|
|
4499
|
+
const repaired = await executeBoundedSqlRepair({
|
|
4500
|
+
question,
|
|
4501
|
+
sourceSql,
|
|
4502
|
+
targetConnection,
|
|
4503
|
+
targetConnectionName,
|
|
4504
|
+
});
|
|
4505
|
+
const repair = {
|
|
4506
|
+
version: 1,
|
|
4507
|
+
status: 'repaired',
|
|
4508
|
+
source: 'query_generator',
|
|
4509
|
+
mode: repaired.repairMode,
|
|
4510
|
+
attemptedAt,
|
|
4511
|
+
originalFailure,
|
|
4512
|
+
originalSqlFingerprint,
|
|
4513
|
+
repairedSqlFingerprint: `sha256:${createHash('sha256').update(repaired.repairedSql).digest('hex')}`,
|
|
4514
|
+
approvalEligible: false,
|
|
4515
|
+
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.`,
|
|
4516
|
+
};
|
|
4517
|
+
return {
|
|
4518
|
+
...answer,
|
|
4519
|
+
result: repaired.result,
|
|
4520
|
+
proposedSql: repaired.repairedSql,
|
|
4521
|
+
sql: repaired.repairedSql,
|
|
4522
|
+
executionError: undefined,
|
|
4523
|
+
warehouseFailure: undefined,
|
|
4524
|
+
analyticalFailure: undefined,
|
|
4525
|
+
validationWarnings: [
|
|
4526
|
+
...(answer.validationWarnings ?? []),
|
|
4527
|
+
repair.message,
|
|
4528
|
+
],
|
|
4529
|
+
appBuilderRepair: repair,
|
|
4530
|
+
};
|
|
4531
|
+
}
|
|
4532
|
+
catch (error) {
|
|
4533
|
+
return {
|
|
4534
|
+
...answer,
|
|
4535
|
+
appBuilderRepair: failedTrace(error instanceof Error
|
|
4536
|
+
? `The bounded repair could not complete: ${error.message}`
|
|
4537
|
+
: 'The bounded repair could not complete.'),
|
|
4538
|
+
};
|
|
4539
|
+
}
|
|
4540
|
+
};
|
|
4541
|
+
const repairFailedAppTileExecution = async (input) => {
|
|
4542
|
+
const attemptedAt = new Date().toISOString();
|
|
4543
|
+
const driver = input.targetConnection?.driver ?? connection?.driver ?? 'duckdb';
|
|
4544
|
+
const failure = normalizeWarehouseSqlFailure(input.error, driver);
|
|
4545
|
+
const originalFailure = failure.redactedMessage;
|
|
4546
|
+
const originalSqlFingerprint = input.sourceSql
|
|
4547
|
+
? `sha256:${createHash('sha256').update(input.sourceSql).digest('hex')}`
|
|
4548
|
+
: undefined;
|
|
4549
|
+
const failed = (message) => ({
|
|
4550
|
+
ok: false,
|
|
4551
|
+
repair: {
|
|
4552
|
+
version: 1,
|
|
4553
|
+
status: 'failed',
|
|
4554
|
+
source: input.source,
|
|
4555
|
+
attemptedAt,
|
|
4556
|
+
originalFailure,
|
|
4557
|
+
...(originalSqlFingerprint ? { originalSqlFingerprint } : {}),
|
|
4558
|
+
approvalEligible: false,
|
|
4559
|
+
message,
|
|
4560
|
+
},
|
|
4561
|
+
});
|
|
4562
|
+
if (!input.targetConnection) {
|
|
4563
|
+
return failed('Configure the original data target before retrying this App tile.');
|
|
4564
|
+
}
|
|
4565
|
+
if (!input.sourceSql?.trim() && input.sourceDql?.trim()) {
|
|
4566
|
+
try {
|
|
4567
|
+
const repaired = await executeBoundedDqlRepair({
|
|
4568
|
+
question: input.question,
|
|
4569
|
+
source: input.sourceDql,
|
|
4570
|
+
failure: originalFailure,
|
|
4571
|
+
targetConnection: input.targetConnection,
|
|
4572
|
+
targetConnectionName: input.targetConnectionName,
|
|
4573
|
+
parameters: input.variables,
|
|
4574
|
+
artifactName: input.artifactName,
|
|
4575
|
+
});
|
|
4576
|
+
const repair = {
|
|
4577
|
+
version: 1,
|
|
4578
|
+
status: 'repaired',
|
|
4579
|
+
source: input.source,
|
|
4580
|
+
mode: repaired.repairMode,
|
|
4581
|
+
attemptedAt,
|
|
4582
|
+
originalFailure,
|
|
4583
|
+
repairedSqlFingerprint: `sha256:${createHash('sha256').update(repaired.repairedSql).digest('hex')}`,
|
|
4584
|
+
approvalEligible: false,
|
|
4585
|
+
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.`,
|
|
4586
|
+
};
|
|
4587
|
+
return {
|
|
4588
|
+
ok: true,
|
|
4589
|
+
result: repaired.result,
|
|
4590
|
+
repairedSql: repaired.repairedSql,
|
|
4591
|
+
repairedDql: repaired.repairedSource,
|
|
4592
|
+
repair,
|
|
4593
|
+
};
|
|
3982
4594
|
}
|
|
3983
|
-
|
|
3984
|
-
|
|
4595
|
+
catch (error) {
|
|
4596
|
+
return failed(error instanceof Error
|
|
4597
|
+
? `The bounded DQL repair could not complete: ${error.message}`
|
|
4598
|
+
: 'The bounded DQL repair could not complete.');
|
|
3985
4599
|
}
|
|
3986
|
-
}, controller.signal);
|
|
3987
|
-
if (!governedAnswer) {
|
|
3988
|
-
throw new Error(providerError ?? 'The AI provider did not return a governed answer.');
|
|
3989
4600
|
}
|
|
4601
|
+
if (!input.sourceSql?.trim()) {
|
|
4602
|
+
return failed('DQL retained the failure, but no read-only SQL or editable DQL was available for bounded repair.');
|
|
4603
|
+
}
|
|
4604
|
+
if (!analyticalFailureAllowsDeterministicRetry(failure)) {
|
|
4605
|
+
return failed('This failure needs an access, policy, or connection decision and was not sent to AI repair.');
|
|
4606
|
+
}
|
|
4607
|
+
try {
|
|
4608
|
+
const repaired = await executeBoundedSqlRepair({
|
|
4609
|
+
question: input.question,
|
|
4610
|
+
sourceSql: input.sourceSql,
|
|
4611
|
+
targetConnection: input.targetConnection,
|
|
4612
|
+
targetConnectionName: input.targetConnectionName,
|
|
4613
|
+
sqlParams: input.sqlParams,
|
|
4614
|
+
variables: input.variables,
|
|
4615
|
+
});
|
|
4616
|
+
const repair = {
|
|
4617
|
+
version: 1,
|
|
4618
|
+
status: 'repaired',
|
|
4619
|
+
source: input.source,
|
|
4620
|
+
mode: repaired.repairMode,
|
|
4621
|
+
attemptedAt,
|
|
4622
|
+
originalFailure,
|
|
4623
|
+
...(originalSqlFingerprint ? { originalSqlFingerprint } : {}),
|
|
4624
|
+
repairedSqlFingerprint: `sha256:${createHash('sha256').update(repaired.repairedSql).digest('hex')}`,
|
|
4625
|
+
approvalEligible: false,
|
|
4626
|
+
message: `${repaired.repairMode === 'ai' ? 'AI' : 'Deterministic'} repair executed successfully on the same data target. The result remains review-required.`,
|
|
4627
|
+
};
|
|
4628
|
+
return { ok: true, result: repaired.result, repairedSql: repaired.repairedSql, repair };
|
|
4629
|
+
}
|
|
4630
|
+
catch (error) {
|
|
4631
|
+
return failed(error instanceof Error
|
|
4632
|
+
? `The bounded repair could not complete: ${error.message}`
|
|
4633
|
+
: 'The bounded repair could not complete.');
|
|
4634
|
+
}
|
|
4635
|
+
};
|
|
4636
|
+
const generateInvestigationSqlForApp = async (input) => {
|
|
4637
|
+
const request = buildAppCopilotResearchAgentRequest(input);
|
|
4638
|
+
// AGT-007 / AGT-022: App Copilot keeps its server-owned App evidence
|
|
4639
|
+
// adapter, while routing the actual investigation through the uniform
|
|
4640
|
+
// snapshot, retrieval, trust, execution, and bounded-repair orchestration.
|
|
4641
|
+
const governedAnswer = await runGovernedAgentAnswerForRun(request, undefined, 'research');
|
|
3990
4642
|
return {
|
|
3991
|
-
sql:
|
|
4643
|
+
sql: input.mode === 'memo_only' ? undefined : governedAnswer.proposedSql ?? governedAnswer.sql,
|
|
3992
4644
|
answer: governedAnswer.answer ?? governedAnswer.text,
|
|
3993
4645
|
result: governedAnswer.result,
|
|
3994
4646
|
analysisPlan: governedAnswer.analysisPlan,
|
|
@@ -4106,6 +4758,9 @@ export async function startLocalServer(opts) {
|
|
|
4106
4758
|
let generatedSql = notebookResearchString(input.generatedSql) ?? run.generatedSql;
|
|
4107
4759
|
let reviewedSql = notebookResearchString(input.reviewedSql) ?? run.reviewedSql;
|
|
4108
4760
|
let dqlArtifact = normalizeDqlArtifactReference(input.dqlArtifact) ?? run.dqlArtifact;
|
|
4761
|
+
const baselineSql = notebookResearchString(input.baselineSql);
|
|
4762
|
+
const baselineDqlArtifact = normalizeDqlArtifactReference(input.baselineDqlArtifact);
|
|
4763
|
+
let usedBaselineFallback = false;
|
|
4109
4764
|
const startedAt = new Date().toISOString();
|
|
4110
4765
|
storage.updateRun(run.id, {
|
|
4111
4766
|
domain,
|
|
@@ -4156,7 +4811,7 @@ export async function startLocalServer(opts) {
|
|
|
4156
4811
|
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
4812
|
}
|
|
4158
4813
|
else {
|
|
4159
|
-
const
|
|
4814
|
+
const researchSignal = input.signal ?? new AbortController().signal;
|
|
4160
4815
|
try {
|
|
4161
4816
|
await runner.run({
|
|
4162
4817
|
provider: resolvedProvider,
|
|
@@ -4178,18 +4833,19 @@ export async function startLocalServer(opts) {
|
|
|
4178
4833
|
reasoningEffort: resolveRunReasoningEffort(projectRoot, resolvedProvider, 'research', false),
|
|
4179
4834
|
analysisDepth: 'deep',
|
|
4180
4835
|
projectRoot,
|
|
4181
|
-
executeCertifiedBlock: executeCertifiedBlockForAgent,
|
|
4182
|
-
executeGeneratedSql:
|
|
4183
|
-
getSchemaContext: getSchemaContextForAgent,
|
|
4836
|
+
executeCertifiedBlock: (node, invocation) => executeCertifiedBlockForAgent(node, invocation, input.executionConnection, input.executionConnectionName),
|
|
4837
|
+
executeGeneratedSql: (sql, artifact) => executeGeneratedArtifactForAgent(question, sql, artifact, input.executionConnection, input.executionConnectionName),
|
|
4838
|
+
getSchemaContext: (schemaQuestion, preparedContextPack) => getSchemaContextForAgent(schemaQuestion, preparedContextPack, input.executionConnection, input.executionConnectionName),
|
|
4184
4839
|
}, (turn) => {
|
|
4185
4840
|
if (turn.kind === 'tool_result' && turn.id === 'governed_answer') {
|
|
4186
4841
|
governedAnswer = turn.output;
|
|
4187
4842
|
}
|
|
4188
4843
|
if (turn.kind === 'error')
|
|
4189
4844
|
providerError = turn.message;
|
|
4190
|
-
},
|
|
4845
|
+
}, researchSignal);
|
|
4191
4846
|
}
|
|
4192
4847
|
catch (error) {
|
|
4848
|
+
rethrowIfCancelled(error, input.signal);
|
|
4193
4849
|
providerError = error instanceof Error ? error.message : String(error);
|
|
4194
4850
|
}
|
|
4195
4851
|
if (!governedAnswer) {
|
|
@@ -4204,6 +4860,25 @@ export async function startLocalServer(opts) {
|
|
|
4204
4860
|
}
|
|
4205
4861
|
}
|
|
4206
4862
|
}
|
|
4863
|
+
const deeperCompositionFailed = !governedAnswer
|
|
4864
|
+
|| governedAnswer.kind === 'no_answer'
|
|
4865
|
+
|| agentAnswerHasExecutionFailure(governedAnswer)
|
|
4866
|
+
|| Boolean(governedAnswer.analyticalFailure)
|
|
4867
|
+
|| (!governedAnswer.result && !generatedSql && !dqlArtifact);
|
|
4868
|
+
if (!reviewedSql && baselineSql && deeperCompositionFailed) {
|
|
4869
|
+
const failure = governedAnswer?.executionError
|
|
4870
|
+
?? governedAnswer?.analyticalFailure?.message
|
|
4871
|
+
?? governedAnswer?.answer
|
|
4872
|
+
?? governedAnswer?.text
|
|
4873
|
+
?? providerError;
|
|
4874
|
+
generatedSql = baselineSql;
|
|
4875
|
+
dqlArtifact = baselineDqlArtifact ?? dqlArtifact;
|
|
4876
|
+
usedBaselineFallback = true;
|
|
4877
|
+
generationWarnings.push([
|
|
4878
|
+
'The deeper composition did not produce a safer executable query, so DQL kept and revalidated the successful Ask baseline on the same data target.',
|
|
4879
|
+
failure ? `Research detail: ${failure}` : '',
|
|
4880
|
+
].filter(Boolean).join(' '));
|
|
4881
|
+
}
|
|
4207
4882
|
const sqlForPreview = reviewedSql || generatedSql;
|
|
4208
4883
|
const warnings = [
|
|
4209
4884
|
...(contextPack.warnings ?? []),
|
|
@@ -4212,14 +4887,14 @@ export async function startLocalServer(opts) {
|
|
|
4212
4887
|
].filter(Boolean);
|
|
4213
4888
|
let resultPreview;
|
|
4214
4889
|
let previewError;
|
|
4215
|
-
if (governedAnswer?.result?.rows && !reviewedSql) {
|
|
4890
|
+
if (!usedBaselineFallback && governedAnswer?.result?.rows && !reviewedSql) {
|
|
4216
4891
|
resultPreview = normalizeNotebookAgentResult(governedAnswer.result);
|
|
4217
4892
|
}
|
|
4218
4893
|
else if (sqlForPreview) {
|
|
4219
4894
|
try {
|
|
4220
4895
|
const previewSql = buildAgentPreviewSql(sqlForPreview);
|
|
4221
4896
|
const previewStart = Date.now();
|
|
4222
|
-
resultPreview = await executeLocalSqlForStoredResult(previewSql);
|
|
4897
|
+
resultPreview = await executeLocalSqlForStoredResult(previewSql, input.executionConnection);
|
|
4223
4898
|
recordNotebookQueryRun(projectRoot, {
|
|
4224
4899
|
notebookPath: run.notebookPath,
|
|
4225
4900
|
cellId: run.sourceCellId,
|
|
@@ -4259,8 +4934,10 @@ export async function startLocalServer(opts) {
|
|
|
4259
4934
|
: undefined;
|
|
4260
4935
|
const evidence = {
|
|
4261
4936
|
trustStatus: {
|
|
4262
|
-
label:
|
|
4263
|
-
|
|
4937
|
+
label: usedBaselineFallback
|
|
4938
|
+
? 'Successful Ask baseline · revalidated on the same target'
|
|
4939
|
+
: governedAnswer?.trustLabel
|
|
4940
|
+
?? (reviewedSql ? 'Reviewed notebook SQL' : generatedSql ? 'AI-generated research SQL' : dqlArtifact ? 'AI-generated DQL artifact' : 'Metadata-grounded research plan'),
|
|
4264
4941
|
reviewRequired: true,
|
|
4265
4942
|
},
|
|
4266
4943
|
contextPackId: contextPack.id,
|
|
@@ -4294,9 +4971,11 @@ export async function startLocalServer(opts) {
|
|
|
4294
4971
|
...(governedAnswer?.citations ?? []),
|
|
4295
4972
|
].slice(0, 40),
|
|
4296
4973
|
};
|
|
4297
|
-
const summary =
|
|
4298
|
-
|
|
4299
|
-
|
|
4974
|
+
const summary = usedBaselineFallback && resultPreview && !previewError
|
|
4975
|
+
? '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.'
|
|
4976
|
+
: notebookResearchString(governedAnswer?.answer)
|
|
4977
|
+
?? notebookResearchString(governedAnswer?.text)
|
|
4978
|
+
?? notebookResearchSummary(question, resultPreview, previewError);
|
|
4300
4979
|
const recommendation = previewError
|
|
4301
4980
|
? 'Review the SQL, selected metadata, and connection context before rerunning.'
|
|
4302
4981
|
: dqlArtifact && !reviewedSql
|
|
@@ -4334,6 +5013,9 @@ export async function startLocalServer(opts) {
|
|
|
4334
5013
|
contextPackId: contextPack.id,
|
|
4335
5014
|
routeDecision,
|
|
4336
5015
|
warnings: [
|
|
5016
|
+
...(usedBaselineFallback && input.baselineRunId
|
|
5017
|
+
? [`Baseline Ask run: ${input.baselineRunId}`]
|
|
5018
|
+
: []),
|
|
4337
5019
|
...warnings,
|
|
4338
5020
|
...(display && !display.ok ? [display.error] : []),
|
|
4339
5021
|
...(display && display.ok ? display.warnings : []),
|
|
@@ -4344,6 +5026,7 @@ export async function startLocalServer(opts) {
|
|
|
4344
5026
|
}) ?? run;
|
|
4345
5027
|
}
|
|
4346
5028
|
catch (error) {
|
|
5029
|
+
rethrowIfCancelled(error, input.signal);
|
|
4347
5030
|
return storage.updateRun(run.id, {
|
|
4348
5031
|
domain,
|
|
4349
5032
|
owner,
|
|
@@ -4570,16 +5253,19 @@ export async function startLocalServer(opts) {
|
|
|
4570
5253
|
});
|
|
4571
5254
|
// SSE clients for /api/watch hot-reload
|
|
4572
5255
|
const sseClients = new Set();
|
|
5256
|
+
const projectWatchers = [];
|
|
5257
|
+
let runtimeClosing = false;
|
|
4573
5258
|
// Watch all Git-owned authoring roots. Domain Packages are recursive and are
|
|
4574
5259
|
// the canonical home for modeling, skills, blocks, notebooks, Apps, and evals.
|
|
4575
5260
|
if (projectRoot) {
|
|
4576
|
-
let sourceRefreshTimer;
|
|
4577
5261
|
for (const dir of ['domains', 'skills', 'tests', 'notebooks', 'workbooks', 'blocks', 'apps', 'dashboards', 'semantic-layer', 'data']) {
|
|
4578
5262
|
const watchDir = join(projectRoot, dir);
|
|
4579
5263
|
if (!existsSync(watchDir))
|
|
4580
5264
|
continue;
|
|
4581
5265
|
try {
|
|
4582
|
-
watch(watchDir, { persistent: false, recursive: true }, (eventType, filename) => {
|
|
5266
|
+
const watcher = watch(watchDir, { persistent: false, recursive: true }, (eventType, filename) => {
|
|
5267
|
+
if (runtimeClosing)
|
|
5268
|
+
return;
|
|
4583
5269
|
if (!filename)
|
|
4584
5270
|
return;
|
|
4585
5271
|
const path = `${dir}/${filename}`;
|
|
@@ -4594,11 +5280,7 @@ export async function startLocalServer(opts) {
|
|
|
4594
5280
|
}
|
|
4595
5281
|
if (['domains', 'skills', 'tests', 'blocks', 'apps', 'notebooks'].includes(dir)) {
|
|
4596
5282
|
invalidateAgentProjectState(projectRoot);
|
|
4597
|
-
|
|
4598
|
-
clearTimeout(sourceRefreshTimer);
|
|
4599
|
-
sourceRefreshTimer = setTimeout(() => {
|
|
4600
|
-
void refreshUnifiedProjectIndexes(projectRoot).catch(() => undefined);
|
|
4601
|
-
}, 250);
|
|
5283
|
+
scheduleProjectRefresh(`file:${path}`);
|
|
4602
5284
|
}
|
|
4603
5285
|
// Hot-reload semantic layer on change and notify frontend
|
|
4604
5286
|
if (dir === 'semantic-layer') {
|
|
@@ -4616,21 +5298,47 @@ export async function startLocalServer(opts) {
|
|
|
4616
5298
|
}).catch(() => { });
|
|
4617
5299
|
}
|
|
4618
5300
|
});
|
|
5301
|
+
projectWatchers.push(watcher);
|
|
4619
5302
|
}
|
|
4620
5303
|
catch { /* dir not watchable */ }
|
|
4621
5304
|
}
|
|
5305
|
+
// A dbt repository commonly keeps its shared Skills folder beside the DQL
|
|
5306
|
+
// project (`../skills`). It is Git-owned context too, so watch the resolved
|
|
5307
|
+
// configured root even when it is not the default projectRoot/skills path.
|
|
5308
|
+
const configuredSkillsRoot = skillsDir(projectRoot);
|
|
5309
|
+
const defaultSkillsRoot = join(projectRoot, 'skills');
|
|
5310
|
+
if (existsSync(configuredSkillsRoot) && resolve(configuredSkillsRoot) !== resolve(defaultSkillsRoot)) {
|
|
5311
|
+
try {
|
|
5312
|
+
const watcher = watch(configuredSkillsRoot, { persistent: false, recursive: true }, (eventType, filename) => {
|
|
5313
|
+
if (runtimeClosing || !filename)
|
|
5314
|
+
return;
|
|
5315
|
+
const path = relative(projectRoot, join(configuredSkillsRoot, String(filename))).replace(/\\/g, '/');
|
|
5316
|
+
invalidateAgentProjectState(projectRoot);
|
|
5317
|
+
scheduleProjectRefresh(`skills:${path}`);
|
|
5318
|
+
const payload = JSON.stringify({ type: eventType === 'rename' ? 'file-added' : 'file-changed', path });
|
|
5319
|
+
for (const client of sseClients) {
|
|
5320
|
+
try {
|
|
5321
|
+
client.write(`event: change\ndata: ${payload}\n\n`);
|
|
5322
|
+
}
|
|
5323
|
+
catch {
|
|
5324
|
+
sseClients.delete(client);
|
|
5325
|
+
}
|
|
5326
|
+
}
|
|
5327
|
+
});
|
|
5328
|
+
projectWatchers.push(watcher);
|
|
5329
|
+
}
|
|
5330
|
+
catch { /* configured Skills folder is not watchable */ }
|
|
5331
|
+
}
|
|
4622
5332
|
const configuredDbtManifest = resolveDbtManifestPath(projectRoot, projectConfig);
|
|
4623
5333
|
if (configuredDbtManifest && existsSync(dirname(configuredDbtManifest))) {
|
|
4624
5334
|
try {
|
|
4625
|
-
watch(dirname(configuredDbtManifest), { persistent: false }, (_eventType, filename) => {
|
|
5335
|
+
const watcher = watch(dirname(configuredDbtManifest), { persistent: false }, (_eventType, filename) => {
|
|
5336
|
+
if (runtimeClosing)
|
|
5337
|
+
return;
|
|
4626
5338
|
if (!filename || !['manifest.json', 'catalog.json', 'semantic_manifest.json', 'run_results.json'].includes(String(filename)))
|
|
4627
5339
|
return;
|
|
4628
5340
|
invalidateAgentProjectState(projectRoot);
|
|
4629
|
-
|
|
4630
|
-
clearTimeout(sourceRefreshTimer);
|
|
4631
|
-
sourceRefreshTimer = setTimeout(() => {
|
|
4632
|
-
void refreshUnifiedProjectIndexes(projectRoot).catch(() => undefined);
|
|
4633
|
-
}, 250);
|
|
5341
|
+
scheduleProjectRefresh(`dbt:${String(filename)}`);
|
|
4634
5342
|
const payload = JSON.stringify({ type: 'dbt-artifacts-changed', path: String(filename) });
|
|
4635
5343
|
for (const client of sseClients) {
|
|
4636
5344
|
try {
|
|
@@ -4652,6 +5360,7 @@ export async function startLocalServer(opts) {
|
|
|
4652
5360
|
}
|
|
4653
5361
|
}).catch(() => { });
|
|
4654
5362
|
});
|
|
5363
|
+
projectWatchers.push(watcher);
|
|
4655
5364
|
}
|
|
4656
5365
|
catch { /* dbt artifact directory not watchable */ }
|
|
4657
5366
|
}
|
|
@@ -4734,11 +5443,15 @@ export async function startLocalServer(opts) {
|
|
|
4734
5443
|
result: normalizeQueryResult(result),
|
|
4735
5444
|
chartConfig: plan?.chartConfig ?? validation.chartConfig ?? null,
|
|
4736
5445
|
invocation,
|
|
5446
|
+
tests: plan?.tests ?? [],
|
|
4737
5447
|
};
|
|
4738
5448
|
};
|
|
4739
|
-
const runBlockStudioTestSummary = async (source, targetConnection) => {
|
|
4740
|
-
const activeConnection = requireActiveConnection(targetConnection);
|
|
5449
|
+
const runBlockStudioTestSummary = async (source, targetConnection, executedPreview) => {
|
|
4741
5450
|
const start = Date.now();
|
|
5451
|
+
if (executedPreview) {
|
|
5452
|
+
return evaluateBlockStudioTests(executedPreview.tests, executedPreview.result.rows, executedPreview.result.columns, start);
|
|
5453
|
+
}
|
|
5454
|
+
const activeConnection = requireActiveConnection(targetConnection);
|
|
4742
5455
|
const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer, projectRoot);
|
|
4743
5456
|
// Run tests against the SAME SQL the preview runs: for a semantic block with a
|
|
4744
5457
|
// pre-compiled query, that's the query (not a recompiled metric), so the test's
|
|
@@ -4783,40 +5496,7 @@ export async function startLocalServer(opts) {
|
|
|
4783
5496
|
const columns = Array.isArray(rawResult?.columns)
|
|
4784
5497
|
? rawResult.columns.map((column) => typeof column === 'string' ? column : column?.name ?? String(column))
|
|
4785
5498
|
: [];
|
|
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() };
|
|
5499
|
+
return evaluateBlockStudioTests(tests, rows, columns, start);
|
|
4820
5500
|
};
|
|
4821
5501
|
const saveDqlGenerationDraft = (importId, candidate) => saveDqlGenerationDraftForProject(projectRoot, importId, candidate);
|
|
4822
5502
|
const createDqlGenerationSessionFromBody = async (body) => {
|
|
@@ -4875,7 +5555,9 @@ export async function startLocalServer(opts) {
|
|
|
4875
5555
|
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4876
5556
|
}
|
|
4877
5557
|
try {
|
|
4878
|
-
testResults =
|
|
5558
|
+
testResults = preview
|
|
5559
|
+
? await runBlockStudioTestSummary(source, undefined, preview)
|
|
5560
|
+
: await runBlockStudioTestSummary(source);
|
|
4879
5561
|
}
|
|
4880
5562
|
catch (error) {
|
|
4881
5563
|
testResults = {
|
|
@@ -5032,15 +5714,24 @@ export async function startLocalServer(opts) {
|
|
|
5032
5714
|
writeHead(status, { ...headers, ...validatorHeaders });
|
|
5033
5715
|
return end(raw);
|
|
5034
5716
|
}
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5717
|
+
gzip(raw, (error, gz) => {
|
|
5718
|
+
if (response.destroyed || response.writableEnded)
|
|
5719
|
+
return;
|
|
5720
|
+
if (error) {
|
|
5721
|
+
writeHead(status, { ...headers, ...validatorHeaders });
|
|
5722
|
+
end(raw);
|
|
5723
|
+
return;
|
|
5724
|
+
}
|
|
5725
|
+
writeHead(status, {
|
|
5726
|
+
...headers,
|
|
5727
|
+
...validatorHeaders,
|
|
5728
|
+
'Content-Encoding': 'gzip',
|
|
5729
|
+
'Content-Length': gz.byteLength,
|
|
5730
|
+
Vary: 'Accept-Encoding, Origin',
|
|
5731
|
+
});
|
|
5732
|
+
end(gz);
|
|
5042
5733
|
});
|
|
5043
|
-
return
|
|
5734
|
+
return response;
|
|
5044
5735
|
});
|
|
5045
5736
|
return rawResponse;
|
|
5046
5737
|
};
|
|
@@ -5059,7 +5750,7 @@ export async function startLocalServer(opts) {
|
|
|
5059
5750
|
res.setHeader('Access-Control-Allow-Origin', requestOrigin);
|
|
5060
5751
|
res.setHeader('Vary', 'Origin');
|
|
5061
5752
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
5062
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
5753
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Idempotency-Key');
|
|
5063
5754
|
if (!originAllowed && path.startsWith('/api/')) {
|
|
5064
5755
|
res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5065
5756
|
res.end(serializeJSON({ error: 'Origin is not allowed.' }));
|
|
@@ -5094,6 +5785,40 @@ export async function startLocalServer(opts) {
|
|
|
5094
5785
|
res.end(serializeJSON({ status: 'ok', version: runtimeVersion, versionStatus, retrievalHealth }));
|
|
5095
5786
|
return;
|
|
5096
5787
|
}
|
|
5788
|
+
if (req.method === 'GET' && path === '/api/operations/events') {
|
|
5789
|
+
res.writeHead(200, {
|
|
5790
|
+
'Content-Type': 'text/event-stream',
|
|
5791
|
+
'Cache-Control': 'no-cache',
|
|
5792
|
+
'Connection': 'keep-alive',
|
|
5793
|
+
'X-Accel-Buffering': 'no',
|
|
5794
|
+
});
|
|
5795
|
+
res.write(': connected\n\n');
|
|
5796
|
+
operationSseClients.add(res);
|
|
5797
|
+
for (const operation of operationCoordinator.list(20).reverse()) {
|
|
5798
|
+
res.write(`event: operation\ndata: ${serializeJSON(operation)}\n\n`);
|
|
5799
|
+
}
|
|
5800
|
+
req.on('close', () => operationSseClients.delete(res));
|
|
5801
|
+
return;
|
|
5802
|
+
}
|
|
5803
|
+
if (req.method === 'GET' && path === '/api/operations') {
|
|
5804
|
+
const limit = Number(url.searchParams.get('limit') ?? 50);
|
|
5805
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5806
|
+
res.end(serializeJSON({ operations: operationCoordinator.list(limit) }));
|
|
5807
|
+
return;
|
|
5808
|
+
}
|
|
5809
|
+
const operationMatch = path.match(/^\/api\/operations\/([^/]+)$/);
|
|
5810
|
+
if (operationMatch && req.method === 'GET') {
|
|
5811
|
+
const operation = operationCoordinator.get(decodeURIComponent(operationMatch[1]));
|
|
5812
|
+
res.writeHead(operation ? 200 : 404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5813
|
+
res.end(serializeJSON(operation ?? { error: 'Operation not found.' }));
|
|
5814
|
+
return;
|
|
5815
|
+
}
|
|
5816
|
+
if (operationMatch && req.method === 'DELETE') {
|
|
5817
|
+
const operation = operationCoordinator.cancel(decodeURIComponent(operationMatch[1]));
|
|
5818
|
+
res.writeHead(operation ? 200 : 404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5819
|
+
res.end(serializeJSON(operation ?? { error: 'Operation not found.' }));
|
|
5820
|
+
return;
|
|
5821
|
+
}
|
|
5097
5822
|
if (req.method === 'GET' && path === '/api/onboarding/launch') {
|
|
5098
5823
|
const requestId = apiRequestId('onboarding-launch');
|
|
5099
5824
|
const prefs = readUserPrefs(userPrefsPath);
|
|
@@ -5856,6 +6581,224 @@ export async function startLocalServer(opts) {
|
|
|
5856
6581
|
}));
|
|
5857
6582
|
return;
|
|
5858
6583
|
}
|
|
6584
|
+
if (req.method === 'POST' && path === '/api/modeling/dbt-first/imports') {
|
|
6585
|
+
const requestId = apiRequestId('modeling-import');
|
|
6586
|
+
try {
|
|
6587
|
+
const body = await readJSON(req);
|
|
6588
|
+
const source = body.source && typeof body.source === 'object' ? body.source : body;
|
|
6589
|
+
const session = createModelingImportSession(projectRoot, gitRoot, loopback, source);
|
|
6590
|
+
res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6591
|
+
res.end(serializeJSON({ requestId, session: publicModelingImportSession(session) }));
|
|
6592
|
+
}
|
|
6593
|
+
catch (error) {
|
|
6594
|
+
const code = apiErrorCode(error, 'MODELING_IMPORT_INVALID');
|
|
6595
|
+
res.writeHead(code === 'PATH_ACCESS_FORBIDDEN' ? 403 : 400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6596
|
+
res.end(serializeJSON(apiErrorEnvelope({ requestId, code, message: apiErrorMessage(error), nextActions: ['Choose DQL or dbt YAML from the current Git workspace, or paste/upload the YAML.'] })));
|
|
6597
|
+
}
|
|
6598
|
+
return;
|
|
6599
|
+
}
|
|
6600
|
+
const modelingImportMatch = path.match(/^\/api\/modeling\/dbt-first\/imports\/([^/]+)$/);
|
|
6601
|
+
if (req.method === 'GET' && modelingImportMatch) {
|
|
6602
|
+
const session = readModelingImportSession(projectRoot, decodeURIComponent(modelingImportMatch[1]));
|
|
6603
|
+
if (!session) {
|
|
6604
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6605
|
+
res.end(serializeJSON({ error: 'Modeling import session not found.', code: 'MODELING_IMPORT_NOT_FOUND' }));
|
|
6606
|
+
return;
|
|
6607
|
+
}
|
|
6608
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6609
|
+
res.end(serializeJSON({ session: publicModelingImportSession(session) }));
|
|
6610
|
+
return;
|
|
6611
|
+
}
|
|
6612
|
+
const modelingImportPreviewMatch = path.match(/^\/api\/modeling\/dbt-first\/imports\/([^/]+)\/preview$/);
|
|
6613
|
+
if (req.method === 'POST' && modelingImportPreviewMatch) {
|
|
6614
|
+
const requestId = apiRequestId('modeling-import-preview');
|
|
6615
|
+
try {
|
|
6616
|
+
const session = readModelingImportSession(projectRoot, decodeURIComponent(modelingImportPreviewMatch[1]));
|
|
6617
|
+
if (!session)
|
|
6618
|
+
throw Object.assign(new Error('Modeling import session not found.'), { code: 'MODELING_IMPORT_NOT_FOUND' });
|
|
6619
|
+
const body = await readJSON(req);
|
|
6620
|
+
const snapshot = projectSnapshot();
|
|
6621
|
+
const selectedCandidateIds = Array.isArray(body.selectedCandidateIds)
|
|
6622
|
+
? body.selectedCandidateIds.filter((value) => typeof value === 'string')
|
|
6623
|
+
: session.candidates.filter((candidate) => candidate.dialect !== 'unsupported' && candidate.action !== 'open_existing').map((candidate) => candidate.id);
|
|
6624
|
+
const domain = typeof body.domain === 'string' ? body.domain.trim() : '';
|
|
6625
|
+
const areaId = typeof body.areaId === 'string' ? body.areaId.trim() : '';
|
|
6626
|
+
if (!domain || !areaId)
|
|
6627
|
+
throw Object.assign(new Error('Choose exactly one Domain and Model Area for imported models.'), { code: 'MODEL_SCOPE_REQUIRED' });
|
|
6628
|
+
const planned = modelingImportOperations(session, selectedCandidateIds, domain, areaId, snapshot.manifest);
|
|
6629
|
+
const proposal = previewContextAuthoring({
|
|
6630
|
+
origin: 'yaml_import',
|
|
6631
|
+
operations: planned.operations,
|
|
6632
|
+
expectedSnapshotId: typeof body.expectedSnapshotId === 'string' ? body.expectedSnapshotId : snapshot.snapshotId,
|
|
6633
|
+
diagnostics: planned.diagnostics,
|
|
6634
|
+
});
|
|
6635
|
+
const updated = { ...session, proposalId: proposal.id, updatedAt: new Date().toISOString() };
|
|
6636
|
+
writeModelingImportSession(projectRoot, updated);
|
|
6637
|
+
res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6638
|
+
res.end(serializeJSON({ requestId, session: publicModelingImportSession(updated), proposal }));
|
|
6639
|
+
}
|
|
6640
|
+
catch (error) {
|
|
6641
|
+
const code = apiErrorCode(error, 'MODELING_IMPORT_PREVIEW_FAILED');
|
|
6642
|
+
res.writeHead(code === 'MODELING_IMPORT_NOT_FOUND' ? 404 : code === 'PROPOSAL_STALE' ? 409 : 400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6643
|
+
res.end(serializeJSON(apiErrorEnvelope({ requestId, code, message: apiErrorMessage(error), nextActions: ['Resolve unmatched models or choose another Domain and Area, then preview again.'] })));
|
|
6644
|
+
}
|
|
6645
|
+
return;
|
|
6646
|
+
}
|
|
6647
|
+
if (req.method === 'POST' && path === '/api/context-proposals') {
|
|
6648
|
+
const requestId = apiRequestId('context-proposal');
|
|
6649
|
+
try {
|
|
6650
|
+
const body = await readJSON(req);
|
|
6651
|
+
const origin = parseContextAuthoringOrigin(body.origin);
|
|
6652
|
+
const operations = parseContextAuthoringOperations(body.operations);
|
|
6653
|
+
const proposal = previewContextAuthoring({
|
|
6654
|
+
origin,
|
|
6655
|
+
operations,
|
|
6656
|
+
expectedSnapshotId: typeof body.expectedSnapshotId === 'string' ? body.expectedSnapshotId : undefined,
|
|
6657
|
+
sourceRunId: typeof body.sourceRunId === 'string' ? body.sourceRunId : undefined,
|
|
6658
|
+
sourceArtifactId: typeof body.sourceArtifactId === 'string' ? body.sourceArtifactId : undefined,
|
|
6659
|
+
revision: typeof body.revision === 'number' ? body.revision : undefined,
|
|
6660
|
+
});
|
|
6661
|
+
res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6662
|
+
res.end(serializeJSON({ requestId, proposal }));
|
|
6663
|
+
}
|
|
6664
|
+
catch (error) {
|
|
6665
|
+
const code = apiErrorCode(error, 'PROPOSAL_INVALID');
|
|
6666
|
+
res.writeHead(code === 'PROPOSAL_STALE' ? 409 : 400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6667
|
+
res.end(serializeJSON(apiErrorEnvelope({ requestId, code, message: apiErrorMessage(error), nextActions: ['Refresh Modeling and preview the proposal again.'] })));
|
|
6668
|
+
}
|
|
6669
|
+
return;
|
|
6670
|
+
}
|
|
6671
|
+
const contextProposalMatch = path.match(/^\/api\/context-proposals\/([^/]+)$/);
|
|
6672
|
+
if (req.method === 'GET' && contextProposalMatch) {
|
|
6673
|
+
const id = decodeURIComponent(contextProposalMatch[1]);
|
|
6674
|
+
const proposal = contextProposalStore.get(id);
|
|
6675
|
+
if (!proposal) {
|
|
6676
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6677
|
+
res.end(serializeJSON({ error: 'Context proposal not found.', code: 'PROPOSAL_NOT_FOUND' }));
|
|
6678
|
+
return;
|
|
6679
|
+
}
|
|
6680
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6681
|
+
res.end(serializeJSON({ proposal }));
|
|
6682
|
+
return;
|
|
6683
|
+
}
|
|
6684
|
+
const contextRepreviewMatch = path.match(/^\/api\/context-proposals\/([^/]+)\/repreview$/);
|
|
6685
|
+
if (req.method === 'POST' && contextRepreviewMatch) {
|
|
6686
|
+
const requestId = apiRequestId('context-repreview');
|
|
6687
|
+
try {
|
|
6688
|
+
const current = contextProposalStore.get(decodeURIComponent(contextRepreviewMatch[1]));
|
|
6689
|
+
if (!current)
|
|
6690
|
+
throw Object.assign(new Error('Context proposal not found.'), { code: 'PROPOSAL_NOT_FOUND' });
|
|
6691
|
+
const body = await readJSON(req);
|
|
6692
|
+
const selected = Array.isArray(body.selectedOperationIds)
|
|
6693
|
+
? body.selectedOperationIds.filter((value) => typeof value === 'string')
|
|
6694
|
+
: current.operations.map((operation) => operation.id);
|
|
6695
|
+
const operations = contextAuthoringDependencyClosure(current.operations, selected);
|
|
6696
|
+
const proposal = previewContextAuthoring({
|
|
6697
|
+
origin: current.origin,
|
|
6698
|
+
operations,
|
|
6699
|
+
expectedSnapshotId: body.rebase === true ? projectSnapshot().snapshotId : current.baseSnapshotId,
|
|
6700
|
+
sourceRunId: current.sourceRunId,
|
|
6701
|
+
sourceArtifactId: current.sourceArtifactId,
|
|
6702
|
+
revision: (current.revision ?? 1) + 1,
|
|
6703
|
+
});
|
|
6704
|
+
res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6705
|
+
res.end(serializeJSON({ requestId, proposal }));
|
|
6706
|
+
}
|
|
6707
|
+
catch (error) {
|
|
6708
|
+
const code = apiErrorCode(error, 'PROPOSAL_INVALID');
|
|
6709
|
+
res.writeHead(code === 'PROPOSAL_STALE' ? 409 : code === 'PROPOSAL_NOT_FOUND' ? 404 : 400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6710
|
+
res.end(serializeJSON(apiErrorEnvelope({ requestId, code, message: apiErrorMessage(error), nextActions: ['Refresh the proposal against the latest project snapshot.'] })));
|
|
6711
|
+
}
|
|
6712
|
+
return;
|
|
6713
|
+
}
|
|
6714
|
+
const contextCommitMatch = path.match(/^\/api\/context-proposals\/([^/]+)\/commit$/);
|
|
6715
|
+
if (req.method === 'POST' && contextCommitMatch) {
|
|
6716
|
+
const requestId = apiRequestId('context-commit');
|
|
6717
|
+
const proposalId = decodeURIComponent(contextCommitMatch[1]);
|
|
6718
|
+
try {
|
|
6719
|
+
const proposal = contextProposalStore.get(proposalId);
|
|
6720
|
+
if (!proposal)
|
|
6721
|
+
throw Object.assign(new Error('Context proposal not found.'), { code: 'PROPOSAL_NOT_FOUND' });
|
|
6722
|
+
const body = await readJSON(req);
|
|
6723
|
+
const unexpected = Object.keys(body).filter((key) => key !== 'expectedProposalHash' && key !== 'idempotencyKey');
|
|
6724
|
+
if (unexpected.length)
|
|
6725
|
+
throw Object.assign(new Error('Commit accepts only expectedProposalHash and idempotencyKey.'), { code: 'INVALID_REQUEST' });
|
|
6726
|
+
const expectedProposalHash = typeof body.expectedProposalHash === 'string' ? body.expectedProposalHash : '';
|
|
6727
|
+
const idempotencyKey = typeof body.idempotencyKey === 'string' ? body.idempotencyKey.trim() : '';
|
|
6728
|
+
if (!expectedProposalHash || !idempotencyKey)
|
|
6729
|
+
throw Object.assign(new Error('The reviewed proposal hash and idempotency key are required.'), { code: 'INVALID_REQUEST' });
|
|
6730
|
+
const receipt = contextProposalStore.getCommitReceipt(idempotencyKey);
|
|
6731
|
+
if (receipt) {
|
|
6732
|
+
if (receipt.proposalId !== proposal.id || receipt.proposalHash !== expectedProposalHash)
|
|
6733
|
+
throw Object.assign(new Error('The idempotency key belongs to another reviewed proposal request.'), { code: 'IDEMPOTENCY_CONFLICT' });
|
|
6734
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6735
|
+
res.end(serializeJSON({ requestId, proposal: contextProposalStore.get(proposal.id), snapshotId: receipt.snapshotId, idempotent: true }));
|
|
6736
|
+
return;
|
|
6737
|
+
}
|
|
6738
|
+
if (proposal.status !== 'proposed')
|
|
6739
|
+
throw Object.assign(new Error('Only a proposed change can be committed.'), { code: 'PROPOSAL_STALE' });
|
|
6740
|
+
if (proposal.proposalHash !== expectedProposalHash)
|
|
6741
|
+
throw Object.assign(new Error('The proposal changed after review.'), { code: 'PROPOSAL_STALE' });
|
|
6742
|
+
if (proposal.diagnostics.some((diagnostic) => diagnostic.severity === 'blocking'))
|
|
6743
|
+
throw Object.assign(new Error('Resolve all blocking proposal diagnostics before saving.'), { code: 'PROPOSAL_BLOCKED' });
|
|
6744
|
+
const beforeSnapshot = projectSnapshot();
|
|
6745
|
+
if (beforeSnapshot.snapshotId !== proposal.baseSnapshotId)
|
|
6746
|
+
throw Object.assign(new Error('Project sources changed after this proposal was reviewed.'), { code: 'PROPOSAL_STALE' });
|
|
6747
|
+
const resolvedPatches = proposal.patches.map((patch) => ({ patch, absolute: resolveContextPatchPath(projectRoot, gitRoot, patch.path) }));
|
|
6748
|
+
for (const { patch, absolute } of resolvedPatches) {
|
|
6749
|
+
const current = existsSync(absolute) ? readFileSync(absolute, 'utf8') : '';
|
|
6750
|
+
if (current !== patch.before)
|
|
6751
|
+
throw Object.assign(new Error(`Source changed after preview: ${patch.path}`), { code: 'SOURCE_CHANGED' });
|
|
6752
|
+
}
|
|
6753
|
+
const written = [];
|
|
6754
|
+
try {
|
|
6755
|
+
for (const item of resolvedPatches) {
|
|
6756
|
+
if (!item.patch.changed)
|
|
6757
|
+
continue;
|
|
6758
|
+
if (!item.patch.after)
|
|
6759
|
+
rmSync(item.absolute, { force: true });
|
|
6760
|
+
else {
|
|
6761
|
+
mkdirSync(dirname(item.absolute), { recursive: true });
|
|
6762
|
+
writeFileSync(item.absolute, item.patch.after, 'utf8');
|
|
6763
|
+
}
|
|
6764
|
+
written.push(item);
|
|
6765
|
+
}
|
|
6766
|
+
projectSnapshots.invalidate();
|
|
6767
|
+
invalidateAgentProjectState(projectRoot);
|
|
6768
|
+
const nextSnapshot = projectSnapshot();
|
|
6769
|
+
if (nextSnapshot.error)
|
|
6770
|
+
throw Object.assign(new Error(nextSnapshot.error), { code: 'COMPILE_FAILED' });
|
|
6771
|
+
await refreshUnifiedProjectIndexes(projectRoot);
|
|
6772
|
+
const committed = { ...proposal, status: 'committed', committedAt: new Date().toISOString(), committedSnapshotId: nextSnapshot.snapshotId };
|
|
6773
|
+
contextProposalStore.save(committed);
|
|
6774
|
+
contextProposalStore.saveCommitReceipt(idempotencyKey, { proposalId: proposal.id, proposalHash: proposal.proposalHash, snapshotId: nextSnapshot.snapshotId });
|
|
6775
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6776
|
+
res.end(serializeJSON({ requestId, proposal: committed, snapshotId: nextSnapshot.snapshotId, modeling: nextSnapshot.manifest.modeling }));
|
|
6777
|
+
}
|
|
6778
|
+
catch (error) {
|
|
6779
|
+
for (const { patch, absolute } of written.reverse()) {
|
|
6780
|
+
if (!patch.before)
|
|
6781
|
+
rmSync(absolute, { force: true });
|
|
6782
|
+
else {
|
|
6783
|
+
mkdirSync(dirname(absolute), { recursive: true });
|
|
6784
|
+
writeFileSync(absolute, patch.before, 'utf8');
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6787
|
+
projectSnapshots.invalidate();
|
|
6788
|
+
invalidateAgentProjectState(projectRoot);
|
|
6789
|
+
contextProposalStore.save(proposal);
|
|
6790
|
+
contextProposalStore.deleteCommitReceipt(idempotencyKey);
|
|
6791
|
+
throw error;
|
|
6792
|
+
}
|
|
6793
|
+
}
|
|
6794
|
+
catch (error) {
|
|
6795
|
+
const code = apiErrorCode(error, /changed after/i.test(apiErrorMessage(error)) ? 'SOURCE_CHANGED' : 'PROPOSAL_COMMIT_FAILED');
|
|
6796
|
+
const status = code === 'PROPOSAL_NOT_FOUND' ? 404 : ['PROPOSAL_STALE', 'SOURCE_CHANGED', 'IDEMPOTENCY_CONFLICT', 'OWNERSHIP_CONFLICT', 'DEPENDENCY_CONFLICT'].includes(code) ? 409 : code === 'PROPOSAL_BLOCKED' ? 422 : 400;
|
|
6797
|
+
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6798
|
+
res.end(serializeJSON(apiErrorEnvelope({ requestId, code, message: apiErrorMessage(error), nextActions: ['Rebase or repreview the proposal before saving.'] })));
|
|
6799
|
+
}
|
|
6800
|
+
return;
|
|
6801
|
+
}
|
|
5859
6802
|
if (req.method === 'POST' && path === '/api/modeling/dbt-first/dbt-source/preview') {
|
|
5860
6803
|
const requestId = apiRequestId('modeling-dbt-source-preview');
|
|
5861
6804
|
const snapshot = projectSnapshot();
|
|
@@ -6018,13 +6961,14 @@ export async function startLocalServer(opts) {
|
|
|
6018
6961
|
return;
|
|
6019
6962
|
}
|
|
6020
6963
|
const sourceSql = repairableSqlFromAgentRun(sourceRun);
|
|
6964
|
+
const sourceDqlArtifact = repairableDqlArtifactFromAgentRun(sourceRun);
|
|
6021
6965
|
const retainedFailure = warehouseFailureFromAgentRun(sourceRun);
|
|
6022
|
-
if (!sourceSql || !analyticalFailureAllowsDeterministicRetry(retainedFailure)) {
|
|
6966
|
+
if ((!sourceSql && !sourceDqlArtifact) || !analyticalFailureAllowsDeterministicRetry(retainedFailure)) {
|
|
6023
6967
|
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6024
6968
|
res.end(serializeJSON({
|
|
6025
|
-
error: sourceSql
|
|
6969
|
+
error: sourceSql || sourceDqlArtifact
|
|
6026
6970
|
? '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.',
|
|
6971
|
+
: 'This run has no retained SQL statement or editable DQL artifact to repair.',
|
|
6028
6972
|
...(retainedFailure ? { warehouseFailure: retainedFailure } : {}),
|
|
6029
6973
|
}));
|
|
6030
6974
|
return;
|
|
@@ -6050,12 +6994,27 @@ export async function startLocalServer(opts) {
|
|
|
6050
6994
|
: projectConfig.defaultConnectionName;
|
|
6051
6995
|
let repaired;
|
|
6052
6996
|
try {
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6997
|
+
// Prefer the editable DQL contract even when the failed run also
|
|
6998
|
+
// retained compiled SQL. This keeps parameters, visualization, tests,
|
|
6999
|
+
// and the corrected wrapper attached to the repaired Ask result.
|
|
7000
|
+
repaired = sourceDqlArtifact
|
|
7001
|
+
? await executeBoundedDqlRepair({
|
|
7002
|
+
question: sourceRun.question,
|
|
7003
|
+
source: sourceDqlArtifact.source,
|
|
7004
|
+
failure: retainedFailure?.redactedMessage
|
|
7005
|
+
?? sourceRun.diagnosticReceipt?.failure?.message
|
|
7006
|
+
?? sourceRun.summary,
|
|
7007
|
+
targetConnection,
|
|
7008
|
+
targetConnectionName,
|
|
7009
|
+
parameters: sourceDqlArtifact.parameterValues,
|
|
7010
|
+
artifactName: sourceDqlArtifact.name,
|
|
7011
|
+
})
|
|
7012
|
+
: await executeBoundedSqlRepair({
|
|
7013
|
+
question: sourceRun.question,
|
|
7014
|
+
sourceSql: sourceSql,
|
|
7015
|
+
targetConnection,
|
|
7016
|
+
targetConnectionName,
|
|
7017
|
+
});
|
|
6059
7018
|
}
|
|
6060
7019
|
catch (error) {
|
|
6061
7020
|
const repairError = error;
|
|
@@ -6078,6 +7037,8 @@ export async function startLocalServer(opts) {
|
|
|
6078
7037
|
...(preview ? { columnFormats: agentColumnDisplayFormats(projectRoot, preview.columns) } : {}),
|
|
6079
7038
|
});
|
|
6080
7039
|
const sourceFailure = analyticalFailedRunFromAgentRun(sourceRun)?.failure;
|
|
7040
|
+
const presentationContext = repairPresentationContextFromAgentRun(sourceRun);
|
|
7041
|
+
const executionResultFingerprint = result.executionReceipt?.resultFingerprint;
|
|
6081
7042
|
const artifact = agentRunArtifact('answer', 'Repaired analytical answer', {
|
|
6082
7043
|
kind: 'uncertified',
|
|
6083
7044
|
certification: 'analyst_review_required',
|
|
@@ -6085,13 +7046,31 @@ export async function startLocalServer(opts) {
|
|
|
6085
7046
|
trustLabel: 'AI-generated · review required',
|
|
6086
7047
|
answer: synthesis.text,
|
|
6087
7048
|
text: synthesis.text,
|
|
7049
|
+
...presentationContext,
|
|
6088
7050
|
result,
|
|
6089
7051
|
proposedSql: repairedSql,
|
|
6090
7052
|
sql: result.sql ?? repairedSql,
|
|
6091
7053
|
dqlArtifact: result.dqlArtifact,
|
|
7054
|
+
diagnosticReceipt: {
|
|
7055
|
+
version: 1,
|
|
7056
|
+
phase: 'run.completed',
|
|
7057
|
+
route: 'generated_answer',
|
|
7058
|
+
repair: {
|
|
7059
|
+
attempt: 1,
|
|
7060
|
+
mode: repairMode,
|
|
7061
|
+
sourceRunId: sourceRun.id,
|
|
7062
|
+
targetPreserved: true,
|
|
7063
|
+
readOnly: true,
|
|
7064
|
+
},
|
|
7065
|
+
execution: {
|
|
7066
|
+
rowCount: result.rowCount,
|
|
7067
|
+
...(executionResultFingerprint ? { resultFingerprint: executionResultFingerprint } : {}),
|
|
7068
|
+
},
|
|
7069
|
+
},
|
|
6092
7070
|
previousAttempt: {
|
|
6093
7071
|
sourceRunId: sourceRun.id,
|
|
6094
|
-
sql: sourceSql,
|
|
7072
|
+
...(sourceSql ? { sql: sourceSql } : {}),
|
|
7073
|
+
...(sourceDqlArtifact?.source ? { dql: sourceDqlArtifact.source } : {}),
|
|
6095
7074
|
failure: retainedFailure ?? firstFailure,
|
|
6096
7075
|
},
|
|
6097
7076
|
repair: { attempt: 1, mode: repairMode },
|
|
@@ -6274,39 +7253,83 @@ export async function startLocalServer(opts) {
|
|
|
6274
7253
|
const wantsStream = url.searchParams.get('stream') === '1' || url.searchParams.get('stream') === 'true';
|
|
6275
7254
|
const runId = parsed.request.runId;
|
|
6276
7255
|
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
7256
|
if (runId)
|
|
6282
7257
|
activeAgentRunControllers.set(runId, runController);
|
|
7258
|
+
let streamConnected = true;
|
|
7259
|
+
res.on('close', () => { streamConnected = false; });
|
|
7260
|
+
const writeStream = (event, data) => {
|
|
7261
|
+
if (!streamConnected || res.destroyed || res.writableEnded)
|
|
7262
|
+
return;
|
|
7263
|
+
try {
|
|
7264
|
+
writeAgentRunSse(res, event, data);
|
|
7265
|
+
}
|
|
7266
|
+
catch {
|
|
7267
|
+
streamConnected = false;
|
|
7268
|
+
}
|
|
7269
|
+
};
|
|
7270
|
+
if (wantsStream) {
|
|
7271
|
+
res.writeHead(200, {
|
|
7272
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
7273
|
+
'Cache-Control': 'no-cache',
|
|
7274
|
+
'Connection': 'keep-alive',
|
|
7275
|
+
'X-Accel-Buffering': 'no',
|
|
7276
|
+
});
|
|
7277
|
+
}
|
|
7278
|
+
const operation = runId ? operationCoordinator.create({
|
|
7279
|
+
type: 'agent_run',
|
|
7280
|
+
scope: `agent-run:${runId}`,
|
|
7281
|
+
resourceRevision: parsed.request.threadId,
|
|
7282
|
+
message: 'AI request accepted. You can change pages while it runs.',
|
|
7283
|
+
cancellable: true,
|
|
7284
|
+
}) : null;
|
|
7285
|
+
if (wantsStream)
|
|
7286
|
+
writeStream('agent-run-accepted', { runId, operationId: operation?.id });
|
|
7287
|
+
let completedRun;
|
|
7288
|
+
let runError;
|
|
6283
7289
|
try {
|
|
7290
|
+
const executeRun = async (operationSignal, report) => {
|
|
7291
|
+
parsed.request.signal = AbortSignal.any([
|
|
7292
|
+
runController.signal,
|
|
7293
|
+
...(operationSignal ? [operationSignal] : []),
|
|
7294
|
+
AbortSignal.timeout(agentRunDeadlineMs(parsed.request, process.env, getActiveProvider(projectRoot))),
|
|
7295
|
+
]);
|
|
7296
|
+
report?.({ phase: 'reasoning', progress: 8, message: 'Grounding the request and selecting the governed route.' });
|
|
7297
|
+
try {
|
|
7298
|
+
const run = await agentRunEngine.run(parsed.request, (event) => {
|
|
7299
|
+
if (wantsStream)
|
|
7300
|
+
writeStream('agent-run-event', event);
|
|
7301
|
+
report?.({ phase: event.type, progress: 45, message: event.message });
|
|
7302
|
+
}, (delta) => {
|
|
7303
|
+
if (wantsStream)
|
|
7304
|
+
writeStream('agent-run-answer-delta', { runId: parsed.request.runId, delta });
|
|
7305
|
+
});
|
|
7306
|
+
completedRun = run;
|
|
7307
|
+
recordConversationTurn(conversationStore, parsed.request.threadId, run);
|
|
7308
|
+
return { runId: run.id, threadId: parsed.request.threadId, status: run.status };
|
|
7309
|
+
}
|
|
7310
|
+
catch (error) {
|
|
7311
|
+
runError = error;
|
|
7312
|
+
throw error;
|
|
7313
|
+
}
|
|
7314
|
+
};
|
|
7315
|
+
if (operation)
|
|
7316
|
+
await operationCoordinator.run(operation.id, (signal, report) => executeRun(signal, report));
|
|
7317
|
+
else
|
|
7318
|
+
await executeRun();
|
|
7319
|
+
if (runError)
|
|
7320
|
+
throw runError;
|
|
7321
|
+
if (!completedRun)
|
|
7322
|
+
throw new Error('The AI request ended before producing a stored run.');
|
|
6284
7323
|
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();
|
|
7324
|
+
// The complete stored run stays in the run-state route; the live
|
|
7325
|
+
// stream ships only the renderable transport projection.
|
|
7326
|
+
writeStream('agent-run-complete', slimAgentRunForTransport(completedRun));
|
|
7327
|
+
if (streamConnected && !res.writableEnded)
|
|
7328
|
+
res.end();
|
|
6304
7329
|
return;
|
|
6305
7330
|
}
|
|
6306
|
-
const run = await agentRunEngine.run(parsed.request);
|
|
6307
|
-
recordConversationTurn(conversationStore, parsed.request.threadId, run);
|
|
6308
7331
|
res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6309
|
-
res.end(serializeJSON({ run }));
|
|
7332
|
+
res.end(serializeJSON({ run: completedRun }));
|
|
6310
7333
|
}
|
|
6311
7334
|
finally {
|
|
6312
7335
|
if (runId)
|
|
@@ -6316,8 +7339,10 @@ export async function startLocalServer(opts) {
|
|
|
6316
7339
|
catch (error) {
|
|
6317
7340
|
const message = error instanceof Error ? error.message : String(error);
|
|
6318
7341
|
if (res.headersSent) {
|
|
6319
|
-
|
|
6320
|
-
|
|
7342
|
+
if (!res.destroyed && !res.writableEnded) {
|
|
7343
|
+
writeAgentRunSse(res, 'agent-run-error', { error: message });
|
|
7344
|
+
res.end();
|
|
7345
|
+
}
|
|
6321
7346
|
}
|
|
6322
7347
|
else {
|
|
6323
7348
|
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -6808,14 +7833,22 @@ export async function startLocalServer(opts) {
|
|
|
6808
7833
|
const metrics = semanticLayer
|
|
6809
7834
|
? semanticLayer.listMetrics(undefined, { includeMeasures: false }).map((m) => m.name).filter(matches).sort().slice(0, limit)
|
|
6810
7835
|
: [];
|
|
6811
|
-
const manifest = buildManifest({
|
|
7836
|
+
const manifest = buildManifest({
|
|
7837
|
+
projectRoot,
|
|
7838
|
+
dqlVersion: 'notebook',
|
|
7839
|
+
dbtManifestPath: resolveDbtManifestPath(projectRoot) ?? undefined,
|
|
7840
|
+
});
|
|
6812
7841
|
const blocks = Object.values(manifest.blocks)
|
|
6813
7842
|
.filter((b) => b.status === 'certified')
|
|
6814
7843
|
.map((b) => b.name)
|
|
6815
7844
|
.filter(matches)
|
|
6816
7845
|
.sort();
|
|
7846
|
+
const modelingRefs = [
|
|
7847
|
+
...Object.values(manifest.modeling?.entities ?? {}).map((entity) => entity.qualifiedId),
|
|
7848
|
+
...Object.values(manifest.modeling?.relationships ?? {}).map((relationship) => relationship.qualifiedId),
|
|
7849
|
+
].filter(matches).sort();
|
|
6817
7850
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6818
|
-
res.end(serializeJSON({ metrics, blocks: blocks.slice(0, limit) }));
|
|
7851
|
+
res.end(serializeJSON({ metrics, blocks: blocks.slice(0, limit), modelingRefs: modelingRefs.slice(0, limit) }));
|
|
6819
7852
|
}
|
|
6820
7853
|
catch (error) {
|
|
6821
7854
|
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -6833,6 +7866,9 @@ export async function startLocalServer(opts) {
|
|
|
6833
7866
|
return;
|
|
6834
7867
|
}
|
|
6835
7868
|
const skill = writeSkill(projectRoot, input);
|
|
7869
|
+
projectSnapshots.invalidate();
|
|
7870
|
+
invalidateAgentProjectState(projectRoot);
|
|
7871
|
+
await refreshUnifiedProjectIndexes(projectRoot);
|
|
6836
7872
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6837
7873
|
res.end(serializeJSON({ skill: serializeSkill(skill) }));
|
|
6838
7874
|
}
|
|
@@ -6852,7 +7888,10 @@ export async function startLocalServer(opts) {
|
|
|
6852
7888
|
res.end(serializeJSON({ error: 'Provide { skill } with scope and body.' }));
|
|
6853
7889
|
return;
|
|
6854
7890
|
}
|
|
6855
|
-
const skill = writeSkill(projectRoot, input);
|
|
7891
|
+
const skill = writeSkill(projectRoot, { ...input, qualifiedId: id });
|
|
7892
|
+
projectSnapshots.invalidate();
|
|
7893
|
+
invalidateAgentProjectState(projectRoot);
|
|
7894
|
+
await refreshUnifiedProjectIndexes(projectRoot);
|
|
6856
7895
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6857
7896
|
res.end(serializeJSON({ skill: serializeSkill(skill) }));
|
|
6858
7897
|
}
|
|
@@ -6866,6 +7905,9 @@ export async function startLocalServer(opts) {
|
|
|
6866
7905
|
try {
|
|
6867
7906
|
const id = decodeURIComponent(path.slice('/api/skills/'.length));
|
|
6868
7907
|
deleteSkill(projectRoot, id);
|
|
7908
|
+
projectSnapshots.invalidate();
|
|
7909
|
+
invalidateAgentProjectState(projectRoot);
|
|
7910
|
+
await refreshUnifiedProjectIndexes(projectRoot);
|
|
6869
7911
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6870
7912
|
res.end(serializeJSON({ ok: true }));
|
|
6871
7913
|
}
|
|
@@ -7989,9 +9031,14 @@ export async function startLocalServer(opts) {
|
|
|
7989
9031
|
? body.variables
|
|
7990
9032
|
: {};
|
|
7991
9033
|
const dashboardVariables = dashboardRuntimeVariables(loaded.dashboard, variables);
|
|
9034
|
+
const requestedTileId = typeof body.tileId === 'string' && body.tileId.trim()
|
|
9035
|
+
? body.tileId.trim()
|
|
9036
|
+
: undefined;
|
|
7992
9037
|
const tiles = [];
|
|
7993
9038
|
let localApps = null;
|
|
7994
9039
|
for (const item of loaded.dashboard.layout.items) {
|
|
9040
|
+
if (requestedTileId && item.i !== requestedTileId)
|
|
9041
|
+
continue;
|
|
7995
9042
|
if (item.text) {
|
|
7996
9043
|
tiles.push({
|
|
7997
9044
|
tileId: item.i,
|
|
@@ -8079,9 +9126,194 @@ export async function startLocalServer(opts) {
|
|
|
8079
9126
|
kind: 'ai_pin',
|
|
8080
9127
|
name: pin.title,
|
|
8081
9128
|
},
|
|
9129
|
+
artifact: {
|
|
9130
|
+
version: 1,
|
|
9131
|
+
sourceKind: 'ai_pin',
|
|
9132
|
+
name: pinArtifact?.name ?? pin.title,
|
|
9133
|
+
...(pinArtifact?.sourcePath ? { sourcePath: pinArtifact.sourcePath } : {}),
|
|
9134
|
+
...(pinArtifact?.source ? { dql: pinArtifact.source.slice(0, 40_000) } : {}),
|
|
9135
|
+
...(pinArtifact?.compiledSql || pin.sql ? { sql: (pinArtifact?.compiledSql ?? pin.sql).slice(0, 40_000) } : {}),
|
|
9136
|
+
trustState: pinArtifact?.trustState === 'certified' || pin.certification === 'certified'
|
|
9137
|
+
? 'certified'
|
|
9138
|
+
: 'review_required',
|
|
9139
|
+
explanation: [
|
|
9140
|
+
'This result comes from a saved App insight.',
|
|
9141
|
+
pinArtifact ? 'The DQL artifact was executed again with the current App parameters.' : 'The saved bounded result is displayed with its original trust state.',
|
|
9142
|
+
],
|
|
9143
|
+
executionTarget: executionTargetDescriptor(body),
|
|
9144
|
+
},
|
|
8082
9145
|
});
|
|
8083
9146
|
continue;
|
|
8084
9147
|
}
|
|
9148
|
+
if (item.draftAnalysis) {
|
|
9149
|
+
let draftSource;
|
|
9150
|
+
let draftAppRelativePath;
|
|
9151
|
+
let draftParameters = {};
|
|
9152
|
+
let draftTargetConnection;
|
|
9153
|
+
let draftRepairPlan;
|
|
9154
|
+
try {
|
|
9155
|
+
const draftPath = join(loaded.appDir, item.draftAnalysis.ref);
|
|
9156
|
+
draftAppRelativePath = relative(loaded.appDir, draftPath);
|
|
9157
|
+
if (draftAppRelativePath.startsWith('..') || draftAppRelativePath.startsWith('/')) {
|
|
9158
|
+
throw new Error('App-scoped analysis reference escapes the App directory.');
|
|
9159
|
+
}
|
|
9160
|
+
draftSource = readFileSync(draftPath, 'utf-8');
|
|
9161
|
+
const fingerprint = `sha256:${createHash('sha256').update(draftSource).digest('hex')}`;
|
|
9162
|
+
if (fingerprint !== item.draftAnalysis.artifactFingerprint) {
|
|
9163
|
+
throw new Error('App-scoped analysis changed after approval. Review the draft again.');
|
|
9164
|
+
}
|
|
9165
|
+
draftParameters = dashboardTileParameterValues({
|
|
9166
|
+
item,
|
|
9167
|
+
dashboardValues: dashboardVariables,
|
|
9168
|
+
requestValues: variables,
|
|
9169
|
+
});
|
|
9170
|
+
draftTargetConnection = isConnectionConfig(body.connection)
|
|
9171
|
+
? body.connection
|
|
9172
|
+
: connection ?? undefined;
|
|
9173
|
+
draftRepairPlan = buildExecutionPlan({ id: item.i, type: 'dql', source: draftSource, title: item.title ?? item.i }, {
|
|
9174
|
+
semanticLayer,
|
|
9175
|
+
driver: draftTargetConnection?.driver ?? 'duckdb',
|
|
9176
|
+
parameters: draftParameters,
|
|
9177
|
+
});
|
|
9178
|
+
let result;
|
|
9179
|
+
let repair;
|
|
9180
|
+
let repairedSql;
|
|
9181
|
+
let repairedDql;
|
|
9182
|
+
try {
|
|
9183
|
+
result = await executeDqlArtifactSourceForAgent(draftSource, { name: item.title ?? item.i, path: draftAppRelativePath }, { question: item.title, parameters: draftParameters });
|
|
9184
|
+
}
|
|
9185
|
+
catch (executionError) {
|
|
9186
|
+
const repaired = await repairFailedAppTileExecution({
|
|
9187
|
+
source: 'draft_analysis',
|
|
9188
|
+
question: item.title ?? item.i,
|
|
9189
|
+
sourceSql: draftRepairPlan?.sql,
|
|
9190
|
+
sourceDql: draftSource,
|
|
9191
|
+
artifactName: item.title ?? item.i,
|
|
9192
|
+
error: executionError,
|
|
9193
|
+
targetConnection: draftTargetConnection,
|
|
9194
|
+
targetConnectionName: projectConfig.defaultConnectionName,
|
|
9195
|
+
sqlParams: draftRepairPlan?.sqlParams,
|
|
9196
|
+
variables: { ...(draftRepairPlan?.variables ?? {}), ...draftParameters },
|
|
9197
|
+
});
|
|
9198
|
+
if (!repaired.ok) {
|
|
9199
|
+
tiles.push({
|
|
9200
|
+
tileId: item.i,
|
|
9201
|
+
status: 'error',
|
|
9202
|
+
tileType: 'draftAnalysis',
|
|
9203
|
+
title: item.title ?? item.i,
|
|
9204
|
+
error: executionError instanceof Error ? executionError.message : String(executionError),
|
|
9205
|
+
trustState: 'review_required',
|
|
9206
|
+
repair: repaired.repair,
|
|
9207
|
+
artifact: {
|
|
9208
|
+
version: 1,
|
|
9209
|
+
sourceKind: 'draft_analysis',
|
|
9210
|
+
name: item.title ?? item.i,
|
|
9211
|
+
sourcePath: draftAppRelativePath,
|
|
9212
|
+
dql: draftSource.slice(0, 40_000),
|
|
9213
|
+
...(draftRepairPlan?.sql ? { sql: draftRepairPlan.sql.slice(0, 40_000) } : {}),
|
|
9214
|
+
trustState: 'review_required',
|
|
9215
|
+
explanation: ['This App-scoped analysis remains review-required.', 'Automatic repair could not produce a safe executable result.'],
|
|
9216
|
+
executionTarget: executionTargetDescriptor(body),
|
|
9217
|
+
},
|
|
9218
|
+
});
|
|
9219
|
+
continue;
|
|
9220
|
+
}
|
|
9221
|
+
result = repaired.result;
|
|
9222
|
+
repair = repaired.repair;
|
|
9223
|
+
repairedSql = repaired.repairedSql;
|
|
9224
|
+
repairedDql = repaired.repairedDql;
|
|
9225
|
+
}
|
|
9226
|
+
tiles.push({
|
|
9227
|
+
tileId: item.i,
|
|
9228
|
+
status: 'ok',
|
|
9229
|
+
tileType: 'draftAnalysis',
|
|
9230
|
+
title: item.title ?? item.i,
|
|
9231
|
+
viz: item.viz,
|
|
9232
|
+
chartConfig: mergeDashboardChartConfig(result.chartConfig, item),
|
|
9233
|
+
result,
|
|
9234
|
+
trustState: 'review_required',
|
|
9235
|
+
citation: { kind: repair ? 'app_draft_analysis_repaired' : 'app_draft_analysis', name: item.title ?? item.i, path: draftAppRelativePath },
|
|
9236
|
+
...(repair ? { repair } : {}),
|
|
9237
|
+
artifact: {
|
|
9238
|
+
version: 1,
|
|
9239
|
+
sourceKind: 'draft_analysis',
|
|
9240
|
+
name: item.title ?? item.i,
|
|
9241
|
+
sourcePath: draftAppRelativePath,
|
|
9242
|
+
dql: (repairedDql ?? draftSource).slice(0, 40_000),
|
|
9243
|
+
...(repairedSql ?? draftRepairPlan?.sql ? { sql: (repairedSql ?? draftRepairPlan.sql).slice(0, 40_000) } : {}),
|
|
9244
|
+
trustState: 'review_required',
|
|
9245
|
+
explanation: [
|
|
9246
|
+
'This is private App analysis and remains review-required.',
|
|
9247
|
+
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.',
|
|
9248
|
+
],
|
|
9249
|
+
executionTarget: executionTargetDescriptor(body),
|
|
9250
|
+
},
|
|
9251
|
+
});
|
|
9252
|
+
}
|
|
9253
|
+
catch (err) {
|
|
9254
|
+
const repaired = await repairFailedAppTileExecution({
|
|
9255
|
+
source: 'draft_analysis',
|
|
9256
|
+
question: item.title ?? item.i,
|
|
9257
|
+
sourceSql: draftRepairPlan?.sql,
|
|
9258
|
+
sourceDql: draftSource,
|
|
9259
|
+
artifactName: item.title ?? item.i,
|
|
9260
|
+
error: err,
|
|
9261
|
+
targetConnection: draftTargetConnection,
|
|
9262
|
+
targetConnectionName: projectConfig.defaultConnectionName,
|
|
9263
|
+
sqlParams: draftRepairPlan?.sqlParams,
|
|
9264
|
+
variables: { ...(draftRepairPlan?.variables ?? {}), ...draftParameters },
|
|
9265
|
+
});
|
|
9266
|
+
if (repaired.ok && draftSource) {
|
|
9267
|
+
tiles.push({
|
|
9268
|
+
tileId: item.i,
|
|
9269
|
+
status: 'ok',
|
|
9270
|
+
tileType: 'draftAnalysis',
|
|
9271
|
+
title: item.title ?? item.i,
|
|
9272
|
+
viz: item.viz,
|
|
9273
|
+
chartConfig: mergeDashboardChartConfig(repaired.result.chartConfig, item),
|
|
9274
|
+
result: repaired.result,
|
|
9275
|
+
trustState: 'review_required',
|
|
9276
|
+
citation: { kind: 'app_draft_analysis_repaired', name: item.title ?? item.i, path: draftAppRelativePath },
|
|
9277
|
+
repair: repaired.repair,
|
|
9278
|
+
artifact: {
|
|
9279
|
+
version: 1,
|
|
9280
|
+
sourceKind: 'draft_analysis',
|
|
9281
|
+
name: item.title ?? item.i,
|
|
9282
|
+
...(draftAppRelativePath ? { sourcePath: draftAppRelativePath } : {}),
|
|
9283
|
+
dql: (repaired.repairedDql ?? draftSource).slice(0, 40_000),
|
|
9284
|
+
sql: repaired.repairedSql.slice(0, 40_000),
|
|
9285
|
+
trustState: 'review_required',
|
|
9286
|
+
explanation: ['A bounded DQL repair executed on the original data target. The saved App analysis was not changed.'],
|
|
9287
|
+
executionTarget: executionTargetDescriptor(body),
|
|
9288
|
+
},
|
|
9289
|
+
});
|
|
9290
|
+
continue;
|
|
9291
|
+
}
|
|
9292
|
+
tiles.push({
|
|
9293
|
+
tileId: item.i,
|
|
9294
|
+
status: 'error',
|
|
9295
|
+
tileType: 'draftAnalysis',
|
|
9296
|
+
title: item.title ?? item.i,
|
|
9297
|
+
error: err instanceof Error ? err.message : String(err),
|
|
9298
|
+
trustState: 'review_required',
|
|
9299
|
+
repair: repaired.repair,
|
|
9300
|
+
...(draftSource ? {
|
|
9301
|
+
artifact: {
|
|
9302
|
+
version: 1,
|
|
9303
|
+
sourceKind: 'draft_analysis',
|
|
9304
|
+
name: item.title ?? item.i,
|
|
9305
|
+
...(draftAppRelativePath ? { sourcePath: draftAppRelativePath } : {}),
|
|
9306
|
+
dql: draftSource.slice(0, 40_000),
|
|
9307
|
+
...(draftRepairPlan?.sql ? { sql: draftRepairPlan.sql.slice(0, 40_000) } : {}),
|
|
9308
|
+
trustState: 'review_required',
|
|
9309
|
+
explanation: ['Automatic repair could not produce a safe executable result. The saved App analysis was not changed.'],
|
|
9310
|
+
executionTarget: executionTargetDescriptor(body),
|
|
9311
|
+
},
|
|
9312
|
+
} : {}),
|
|
9313
|
+
});
|
|
9314
|
+
}
|
|
9315
|
+
continue;
|
|
9316
|
+
}
|
|
8085
9317
|
if (item.semantic) {
|
|
8086
9318
|
try {
|
|
8087
9319
|
if (!semanticLayer)
|
|
@@ -8122,16 +9354,69 @@ export async function startLocalServer(opts) {
|
|
|
8122
9354
|
if (!composed)
|
|
8123
9355
|
throw new Error('The governed semantic query could not be composed.');
|
|
8124
9356
|
const prepared = prepareLocalExecution(composed.sql, targetConnection, projectRoot, projectConfig);
|
|
8125
|
-
|
|
9357
|
+
let normalizedResult;
|
|
9358
|
+
let repair;
|
|
9359
|
+
let executedSql = composed.sql;
|
|
9360
|
+
try {
|
|
9361
|
+
const result = await executor.executeQuery(prepared.sql, [], {}, prepared.connection);
|
|
9362
|
+
normalizedResult = normalizeQueryResult(result);
|
|
9363
|
+
}
|
|
9364
|
+
catch (executionError) {
|
|
9365
|
+
const repaired = await repairFailedAppTileExecution({
|
|
9366
|
+
source: 'semantic_query',
|
|
9367
|
+
question: item.title ?? item.semantic.id,
|
|
9368
|
+
sourceSql: composed.sql,
|
|
9369
|
+
error: executionError,
|
|
9370
|
+
targetConnection,
|
|
9371
|
+
});
|
|
9372
|
+
if (!repaired.ok) {
|
|
9373
|
+
tiles.push({
|
|
9374
|
+
tileId: item.i,
|
|
9375
|
+
status: 'error',
|
|
9376
|
+
tileType: 'semantic',
|
|
9377
|
+
title: item.title ?? item.semantic.id,
|
|
9378
|
+
error: executionError instanceof Error ? executionError.message : String(executionError),
|
|
9379
|
+
trustState: 'review_required',
|
|
9380
|
+
repair: repaired.repair,
|
|
9381
|
+
artifact: {
|
|
9382
|
+
version: 1,
|
|
9383
|
+
sourceKind: 'semantic_query',
|
|
9384
|
+
name: item.title ?? item.semantic.id,
|
|
9385
|
+
sql: composed.sql.slice(0, 40_000),
|
|
9386
|
+
trustState: 'review_required',
|
|
9387
|
+
explanation: ['Automatic repair could not execute the governed semantic query safely.'],
|
|
9388
|
+
executionTarget: executionTargetDescriptor(body),
|
|
9389
|
+
},
|
|
9390
|
+
});
|
|
9391
|
+
continue;
|
|
9392
|
+
}
|
|
9393
|
+
normalizedResult = normalizeQueryResult(repaired.result);
|
|
9394
|
+
repair = repaired.repair;
|
|
9395
|
+
executedSql = repaired.repairedSql;
|
|
9396
|
+
}
|
|
8126
9397
|
tiles.push({
|
|
8127
9398
|
tileId: item.i,
|
|
8128
9399
|
status: 'ok',
|
|
8129
9400
|
tileType: 'semantic',
|
|
8130
9401
|
title: item.title ?? item.semantic.id,
|
|
8131
9402
|
viz: item.viz,
|
|
8132
|
-
result:
|
|
9403
|
+
result: normalizedResult,
|
|
8133
9404
|
trustState: 'review_required',
|
|
8134
|
-
citation: { kind: 'semantic_query', name: item.semantic.id },
|
|
9405
|
+
citation: { kind: repair ? 'semantic_query_repaired' : 'semantic_query', name: item.semantic.id },
|
|
9406
|
+
...(repair ? { repair } : {}),
|
|
9407
|
+
artifact: {
|
|
9408
|
+
version: 1,
|
|
9409
|
+
sourceKind: 'semantic_query',
|
|
9410
|
+
name: item.title ?? item.semantic.id,
|
|
9411
|
+
sql: executedSql.slice(0, 40_000),
|
|
9412
|
+
trustState: 'review_required',
|
|
9413
|
+
explanation: [
|
|
9414
|
+
`Metrics: ${item.semantic.metrics.join(', ') || 'none'}.`,
|
|
9415
|
+
`Dimensions: ${item.semantic.dimensions?.join(', ') || 'none'}.`,
|
|
9416
|
+
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.',
|
|
9417
|
+
],
|
|
9418
|
+
executionTarget: executionTargetDescriptor(body),
|
|
9419
|
+
},
|
|
8135
9420
|
});
|
|
8136
9421
|
}
|
|
8137
9422
|
catch (err) {
|
|
@@ -8156,6 +9441,11 @@ export async function startLocalServer(opts) {
|
|
|
8156
9441
|
});
|
|
8157
9442
|
continue;
|
|
8158
9443
|
}
|
|
9444
|
+
let blockSource;
|
|
9445
|
+
let blockTargetConnection;
|
|
9446
|
+
let blockPlan;
|
|
9447
|
+
let blockFilterApplication;
|
|
9448
|
+
let blockInvocation;
|
|
8159
9449
|
try {
|
|
8160
9450
|
assertAppAccess({
|
|
8161
9451
|
app: loaded.app,
|
|
@@ -8163,55 +9453,55 @@ export async function startLocalServer(opts) {
|
|
|
8163
9453
|
level: 'execute',
|
|
8164
9454
|
});
|
|
8165
9455
|
const absBlockPath = join(projectRoot, block.filePath);
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
const tableMapping = await resolveSemanticTableMapping(executor,
|
|
9456
|
+
blockSource = readFileSync(absBlockPath, 'utf-8');
|
|
9457
|
+
blockTargetConnection = requireActiveConnection(isConnectionConfig(body.connection) ? body.connection : connection);
|
|
9458
|
+
const tableMapping = await resolveSemanticTableMapping(executor, blockTargetConnection, semanticLayer, projectRoot);
|
|
8169
9459
|
const boundParameters = dashboardTileParameterValues({
|
|
8170
9460
|
item,
|
|
8171
9461
|
dashboardValues: dashboardVariables,
|
|
8172
9462
|
requestValues: variables,
|
|
8173
9463
|
});
|
|
8174
|
-
|
|
9464
|
+
blockInvocation = prepareBlockInvocation({
|
|
8175
9465
|
block: block.name,
|
|
8176
|
-
source,
|
|
9466
|
+
source: blockSource,
|
|
8177
9467
|
parameters: { ...dashboardVariables, ...boundParameters },
|
|
8178
9468
|
parameterSources: Object.fromEntries(Object.keys(boundParameters).map((name) => [name, 'surface'])),
|
|
8179
9469
|
surface: 'app',
|
|
8180
9470
|
});
|
|
8181
|
-
if (
|
|
9471
|
+
if (blockInvocation.errors.length || blockInvocation.unresolvedParameters.length) {
|
|
8182
9472
|
tiles.push({
|
|
8183
9473
|
tileId: item.i,
|
|
8184
9474
|
status: 'unresolved',
|
|
8185
9475
|
tileType: 'block',
|
|
8186
9476
|
blockId: block.name,
|
|
8187
9477
|
title: item.title ?? block.name,
|
|
8188
|
-
error:
|
|
9478
|
+
error: blockInvocation.errors[0] ?? `Needs values for: ${blockInvocation.unresolvedParameters.join(', ')}`,
|
|
8189
9479
|
invocation: {
|
|
8190
|
-
resolvedParameters:
|
|
8191
|
-
unresolvedParameters:
|
|
8192
|
-
auditId:
|
|
9480
|
+
resolvedParameters: blockInvocation.resolvedParameters,
|
|
9481
|
+
unresolvedParameters: blockInvocation.unresolvedParameters,
|
|
9482
|
+
auditId: blockInvocation.auditId,
|
|
8193
9483
|
},
|
|
8194
9484
|
});
|
|
8195
9485
|
continue;
|
|
8196
9486
|
}
|
|
8197
9487
|
const semanticCompose = semanticLayer
|
|
8198
|
-
? await composeSemanticBlockSqlForRuntime(
|
|
8199
|
-
driver:
|
|
9488
|
+
? await composeSemanticBlockSqlForRuntime(blockSource, semanticLayer, {
|
|
9489
|
+
driver: blockTargetConnection.driver,
|
|
8200
9490
|
tableMapping,
|
|
8201
9491
|
projectRoot,
|
|
8202
9492
|
projectConfig,
|
|
8203
9493
|
detectedProvider: semanticDetectedProvider,
|
|
8204
|
-
parameters:
|
|
9494
|
+
parameters: blockInvocation.values,
|
|
8205
9495
|
})
|
|
8206
9496
|
: null;
|
|
8207
|
-
|
|
9497
|
+
blockPlan = buildExecutionPlan({ id: item.i, type: 'dql', source: blockSource, title: item.title ?? block.name }, {
|
|
8208
9498
|
semanticLayer,
|
|
8209
|
-
driver:
|
|
9499
|
+
driver: blockTargetConnection.driver,
|
|
8210
9500
|
tableMapping,
|
|
8211
|
-
parameters:
|
|
9501
|
+
parameters: blockInvocation.values,
|
|
8212
9502
|
semanticSql: semanticCompose?.sql ?? undefined,
|
|
8213
9503
|
});
|
|
8214
|
-
if (!
|
|
9504
|
+
if (!blockPlan && !semanticCompose?.sql) {
|
|
8215
9505
|
tiles.push({
|
|
8216
9506
|
tileId: item.i,
|
|
8217
9507
|
status: 'error',
|
|
@@ -8220,15 +9510,15 @@ export async function startLocalServer(opts) {
|
|
|
8220
9510
|
});
|
|
8221
9511
|
continue;
|
|
8222
9512
|
}
|
|
8223
|
-
|
|
8224
|
-
sql: semanticCompose?.sql ??
|
|
8225
|
-
sqlParams:
|
|
8226
|
-
variables: { ...(
|
|
9513
|
+
blockFilterApplication = applyDashboardFiltersToBlockExecution({
|
|
9514
|
+
sql: semanticCompose?.sql ?? blockPlan.sql,
|
|
9515
|
+
sqlParams: blockPlan?.sqlParams ?? [],
|
|
9516
|
+
variables: { ...(blockPlan?.variables ?? {}), ...dashboardVariables },
|
|
8227
9517
|
block,
|
|
8228
9518
|
dashboard: loaded.dashboard,
|
|
8229
9519
|
});
|
|
8230
|
-
const prepared = prepareLocalExecution(
|
|
8231
|
-
const result = await executor.executeQuery(prepared.sql,
|
|
9520
|
+
const prepared = prepareLocalExecution(blockFilterApplication.sql, blockTargetConnection, projectRoot, projectConfig);
|
|
9521
|
+
const result = await executor.executeQuery(prepared.sql, blockFilterApplication.sqlParams, runtimeVariables({ ...blockFilterApplication.variables, ...blockInvocation.values }), prepared.connection);
|
|
8232
9522
|
tiles.push({
|
|
8233
9523
|
tileId: item.i,
|
|
8234
9524
|
status: 'ok',
|
|
@@ -8237,22 +9527,37 @@ export async function startLocalServer(opts) {
|
|
|
8237
9527
|
certificationStatus: block.status ?? null,
|
|
8238
9528
|
title: item.title ?? block.name,
|
|
8239
9529
|
viz: item.viz,
|
|
8240
|
-
chartConfig: mergeDashboardChartConfig(
|
|
9530
|
+
chartConfig: mergeDashboardChartConfig(blockPlan?.chartConfig, item),
|
|
8241
9531
|
result: normalizeQueryResult(result),
|
|
8242
9532
|
filters: {
|
|
8243
|
-
applied:
|
|
8244
|
-
skipped:
|
|
9533
|
+
applied: blockFilterApplication.appliedFilters,
|
|
9534
|
+
skipped: blockFilterApplication.skippedFilters,
|
|
8245
9535
|
},
|
|
8246
9536
|
invocation: {
|
|
8247
|
-
resolvedParameters:
|
|
8248
|
-
unresolvedParameters:
|
|
8249
|
-
auditId:
|
|
9537
|
+
resolvedParameters: blockInvocation.resolvedParameters,
|
|
9538
|
+
unresolvedParameters: blockInvocation.unresolvedParameters,
|
|
9539
|
+
auditId: blockInvocation.auditId,
|
|
8250
9540
|
},
|
|
9541
|
+
trustState: block.status === 'certified' ? 'certified' : 'review_required',
|
|
8251
9542
|
citation: {
|
|
8252
9543
|
kind: 'block',
|
|
8253
9544
|
name: block.name,
|
|
8254
9545
|
path: block.filePath,
|
|
8255
9546
|
},
|
|
9547
|
+
artifact: {
|
|
9548
|
+
version: 1,
|
|
9549
|
+
sourceKind: 'certified_block',
|
|
9550
|
+
name: block.name,
|
|
9551
|
+
sourcePath: block.filePath,
|
|
9552
|
+
dql: blockSource.slice(0, 40_000),
|
|
9553
|
+
sql: blockFilterApplication.sql.slice(0, 40_000),
|
|
9554
|
+
trustState: block.status === 'certified' ? 'certified' : 'review_required',
|
|
9555
|
+
explanation: [
|
|
9556
|
+
block.status === 'certified' ? 'This tile executed a certified project block.' : 'This project block is not certified and remains review-required.',
|
|
9557
|
+
'Dashboard filters and typed parameters were bound before read-only execution.',
|
|
9558
|
+
],
|
|
9559
|
+
executionTarget: executionTargetDescriptor(body),
|
|
9560
|
+
},
|
|
8256
9561
|
});
|
|
8257
9562
|
}
|
|
8258
9563
|
catch (err) {
|
|
@@ -8265,11 +9570,87 @@ export async function startLocalServer(opts) {
|
|
|
8265
9570
|
});
|
|
8266
9571
|
}
|
|
8267
9572
|
else {
|
|
9573
|
+
const repaired = await repairFailedAppTileExecution({
|
|
9574
|
+
source: 'certified_block',
|
|
9575
|
+
question: item.title ?? block.name,
|
|
9576
|
+
sourceSql: blockFilterApplication?.sql ?? blockPlan?.sql,
|
|
9577
|
+
sourceDql: blockSource,
|
|
9578
|
+
artifactName: block.name,
|
|
9579
|
+
error: err,
|
|
9580
|
+
targetConnection: blockTargetConnection,
|
|
9581
|
+
targetConnectionName: projectConfig.defaultConnectionName,
|
|
9582
|
+
sqlParams: blockFilterApplication?.sqlParams ?? blockPlan?.sqlParams,
|
|
9583
|
+
variables: {
|
|
9584
|
+
...(blockPlan?.variables ?? {}),
|
|
9585
|
+
...(blockFilterApplication?.variables ?? {}),
|
|
9586
|
+
...(blockInvocation?.values ?? {}),
|
|
9587
|
+
},
|
|
9588
|
+
});
|
|
9589
|
+
if (repaired.ok) {
|
|
9590
|
+
tiles.push({
|
|
9591
|
+
tileId: item.i,
|
|
9592
|
+
status: 'ok',
|
|
9593
|
+
tileType: 'block',
|
|
9594
|
+
blockId: block.name,
|
|
9595
|
+
blockPath: block.filePath,
|
|
9596
|
+
certificationStatus: block.status ?? null,
|
|
9597
|
+
title: item.title ?? block.name,
|
|
9598
|
+
viz: item.viz,
|
|
9599
|
+
chartConfig: mergeDashboardChartConfig(blockPlan?.chartConfig, item),
|
|
9600
|
+
result: normalizeQueryResult(repaired.result),
|
|
9601
|
+
...(blockFilterApplication ? {
|
|
9602
|
+
filters: {
|
|
9603
|
+
applied: blockFilterApplication.appliedFilters,
|
|
9604
|
+
skipped: blockFilterApplication.skippedFilters,
|
|
9605
|
+
},
|
|
9606
|
+
} : {}),
|
|
9607
|
+
...(blockInvocation ? {
|
|
9608
|
+
invocation: {
|
|
9609
|
+
resolvedParameters: blockInvocation.resolvedParameters,
|
|
9610
|
+
unresolvedParameters: blockInvocation.unresolvedParameters,
|
|
9611
|
+
auditId: blockInvocation.auditId,
|
|
9612
|
+
},
|
|
9613
|
+
} : {}),
|
|
9614
|
+
trustState: 'review_required',
|
|
9615
|
+
repair: repaired.repair,
|
|
9616
|
+
citation: { kind: 'block_repaired', name: block.name, path: block.filePath },
|
|
9617
|
+
artifact: {
|
|
9618
|
+
version: 1,
|
|
9619
|
+
sourceKind: 'certified_block',
|
|
9620
|
+
name: block.name,
|
|
9621
|
+
sourcePath: block.filePath,
|
|
9622
|
+
...(repaired.repairedDql ?? blockSource ? { dql: (repaired.repairedDql ?? blockSource).slice(0, 40_000) } : {}),
|
|
9623
|
+
sql: repaired.repairedSql.slice(0, 40_000),
|
|
9624
|
+
trustState: 'review_required',
|
|
9625
|
+
explanation: [
|
|
9626
|
+
'The saved certified block was not changed.',
|
|
9627
|
+
'A bounded repair executed on the original data target; this repaired result requires human review.',
|
|
9628
|
+
],
|
|
9629
|
+
executionTarget: executionTargetDescriptor(body),
|
|
9630
|
+
},
|
|
9631
|
+
});
|
|
9632
|
+
continue;
|
|
9633
|
+
}
|
|
8268
9634
|
tiles.push({
|
|
8269
9635
|
tileId: item.i,
|
|
8270
9636
|
status: 'error',
|
|
9637
|
+
tileType: 'block',
|
|
8271
9638
|
blockId: block.name,
|
|
8272
9639
|
error: err instanceof Error ? err.message : String(err),
|
|
9640
|
+
repair: repaired.repair,
|
|
9641
|
+
...(blockSource ? {
|
|
9642
|
+
artifact: {
|
|
9643
|
+
version: 1,
|
|
9644
|
+
sourceKind: 'certified_block',
|
|
9645
|
+
name: block.name,
|
|
9646
|
+
sourcePath: block.filePath,
|
|
9647
|
+
dql: blockSource.slice(0, 40_000),
|
|
9648
|
+
...(blockFilterApplication?.sql ?? blockPlan?.sql ? { sql: (blockFilterApplication?.sql ?? blockPlan.sql).slice(0, 40_000) } : {}),
|
|
9649
|
+
trustState: 'review_required',
|
|
9650
|
+
explanation: ['Automatic repair could not produce a safe executable result. The saved block was not changed.'],
|
|
9651
|
+
executionTarget: executionTargetDescriptor(body),
|
|
9652
|
+
},
|
|
9653
|
+
} : {}),
|
|
8273
9654
|
});
|
|
8274
9655
|
}
|
|
8275
9656
|
}
|
|
@@ -8304,6 +9685,10 @@ export async function startLocalServer(opts) {
|
|
|
8304
9685
|
filterFingerprint,
|
|
8305
9686
|
resultFingerprint,
|
|
8306
9687
|
personaFingerprint,
|
|
9688
|
+
successfulTileIds: tiles.filter((tile) => tile.status === 'ok').map((tile) => tile.tileId),
|
|
9689
|
+
semanticApprovalEligibleTileIds: tiles.filter((tile) => (tile.status === 'ok'
|
|
9690
|
+
&& tile.tileType === 'semantic'
|
|
9691
|
+
&& (!('repair' in tile) || tile.repair?.approvalEligible !== false))).map((tile) => tile.tileId),
|
|
8307
9692
|
facts: storyResult.facts,
|
|
8308
9693
|
story: storyResult.story,
|
|
8309
9694
|
expiresAt: Date.now() + 15 * 60_000,
|
|
@@ -8355,7 +9740,30 @@ export async function startLocalServer(opts) {
|
|
|
8355
9740
|
// Two-phase app build gap-fill: bounded governed answers for uncovered
|
|
8356
9741
|
// questions. Throws when no provider is configured — propose degrades
|
|
8357
9742
|
// gracefully by listing the gap instead.
|
|
8358
|
-
generateGovernedAnswer: (question) =>
|
|
9743
|
+
generateGovernedAnswer: (question, appContext) => generateAppBuildAnswerWithRepair(question, appContext),
|
|
9744
|
+
queueOperation: (input, task) => {
|
|
9745
|
+
const operation = operationCoordinator.create(input);
|
|
9746
|
+
void operationCoordinator.run(operation.id, task);
|
|
9747
|
+
return operation;
|
|
9748
|
+
},
|
|
9749
|
+
scheduleProjectRefresh: (reason) => { scheduleProjectRefresh(reason); },
|
|
9750
|
+
verifyDashboardRun: ({ runId, appId, dashboardId, tileIds }) => {
|
|
9751
|
+
const evidence = dashboardRunEvidence.get(runId);
|
|
9752
|
+
if (!evidence || evidence.expiresAt < Date.now())
|
|
9753
|
+
return { ok: false, error: 'The dashboard run receipt expired. Run the dashboard again.' };
|
|
9754
|
+
if (evidence.appId !== appId || evidence.dashboardId !== dashboardId)
|
|
9755
|
+
return { ok: false, error: 'The dashboard run receipt does not match this App page.' };
|
|
9756
|
+
const successful = new Set(evidence.successfulTileIds);
|
|
9757
|
+
const failed = tileIds.filter((tileId) => !successful.has(tileId));
|
|
9758
|
+
if (failed.length > 0)
|
|
9759
|
+
return { ok: false, error: `These semantic tiles did not complete successfully: ${failed.join(', ')}` };
|
|
9760
|
+
const approvalEligible = new Set(evidence.semanticApprovalEligibleTileIds);
|
|
9761
|
+
const repaired = tileIds.filter((tileId) => !approvalEligible.has(tileId));
|
|
9762
|
+
if (repaired.length > 0) {
|
|
9763
|
+
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.` };
|
|
9764
|
+
}
|
|
9765
|
+
return { ok: true, snapshotId: evidence.snapshotId, resultFingerprint: evidence.resultFingerprint };
|
|
9766
|
+
},
|
|
8359
9767
|
// Story narration for commit — LLM-backed with deterministic fallback.
|
|
8360
9768
|
narrate: narrateForAgentRun,
|
|
8361
9769
|
});
|
|
@@ -8531,13 +9939,21 @@ export async function startLocalServer(opts) {
|
|
|
8531
9939
|
res.end(serializeJSON({ error: 'Invalid path' }));
|
|
8532
9940
|
return;
|
|
8533
9941
|
}
|
|
8534
|
-
|
|
9942
|
+
await mkdirAsync(dirname(absPath), { recursive: true });
|
|
8535
9943
|
const toWrite = absPath.endsWith('.dql')
|
|
8536
9944
|
? canonicalizeSafe(content)
|
|
8537
9945
|
: absPath.endsWith('.dqlnb')
|
|
8538
9946
|
? canonicalizeNotebookSafe(content)
|
|
8539
9947
|
: content;
|
|
8540
|
-
|
|
9948
|
+
const tempPath = `${absPath}.tmp-${process.pid}-${Date.now()}`;
|
|
9949
|
+
try {
|
|
9950
|
+
await writeFileAsync(tempPath, toWrite, 'utf-8');
|
|
9951
|
+
await renameAsync(tempPath, absPath);
|
|
9952
|
+
}
|
|
9953
|
+
catch (error) {
|
|
9954
|
+
await rmAsync(tempPath, { force: true }).catch(() => undefined);
|
|
9955
|
+
throw error;
|
|
9956
|
+
}
|
|
8541
9957
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8542
9958
|
res.end(serializeJSON({ ok: true }));
|
|
8543
9959
|
}
|
|
@@ -9835,12 +11251,185 @@ export async function startLocalServer(opts) {
|
|
|
9835
11251
|
passed: assertion.passed,
|
|
9836
11252
|
actual: assertion.error ?? (assertion.actual !== undefined ? String(assertion.actual) : undefined),
|
|
9837
11253
|
}));
|
|
9838
|
-
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
9839
|
-
res.end(serializeJSON({ assertions: results, passed: summary.passed, failed: summary.failed, duration: summary.duration }));
|
|
11254
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11255
|
+
res.end(serializeJSON({ assertions: results, passed: summary.passed, failed: summary.failed, duration: summary.duration }));
|
|
11256
|
+
}
|
|
11257
|
+
catch (error) {
|
|
11258
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11259
|
+
res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
|
|
11260
|
+
}
|
|
11261
|
+
return;
|
|
11262
|
+
}
|
|
11263
|
+
if (req.method === 'POST' && path === '/api/block-studio/certifications') {
|
|
11264
|
+
try {
|
|
11265
|
+
const body = await readJSON(req);
|
|
11266
|
+
const source = typeof body.source === 'string' ? body.source : '';
|
|
11267
|
+
const suppliedMetadata = body.metadata && typeof body.metadata === 'object'
|
|
11268
|
+
? body.metadata
|
|
11269
|
+
: {};
|
|
11270
|
+
const currentPath = typeof body.path === 'string'
|
|
11271
|
+
? normalize(body.path).replace(/^\/+/, '').replaceAll('\\', '/')
|
|
11272
|
+
: undefined;
|
|
11273
|
+
const expectedSourceFingerprint = typeof body.expectedSourceFingerprint === 'string'
|
|
11274
|
+
? body.expectedSourceFingerprint
|
|
11275
|
+
: undefined;
|
|
11276
|
+
const idempotencyKey = typeof req.headers['idempotency-key'] === 'string'
|
|
11277
|
+
? req.headers['idempotency-key'].trim()
|
|
11278
|
+
: '';
|
|
11279
|
+
if (!source.trim()) {
|
|
11280
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11281
|
+
res.end(serializeJSON({ error: 'source is required' }));
|
|
11282
|
+
return;
|
|
11283
|
+
}
|
|
11284
|
+
if (idempotencyKey) {
|
|
11285
|
+
const existingId = operationIdempotency.get(idempotencyKey);
|
|
11286
|
+
const existing = existingId ? operationCoordinator.get(existingId) : null;
|
|
11287
|
+
if (existing) {
|
|
11288
|
+
res.writeHead(202, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11289
|
+
res.end(serializeJSON({ operation: existing, deduplicated: true }));
|
|
11290
|
+
return;
|
|
11291
|
+
}
|
|
11292
|
+
}
|
|
11293
|
+
if (currentPath && expectedSourceFingerprint) {
|
|
11294
|
+
const absoluteCurrentPath = safeJoin(projectRoot, currentPath);
|
|
11295
|
+
if (!absoluteCurrentPath || !existsSync(absoluteCurrentPath)) {
|
|
11296
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11297
|
+
res.end(serializeJSON({ error: 'The block changed or moved. Reopen it before certifying.', code: 'SOURCE_CHANGED' }));
|
|
11298
|
+
return;
|
|
11299
|
+
}
|
|
11300
|
+
const actualFingerprint = createHash('sha256')
|
|
11301
|
+
.update(readFileSync(absoluteCurrentPath, 'utf-8'))
|
|
11302
|
+
.digest('hex');
|
|
11303
|
+
if (actualFingerprint !== expectedSourceFingerprint) {
|
|
11304
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11305
|
+
res.end(serializeJSON({ error: 'The block changed after it was opened. Reopen it before certifying.', code: 'SOURCE_CHANGED' }));
|
|
11306
|
+
return;
|
|
11307
|
+
}
|
|
11308
|
+
}
|
|
11309
|
+
const draftSource = setBlockStudioStatusInSource(source, 'draft');
|
|
11310
|
+
const parsed = parseBlockSourceMetadata(draftSource);
|
|
11311
|
+
const name = typeof suppliedMetadata.name === 'string' && suppliedMetadata.name.trim()
|
|
11312
|
+
? suppliedMetadata.name.trim()
|
|
11313
|
+
: parsed.name;
|
|
11314
|
+
if (!name) {
|
|
11315
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11316
|
+
res.end(serializeJSON({ error: 'Block name is required.' }));
|
|
11317
|
+
return;
|
|
11318
|
+
}
|
|
11319
|
+
const canonicalPath = currentPath && !isDraftBlockPath(currentPath) ? currentPath : undefined;
|
|
11320
|
+
const stableSuffix = createHash('sha256')
|
|
11321
|
+
.update(currentPath ?? `${name}:${draftSource}`)
|
|
11322
|
+
.digest('hex')
|
|
11323
|
+
.slice(0, 16);
|
|
11324
|
+
const draftPath = saveBlockStudioDraftArtifacts(projectRoot, {
|
|
11325
|
+
currentPath: currentPath && isDraftBlockPath(currentPath) ? currentPath : undefined,
|
|
11326
|
+
source: draftSource,
|
|
11327
|
+
name,
|
|
11328
|
+
domain: typeof suppliedMetadata.domain === 'string' ? suppliedMetadata.domain : parsed.domain,
|
|
11329
|
+
description: typeof suppliedMetadata.description === 'string' ? suppliedMetadata.description : parsed.description,
|
|
11330
|
+
owner: typeof suppliedMetadata.owner === 'string' ? suppliedMetadata.owner : parsed.owner,
|
|
11331
|
+
tags: Array.isArray(suppliedMetadata.tags) ? suppliedMetadata.tags.map(String) : parsed.tags,
|
|
11332
|
+
lineage: Array.isArray(suppliedMetadata.lineage) ? suppliedMetadata.lineage.map(String) : undefined,
|
|
11333
|
+
stableSuffix,
|
|
11334
|
+
});
|
|
11335
|
+
const operation = operationCoordinator.create({
|
|
11336
|
+
type: 'block_certification',
|
|
11337
|
+
scope: `block:${draftPath}`,
|
|
11338
|
+
resourceRevision: typeof body.clientRevision === 'string'
|
|
11339
|
+
? body.clientRevision
|
|
11340
|
+
: createHash('sha256').update(draftSource).digest('hex'),
|
|
11341
|
+
message: `Certification queued for ${name}.`,
|
|
11342
|
+
cancellable: true,
|
|
11343
|
+
});
|
|
11344
|
+
if (idempotencyKey) {
|
|
11345
|
+
operationIdempotency.set(idempotencyKey, operation.id);
|
|
11346
|
+
while (operationIdempotency.size > 100) {
|
|
11347
|
+
const oldest = operationIdempotency.keys().next().value;
|
|
11348
|
+
if (typeof oldest !== 'string')
|
|
11349
|
+
break;
|
|
11350
|
+
operationIdempotency.delete(oldest);
|
|
11351
|
+
}
|
|
11352
|
+
}
|
|
11353
|
+
operationCoordinator.run(operation.id, async (signal, report) => {
|
|
11354
|
+
report({ phase: 'validating', progress: 15, message: 'Validating and running the saved draft once.' });
|
|
11355
|
+
const result = await certifyBlockStudioSource(draftSource, draftPath, { enterprise: body.enterprise === true });
|
|
11356
|
+
const blockers = Array.from(new Set(result.checklist.blockers));
|
|
11357
|
+
if (signal.aborted)
|
|
11358
|
+
throw Object.assign(new Error('Certification cancelled.'), { code: 'OPERATION_CANCELLED' });
|
|
11359
|
+
if (!result.certification.certified || blockers.length > 0) {
|
|
11360
|
+
return {
|
|
11361
|
+
outcome: 'draft_saved_with_blockers',
|
|
11362
|
+
oldPath: currentPath ?? draftPath,
|
|
11363
|
+
draftPath,
|
|
11364
|
+
blockers,
|
|
11365
|
+
checklist: result.checklist,
|
|
11366
|
+
block: openBlockStudioDocument(projectRoot, draftPath, semanticLayer),
|
|
11367
|
+
};
|
|
11368
|
+
}
|
|
11369
|
+
report({ phase: 'publishing', progress: 80, message: 'Publishing the certified block.' });
|
|
11370
|
+
const certifiedSource = setBlockStudioStatusInSource(draftSource, 'certified');
|
|
11371
|
+
const certifiedMetadata = parseBlockSourceMetadata(certifiedSource);
|
|
11372
|
+
const savedPath = saveBlockStudioArtifacts(projectRoot, {
|
|
11373
|
+
currentPath: canonicalPath ?? draftPath,
|
|
11374
|
+
source: certifiedSource,
|
|
11375
|
+
name: certifiedMetadata.name || name,
|
|
11376
|
+
domain: certifiedMetadata.domain,
|
|
11377
|
+
description: certifiedMetadata.description,
|
|
11378
|
+
owner: certifiedMetadata.owner,
|
|
11379
|
+
tags: certifiedMetadata.tags,
|
|
11380
|
+
});
|
|
11381
|
+
if (canonicalPath && draftPath !== savedPath) {
|
|
11382
|
+
try {
|
|
11383
|
+
deleteBlockStudioArtifacts(projectRoot, draftPath);
|
|
11384
|
+
}
|
|
11385
|
+
catch { /* certified source is already durable */ }
|
|
11386
|
+
}
|
|
11387
|
+
// Shared learning remains the same deterministic, advisory write used
|
|
11388
|
+
// by the synchronous compatibility route. It never gates publication.
|
|
11389
|
+
try {
|
|
11390
|
+
const learnedOutputs = Array.isArray(certifiedMetadata.outputs)
|
|
11391
|
+
? certifiedMetadata.outputs.filter((output) => typeof output === 'string')
|
|
11392
|
+
: [];
|
|
11393
|
+
if (certifiedMetadata.name) {
|
|
11394
|
+
const memory = new MemoryStore(defaultMemoryPath(projectRoot));
|
|
11395
|
+
memory.upsert({
|
|
11396
|
+
id: `mem_certify_${certifiedMetadata.name}`,
|
|
11397
|
+
scope: 'project',
|
|
11398
|
+
title: `Certified block: ${certifiedMetadata.name}`,
|
|
11399
|
+
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.`,
|
|
11400
|
+
tags: [certifiedMetadata.domain, certifiedMetadata.name, ...learnedOutputs.slice(0, 4)].filter((value) => Boolean(value)),
|
|
11401
|
+
source: 'certify',
|
|
11402
|
+
confidence: 0.95,
|
|
11403
|
+
importance: 0.85,
|
|
11404
|
+
enabled: true,
|
|
11405
|
+
});
|
|
11406
|
+
}
|
|
11407
|
+
}
|
|
11408
|
+
catch {
|
|
11409
|
+
/* best-effort: learning capture must never block certification */
|
|
11410
|
+
}
|
|
11411
|
+
const refreshOperation = scheduleProjectRefresh('block-studio-certification');
|
|
11412
|
+
return {
|
|
11413
|
+
outcome: 'certified',
|
|
11414
|
+
oldPath: currentPath ?? draftPath,
|
|
11415
|
+
draftPath,
|
|
11416
|
+
newPath: savedPath,
|
|
11417
|
+
block: openBlockStudioDocument(projectRoot, savedPath, semanticLayer),
|
|
11418
|
+
checklist: result.checklist,
|
|
11419
|
+
indexRefresh: { status: 'queued', operationId: refreshOperation.id },
|
|
11420
|
+
};
|
|
11421
|
+
});
|
|
11422
|
+
res.writeHead(202, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11423
|
+
res.end(serializeJSON({
|
|
11424
|
+
operation: operationCoordinator.get(operation.id) ?? operation,
|
|
11425
|
+
draft: openBlockStudioDocument(projectRoot, draftPath, semanticLayer),
|
|
11426
|
+
}));
|
|
9840
11427
|
}
|
|
9841
11428
|
catch (error) {
|
|
9842
|
-
|
|
9843
|
-
|
|
11429
|
+
const code = apiErrorCode(error, 'CERTIFICATION_REQUEST_FAILED');
|
|
11430
|
+
const status = code === 'SOURCE_CHANGED' || apiErrorMessage(error) === 'BLOCK_EXISTS' ? 409 : 500;
|
|
11431
|
+
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11432
|
+
res.end(serializeJSON({ error: apiErrorMessage(error), code }));
|
|
9844
11433
|
}
|
|
9845
11434
|
return;
|
|
9846
11435
|
}
|
|
@@ -9913,7 +11502,7 @@ export async function startLocalServer(opts) {
|
|
|
9913
11502
|
catch {
|
|
9914
11503
|
/* best-effort: learning capture must never block certification */
|
|
9915
11504
|
}
|
|
9916
|
-
|
|
11505
|
+
const refreshOperation = scheduleProjectRefresh('block-studio-certify');
|
|
9917
11506
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
9918
11507
|
res.end(serializeJSON({
|
|
9919
11508
|
ok: true,
|
|
@@ -9924,6 +11513,7 @@ export async function startLocalServer(opts) {
|
|
|
9924
11513
|
metadata: certifiedPayload?.metadata,
|
|
9925
11514
|
companionPath: certifiedPayload?.companionPath ?? null,
|
|
9926
11515
|
validation: certifiedPayload?.validation ?? result.validation,
|
|
11516
|
+
lineageRefresh: { status: 'queued', operationId: refreshOperation.id },
|
|
9927
11517
|
}));
|
|
9928
11518
|
}
|
|
9929
11519
|
catch (error) {
|
|
@@ -10747,6 +12337,20 @@ export async function startLocalServer(opts) {
|
|
|
10747
12337
|
res.end(serializeJSON({ error: 'Block name is required.' }));
|
|
10748
12338
|
return;
|
|
10749
12339
|
}
|
|
12340
|
+
const validation = validateBlockStudioSource(source, semanticLayer);
|
|
12341
|
+
if (!blockStudioValidationAllowsDraftSave(validation)) {
|
|
12342
|
+
const errors = validation.diagnostics.filter((diagnostic) => diagnostic.severity === 'error'
|
|
12343
|
+
&& !BLOCK_STUDIO_DRAFT_RUNTIME_DIAGNOSTICS.has(diagnostic.code ?? ''));
|
|
12344
|
+
res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
12345
|
+
res.end(serializeJSON({
|
|
12346
|
+
error: errors[0]?.message ?? 'Fix the block errors before saving.',
|
|
12347
|
+
code: 'BLOCK_VALIDATION_FAILED',
|
|
12348
|
+
recoverable: true,
|
|
12349
|
+
details: { validation: { ...validation, saveable: false } },
|
|
12350
|
+
nextActions: ['Edit the DQL source', 'Fix with AI and run'],
|
|
12351
|
+
}));
|
|
12352
|
+
return;
|
|
12353
|
+
}
|
|
10750
12354
|
const currentPath = typeof body.path === 'string' ? body.path : undefined;
|
|
10751
12355
|
const saveOptions = {
|
|
10752
12356
|
currentPath,
|
|
@@ -10773,25 +12377,13 @@ export async function startLocalServer(opts) {
|
|
|
10773
12377
|
stableSuffix: metadata.candidateId,
|
|
10774
12378
|
})
|
|
10775
12379
|
: 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);
|
|
12380
|
+
const refreshOperation = scheduleProjectRefresh('block-studio-save');
|
|
10792
12381
|
const payload = {
|
|
10793
12382
|
...openBlockStudioDocument(projectRoot, savedPath, semanticLayer),
|
|
10794
|
-
lineageRefresh
|
|
12383
|
+
lineageRefresh: {
|
|
12384
|
+
status: 'queued',
|
|
12385
|
+
operationId: refreshOperation.id,
|
|
12386
|
+
},
|
|
10795
12387
|
};
|
|
10796
12388
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
10797
12389
|
res.end(serializeJSON(payload));
|
|
@@ -10811,21 +12403,13 @@ export async function startLocalServer(opts) {
|
|
|
10811
12403
|
try {
|
|
10812
12404
|
const blockPath = url.searchParams.get('path');
|
|
10813
12405
|
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);
|
|
12406
|
+
const refreshOperation = scheduleProjectRefresh('block-studio-delete');
|
|
10827
12407
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
10828
|
-
res.end(serializeJSON({
|
|
12408
|
+
res.end(serializeJSON({
|
|
12409
|
+
ok: true,
|
|
12410
|
+
...deleted,
|
|
12411
|
+
lineageRefresh: { status: 'queued', operationId: refreshOperation.id },
|
|
12412
|
+
}));
|
|
10829
12413
|
}
|
|
10830
12414
|
catch (error) {
|
|
10831
12415
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -12422,11 +14006,12 @@ export async function startLocalServer(opts) {
|
|
|
12422
14006
|
return;
|
|
12423
14007
|
}
|
|
12424
14008
|
/**
|
|
12425
|
-
* Repair an editable notebook cell
|
|
12426
|
-
* The returned source is applied only to the in-memory
|
|
12427
|
-
* client; this endpoint never writes
|
|
14009
|
+
* Repair an editable notebook cell or Block Studio draft without starting
|
|
14010
|
+
* an AI conversation. The returned source is applied only to the in-memory
|
|
14011
|
+
* editor by the client; this endpoint never writes governed source files.
|
|
12428
14012
|
*/
|
|
12429
|
-
if (req.method === 'POST' && path === '/api/notebook/repair-execution'
|
|
14013
|
+
if (req.method === 'POST' && (path === '/api/notebook/repair-execution'
|
|
14014
|
+
|| path === '/api/block-studio/repair-and-run')) {
|
|
12430
14015
|
let body = {};
|
|
12431
14016
|
let execContext = null;
|
|
12432
14017
|
const start = Date.now();
|
|
@@ -12445,9 +14030,9 @@ export async function startLocalServer(opts) {
|
|
|
12445
14030
|
return;
|
|
12446
14031
|
}
|
|
12447
14032
|
const embeddedSourceSql = notebookCellRepairSql(cell);
|
|
12448
|
-
if (!embeddedSourceSql) {
|
|
14033
|
+
if (!embeddedSourceSql && (cell.type !== 'dql' || !notebookDqlSourceAllowsBackgroundRepair(cell.source))) {
|
|
12449
14034
|
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
12450
|
-
res.end(serializeJSON({ error: 'Background repair supports SQL cells and
|
|
14035
|
+
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
14036
|
return;
|
|
12452
14037
|
}
|
|
12453
14038
|
if (cell.type === 'sql' && /\{\{[^}]+\}\}/.test(cell.source)) {
|
|
@@ -12480,31 +14065,33 @@ export async function startLocalServer(opts) {
|
|
|
12480
14065
|
const parameters = body.parameters && typeof body.parameters === 'object' && !Array.isArray(body.parameters)
|
|
12481
14066
|
? body.parameters
|
|
12482
14067
|
: {};
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
if (
|
|
12487
|
-
|
|
12488
|
-
|
|
12489
|
-
|
|
12490
|
-
|
|
14068
|
+
let invocation = null;
|
|
14069
|
+
let plan = null;
|
|
14070
|
+
let dqlCompilationError;
|
|
14071
|
+
if (cell.type === 'dql') {
|
|
14072
|
+
try {
|
|
14073
|
+
invocation = prepareBlockInvocation({ source: cell.source, parameters, surface: 'notebook' });
|
|
14074
|
+
if (invocation.errors.length)
|
|
14075
|
+
throw boundedRepairError(invocation.errors.join(' '), 409);
|
|
14076
|
+
if (invocation.unresolvedParameters.length) {
|
|
14077
|
+
throw boundedRepairError(`Provide required parameter${invocation.unresolvedParameters.length === 1 ? '' : 's'} before retrying: ${invocation.unresolvedParameters.join(', ')}.`, 409);
|
|
14078
|
+
}
|
|
14079
|
+
plan = buildExecutionPlan(cell, {
|
|
14080
|
+
semanticLayer,
|
|
14081
|
+
driver: targetConnection.driver,
|
|
14082
|
+
parameters: invocation.values,
|
|
14083
|
+
});
|
|
14084
|
+
}
|
|
14085
|
+
catch (error) {
|
|
14086
|
+
const repairError = error;
|
|
14087
|
+
if (repairError.status === 409)
|
|
14088
|
+
throw error;
|
|
14089
|
+
dqlCompilationError = repairError.message || String(error);
|
|
14090
|
+
}
|
|
12491
14091
|
}
|
|
12492
|
-
const plan = cell.type === 'dql'
|
|
12493
|
-
? buildExecutionPlan(cell, {
|
|
12494
|
-
semanticLayer,
|
|
12495
|
-
driver: targetConnection.driver,
|
|
12496
|
-
parameters: invocation?.values,
|
|
12497
|
-
})
|
|
12498
|
-
: null;
|
|
12499
14092
|
const attemptedSql = typeof body.attemptedSql === 'string' && body.attemptedSql.trim()
|
|
12500
14093
|
? body.attemptedSql.trim()
|
|
12501
14094
|
: embeddedSourceSql;
|
|
12502
|
-
const sourceSql = plan?.sql ?? attemptedSql;
|
|
12503
|
-
const sqlParams = plan?.sqlParams ?? [];
|
|
12504
|
-
const variables = {
|
|
12505
|
-
...(plan?.variables ?? {}),
|
|
12506
|
-
...(invocation?.values ?? {}),
|
|
12507
|
-
};
|
|
12508
14095
|
const executionTarget = executionTargetDescriptor(body);
|
|
12509
14096
|
const targetConnectionName = executionTarget.target === 'connection'
|
|
12510
14097
|
? executionTarget.connectionName
|
|
@@ -12515,24 +14102,48 @@ export async function startLocalServer(opts) {
|
|
|
12515
14102
|
`Repair notebook cell ${cell.title?.trim() || cell.id}.`,
|
|
12516
14103
|
submittedError ? `Failure: ${submittedError.slice(0, 1_000)}` : '',
|
|
12517
14104
|
].filter(Boolean).join(' ');
|
|
12518
|
-
|
|
12519
|
-
question,
|
|
12520
|
-
sourceSql,
|
|
12521
|
-
targetConnection,
|
|
12522
|
-
targetConnectionName,
|
|
12523
|
-
sqlParams,
|
|
12524
|
-
variables,
|
|
12525
|
-
});
|
|
14105
|
+
let repaired;
|
|
12526
14106
|
let repairedSource;
|
|
12527
|
-
if (cell.type === 'dql') {
|
|
12528
|
-
const
|
|
12529
|
-
|
|
12530
|
-
|
|
12531
|
-
:
|
|
12532
|
-
|
|
14107
|
+
if (cell.type === 'dql' && dqlCompilationError) {
|
|
14108
|
+
const repairedDql = await executeBoundedDqlRepair({
|
|
14109
|
+
question,
|
|
14110
|
+
source: cell.source,
|
|
14111
|
+
failure: [submittedError, dqlCompilationError].filter(Boolean).join('\n'),
|
|
14112
|
+
targetConnection,
|
|
14113
|
+
targetConnectionName,
|
|
14114
|
+
parameters,
|
|
14115
|
+
artifactName: cell.title,
|
|
14116
|
+
});
|
|
14117
|
+
repaired = repairedDql;
|
|
14118
|
+
repairedSource = repairedDql.repairedSource;
|
|
12533
14119
|
}
|
|
12534
14120
|
else {
|
|
12535
|
-
|
|
14121
|
+
const sourceSql = plan?.sql ?? attemptedSql;
|
|
14122
|
+
if (!sourceSql?.trim()) {
|
|
14123
|
+
throw boundedRepairError('DQL could not identify an embedded query to repair.', 422);
|
|
14124
|
+
}
|
|
14125
|
+
const sqlParams = plan?.sqlParams ?? [];
|
|
14126
|
+
repaired = await executeBoundedSqlRepair({
|
|
14127
|
+
question,
|
|
14128
|
+
sourceSql,
|
|
14129
|
+
targetConnection,
|
|
14130
|
+
targetConnectionName,
|
|
14131
|
+
sqlParams,
|
|
14132
|
+
variables: {
|
|
14133
|
+
...(plan?.variables ?? {}),
|
|
14134
|
+
...(invocation?.values ?? {}),
|
|
14135
|
+
},
|
|
14136
|
+
});
|
|
14137
|
+
if (cell.type === 'dql') {
|
|
14138
|
+
const originalQuery = notebookCellRepairSql(cell);
|
|
14139
|
+
const repairedQuery = repaired.repairMode === 'deterministic'
|
|
14140
|
+
? applyNotebookRepairRewrites(originalQuery, repaired.rewrites)
|
|
14141
|
+
: restoreNotebookDqlParameterInterpolations(repaired.repairedSql, sqlParams);
|
|
14142
|
+
repairedSource = replaceNotebookDqlQueryForRepair(cell.source, repairedQuery);
|
|
14143
|
+
}
|
|
14144
|
+
else {
|
|
14145
|
+
repairedSource = repaired.repairedSql;
|
|
14146
|
+
}
|
|
12536
14147
|
}
|
|
12537
14148
|
if (!repairedSource) {
|
|
12538
14149
|
throw boundedRepairError('DQL could not safely replace the embedded query in this cell.', 422);
|
|
@@ -12543,12 +14154,13 @@ export async function startLocalServer(opts) {
|
|
|
12543
14154
|
const validationPlan = buildExecutionPlan({ ...cell, source: repairedSource }, {
|
|
12544
14155
|
semanticLayer,
|
|
12545
14156
|
driver: targetConnection.driver,
|
|
12546
|
-
parameters: invocation?.values,
|
|
14157
|
+
parameters: invocation?.values ?? parameters,
|
|
12547
14158
|
});
|
|
12548
14159
|
if (!validationPlan?.sql) {
|
|
12549
14160
|
throw boundedRepairError('The repaired SQL ran, but the DQL wrapper no longer compiles.', 422);
|
|
12550
14161
|
}
|
|
12551
|
-
const beforeParameters = sqlParams
|
|
14162
|
+
const beforeParameters = (plan?.sqlParams ?? ('sqlParams' in repaired ? repaired.sqlParams : []))
|
|
14163
|
+
.map((parameter) => `${parameter.position}:${parameter.name}`);
|
|
12552
14164
|
const afterParameters = validationPlan.sqlParams.map((parameter) => `${parameter.position}:${parameter.name}`);
|
|
12553
14165
|
if (beforeParameters.join('|') !== afterParameters.join('|')) {
|
|
12554
14166
|
throw boundedRepairError('The repaired DQL changed the cell parameter contract, so the update was stopped.', 422);
|
|
@@ -13814,6 +15426,22 @@ table: ${table}${tagList}
|
|
|
13814
15426
|
})));
|
|
13815
15427
|
});
|
|
13816
15428
|
});
|
|
15429
|
+
server.once('close', () => {
|
|
15430
|
+
runtimeClosing = true;
|
|
15431
|
+
for (const watcher of projectWatchers)
|
|
15432
|
+
watcher.close();
|
|
15433
|
+
projectWatchers.length = 0;
|
|
15434
|
+
unsubscribeOperationEvents();
|
|
15435
|
+
projectRefreshCoordinator.close();
|
|
15436
|
+
for (const client of operationSseClients) {
|
|
15437
|
+
try {
|
|
15438
|
+
client.end();
|
|
15439
|
+
}
|
|
15440
|
+
catch { /* connection already closed */ }
|
|
15441
|
+
}
|
|
15442
|
+
operationSseClients.clear();
|
|
15443
|
+
operationCoordinator.close();
|
|
15444
|
+
});
|
|
13817
15445
|
opts.captureServer?.(server);
|
|
13818
15446
|
return new Promise((resolvePromise, reject) => {
|
|
13819
15447
|
let retriedWithRandomPort = false;
|
|
@@ -14255,7 +15883,7 @@ function loadAppDashboard(projectRoot, appId, dashboardId) {
|
|
|
14255
15883
|
for (const d of findDashboardsForApp(appDir)) {
|
|
14256
15884
|
const { document: dashboard } = loadDashboardDocument(d);
|
|
14257
15885
|
if (dashboard?.id === dashboardId)
|
|
14258
|
-
return { app, dashboard };
|
|
15886
|
+
return { app, dashboard, appDir };
|
|
14259
15887
|
}
|
|
14260
15888
|
}
|
|
14261
15889
|
return null;
|
|
@@ -14360,6 +15988,7 @@ function serializeSkill(skill) {
|
|
|
14360
15988
|
clarifyWhen: skill.clarifyWhen,
|
|
14361
15989
|
examples: skill.examples,
|
|
14362
15990
|
sourceRefs: skill.sourceRefs,
|
|
15991
|
+
analyticalPolicy: skill.analyticalPolicy,
|
|
14363
15992
|
vocabulary: skill.vocabulary,
|
|
14364
15993
|
sourcePath: skill.sourcePath,
|
|
14365
15994
|
isStarter: skill.isStarter,
|
|
@@ -14415,9 +16044,27 @@ function parseSkillInput(raw, fallbackId) {
|
|
|
14415
16044
|
examples: asStrings(skill.examples),
|
|
14416
16045
|
sourceRefs: asStrings(skill.sourceRefs),
|
|
14417
16046
|
vocabulary: asMap(skill.vocabulary),
|
|
16047
|
+
analyticalPolicy: parseSkillAnalyticalPolicyInput(skill.analyticalPolicy),
|
|
14418
16048
|
isStarter: skill.isStarter === true ? true : undefined,
|
|
14419
16049
|
};
|
|
14420
16050
|
}
|
|
16051
|
+
function parseSkillAnalyticalPolicyInput(value) {
|
|
16052
|
+
const policy = contextRecord(value);
|
|
16053
|
+
if (!Object.keys(policy).length)
|
|
16054
|
+
return undefined;
|
|
16055
|
+
const strings = (input) => Array.isArray(input) ? input.filter((item) => typeof item === 'string') : [];
|
|
16056
|
+
const pick = (input) => typeof input === 'string' && input.trim() ? input.trim() : undefined;
|
|
16057
|
+
return {
|
|
16058
|
+
metricIds: strings(policy.metricIds ?? policy.metric_ids),
|
|
16059
|
+
timeRole: pick(policy.timeRole ?? policy.time_role),
|
|
16060
|
+
calendarId: pick(policy.calendarId ?? policy.calendar_id),
|
|
16061
|
+
timezone: pick(policy.timezone),
|
|
16062
|
+
completenessPolicy: ['partial_current', 'latest_complete', 'closed_period'].includes(String(policy.completenessPolicy ?? policy.completeness_policy)) ? String(policy.completenessPolicy ?? policy.completeness_policy) : undefined,
|
|
16063
|
+
comparisonAlignment: ['elapsed_period', 'calendar_period', 'fiscal_period'].includes(String(policy.comparisonAlignment ?? policy.comparison_alignment)) ? String(policy.comparisonAlignment ?? policy.comparison_alignment) : undefined,
|
|
16064
|
+
defaultRankingPeriod: ['current', 'comparison'].includes(String(policy.defaultRankingPeriod ?? policy.default_ranking_period)) ? String(policy.defaultRankingPeriod ?? policy.default_ranking_period) : undefined,
|
|
16065
|
+
narrativeGuidance: strings(policy.narrativeGuidance ?? policy.narrative_guidance),
|
|
16066
|
+
};
|
|
16067
|
+
}
|
|
14421
16068
|
/** Loose, case/slug-insensitive domain key for counting membership. */
|
|
14422
16069
|
function domainKey(value) {
|
|
14423
16070
|
if (!value)
|
|
@@ -14577,6 +16224,553 @@ export function parseDomainInput(raw, fallbackId) {
|
|
|
14577
16224
|
sourcePath: typeof domain.sourcePath === 'string' ? domain.sourcePath : undefined,
|
|
14578
16225
|
};
|
|
14579
16226
|
}
|
|
16227
|
+
function modelingImportDirectory(projectRoot) {
|
|
16228
|
+
return join(projectRoot, '.dql', 'local', 'modeling-imports');
|
|
16229
|
+
}
|
|
16230
|
+
function modelingImportSessionPath(projectRoot, id) {
|
|
16231
|
+
const safe = id.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
16232
|
+
if (!safe || safe !== id)
|
|
16233
|
+
throw new Error('Invalid modeling import id.');
|
|
16234
|
+
return join(modelingImportDirectory(projectRoot), `${safe}.json`);
|
|
16235
|
+
}
|
|
16236
|
+
function writeModelingImportSession(projectRoot, session) {
|
|
16237
|
+
mkdirSync(modelingImportDirectory(projectRoot), { recursive: true });
|
|
16238
|
+
writeFileSync(modelingImportSessionPath(projectRoot, session.id), `${JSON.stringify(session, null, 2)}\n`, 'utf8');
|
|
16239
|
+
}
|
|
16240
|
+
function readModelingImportSession(projectRoot, id) {
|
|
16241
|
+
const path = modelingImportSessionPath(projectRoot, id);
|
|
16242
|
+
if (!existsSync(path))
|
|
16243
|
+
return null;
|
|
16244
|
+
try {
|
|
16245
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
16246
|
+
return parsed.version === 1 && parsed.id === id ? parsed : null;
|
|
16247
|
+
}
|
|
16248
|
+
catch {
|
|
16249
|
+
return null;
|
|
16250
|
+
}
|
|
16251
|
+
}
|
|
16252
|
+
function publicModelingImportSession(session) {
|
|
16253
|
+
return {
|
|
16254
|
+
...session,
|
|
16255
|
+
candidates: session.candidates.map(({ document: _document, ...candidate }) => candidate),
|
|
16256
|
+
};
|
|
16257
|
+
}
|
|
16258
|
+
function createModelingImportSession(projectRoot, gitRoot, loopback, source) {
|
|
16259
|
+
const mode = source.mode;
|
|
16260
|
+
if (mode !== 'current_project' && mode !== 'path' && mode !== 'upload' && mode !== 'paste')
|
|
16261
|
+
throw Object.assign(new Error('Choose current_project, path, upload, or paste.'), { code: 'INVALID_REQUEST' });
|
|
16262
|
+
const files = [];
|
|
16263
|
+
if (mode === 'paste' || mode === 'upload') {
|
|
16264
|
+
const content = typeof source.content === 'string' ? source.content : '';
|
|
16265
|
+
if (!content.trim() || Buffer.byteLength(content) > 1_000_000)
|
|
16266
|
+
throw Object.assign(new Error('YAML content is required and must be 1 MB or smaller.'), { code: 'MODELING_IMPORT_LIMIT' });
|
|
16267
|
+
files.push({ name: typeof source.filename === 'string' ? source.filename : mode === 'paste' ? 'pasted-modeling.yaml' : 'uploaded-modeling.yaml', content, inProject: false });
|
|
16268
|
+
}
|
|
16269
|
+
else {
|
|
16270
|
+
if (mode === 'path' && !loopback)
|
|
16271
|
+
throw Object.assign(new Error('Local path import is available only on the loopback Notebook runtime.'), { code: 'PATH_ACCESS_FORBIDDEN' });
|
|
16272
|
+
const allowedRoot = realpathSync(resolve(gitRoot ?? projectRoot));
|
|
16273
|
+
const requestedInput = mode === 'current_project'
|
|
16274
|
+
? resolve(projectRoot)
|
|
16275
|
+
: resolve(projectRoot, typeof source.path === 'string' ? source.path : '');
|
|
16276
|
+
if (!existsSync(requestedInput))
|
|
16277
|
+
throw Object.assign(new Error('The selected YAML path does not exist.'), { code: 'MODELING_IMPORT_NOT_FOUND' });
|
|
16278
|
+
const requested = realpathSync(requestedInput);
|
|
16279
|
+
if (requested !== allowedRoot && !requested.startsWith(`${allowedRoot}${sep}`))
|
|
16280
|
+
throw Object.assign(new Error('Import path must stay inside the current Git workspace.'), { code: 'PATH_ACCESS_FORBIDDEN' });
|
|
16281
|
+
const paths = collectModelingYamlPaths(requested, 100);
|
|
16282
|
+
for (const path of paths) {
|
|
16283
|
+
const size = statSync(path).size;
|
|
16284
|
+
if (size > 1_000_000)
|
|
16285
|
+
continue;
|
|
16286
|
+
const resolvedPath = realpathSync(path);
|
|
16287
|
+
if (resolvedPath !== allowedRoot && !resolvedPath.startsWith(`${allowedRoot}${sep}`))
|
|
16288
|
+
continue;
|
|
16289
|
+
const resolvedProject = realpathSync(projectRoot);
|
|
16290
|
+
files.push({ name: relative(requested, resolvedPath).replace(/\\/g, '/') || resolvedPath.split(sep).at(-1), path: relative(resolvedProject, resolvedPath).replace(/\\/g, '/'), content: readFileSync(resolvedPath, 'utf8'), inProject: resolvedPath === resolvedProject || resolvedPath.startsWith(`${resolvedProject}${sep}`) });
|
|
16291
|
+
}
|
|
16292
|
+
}
|
|
16293
|
+
if (files.length === 0)
|
|
16294
|
+
throw Object.assign(new Error('No YAML files were found in the selected source.'), { code: 'MODELING_IMPORT_EMPTY' });
|
|
16295
|
+
const candidates = files.map((file, index) => classifyModelingYamlCandidate(file, index));
|
|
16296
|
+
const now = new Date().toISOString();
|
|
16297
|
+
const session = {
|
|
16298
|
+
version: 1,
|
|
16299
|
+
id: apiRequestId('import'),
|
|
16300
|
+
createdAt: now,
|
|
16301
|
+
updatedAt: now,
|
|
16302
|
+
source: { mode, label: mode === 'path' ? String(source.path ?? '') : mode === 'current_project' ? 'Current project' : files[0].name },
|
|
16303
|
+
candidates,
|
|
16304
|
+
};
|
|
16305
|
+
writeModelingImportSession(projectRoot, session);
|
|
16306
|
+
return session;
|
|
16307
|
+
}
|
|
16308
|
+
function collectModelingYamlPaths(start, limit) {
|
|
16309
|
+
if (!existsSync(start))
|
|
16310
|
+
throw Object.assign(new Error('The selected YAML path does not exist.'), { code: 'MODELING_IMPORT_NOT_FOUND' });
|
|
16311
|
+
if (statSync(start).isFile())
|
|
16312
|
+
return /\.ya?ml$/i.test(start) ? [start] : [];
|
|
16313
|
+
const output = [];
|
|
16314
|
+
const visit = (directory) => {
|
|
16315
|
+
if (output.length >= limit)
|
|
16316
|
+
return;
|
|
16317
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
16318
|
+
if (output.length >= limit)
|
|
16319
|
+
return;
|
|
16320
|
+
if (entry.name.startsWith('.') || ['node_modules', 'target', 'dist', 'build'].includes(entry.name))
|
|
16321
|
+
continue;
|
|
16322
|
+
const path = join(directory, entry.name);
|
|
16323
|
+
if (entry.isDirectory())
|
|
16324
|
+
visit(path);
|
|
16325
|
+
else if (entry.isFile() && /\.ya?ml$/i.test(entry.name))
|
|
16326
|
+
output.push(path);
|
|
16327
|
+
}
|
|
16328
|
+
};
|
|
16329
|
+
visit(start);
|
|
16330
|
+
return output.sort();
|
|
16331
|
+
}
|
|
16332
|
+
function classifyModelingYamlCandidate(file, index) {
|
|
16333
|
+
let document = {};
|
|
16334
|
+
try {
|
|
16335
|
+
const parsed = loadYaml(file.content);
|
|
16336
|
+
document = contextRecord(parsed);
|
|
16337
|
+
}
|
|
16338
|
+
catch (error) {
|
|
16339
|
+
return { id: `file-${index + 1}`, name: file.name, path: file.path, dialect: 'unsupported', action: 'unsupported', summary: 'Malformed YAML', warnings: [error instanceof Error ? error.message : String(error)] };
|
|
16340
|
+
}
|
|
16341
|
+
const keys = new Set(Object.keys(document));
|
|
16342
|
+
const dialect = keys.has('entities') || keys.has('relationships') || keys.has('area') || keys.has('domain')
|
|
16343
|
+
? 'dql_modeling'
|
|
16344
|
+
: keys.has('semantic_models') || keys.has('metrics')
|
|
16345
|
+
? 'dbt_semantic'
|
|
16346
|
+
: keys.has('models') || keys.has('sources')
|
|
16347
|
+
? 'dbt_resource'
|
|
16348
|
+
: 'unsupported';
|
|
16349
|
+
const objectCount = ['entities', 'relationships', 'models', 'sources', 'semantic_models', 'metrics']
|
|
16350
|
+
.reduce((total, key) => total + (Array.isArray(document[key]) ? document[key].length : 0), 0);
|
|
16351
|
+
const openExisting = file.inProject && dialect === 'dql_modeling' && Boolean(file.path?.startsWith('domains/'));
|
|
16352
|
+
return {
|
|
16353
|
+
id: `file-${index + 1}`,
|
|
16354
|
+
name: file.name,
|
|
16355
|
+
path: file.path,
|
|
16356
|
+
dialect,
|
|
16357
|
+
action: dialect === 'unsupported' ? 'unsupported' : openExisting ? 'open_existing' : 'import',
|
|
16358
|
+
summary: dialect === 'unsupported' ? 'Unsupported generic YAML' : `${objectCount || 1} ${dialect.replace('_', ' ')} candidate${objectCount === 1 ? '' : 's'}`,
|
|
16359
|
+
warnings: dialect === 'unsupported' ? ['v1 supports DQL modeling, dbt resource, and dbt semantic YAML only.'] : openExisting ? ['This is already governed by the current project and should be opened in place.'] : [],
|
|
16360
|
+
document,
|
|
16361
|
+
};
|
|
16362
|
+
}
|
|
16363
|
+
function modelingImportOperations(session, selectedIds, domain, areaId, manifest) {
|
|
16364
|
+
const diagnostics = [];
|
|
16365
|
+
const operations = [];
|
|
16366
|
+
const selected = session.candidates.filter((candidate) => selectedIds.includes(candidate.id));
|
|
16367
|
+
if (!selected.length)
|
|
16368
|
+
throw Object.assign(new Error('Select at least one supported YAML candidate.'), { code: 'MODELING_IMPORT_EMPTY' });
|
|
16369
|
+
const packageExists = Boolean(manifest.modeling?.packages?.[domain]);
|
|
16370
|
+
const area = Object.values(manifest.modeling?.areas ?? {}).find((item) => item.domain === domain && (item.localId === areaId || item.qualifiedId === areaId));
|
|
16371
|
+
if (!packageExists)
|
|
16372
|
+
diagnostics.push({ code: 'DOMAIN_NOT_FOUND', severity: 'blocking', message: `Domain not found: ${domain}` });
|
|
16373
|
+
if (!area)
|
|
16374
|
+
diagnostics.push({ code: 'MODEL_AREA_NOT_FOUND', severity: 'blocking', message: `Model Area not found in ${domain}: ${areaId}` });
|
|
16375
|
+
const nodes = Object.values(manifest.dbtProvenance?.nodes ?? {});
|
|
16376
|
+
const resolveDbt = (reference, operationId) => {
|
|
16377
|
+
const cleaned = reference.replace(/^ref\(['"]|['"]\)$/g, '').replace(/^model\./, '');
|
|
16378
|
+
const matches = nodes.filter((node) => node.uniqueId === reference || node.name === reference || node.name === cleaned || node.uniqueId.endsWith(`.${cleaned}`));
|
|
16379
|
+
if (matches.length !== 1)
|
|
16380
|
+
diagnostics.push({ code: matches.length ? 'AMBIGUOUS_DBT_MODEL' : 'DBT_MODEL_NOT_FOUND', severity: 'blocking', operationId, message: `${reference} ${matches.length ? 'matches multiple dbt models' : 'was not found in the active dbt manifest'}.` });
|
|
16381
|
+
return matches.length === 1 ? matches[0].uniqueId : undefined;
|
|
16382
|
+
};
|
|
16383
|
+
const used = new Set(Object.values(manifest.modeling?.entities ?? {}).filter((entity) => entity.domain === domain).map((entity) => entity.localId));
|
|
16384
|
+
const allocate = (value) => {
|
|
16385
|
+
const base = authoringSlug(value).replace(/-/g, '_') || 'model';
|
|
16386
|
+
let id = base;
|
|
16387
|
+
let suffix = 2;
|
|
16388
|
+
while (used.has(id))
|
|
16389
|
+
id = `${base}_${suffix++}`;
|
|
16390
|
+
used.add(id);
|
|
16391
|
+
return id;
|
|
16392
|
+
};
|
|
16393
|
+
for (const candidate of selected) {
|
|
16394
|
+
if (candidate.action === 'open_existing') {
|
|
16395
|
+
diagnostics.push({ code: 'OPEN_EXISTING_SOURCE', severity: 'info', message: `${candidate.path ?? candidate.name} is already in the current Domain Package and was not copied.` });
|
|
16396
|
+
continue;
|
|
16397
|
+
}
|
|
16398
|
+
if (candidate.dialect === 'unsupported' || !candidate.document) {
|
|
16399
|
+
diagnostics.push({ code: 'UNSUPPORTED_YAML', severity: 'blocking', message: `${candidate.name} is not DQL or dbt modeling YAML.` });
|
|
16400
|
+
continue;
|
|
16401
|
+
}
|
|
16402
|
+
const document = candidate.document;
|
|
16403
|
+
if (candidate.dialect === 'dql_modeling') {
|
|
16404
|
+
const importedEntities = new Map();
|
|
16405
|
+
for (const [index, raw] of contextArray(document.entities).entries()) {
|
|
16406
|
+
const entity = contextRecord(raw);
|
|
16407
|
+
const operationId = `${candidate.id}:entity:${index + 1}`;
|
|
16408
|
+
const dbtModel = resolveDbt(String(entity.dbt_model ?? entity.dbtModel ?? ''), operationId);
|
|
16409
|
+
if (!dbtModel)
|
|
16410
|
+
continue;
|
|
16411
|
+
const sourceId = String(entity.id ?? nodes.find((node) => node.uniqueId === dbtModel)?.name ?? `model_${index + 1}`);
|
|
16412
|
+
const id = allocate(sourceId);
|
|
16413
|
+
importedEntities.set(sourceId, { id, operationId });
|
|
16414
|
+
importedEntities.set(String(entity.qualified_id ?? entity.qualifiedId ?? ''), { id, operationId });
|
|
16415
|
+
importedEntities.set(dbtModel, { id, operationId });
|
|
16416
|
+
operations.push({ id: operationId, kind: 'modeling_change', evidence: [candidate.path ?? candidate.name, dbtModel], change: { operation: 'upsert_entity', value: { id, domain, areaId: area?.localId ?? areaId, dbtModel, businessName: typeof entity.business_name === 'string' ? entity.business_name : undefined, businessContext: typeof entity.business_context === 'string' ? entity.business_context : undefined, conceptRefs: contextStrings(entity.concept_refs), analyticalRole: parseImportedAnalyticalRole(entity.analytical_role), status: 'draft', owner: typeof entity.owner === 'string' ? entity.owner : undefined, grain: typeof entity.grain === 'string' ? entity.grain : undefined, keys: contextStrings(entity.keys) } } });
|
|
16417
|
+
}
|
|
16418
|
+
for (const [index, raw] of contextArray(document.relationships).entries()) {
|
|
16419
|
+
const relationship = contextRecord(raw);
|
|
16420
|
+
const operationId = `${candidate.id}:relationship:${index + 1}`;
|
|
16421
|
+
const keys = contextArray(relationship.keys).flatMap((value) => {
|
|
16422
|
+
const key = contextRecord(value);
|
|
16423
|
+
return typeof key.from === 'string' && typeof key.to === 'string' ? [{ from: key.from, to: key.to }] : [];
|
|
16424
|
+
});
|
|
16425
|
+
if (!relationship.from || !relationship.to || !keys.length) {
|
|
16426
|
+
diagnostics.push({ code: 'RELATIONSHIP_INCOMPLETE', severity: 'blocking', operationId, message: 'Imported relationship requires from, to, and at least one key pair.' });
|
|
16427
|
+
continue;
|
|
16428
|
+
}
|
|
16429
|
+
const resolveEndpoint = (rawRef) => {
|
|
16430
|
+
const ref = String(rawRef);
|
|
16431
|
+
const imported = importedEntities.get(ref);
|
|
16432
|
+
if (imported)
|
|
16433
|
+
return { id: imported.id, dependency: imported.operationId };
|
|
16434
|
+
const matches = Object.values(manifest.modeling?.entities ?? {}).filter((entity) => entity.domain === domain && (entity.localId === ref || entity.id === ref || entity.qualifiedId === ref));
|
|
16435
|
+
return matches.length === 1 ? { id: matches[0].localId } : {};
|
|
16436
|
+
};
|
|
16437
|
+
const from = resolveEndpoint(relationship.from);
|
|
16438
|
+
const to = resolveEndpoint(relationship.to);
|
|
16439
|
+
if (!from.id || !to.id) {
|
|
16440
|
+
diagnostics.push({ code: 'RELATIONSHIP_REFERENCE_UNRESOLVED', severity: 'blocking', operationId, message: `Relationship endpoints must resolve inside ${domain}; simple import cannot create cross-domain relationships.` });
|
|
16441
|
+
continue;
|
|
16442
|
+
}
|
|
16443
|
+
operations.push({ id: operationId, kind: 'modeling_change', dependsOn: [...new Set([from.dependency, to.dependency].filter((value) => Boolean(value)))], evidence: [candidate.path ?? candidate.name], change: { operation: 'upsert_relationship', value: { id: String(relationship.id ?? `${relationship.from}_to_${relationship.to}`), domain, areaId: area?.localId ?? areaId, from: from.id, to: to.id, keys, cardinality: parseImportedCardinality(relationship.cardinality), fanout: parseImportedFanout(relationship.fanout), status: 'draft', description: typeof relationship.description === 'string' ? relationship.description : undefined, rationale: 'Imported as a draft; certification and warehouse proof were intentionally removed.' } } });
|
|
16444
|
+
}
|
|
16445
|
+
}
|
|
16446
|
+
else {
|
|
16447
|
+
const modelRecords = candidate.dialect === 'dbt_resource'
|
|
16448
|
+
? contextArray(document.models)
|
|
16449
|
+
: contextArray(document.semantic_models);
|
|
16450
|
+
for (const [index, raw] of modelRecords.entries()) {
|
|
16451
|
+
const model = contextRecord(raw);
|
|
16452
|
+
const operationId = `${candidate.id}:model:${index + 1}`;
|
|
16453
|
+
const reference = String(model.model ?? model.name ?? '');
|
|
16454
|
+
const dbtModel = resolveDbt(reference, operationId);
|
|
16455
|
+
if (!dbtModel)
|
|
16456
|
+
continue;
|
|
16457
|
+
const existing = Object.values(manifest.modeling?.entities ?? {}).find((entity) => entity.dbtUniqueId === dbtModel);
|
|
16458
|
+
if (existing && existing.domain !== domain) {
|
|
16459
|
+
diagnostics.push({ code: 'OWNERSHIP_CONFLICT', severity: 'blocking', operationId, message: `${dbtModel} is already owned by ${existing.domain}; import cannot move it implicitly.` });
|
|
16460
|
+
continue;
|
|
16461
|
+
}
|
|
16462
|
+
const id = existing?.localId ?? allocate(String(model.name ?? nodes.find((node) => node.uniqueId === dbtModel)?.name ?? `model_${index + 1}`));
|
|
16463
|
+
operations.push({ id: operationId, kind: 'modeling_change', evidence: [candidate.path ?? candidate.name, dbtModel], change: { operation: 'upsert_entity', value: { id, domain, areaId: area?.localId ?? areaId, dbtModel, businessName: typeof model.name === 'string' ? titleCaseAuthoring(model.name) : undefined, businessContext: typeof model.description === 'string' ? model.description : undefined, status: 'draft', analyticalRole: 'unknown' } } });
|
|
16464
|
+
}
|
|
16465
|
+
}
|
|
16466
|
+
}
|
|
16467
|
+
if (!operations.length)
|
|
16468
|
+
throw Object.assign(new Error('The selected YAML did not produce any resolvable modeling changes.'), { code: 'MODELING_IMPORT_EMPTY', diagnostics });
|
|
16469
|
+
return { operations, diagnostics };
|
|
16470
|
+
}
|
|
16471
|
+
function contextRecord(value) {
|
|
16472
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
16473
|
+
}
|
|
16474
|
+
function contextArray(value) {
|
|
16475
|
+
return Array.isArray(value) ? value : [];
|
|
16476
|
+
}
|
|
16477
|
+
function contextStrings(value) {
|
|
16478
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
16479
|
+
}
|
|
16480
|
+
function parseImportedAnalyticalRole(value) {
|
|
16481
|
+
return value === 'event' || value === 'dimension' || value === 'snapshot' || value === 'bridge' ? value : 'unknown';
|
|
16482
|
+
}
|
|
16483
|
+
function parseImportedCardinality(value) {
|
|
16484
|
+
return value === 'one_to_one' || value === 'one_to_many' || value === 'many_to_one' || value === 'many_to_many' ? value : 'unknown';
|
|
16485
|
+
}
|
|
16486
|
+
function parseImportedFanout(value) {
|
|
16487
|
+
if (value === 'unsafe' || value === 'forbidden')
|
|
16488
|
+
return 'forbidden';
|
|
16489
|
+
return value === 'safe' || value === 'dedupe_required' || value === 'attribution_required' ? value : 'unknown';
|
|
16490
|
+
}
|
|
16491
|
+
function contextSourceReferenceExists(manifest, reference) {
|
|
16492
|
+
if (!reference.includes('::entity::') && !reference.includes('::relationship::'))
|
|
16493
|
+
return true;
|
|
16494
|
+
return Object.values(manifest.modeling?.entities ?? {}).some((entity) => entity.qualifiedId === reference)
|
|
16495
|
+
|| Object.values(manifest.modeling?.relationships ?? {}).some((relationship) => relationship.qualifiedId === reference);
|
|
16496
|
+
}
|
|
16497
|
+
function parseContextAuthoringOrigin(value) {
|
|
16498
|
+
if (value === 'manual' || value === 'yaml_import' || value === 'dbt_discovery' || value === 'ai' || value === 'correction')
|
|
16499
|
+
return value;
|
|
16500
|
+
throw Object.assign(new Error('A valid context proposal origin is required.'), { code: 'INVALID_REQUEST' });
|
|
16501
|
+
}
|
|
16502
|
+
function parseContextAuthoringOperations(value) {
|
|
16503
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 100)
|
|
16504
|
+
throw Object.assign(new Error('Provide 1-100 authoring operations.'), { code: 'INVALID_REQUEST' });
|
|
16505
|
+
const ids = new Set();
|
|
16506
|
+
return value.map((raw, index) => {
|
|
16507
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
16508
|
+
throw Object.assign(new Error(`Operation ${index + 1} is invalid.`), { code: 'INVALID_REQUEST' });
|
|
16509
|
+
const operation = raw;
|
|
16510
|
+
const id = typeof operation.id === 'string' ? operation.id.trim() : '';
|
|
16511
|
+
if (!id || ids.has(id))
|
|
16512
|
+
throw Object.assign(new Error(`Operation ${index + 1} requires a unique id.`), { code: 'INVALID_REQUEST' });
|
|
16513
|
+
ids.add(id);
|
|
16514
|
+
const dependsOn = Array.isArray(operation.dependsOn) ? operation.dependsOn.filter((item) => typeof item === 'string') : undefined;
|
|
16515
|
+
const evidence = Array.isArray(operation.evidence) ? operation.evidence.filter((item) => typeof item === 'string').slice(0, 32) : undefined;
|
|
16516
|
+
if (operation.kind === 'modeling_change') {
|
|
16517
|
+
const change = operation.change;
|
|
16518
|
+
const allowed = new Set(['upsert_domain', 'upsert_area', 'upsert_entity', 'upsert_relationship', 'upsert_contract', 'upsert_export', 'upsert_import', 'remove_entity', 'remove_relationship', 'remove_area']);
|
|
16519
|
+
if (!change || typeof change !== 'object' || !allowed.has(change.operation) || !('value' in change) || !change.value || typeof change.value !== 'object')
|
|
16520
|
+
throw Object.assign(new Error(`Modeling operation ${id} is invalid.`), { code: 'INVALID_REQUEST' });
|
|
16521
|
+
return { id, kind: 'modeling_change', change, dependsOn, evidence };
|
|
16522
|
+
}
|
|
16523
|
+
if (operation.kind === 'skill_change') {
|
|
16524
|
+
const skillOperation = operation.operation;
|
|
16525
|
+
const skill = operation.value;
|
|
16526
|
+
if ((skillOperation !== 'create' && skillOperation !== 'update' && skillOperation !== 'move') || !skill || typeof skill.id !== 'string' || !skill.id.trim() || typeof skill.body !== 'string')
|
|
16527
|
+
throw Object.assign(new Error(`Skill operation ${id} is invalid.`), { code: 'INVALID_REQUEST' });
|
|
16528
|
+
return {
|
|
16529
|
+
id,
|
|
16530
|
+
kind: 'skill_change',
|
|
16531
|
+
operation: skillOperation,
|
|
16532
|
+
value: { ...skill, scope: 'project', status: 'draft' },
|
|
16533
|
+
targetQualifiedId: typeof operation.targetQualifiedId === 'string' ? operation.targetQualifiedId : undefined,
|
|
16534
|
+
expectedSourceHash: typeof operation.expectedSourceHash === 'string' ? operation.expectedSourceHash : undefined,
|
|
16535
|
+
dependsOn,
|
|
16536
|
+
evidence,
|
|
16537
|
+
};
|
|
16538
|
+
}
|
|
16539
|
+
if (operation.kind === 'dbt_source_change') {
|
|
16540
|
+
const change = operation.change;
|
|
16541
|
+
if (!change || typeof change.uniqueId !== 'string' || !change.uniqueId.trim())
|
|
16542
|
+
throw Object.assign(new Error(`dbt source operation ${id} is invalid.`), { code: 'INVALID_REQUEST' });
|
|
16543
|
+
return { id, kind: 'dbt_source_change', change, dependsOn, evidence };
|
|
16544
|
+
}
|
|
16545
|
+
throw Object.assign(new Error(`Unsupported context operation kind for ${id}.`), { code: 'INVALID_REQUEST' });
|
|
16546
|
+
});
|
|
16547
|
+
}
|
|
16548
|
+
function resolveContextPatchPath(projectRoot, gitRoot, relativePath) {
|
|
16549
|
+
const allowedRoot = resolve(gitRoot ?? projectRoot);
|
|
16550
|
+
const absolute = resolve(projectRoot, relativePath);
|
|
16551
|
+
if (absolute !== allowedRoot && !absolute.startsWith(`${allowedRoot}${sep}`)) {
|
|
16552
|
+
throw Object.assign(new Error(`Proposal path escapes the Git workspace: ${relativePath}`), { code: 'OWNERSHIP_CONFLICT' });
|
|
16553
|
+
}
|
|
16554
|
+
return absolute;
|
|
16555
|
+
}
|
|
16556
|
+
function buildMetadataBoundModelingOperations(projectRoot, manifest, request) {
|
|
16557
|
+
const modeling = manifest.modeling;
|
|
16558
|
+
if (!modeling || !manifest.dbtProvenance)
|
|
16559
|
+
throw new Error('Modeling AI requires a compiled dbt-first project snapshot.');
|
|
16560
|
+
const context = request.workspaceContext ?? {};
|
|
16561
|
+
const requestedDomain = typeof context.domain === 'string'
|
|
16562
|
+
? context.domain
|
|
16563
|
+
: request.selectedObject?.kind === 'domain'
|
|
16564
|
+
? request.selectedObject.id
|
|
16565
|
+
: undefined;
|
|
16566
|
+
const domain = requestedDomain ?? Object.keys(modeling.packages)[0];
|
|
16567
|
+
if (!domain || !modeling.packages[domain])
|
|
16568
|
+
throw new Error('Select or create a Domain before building models with AI.');
|
|
16569
|
+
const selectedIdentity = request.selectedObject?.id;
|
|
16570
|
+
const selectedRelationship = request.selectedObject?.kind === 'relationship'
|
|
16571
|
+
? Object.values(modeling.relationships).find((item) => item.qualifiedId === selectedIdentity || item.id === selectedIdentity || item.localId === selectedIdentity)
|
|
16572
|
+
: undefined;
|
|
16573
|
+
if (selectedRelationship) {
|
|
16574
|
+
const cardinality = /one[-_ ]to[-_ ]one|1\s*:\s*1/i.test(request.question)
|
|
16575
|
+
? 'one_to_one'
|
|
16576
|
+
: /one[-_ ]to[-_ ]many|1\s*:\s*n/i.test(request.question)
|
|
16577
|
+
? 'one_to_many'
|
|
16578
|
+
: /many[-_ ]to[-_ ]one|n\s*:\s*1/i.test(request.question)
|
|
16579
|
+
? 'many_to_one'
|
|
16580
|
+
: /many[-_ ]to[-_ ]many|n\s*:\s*n/i.test(request.question)
|
|
16581
|
+
? 'many_to_many'
|
|
16582
|
+
: selectedRelationship.cardinality;
|
|
16583
|
+
return [{
|
|
16584
|
+
id: `relationship:${selectedRelationship.qualifiedId}`,
|
|
16585
|
+
kind: 'modeling_change',
|
|
16586
|
+
evidence: [selectedRelationship.sourcePath, ...(selectedRelationship.validation?.queryFingerprint ? [`validation:${selectedRelationship.validation.queryFingerprint}`] : [])],
|
|
16587
|
+
change: {
|
|
16588
|
+
operation: 'upsert_relationship',
|
|
16589
|
+
value: {
|
|
16590
|
+
id: selectedRelationship.localId,
|
|
16591
|
+
domain: selectedRelationship.ownerDomain ?? domain,
|
|
16592
|
+
areaId: selectedRelationship.areaId?.split('::area::').at(-1),
|
|
16593
|
+
from: modeling.entities[selectedRelationship.from]?.localId ?? selectedRelationship.from,
|
|
16594
|
+
to: modeling.entities[selectedRelationship.to]?.localId ?? selectedRelationship.to,
|
|
16595
|
+
keys: selectedRelationship.keys,
|
|
16596
|
+
cardinality,
|
|
16597
|
+
fanout: selectedRelationship.fanout === 'unsafe' ? 'forbidden' : selectedRelationship.fanout,
|
|
16598
|
+
status: 'draft',
|
|
16599
|
+
owner: selectedRelationship.owner,
|
|
16600
|
+
ownerDomain: selectedRelationship.ownerDomain,
|
|
16601
|
+
verb: selectedRelationship.verb,
|
|
16602
|
+
description: selectedRelationship.description,
|
|
16603
|
+
rationale: request.question.trim(),
|
|
16604
|
+
roles: selectedRelationship.roles,
|
|
16605
|
+
optionality: selectedRelationship.optionality,
|
|
16606
|
+
joinTypes: selectedRelationship.joinTypes,
|
|
16607
|
+
aggregation: selectedRelationship.aggregation,
|
|
16608
|
+
temporal: selectedRelationship.temporal,
|
|
16609
|
+
attributionBlock: selectedRelationship.attributionBlock,
|
|
16610
|
+
importRefs: selectedRelationship.importRefs,
|
|
16611
|
+
evidenceExpiresAt: undefined,
|
|
16612
|
+
},
|
|
16613
|
+
},
|
|
16614
|
+
}];
|
|
16615
|
+
}
|
|
16616
|
+
const selectedEntity = request.selectedObject?.kind === 'model'
|
|
16617
|
+
? Object.values(modeling.entities).find((item) => item.qualifiedId === selectedIdentity || item.id === selectedIdentity || item.localId === selectedIdentity)
|
|
16618
|
+
: undefined;
|
|
16619
|
+
if (selectedEntity) {
|
|
16620
|
+
return [{
|
|
16621
|
+
id: `entity:${selectedEntity.qualifiedId}`,
|
|
16622
|
+
kind: 'modeling_change',
|
|
16623
|
+
evidence: [selectedEntity.sourcePath, selectedEntity.dbtUniqueId],
|
|
16624
|
+
change: {
|
|
16625
|
+
operation: 'upsert_entity',
|
|
16626
|
+
value: {
|
|
16627
|
+
id: selectedEntity.localId,
|
|
16628
|
+
domain: selectedEntity.domain,
|
|
16629
|
+
areaId: selectedEntity.areaId?.split('::area::').at(-1),
|
|
16630
|
+
dbtModel: selectedEntity.dbtUniqueId,
|
|
16631
|
+
businessName: selectedEntity.businessName,
|
|
16632
|
+
businessContext: request.question.trim(),
|
|
16633
|
+
conceptRefs: selectedEntity.conceptRefs,
|
|
16634
|
+
analyticalRole: selectedEntity.analyticalRole,
|
|
16635
|
+
status: 'draft',
|
|
16636
|
+
owner: selectedEntity.owner,
|
|
16637
|
+
grain: selectedEntity.grain,
|
|
16638
|
+
keys: selectedEntity.keys,
|
|
16639
|
+
},
|
|
16640
|
+
},
|
|
16641
|
+
}];
|
|
16642
|
+
}
|
|
16643
|
+
const requestedArea = typeof context.modelAreaId === 'string' ? context.modelAreaId : typeof context.areaId === 'string' ? context.areaId : undefined;
|
|
16644
|
+
const existingArea = Object.values(modeling.areas).find((area) => area.domain === domain
|
|
16645
|
+
&& (!requestedArea || area.qualifiedId === requestedArea || area.localId === requestedArea));
|
|
16646
|
+
const areaId = existingArea?.localId ?? requestedArea?.split('::area::').at(-1) ?? `${authoringSlug(request.question).slice(0, 36) || 'metadata'}_models`;
|
|
16647
|
+
const operations = [];
|
|
16648
|
+
if (!existingArea) {
|
|
16649
|
+
operations.push({
|
|
16650
|
+
id: `area:${domain}:${areaId}`,
|
|
16651
|
+
kind: 'modeling_change',
|
|
16652
|
+
evidence: [`domain:${domain}`],
|
|
16653
|
+
change: { operation: 'upsert_area', value: { id: areaId, domain, name: titleCaseAuthoring(areaId), description: `Metadata-backed modeling area requested by the author: ${request.question.trim()}`, intentExamples: [request.question.trim()] } },
|
|
16654
|
+
});
|
|
16655
|
+
}
|
|
16656
|
+
const bound = new Set(Object.values(modeling.entities).map((entity) => entity.dbtUniqueId));
|
|
16657
|
+
const tokens = authoringTokens(request.question);
|
|
16658
|
+
let candidates = Object.values(manifest.dbtProvenance.nodes).filter((node) => !bound.has(node.uniqueId));
|
|
16659
|
+
const matched = candidates.filter((node) => tokens.some((token) => `${node.name} ${node.uniqueId} ${node.sourcePath ?? ''}`.toLowerCase().includes(token)));
|
|
16660
|
+
if (matched.length)
|
|
16661
|
+
candidates = matched;
|
|
16662
|
+
candidates = candidates.slice(0, 12);
|
|
16663
|
+
if (candidates.length === 0)
|
|
16664
|
+
throw new Error('No unbound dbt models match this request. Focus an existing model to update its business context.');
|
|
16665
|
+
const usedIds = new Set(Object.values(modeling.entities).filter((entity) => entity.domain === domain).map((entity) => entity.localId));
|
|
16666
|
+
for (const node of candidates) {
|
|
16667
|
+
const base = authoringSlug(node.name).replace(/-/g, '_') || 'model';
|
|
16668
|
+
let id = base;
|
|
16669
|
+
let suffix = 2;
|
|
16670
|
+
while (usedIds.has(id))
|
|
16671
|
+
id = `${base}_${suffix++}`;
|
|
16672
|
+
usedIds.add(id);
|
|
16673
|
+
operations.push({
|
|
16674
|
+
id: `entity:${domain}:${id}`,
|
|
16675
|
+
kind: 'modeling_change',
|
|
16676
|
+
dependsOn: existingArea ? [] : [`area:${domain}:${areaId}`],
|
|
16677
|
+
evidence: [node.uniqueId, ...(node.sourcePath ? [node.sourcePath] : [])],
|
|
16678
|
+
change: { operation: 'upsert_entity', value: { id, domain, areaId, dbtModel: node.uniqueId, businessName: titleCaseAuthoring(node.name), status: 'draft', analyticalRole: 'unknown' } },
|
|
16679
|
+
});
|
|
16680
|
+
}
|
|
16681
|
+
if (/\b(connect|relationship|join|related)\b/i.test(request.question) && candidates.length >= 2) {
|
|
16682
|
+
const manifestPath = resolveDbtManifestPath(projectRoot);
|
|
16683
|
+
const left = manifestPath ? loadDbtNodeAuthoringDetail(manifestPath, candidates[0].uniqueId) : undefined;
|
|
16684
|
+
const right = manifestPath ? loadDbtNodeAuthoringDetail(manifestPath, candidates[1].uniqueId) : undefined;
|
|
16685
|
+
const rightColumns = new Set((right?.columns ?? []).map((column) => column.name.toLowerCase()));
|
|
16686
|
+
const sharedKey = (left?.columns ?? []).map((column) => column.name).find((name) => /(^id$|_id$)/i.test(name) && rightColumns.has(name.toLowerCase()));
|
|
16687
|
+
if (sharedKey) {
|
|
16688
|
+
const fromOperation = operations.find((operation) => operation.id.startsWith('entity:') && operation.kind === 'modeling_change' && operation.change.operation === 'upsert_entity' && operation.change.value.dbtModel === candidates[0].uniqueId);
|
|
16689
|
+
const toOperation = operations.find((operation) => operation.id.startsWith('entity:') && operation.kind === 'modeling_change' && operation.change.operation === 'upsert_entity' && operation.change.value.dbtModel === candidates[1].uniqueId);
|
|
16690
|
+
if (fromOperation?.kind === 'modeling_change' && fromOperation.change.operation === 'upsert_entity' && toOperation?.kind === 'modeling_change' && toOperation.change.operation === 'upsert_entity') {
|
|
16691
|
+
const relationshipId = `${fromOperation.change.value.id}_to_${toOperation.change.value.id}`;
|
|
16692
|
+
operations.push({
|
|
16693
|
+
id: `relationship:${domain}:${relationshipId}`,
|
|
16694
|
+
kind: 'modeling_change',
|
|
16695
|
+
dependsOn: [fromOperation.id, toOperation.id],
|
|
16696
|
+
evidence: [`shared column suggestion:${sharedKey}`, left?.uniqueId ?? '', right?.uniqueId ?? ''].filter(Boolean),
|
|
16697
|
+
change: { operation: 'upsert_relationship', value: { id: relationshipId, domain, areaId, from: fromOperation.change.value.id, to: toOperation.change.value.id, keys: [{ from: sharedKey, to: sharedKey }], cardinality: 'unknown', fanout: 'unknown', status: 'draft', rationale: 'Candidate only: shared dbt column name requires warehouse validation.' } },
|
|
16698
|
+
});
|
|
16699
|
+
}
|
|
16700
|
+
}
|
|
16701
|
+
}
|
|
16702
|
+
return operations;
|
|
16703
|
+
}
|
|
16704
|
+
function buildMetadataBoundSkillOperations(projectRoot, manifest, request) {
|
|
16705
|
+
const context = request.workspaceContext ?? {};
|
|
16706
|
+
const selectedIdentity = request.selectedObject?.kind === 'skill' ? request.selectedObject.id : undefined;
|
|
16707
|
+
const existing = selectedIdentity
|
|
16708
|
+
? loadSkills(projectRoot).skills.find((skill) => skill.qualifiedId === selectedIdentity || skill.id === selectedIdentity)
|
|
16709
|
+
: undefined;
|
|
16710
|
+
const domain = typeof context.domain === 'string'
|
|
16711
|
+
? context.domain
|
|
16712
|
+
: existing?.domain
|
|
16713
|
+
?? (request.selectedObject?.kind === 'domain' ? request.selectedObject.id : undefined)
|
|
16714
|
+
?? Object.keys(manifest.modeling?.packages ?? {})[0];
|
|
16715
|
+
if (!domain)
|
|
16716
|
+
throw new Error('Select a Domain before building a Skill with AI.');
|
|
16717
|
+
const area = typeof context.modelAreaId === 'string' ? context.modelAreaId : existing?.modelAreaRefs?.[0];
|
|
16718
|
+
const focusReference = request.selectedObject?.kind === 'model' || request.selectedObject?.kind === 'relationship'
|
|
16719
|
+
? request.selectedObject.id
|
|
16720
|
+
: undefined;
|
|
16721
|
+
if (focusReference && !contextSourceReferenceExists(manifest, focusReference))
|
|
16722
|
+
throw new Error('The focused modeling object is not present in the current snapshot.');
|
|
16723
|
+
const id = existing?.localId ?? existing?.id ?? `${authoringSlug(request.question).slice(0, 48) || domain.replace(/[^a-z0-9]+/gi, '-')}-guidance`;
|
|
16724
|
+
const body = existing
|
|
16725
|
+
? `${existing.body.trim()}\n\n## Proposed correction\n${request.question.trim()}\n`
|
|
16726
|
+
: `## Purpose\nGuide analysis for ${domain} using reviewed repository metadata.\n\n## Guardrails\n${request.question.trim()}\n\n## Clarify\nAsk when the requested metric, model, or relationship is not governed.`;
|
|
16727
|
+
const value = {
|
|
16728
|
+
id,
|
|
16729
|
+
qualifiedId: existing?.qualifiedId,
|
|
16730
|
+
sourcePath: existing?.sourcePath,
|
|
16731
|
+
scope: 'project',
|
|
16732
|
+
domain,
|
|
16733
|
+
domains: [domain],
|
|
16734
|
+
modelAreaRefs: area ? [area] : [],
|
|
16735
|
+
kind: existing?.kind ?? 'domain_reference',
|
|
16736
|
+
status: 'draft',
|
|
16737
|
+
owner: existing?.owner,
|
|
16738
|
+
triggers: existing?.triggers ?? authoringTokens(request.question).slice(0, 8),
|
|
16739
|
+
exclusions: existing?.exclusions ?? [],
|
|
16740
|
+
description: existing?.description ?? `Metadata-backed guidance for ${titleCaseAuthoring(domain)}.`,
|
|
16741
|
+
preferredMetrics: existing?.preferredMetrics ?? Object.values(manifest.metrics ?? {}).filter((metric) => metric.domain === domain).slice(0, 12).map((metric) => metric.name),
|
|
16742
|
+
preferredBlocks: existing?.preferredBlocks ?? Object.values(manifest.blocks ?? {}).filter((block) => block.domain === domain && block.status === 'certified').slice(0, 12).map((block) => block.name),
|
|
16743
|
+
preferredDimensions: existing?.preferredDimensions ?? [],
|
|
16744
|
+
requiredFilters: existing?.requiredFilters ?? [],
|
|
16745
|
+
clarifyWhen: existing?.clarifyWhen ?? ['The request needs a model, metric, or relationship not present in the current governed context.'],
|
|
16746
|
+
examples: existing?.examples ?? [request.question.trim()],
|
|
16747
|
+
sourceRefs: [...new Set([...(existing?.sourceRefs ?? []), ...(focusReference ? [focusReference] : [])])],
|
|
16748
|
+
vocabulary: existing?.vocabulary ?? {},
|
|
16749
|
+
analyticalPolicy: existing?.analyticalPolicy,
|
|
16750
|
+
body,
|
|
16751
|
+
isStarter: existing?.isStarter,
|
|
16752
|
+
};
|
|
16753
|
+
const before = existing?.sourcePath && existsSync(existing.sourcePath) ? readFileSync(existing.sourcePath, 'utf8') : '';
|
|
16754
|
+
return [{
|
|
16755
|
+
id: `skill:${existing?.qualifiedId ?? `${domain}::skill::${id}`}`,
|
|
16756
|
+
kind: 'skill_change',
|
|
16757
|
+
operation: existing ? 'update' : 'create',
|
|
16758
|
+
targetQualifiedId: existing?.qualifiedId,
|
|
16759
|
+
expectedSourceHash: existing ? createHash('sha256').update(before).digest('hex') : undefined,
|
|
16760
|
+
evidence: [existing?.sourcePath ?? `domain:${domain}`, ...(focusReference ? [focusReference] : [])],
|
|
16761
|
+
value,
|
|
16762
|
+
}];
|
|
16763
|
+
}
|
|
16764
|
+
function authoringTokens(value) {
|
|
16765
|
+
const stop = new Set(['the', 'and', 'for', 'with', 'from', 'this', 'that', 'build', 'create', 'model', 'models', 'skill', 'please', 'into', 'about']);
|
|
16766
|
+
return [...new Set(value.toLowerCase().split(/[^a-z0-9_]+/).filter((token) => token.length > 2 && !stop.has(token)))];
|
|
16767
|
+
}
|
|
16768
|
+
function authoringSlug(value) {
|
|
16769
|
+
return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
16770
|
+
}
|
|
16771
|
+
function titleCaseAuthoring(value) {
|
|
16772
|
+
return value.replace(/[._-]+/g, ' ').replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
16773
|
+
}
|
|
14580
16774
|
function contextBootstrapDir(projectRoot) {
|
|
14581
16775
|
return join(projectRoot, '.dql', 'local', 'context-bootstrap');
|
|
14582
16776
|
}
|
|
@@ -17157,6 +19351,11 @@ export function notebookCellRepairSql(cell) {
|
|
|
17157
19351
|
const match = /\bquery\s*=\s*"""([\s\S]*?)"""/i.exec(cell.source);
|
|
17158
19352
|
return match?.[1]?.trim() || undefined;
|
|
17159
19353
|
}
|
|
19354
|
+
export function notebookDqlSourceAllowsBackgroundRepair(source) {
|
|
19355
|
+
if (/^\s*@block\(/i.test(source) || /\btype\s*=\s*"semantic"/i.test(source))
|
|
19356
|
+
return false;
|
|
19357
|
+
return /\bblock\b/i.test(source) && /\bquery\b/i.test(source);
|
|
19358
|
+
}
|
|
17160
19359
|
export function replaceNotebookDqlQueryForRepair(source, sql) {
|
|
17161
19360
|
const queryField = /\b(query\s*=\s*""")([\s\S]*?)(""")/i;
|
|
17162
19361
|
if (!queryField.test(source) || !sql.trim())
|
|
@@ -17618,6 +19817,7 @@ export function openBlockStudioDocument(projectRoot, relativePath, semanticLayer
|
|
|
17618
19817
|
owner: parsedMetadata.owner || companion?.owner || '',
|
|
17619
19818
|
tags: parsedMetadata.tags.length > 0 ? parsedMetadata.tags : companion?.tags ?? [],
|
|
17620
19819
|
reviewStatus: parsedMetadata.status || companion?.reviewStatus || 'draft',
|
|
19820
|
+
sourceFingerprint: createHash('sha256').update(source).digest('hex'),
|
|
17621
19821
|
};
|
|
17622
19822
|
return {
|
|
17623
19823
|
path: normalizedPath,
|
|
@@ -17627,6 +19827,22 @@ export function openBlockStudioDocument(projectRoot, relativePath, semanticLayer
|
|
|
17627
19827
|
validation: validateBlockStudioSource(source, semanticLayer),
|
|
17628
19828
|
};
|
|
17629
19829
|
}
|
|
19830
|
+
const BLOCK_STUDIO_DRAFT_RUNTIME_DIAGNOSTICS = new Set([
|
|
19831
|
+
'metricflow_unavailable',
|
|
19832
|
+
'semantic_compose_failed',
|
|
19833
|
+
'semantic_layer_missing',
|
|
19834
|
+
'semantic_runtime_required',
|
|
19835
|
+
]);
|
|
19836
|
+
/**
|
|
19837
|
+
* Draft persistence is stricter than loose text storage but does not require a
|
|
19838
|
+
* configured execution runtime. Syntax, unsafe SQL, invalid semantic identity,
|
|
19839
|
+
* and parameter-contract errors must be corrected before a block file is
|
|
19840
|
+
* written; runtime availability remains a Run/Certify concern.
|
|
19841
|
+
*/
|
|
19842
|
+
export function blockStudioValidationAllowsDraftSave(validation) {
|
|
19843
|
+
return !validation.diagnostics.some((diagnostic) => diagnostic.severity === 'error'
|
|
19844
|
+
&& !BLOCK_STUDIO_DRAFT_RUNTIME_DIAGNOSTICS.has(diagnostic.code ?? ''));
|
|
19845
|
+
}
|
|
17630
19846
|
/**
|
|
17631
19847
|
* Replace the CONTENTS of every string literal with same-length filler, keeping
|
|
17632
19848
|
* the quotes and every offset intact.
|
|
@@ -18234,17 +20450,33 @@ export function validateBlockStudioSource(source, semanticLayer) {
|
|
|
18234
20450
|
});
|
|
18235
20451
|
}
|
|
18236
20452
|
}
|
|
18237
|
-
|
|
18238
|
-
|
|
18239
|
-
|
|
20453
|
+
let parameters = [];
|
|
20454
|
+
try {
|
|
20455
|
+
const parameterInvocation = prepareBlockInvocation({ source, surface: 'block_studio' });
|
|
20456
|
+
parameters = parameterInvocation.parameters;
|
|
20457
|
+
if (parameterInvocation.errors.length > 0) {
|
|
20458
|
+
diagnostics.push(...parameterInvocation.errors.map((message) => ({ severity: 'error', code: 'parameter_parse', message })));
|
|
20459
|
+
}
|
|
20460
|
+
}
|
|
20461
|
+
catch (error) {
|
|
20462
|
+
// The main parser diagnostic above owns malformed source. Parameter
|
|
20463
|
+
// extraction must never turn a recoverable validation response into a 500.
|
|
20464
|
+
if (!diagnostics.some((diagnostic) => diagnostic.code === 'syntax')) {
|
|
20465
|
+
diagnostics.push({
|
|
20466
|
+
severity: 'error',
|
|
20467
|
+
code: 'parameter_parse',
|
|
20468
|
+
message: error instanceof Error ? error.message : String(error),
|
|
20469
|
+
});
|
|
20470
|
+
}
|
|
18240
20471
|
}
|
|
18241
20472
|
return {
|
|
18242
20473
|
valid: diagnostics.every((diagnostic) => diagnostic.severity !== 'error'),
|
|
20474
|
+
saveable: blockStudioValidationAllowsDraftSave({ diagnostics }),
|
|
18243
20475
|
diagnostics,
|
|
18244
20476
|
semanticRefs,
|
|
18245
20477
|
chartConfig: chartConfig ?? undefined,
|
|
18246
20478
|
executableSql,
|
|
18247
|
-
parameters
|
|
20479
|
+
parameters,
|
|
18248
20480
|
};
|
|
18249
20481
|
}
|
|
18250
20482
|
export async function createDqlArtifactGenerationSessionForProject(projectRoot, options, semanticLayer) {
|
|
@@ -19182,6 +21414,45 @@ function formatBlockStudioExpected(expected) {
|
|
|
19182
21414
|
return String(normalized);
|
|
19183
21415
|
return JSON.stringify(normalized);
|
|
19184
21416
|
}
|
|
21417
|
+
function evaluateBlockStudioTests(tests, rows, columns, startedAt) {
|
|
21418
|
+
if (tests.length === 0) {
|
|
21419
|
+
return { passed: 0, failed: 0, skipped: 0, duration: Date.now() - startedAt, assertions: [], runAt: new Date() };
|
|
21420
|
+
}
|
|
21421
|
+
const assertions = [];
|
|
21422
|
+
let passed = 0;
|
|
21423
|
+
let failed = 0;
|
|
21424
|
+
for (const test of tests) {
|
|
21425
|
+
const name = `assert ${test.field} ${test.operator} ${formatBlockStudioExpected(test.expected)}`;
|
|
21426
|
+
let actual;
|
|
21427
|
+
if (test.field === 'row_count') {
|
|
21428
|
+
actual = rows.length;
|
|
21429
|
+
}
|
|
21430
|
+
else if (!columns.includes(test.field)) {
|
|
21431
|
+
assertions.push({ name, passed: false, expected: test.expected, error: `Column '${test.field}' not found in results` });
|
|
21432
|
+
failed += 1;
|
|
21433
|
+
continue;
|
|
21434
|
+
}
|
|
21435
|
+
else {
|
|
21436
|
+
actual = rows[0]?.[test.field];
|
|
21437
|
+
}
|
|
21438
|
+
const ok = compareBlockStudioValues(actual, test.operator, test.expected);
|
|
21439
|
+
if (ok) {
|
|
21440
|
+
assertions.push({ name, passed: true, actual, expected: test.expected });
|
|
21441
|
+
passed += 1;
|
|
21442
|
+
}
|
|
21443
|
+
else {
|
|
21444
|
+
assertions.push({
|
|
21445
|
+
name,
|
|
21446
|
+
passed: false,
|
|
21447
|
+
actual,
|
|
21448
|
+
expected: test.expected,
|
|
21449
|
+
error: `${String(actual)} ${test.operator} ${formatBlockStudioExpected(test.expected)} is false`,
|
|
21450
|
+
});
|
|
21451
|
+
failed += 1;
|
|
21452
|
+
}
|
|
21453
|
+
}
|
|
21454
|
+
return { passed, failed, skipped: 0, duration: Date.now() - startedAt, assertions, runAt: new Date() };
|
|
21455
|
+
}
|
|
19185
21456
|
function buildBlockStudioCertificationChecklist(input) {
|
|
19186
21457
|
const parsed = parseBlockSourceMetadata(input.source);
|
|
19187
21458
|
const sql = extractBlockStudioSql(input.source) ?? '';
|
|
@@ -24272,6 +26543,46 @@ function mergeAgentSchemaContexts(...contexts) {
|
|
|
24272
26543
|
}
|
|
24273
26544
|
return Array.from(byRelation.values()).slice(0, 16);
|
|
24274
26545
|
}
|
|
26546
|
+
function agentRelationMatchKey(relation) {
|
|
26547
|
+
return relation
|
|
26548
|
+
.replace(/["`\[\]]/g, '')
|
|
26549
|
+
.split('.')
|
|
26550
|
+
.filter(Boolean)
|
|
26551
|
+
.slice(-2)
|
|
26552
|
+
.join('.')
|
|
26553
|
+
.toLowerCase();
|
|
26554
|
+
}
|
|
26555
|
+
/**
|
|
26556
|
+
* Keep dbt descriptions and semantic meaning, but make a complete live point
|
|
26557
|
+
* lookup authoritative for executable column names on the selected connection.
|
|
26558
|
+
* No fuzzy aliases are invented: a stale `customer_name` declaration disappears
|
|
26559
|
+
* when the live relation exposes only `name`.
|
|
26560
|
+
*/
|
|
26561
|
+
export function reconcileAgentSchemaContextWithLive(catalog, live) {
|
|
26562
|
+
const liveByRelation = new Map(live
|
|
26563
|
+
.filter((table) => table.columns.length > 0)
|
|
26564
|
+
.map((table) => [agentRelationMatchKey(table.relation), table]));
|
|
26565
|
+
return catalog.map((table) => {
|
|
26566
|
+
const verified = liveByRelation.get(agentRelationMatchKey(table.relation));
|
|
26567
|
+
if (!verified)
|
|
26568
|
+
return table;
|
|
26569
|
+
const metadataColumns = new Map(table.columns.map((column) => [column.name.toLowerCase(), column]));
|
|
26570
|
+
return {
|
|
26571
|
+
...table,
|
|
26572
|
+
source: 'dbt metadata verified against live runtime schema',
|
|
26573
|
+
columnCompleteness: 'complete',
|
|
26574
|
+
columns: verified.columns.map((column) => {
|
|
26575
|
+
const metadata = metadataColumns.get(column.name.toLowerCase());
|
|
26576
|
+
return {
|
|
26577
|
+
...column,
|
|
26578
|
+
type: column.type ?? metadata?.type,
|
|
26579
|
+
description: metadata?.description,
|
|
26580
|
+
sampleValues: metadata?.sampleValues,
|
|
26581
|
+
};
|
|
26582
|
+
}),
|
|
26583
|
+
};
|
|
26584
|
+
});
|
|
26585
|
+
}
|
|
24275
26586
|
function mergeAgentSchemaSampleValues(snapshot, enriched) {
|
|
24276
26587
|
const samples = new Map();
|
|
24277
26588
|
for (const table of enriched) {
|