@duckcodeailabs/dql-cli 1.13.0 → 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/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-Cp0eh6zU.js → AgentAnswerCard-c8QUo6NH.js} +4 -4
- package/dist/assets/dql-notebook/assets/{AgentLogPage-CBGEoGmU.js → AgentLogPage-D6fgEAEy.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildDialog-CwAD3tXE.js → AiBuildDialog-8miXFPse.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildResult-CyqTuQe5.js → AiBuildResult-DPJ_SI0R.js} +3 -3
- package/dist/assets/dql-notebook/assets/{AiSidePanel-DjB8PWHG.js → AiSidePanel-NVA6lZnn.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AnalyticsHome-B7BkvA-R.js → AnalyticsHome-DgwnRW71.js} +2 -2
- 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-C4SZVNW-.js → BusinessArtifactView-DtRMiaF4.js} +1 -1
- package/dist/assets/dql-notebook/assets/DbtFirstModelingPage-DhBsLCYr.js +22 -0
- package/dist/assets/dql-notebook/assets/{GitPage-BfVD4tVq.js → GitPage-CazC-eTR.js} +1 -1
- 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-BDPNVot1.js → HelpDocsPage-DMHPsVqP.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HomePage-DiOnRhEG.js → HomePage-DcSI4dnU.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDAG-BiUFX2KL.js → LineageDAG-BJ6xz06u.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDetailView-CfbSvlPJ.js → LineageDetailView-8WrQm-MF.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDrawer-CgTScaGB.js → LineageDrawer-hnqCW_t-.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-D7ZA7k68.js → LineagePathBreadcrumb-B4LZTUsl.js} +1 -1
- package/dist/assets/dql-notebook/assets/{MiniLineageGraph-GkD4y17x.js → MiniLineageGraph-CotnRuxa.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewBlockModal-B9TfRjt7.js → NewBlockModal-DHn-CEEW.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewNotebookModal-BbO9NKUl.js → NewNotebookModal-BPGp-8oq.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NotebookEditor-CuMpvXR9.js → NotebookEditor-B0OTHM6W.js} +26 -31
- package/dist/assets/dql-notebook/assets/{ReadinessPage-BROJ9YBM.js → ReadinessPage-CoOIpxWq.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SetupOnboarding-BCcZlpuQ.js → SetupOnboarding-DxK_mFzf.js} +1 -1
- package/dist/assets/dql-notebook/assets/{TrustBadge-vQQh8d0D.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-DuqCcDDl.js → answer-to-notebook-WAVAYjBv.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-left-CMe1kkOL.js → arrow-left-L8odLbyN.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-right-DvB_DONr.js → arrow-right-WIF1V9a9.js} +1 -1
- package/dist/assets/dql-notebook/assets/{book-open-text-CJ3NAvrs.js → book-open-text-BpqWMa4m.js} +1 -1
- package/dist/assets/dql-notebook/assets/{bot-CV8DYRrt.js → bot-CLR1V6dO.js} +1 -1
- package/dist/assets/dql-notebook/assets/{circle-x-BCgtha1W.js → circle-x-CQBOM0b0.js} +1 -1
- package/dist/assets/dql-notebook/assets/{dagre.esm-GKp7qoUL.js → dagre.esm-DlZi7NR3.js} +1 -1
- package/dist/assets/dql-notebook/assets/{external-link-C-PXokWV.js → external-link-DYqdQIGp.js} +1 -1
- package/dist/assets/dql-notebook/assets/{grip-vertical-B4EHbGXW.js → grip-vertical-HmUijbwN.js} +1 -1
- package/dist/assets/dql-notebook/assets/{index-1mIC_XX4.js → index-Cpx47jCH.js} +43 -43
- package/dist/assets/dql-notebook/assets/{list-tree-BRFpEmvV.js → list-tree-DGKqCxmz.js} +1 -1
- package/dist/assets/dql-notebook/assets/{minimize-2-CDwSCGlO.js → minimize-2-C8dvqCxX.js} +1 -1
- package/dist/assets/dql-notebook/assets/{panel-right-open-CunA1xO7.js → panel-right-open-yuVkIo5n.js} +1 -1
- package/dist/assets/dql-notebook/assets/{rotate-ccw-DOrfTZk8.js → rotate-ccw-Di0kRnrM.js} +1 -1
- package/dist/assets/dql-notebook/assets/{semantic-fields-AfQvjt5R.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-DDhv4e8M.js → star-CgCQbnXc.js} +1 -1
- package/dist/assets/dql-notebook/assets/{triangle-alert-2ZXx8wAu.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-BA074kiQ.js → user-round-J4PQHxo9.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wand-sparkles-CEYHLt9b.js → wand-sparkles-c4WDXTCk.js} +1 -1
- package/dist/assets/dql-notebook/assets/{workflow-CZtyrcR-.js → workflow-NtRLaq7n.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wrench-Ong23-wu.js → wrench-CS1f1Zv4.js} +1 -1
- package/dist/assets/dql-notebook/assets/{x-CSo1M3tu.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/local-runtime.d.ts +10 -0
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +1061 -17
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- package/package.json +10 -10
- package/dist/assets/dql-notebook/assets/AppsView-CUrqtEGC.js +0 -4583
- package/dist/assets/dql-notebook/assets/BlockStudio-Bl0aGQJR.js +0 -60
- package/dist/assets/dql-notebook/assets/DbtFirstModelingPage-B0uxitnx.js +0 -22
- package/dist/assets/dql-notebook/assets/GlobalAiRail-DUwdy3fB.js +0 -1
- package/dist/assets/dql-notebook/assets/GovernedContextPage-D9f26ZFb.js +0 -7
- package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-Dj4AXAhT.js +0 -83
- package/dist/assets/dql-notebook/assets/chevron-up-HxU1l85K.js +0 -6
- package/dist/assets/dql-notebook/assets/link-2-BLx-goet.js +0 -6
- package/dist/assets/dql-notebook/assets/maximize-2-CCd5uRIZ.js +0 -6
- package/dist/assets/dql-notebook/assets/pencil-D_I14Wyk.js +0 -6
- package/dist/assets/dql-notebook/assets/usePersistedAgentThreadId-3rzkZrqc.js +0 -18
package/dist/local-runtime.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFileSync, execSync } from "node:child_process";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
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
6
|
import { mkdir as mkdirAsync, rename as renameAsync, rm as rmAsync, writeFile as writeFileAsync, } from 'node:fs/promises';
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { homedir } from "node:os";
|
|
@@ -15,7 +15,7 @@ import { buildMixedSourceWarehouseFallbackSql, findMentionedNotebookDataset, pla
|
|
|
15
15
|
import { resolveNpmInvocation } from './npm-runtime.js';
|
|
16
16
|
import { ensureDqlGitignore, isGovernedSourceFile, isLegacyBroadDqlIgnore, } from './git-contract.js';
|
|
17
17
|
import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, hasStandaloneSemanticRef, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
|
|
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, 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';
|
|
19
19
|
import { load as loadYaml } from 'js-yaml';
|
|
20
20
|
import { listBlockTemplates } from './block-templates.js';
|
|
21
21
|
import { getRunner as getLLMRunner } from './llm/index.js';
|
|
@@ -24,7 +24,7 @@ import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from '
|
|
|
24
24
|
import { resolveRetrievalHealthStatus } from './retrieval-health.js';
|
|
25
25
|
import { createDqlAgentProviderRunner, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
|
|
26
26
|
import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
|
|
27
|
-
import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, prepareConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, normalizeWarehouseSqlFailure, parseProposal, propose, proposePlan, recordGovernedCorrection, HintStore, defaultHintIndexPath, ensureHintIndexFresh, listHintsFromGit, getHintEvaluationFromGit, getCorrectionTraceFromGit, inspectGovernedHint, editGovernedHintCandidate, reopenGovernedHint, retireHint, supersedeHint, hintsConflict, mineJoinPatterns, reviewGovernedHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, internalRelationIdsInSql, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, projectEmbeddingProvider, isHashedEmbeddingProvider, clearProjectEmbeddingCache, upgradeVectorIndexForProject, openMetadataCatalog, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, isTrustedConversationTurn, resolveInternalRelationIds, analyticalError, tagAnalyticalError, withAnalyticalErrorOrigin, withAnalyticalErrorOriginSync, } from '@duckcodeailabs/dql-agent';
|
|
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';
|
|
28
28
|
import { addSqlResultFilter, filterableResultColumns, replaceBlockStudioSql } from './sql-result-filter.js';
|
|
29
29
|
import { gatherProposeEnrichment } from './propose-enrich.js';
|
|
30
30
|
import { handleAppsApi, proposeAppAiBuild, recommendVisualization, } from './apps-api.js';
|
|
@@ -2412,7 +2412,12 @@ export async function startLocalServer(opts) {
|
|
|
2412
2412
|
: isTerminalFailure
|
|
2413
2413
|
? terminalFailureActions
|
|
2414
2414
|
: isGroundingGap
|
|
2415
|
-
?
|
|
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' }]
|
|
2416
2421
|
: [
|
|
2417
2422
|
{ id: 'create-block', label: governedAnswer.dqlArtifact ? 'Review DQL draft' : 'Create DQL draft', route: 'dql_block_draft', artifactKind: 'dql_block_draft' },
|
|
2418
2423
|
{ id: 'research-gap', label: 'Research deeper', route: 'research' },
|
|
@@ -2585,11 +2590,175 @@ export async function startLocalServer(opts) {
|
|
|
2585
2590
|
nextActions,
|
|
2586
2591
|
};
|
|
2587
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
|
+
};
|
|
2588
2755
|
const agentRunExecutors = {
|
|
2589
2756
|
conversation: conversationRunExecutor,
|
|
2590
2757
|
certified_answer: answerRunExecutor,
|
|
2591
2758
|
semantic_answer: answerRunExecutor,
|
|
2592
2759
|
generated_answer: answerRunExecutor,
|
|
2760
|
+
modeling_draft: modelingAuthoringRunExecutor,
|
|
2761
|
+
skill_draft: skillAuthoringRunExecutor,
|
|
2593
2762
|
research: async (researchContext) => {
|
|
2594
2763
|
const { runId, request, routeDecision, emit } = researchContext;
|
|
2595
2764
|
const metrics = loadSemanticMetrics(projectRoot);
|
|
@@ -4122,7 +4291,7 @@ export async function startLocalServer(opts) {
|
|
|
4122
4291
|
const executeBoundedDqlRepair = async (input) => {
|
|
4123
4292
|
const provider = await createBlockStudioAssistProvider(projectRoot);
|
|
4124
4293
|
if (!provider) {
|
|
4125
|
-
throw boundedRepairError('Configure an available AI provider in Settings to repair this DQL
|
|
4294
|
+
throw boundedRepairError('Configure an available AI provider in Settings to repair this DQL draft.', 409);
|
|
4126
4295
|
}
|
|
4127
4296
|
let schemaContext;
|
|
4128
4297
|
try {
|
|
@@ -4185,7 +4354,7 @@ export async function startLocalServer(opts) {
|
|
|
4185
4354
|
throw boundedRepairError('The AI repair did not return a complete DQL block.', 422);
|
|
4186
4355
|
}
|
|
4187
4356
|
if (repairedSource.length > 50_000) {
|
|
4188
|
-
throw boundedRepairError('The repaired DQL exceeded the safe editable-
|
|
4357
|
+
throw boundedRepairError('The repaired DQL exceeded the safe editable-draft size limit.', 422);
|
|
4189
4358
|
}
|
|
4190
4359
|
if (/^\s*@block\(/i.test(repairedSource) || /\btype\s*=\s*"semantic"/i.test(repairedSource)) {
|
|
4191
4360
|
throw boundedRepairError('The repair changed this editable custom block into a governed reference or semantic block.', 422);
|
|
@@ -5133,6 +5302,33 @@ export async function startLocalServer(opts) {
|
|
|
5133
5302
|
}
|
|
5134
5303
|
catch { /* dir not watchable */ }
|
|
5135
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
|
+
}
|
|
5136
5332
|
const configuredDbtManifest = resolveDbtManifestPath(projectRoot, projectConfig);
|
|
5137
5333
|
if (configuredDbtManifest && existsSync(dirname(configuredDbtManifest))) {
|
|
5138
5334
|
try {
|
|
@@ -6385,6 +6581,224 @@ export async function startLocalServer(opts) {
|
|
|
6385
6581
|
}));
|
|
6386
6582
|
return;
|
|
6387
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
|
+
}
|
|
6388
6802
|
if (req.method === 'POST' && path === '/api/modeling/dbt-first/dbt-source/preview') {
|
|
6389
6803
|
const requestId = apiRequestId('modeling-dbt-source-preview');
|
|
6390
6804
|
const snapshot = projectSnapshot();
|
|
@@ -7419,14 +7833,22 @@ export async function startLocalServer(opts) {
|
|
|
7419
7833
|
const metrics = semanticLayer
|
|
7420
7834
|
? semanticLayer.listMetrics(undefined, { includeMeasures: false }).map((m) => m.name).filter(matches).sort().slice(0, limit)
|
|
7421
7835
|
: [];
|
|
7422
|
-
const manifest = buildManifest({
|
|
7836
|
+
const manifest = buildManifest({
|
|
7837
|
+
projectRoot,
|
|
7838
|
+
dqlVersion: 'notebook',
|
|
7839
|
+
dbtManifestPath: resolveDbtManifestPath(projectRoot) ?? undefined,
|
|
7840
|
+
});
|
|
7423
7841
|
const blocks = Object.values(manifest.blocks)
|
|
7424
7842
|
.filter((b) => b.status === 'certified')
|
|
7425
7843
|
.map((b) => b.name)
|
|
7426
7844
|
.filter(matches)
|
|
7427
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();
|
|
7428
7850
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7429
|
-
res.end(serializeJSON({ metrics, blocks: blocks.slice(0, limit) }));
|
|
7851
|
+
res.end(serializeJSON({ metrics, blocks: blocks.slice(0, limit), modelingRefs: modelingRefs.slice(0, limit) }));
|
|
7430
7852
|
}
|
|
7431
7853
|
catch (error) {
|
|
7432
7854
|
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -7444,6 +7866,9 @@ export async function startLocalServer(opts) {
|
|
|
7444
7866
|
return;
|
|
7445
7867
|
}
|
|
7446
7868
|
const skill = writeSkill(projectRoot, input);
|
|
7869
|
+
projectSnapshots.invalidate();
|
|
7870
|
+
invalidateAgentProjectState(projectRoot);
|
|
7871
|
+
await refreshUnifiedProjectIndexes(projectRoot);
|
|
7447
7872
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7448
7873
|
res.end(serializeJSON({ skill: serializeSkill(skill) }));
|
|
7449
7874
|
}
|
|
@@ -7463,7 +7888,10 @@ export async function startLocalServer(opts) {
|
|
|
7463
7888
|
res.end(serializeJSON({ error: 'Provide { skill } with scope and body.' }));
|
|
7464
7889
|
return;
|
|
7465
7890
|
}
|
|
7466
|
-
const skill = writeSkill(projectRoot, input);
|
|
7891
|
+
const skill = writeSkill(projectRoot, { ...input, qualifiedId: id });
|
|
7892
|
+
projectSnapshots.invalidate();
|
|
7893
|
+
invalidateAgentProjectState(projectRoot);
|
|
7894
|
+
await refreshUnifiedProjectIndexes(projectRoot);
|
|
7467
7895
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7468
7896
|
res.end(serializeJSON({ skill: serializeSkill(skill) }));
|
|
7469
7897
|
}
|
|
@@ -7477,6 +7905,9 @@ export async function startLocalServer(opts) {
|
|
|
7477
7905
|
try {
|
|
7478
7906
|
const id = decodeURIComponent(path.slice('/api/skills/'.length));
|
|
7479
7907
|
deleteSkill(projectRoot, id);
|
|
7908
|
+
projectSnapshots.invalidate();
|
|
7909
|
+
invalidateAgentProjectState(projectRoot);
|
|
7910
|
+
await refreshUnifiedProjectIndexes(projectRoot);
|
|
7480
7911
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7481
7912
|
res.end(serializeJSON({ ok: true }));
|
|
7482
7913
|
}
|
|
@@ -11906,6 +12337,20 @@ export async function startLocalServer(opts) {
|
|
|
11906
12337
|
res.end(serializeJSON({ error: 'Block name is required.' }));
|
|
11907
12338
|
return;
|
|
11908
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
|
+
}
|
|
11909
12354
|
const currentPath = typeof body.path === 'string' ? body.path : undefined;
|
|
11910
12355
|
const saveOptions = {
|
|
11911
12356
|
currentPath,
|
|
@@ -13561,11 +14006,12 @@ export async function startLocalServer(opts) {
|
|
|
13561
14006
|
return;
|
|
13562
14007
|
}
|
|
13563
14008
|
/**
|
|
13564
|
-
* Repair an editable notebook cell
|
|
13565
|
-
* The returned source is applied only to the in-memory
|
|
13566
|
-
* 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.
|
|
13567
14012
|
*/
|
|
13568
|
-
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')) {
|
|
13569
14015
|
let body = {};
|
|
13570
14016
|
let execContext = null;
|
|
13571
14017
|
const start = Date.now();
|
|
@@ -15542,6 +15988,7 @@ function serializeSkill(skill) {
|
|
|
15542
15988
|
clarifyWhen: skill.clarifyWhen,
|
|
15543
15989
|
examples: skill.examples,
|
|
15544
15990
|
sourceRefs: skill.sourceRefs,
|
|
15991
|
+
analyticalPolicy: skill.analyticalPolicy,
|
|
15545
15992
|
vocabulary: skill.vocabulary,
|
|
15546
15993
|
sourcePath: skill.sourcePath,
|
|
15547
15994
|
isStarter: skill.isStarter,
|
|
@@ -15597,9 +16044,27 @@ function parseSkillInput(raw, fallbackId) {
|
|
|
15597
16044
|
examples: asStrings(skill.examples),
|
|
15598
16045
|
sourceRefs: asStrings(skill.sourceRefs),
|
|
15599
16046
|
vocabulary: asMap(skill.vocabulary),
|
|
16047
|
+
analyticalPolicy: parseSkillAnalyticalPolicyInput(skill.analyticalPolicy),
|
|
15600
16048
|
isStarter: skill.isStarter === true ? true : undefined,
|
|
15601
16049
|
};
|
|
15602
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
|
+
}
|
|
15603
16068
|
/** Loose, case/slug-insensitive domain key for counting membership. */
|
|
15604
16069
|
function domainKey(value) {
|
|
15605
16070
|
if (!value)
|
|
@@ -15759,6 +16224,553 @@ export function parseDomainInput(raw, fallbackId) {
|
|
|
15759
16224
|
sourcePath: typeof domain.sourcePath === 'string' ? domain.sourcePath : undefined,
|
|
15760
16225
|
};
|
|
15761
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
|
+
}
|
|
15762
16774
|
function contextBootstrapDir(projectRoot) {
|
|
15763
16775
|
return join(projectRoot, '.dql', 'local', 'context-bootstrap');
|
|
15764
16776
|
}
|
|
@@ -18815,6 +19827,22 @@ export function openBlockStudioDocument(projectRoot, relativePath, semanticLayer
|
|
|
18815
19827
|
validation: validateBlockStudioSource(source, semanticLayer),
|
|
18816
19828
|
};
|
|
18817
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
|
+
}
|
|
18818
19846
|
/**
|
|
18819
19847
|
* Replace the CONTENTS of every string literal with same-length filler, keeping
|
|
18820
19848
|
* the quotes and every offset intact.
|
|
@@ -19422,17 +20450,33 @@ export function validateBlockStudioSource(source, semanticLayer) {
|
|
|
19422
20450
|
});
|
|
19423
20451
|
}
|
|
19424
20452
|
}
|
|
19425
|
-
|
|
19426
|
-
|
|
19427
|
-
|
|
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
|
+
}
|
|
19428
20471
|
}
|
|
19429
20472
|
return {
|
|
19430
20473
|
valid: diagnostics.every((diagnostic) => diagnostic.severity !== 'error'),
|
|
20474
|
+
saveable: blockStudioValidationAllowsDraftSave({ diagnostics }),
|
|
19431
20475
|
diagnostics,
|
|
19432
20476
|
semanticRefs,
|
|
19433
20477
|
chartConfig: chartConfig ?? undefined,
|
|
19434
20478
|
executableSql,
|
|
19435
|
-
parameters
|
|
20479
|
+
parameters,
|
|
19436
20480
|
};
|
|
19437
20481
|
}
|
|
19438
20482
|
export async function createDqlArtifactGenerationSessionForProject(projectRoot, options, semanticLayer) {
|