@duckcodeailabs/dql-cli 1.7.0 → 1.7.2
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/README.md +14 -2
- package/dist/apps-api.d.ts +11 -3
- package/dist/apps-api.d.ts.map +1 -1
- package/dist/apps-api.js +91 -75
- package/dist/apps-api.js.map +1 -1
- package/dist/assets/dql-notebook/assets/index-BgD9b9Om.js +5869 -0
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +43 -10
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +7 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/local-runtime.d.ts +19 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +501 -55
- package/dist/local-runtime.js.map +1 -1
- package/dist/npm-runtime.d.ts +24 -0
- package/dist/npm-runtime.d.ts.map +1 -0
- package/dist/npm-runtime.js +72 -0
- package/dist/npm-runtime.js.map +1 -0
- package/dist/package.json +10 -10
- package/dist/settings/provider-settings.d.ts +2 -0
- package/dist/settings/provider-settings.d.ts.map +1 -1
- package/dist/settings/provider-settings.js +16 -7
- package/dist/settings/provider-settings.js.map +1 -1
- package/package.json +10 -10
- package/dist/assets/dql-notebook/assets/index-2Tszpipm.js +0 -5914
package/dist/local-runtime.js
CHANGED
|
@@ -4,19 +4,19 @@ import { createServer } from "node:http";
|
|
|
4
4
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, watch, writeFileSync, } from "node:fs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { dirname, extname, join, normalize, relative, resolve, } from "node:path";
|
|
8
9
|
import { Readable } from "node:stream";
|
|
9
10
|
import { buildMixedSourceWarehouseFallbackSql, findMentionedNotebookDataset, planMixedSourceNotebookSql, planMixedSourceSql, } from "./mixed-source-sql.js";
|
|
10
|
-
import
|
|
11
|
-
import OpenAI from "openai";
|
|
11
|
+
import { resolveNpmInvocation } from './npm-runtime.js';
|
|
12
12
|
import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, hasStandaloneSemanticRef, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
|
|
13
|
-
import { loadSemanticLayerFromDir, normalizeDqlArtifactReference, serializeMetricDefinitionToYaml, resolveSemanticLayerAsync, 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, relationshipValidationProofFingerprint, discoverDbtDomains, renderDomainDeclaration, ProjectSnapshotService, analyzeSqlReferences, } from '@duckcodeailabs/dql-core';
|
|
13
|
+
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, relationshipValidationProofFingerprint, discoverDbtDomains, renderDomainDeclaration, ProjectSnapshotService, analyzeSqlReferences, } from '@duckcodeailabs/dql-core';
|
|
14
14
|
import { load as loadYaml } from 'js-yaml';
|
|
15
15
|
import { listBlockTemplates } from './block-templates.js';
|
|
16
16
|
import { getRunner as getLLMRunner } from './llm/index.js';
|
|
17
17
|
import { createDqlAgentProviderRunner } from './llm/providers/dql-agent-provider.js';
|
|
18
18
|
import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
|
|
19
|
-
import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, recallRelevantTurns, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildLocalContextPack, defaultConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureMetadataCatalogFresh, propose, proposePlan, recordCorrectionTrace, emitCorrectionEvalCase, mineJoinPatterns, reviewHint, AgentRunEngine, FileAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, synthesizeAnswer, streamOrGenerate, narrateResult,
|
|
19
|
+
import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, recallRelevantTurns, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildLocalContextPack, defaultConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureMetadataCatalogFresh, propose, proposePlan, recordCorrectionTrace, emitCorrectionEvalCase, mineJoinPatterns, reviewHint, AgentRunEngine, FileAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, defaultAgentRunStorePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, reindexProject, invalidateAgentProjectState, resolveDomainContextEnvelope, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, probeLocalOllamaEmbeddings, upsertGeneratedDqlArtifactDraft, } from '@duckcodeailabs/dql-agent';
|
|
20
20
|
import { gatherProposeEnrichment } from './propose-enrich.js';
|
|
21
21
|
import { handleAppsApi, proposeAppAiBuild, recommendVisualization } from './apps-api.js';
|
|
22
22
|
import { getActiveProvider, getEffectiveProviderConfig, isProviderSettingsId, listProviderSettings, saveProviderSettings, } from './settings/provider-settings.js';
|
|
@@ -351,6 +351,21 @@ function compactConversationRecord(record) {
|
|
|
351
351
|
function apiRequestId(scope) {
|
|
352
352
|
return `${scope}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
353
353
|
}
|
|
354
|
+
/** UI-007 / E2E-005: report the installed CLI version that owns this runtime. */
|
|
355
|
+
export function readDqlRuntimeVersion(runtimeUrl = import.meta.url) {
|
|
356
|
+
const runtimeDir = dirname(fileURLToPath(runtimeUrl));
|
|
357
|
+
for (const packagePath of [join(runtimeDir, 'package.json'), join(runtimeDir, '../package.json')]) {
|
|
358
|
+
try {
|
|
359
|
+
const parsed = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
360
|
+
if (typeof parsed.version === 'string' && parsed.version.trim())
|
|
361
|
+
return parsed.version.trim();
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// Source and published layouts place package.json at different levels.
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return 'unknown';
|
|
368
|
+
}
|
|
354
369
|
function apiErrorEnvelope(input) {
|
|
355
370
|
return {
|
|
356
371
|
requestId: input.requestId,
|
|
@@ -377,6 +392,7 @@ export async function startLocalServer(opts) {
|
|
|
377
392
|
const bindHost = opts.host ?? process.env.DQL_HOST ?? '127.0.0.1';
|
|
378
393
|
const loopback = bindHost === '127.0.0.1' || bindHost === 'localhost' || bindHost === '::1';
|
|
379
394
|
const authToken = opts.authToken ?? process.env.DQL_SERVER_TOKEN;
|
|
395
|
+
const runtimeVersion = readDqlRuntimeVersion();
|
|
380
396
|
const allowedOrigins = new Set((opts.allowedOrigins ?? (process.env.DQL_ALLOWED_ORIGINS ?? '').split(','))
|
|
381
397
|
.map((value) => value.trim().replace(/\/$/, ''))
|
|
382
398
|
.filter(Boolean));
|
|
@@ -389,13 +405,19 @@ export async function startLocalServer(opts) {
|
|
|
389
405
|
const gitRoot = await resolveGitRoot(projectRoot);
|
|
390
406
|
if (gitRoot)
|
|
391
407
|
ensureLocalRuntimeGitignore(projectRoot);
|
|
392
|
-
let connection = rawConnection ? normalizeProjectConnection(rawConnection, projectRoot) : null;
|
|
393
408
|
let projectConfig = loadProjectConfig(projectRoot);
|
|
409
|
+
const configuredConnection = rawConnection
|
|
410
|
+
? normalizeProjectConnection(rawConnection, projectRoot)
|
|
411
|
+
: projectConfig.defaultConnection
|
|
412
|
+
? normalizeProjectConnection(projectConfig.defaultConnection, projectRoot)
|
|
413
|
+
: null;
|
|
414
|
+
let connection = configuredConnection ?? resolveDbtProfileRuntimeConnection(projectRoot, projectConfig);
|
|
394
415
|
const projectSnapshots = new ProjectSnapshotService();
|
|
416
|
+
const dashboardRunEvidence = new Map();
|
|
395
417
|
const dbtNodeDetailCache = new Map();
|
|
396
418
|
const onboardingJobs = new Map();
|
|
397
419
|
const projectSnapshot = () => {
|
|
398
|
-
const dbtManifestPath = resolveDbtManifestPath(projectRoot) ?? undefined;
|
|
420
|
+
const dbtManifestPath = resolveDbtManifestPath(projectRoot, projectConfig) ?? undefined;
|
|
399
421
|
const inputs = collectInputFiles({ projectRoot, dbtManifestPath });
|
|
400
422
|
const signatureHash = createHash('sha256');
|
|
401
423
|
for (const input of inputs.sort()) {
|
|
@@ -421,14 +443,26 @@ export async function startLocalServer(opts) {
|
|
|
421
443
|
};
|
|
422
444
|
};
|
|
423
445
|
const onboardingDbtPaths = (body = {}) => {
|
|
424
|
-
const
|
|
446
|
+
const repoUrl = typeof body.repoUrl === 'string' && body.repoUrl.trim() ? body.repoUrl.trim() : undefined;
|
|
447
|
+
const branch = typeof body.branch === 'string' && body.branch.trim() ? body.branch.trim() : undefined;
|
|
448
|
+
const subPath = typeof body.subPath === 'string' && body.subPath.trim() ? body.subPath.trim() : undefined;
|
|
449
|
+
const repo = repoUrl ? resolveRepoSource({
|
|
450
|
+
provider: 'dbt', source: repoUrl.includes('gitlab') ? 'gitlab' : 'github', repoUrl, branch, subPath,
|
|
451
|
+
}, projectRoot) : undefined;
|
|
452
|
+
const projectDirInput = repo?.localPath ?? (typeof body.projectDir === 'string' ? body.projectDir : projectConfig.dbt?.projectDir ?? '.');
|
|
425
453
|
const dbtProjectDir = resolve(projectRoot, projectDirInput);
|
|
426
454
|
const manifestInput = typeof body.manifestPath === 'string' ? body.manifestPath : projectConfig.dbt?.manifestPath ?? 'target/manifest.json';
|
|
427
455
|
const manifestPath = resolve(dbtProjectDir, manifestInput);
|
|
428
456
|
if (!(manifestPath === dbtProjectDir || manifestPath.startsWith(`${dbtProjectDir}/`))) {
|
|
429
457
|
throw Object.assign(new Error('dbt manifestPath must stay inside dbt projectDir.'), { code: 'DBT_ARTIFACT_INVALID' });
|
|
430
458
|
}
|
|
431
|
-
|
|
459
|
+
const profilesDirInput = typeof body.profilesDir === 'string'
|
|
460
|
+
? body.profilesDir
|
|
461
|
+
: projectConfig.dbt?.profilesDir;
|
|
462
|
+
const configuredProfilesDir = profilesDirInput ? resolve(projectRoot, profilesDirInput) : undefined;
|
|
463
|
+
const profilePath = findDbtProfilePaths(projectRoot, dbtProjectDir, configuredProfilesDir)[0];
|
|
464
|
+
const profilesDir = configuredProfilesDir ?? (profilePath ? dirname(profilePath) : undefined);
|
|
465
|
+
return { dbtProjectDir, manifestPath, projectDirInput, manifestInput, profilesDir, repoUrl, branch, subPath, repoWarnings: repo?.warnings ?? [] };
|
|
432
466
|
};
|
|
433
467
|
const previewDbtOnboarding = (body = {}) => {
|
|
434
468
|
const paths = onboardingDbtPaths(body);
|
|
@@ -445,8 +479,11 @@ export async function startLocalServer(opts) {
|
|
|
445
479
|
}
|
|
446
480
|
const fingerprint = createHash('sha256').update(readFileSync(paths.manifestPath)).digest('hex');
|
|
447
481
|
return {
|
|
448
|
-
projectDir:
|
|
482
|
+
projectDir: portableProjectPath(projectRoot, paths.dbtProjectDir),
|
|
449
483
|
manifestPath: relative(paths.dbtProjectDir, paths.manifestPath).replace(/\\/g, '/'),
|
|
484
|
+
...(paths.profilesDir ? { profilesDir: portableProjectPath(projectRoot, paths.profilesDir) } : {}),
|
|
485
|
+
...(paths.repoUrl ? { repoUrl: paths.repoUrl, branch: paths.branch, subPath: paths.subPath } : {}),
|
|
486
|
+
...(paths.repoWarnings.length ? { warnings: paths.repoWarnings } : {}),
|
|
450
487
|
fingerprint,
|
|
451
488
|
projectName: typeof raw.metadata?.project_name === 'string' ? raw.metadata.project_name : undefined,
|
|
452
489
|
counts: {
|
|
@@ -1900,17 +1937,19 @@ export async function startLocalServer(opts) {
|
|
|
1900
1937
|
// A real app needs a certified anchor: only offer confirmation when at least
|
|
1901
1938
|
// one certified tile is present. Otherwise fall through to the escalate path
|
|
1902
1939
|
// (research / draft the missing blocks) so the user isn't sent to a dead end.
|
|
1903
|
-
const proposed = Boolean(proposal && proposal.tiles.some((tile) => tile.certification === 'certified' && !tile.error));
|
|
1940
|
+
const proposed = Boolean(proposal && proposal.tiles.some((tile) => (tile.certification === 'certified' || tile.certification === 'reviewed_semantic') && !tile.error));
|
|
1904
1941
|
const sessionPlan = agentRunRecord(session.plan);
|
|
1905
1942
|
const appTitle = agentRunString(sessionPlan?.name) ?? 'App proposal';
|
|
1906
1943
|
const certifiedCount = proposal?.coverage.certifiedTiles ?? 0;
|
|
1907
1944
|
const generatedCount = proposal?.coverage.generatedTiles ?? 0;
|
|
1945
|
+
const semanticCount = proposal?.coverage.semanticTiles ?? 0;
|
|
1908
1946
|
const gapCount = proposal?.coverage.gaps ?? 0;
|
|
1909
1947
|
// A coverage gap is NOT terminal — leave status open so the gate can escalate to
|
|
1910
1948
|
// drafting the missing blocks. Only genuine infra errors (the catch above) block.
|
|
1911
1949
|
return {
|
|
1912
1950
|
summary: proposed
|
|
1913
1951
|
? `Proposed ${certifiedCount} certified tile${certifiedCount === 1 ? '' : 's'}`
|
|
1952
|
+
+ (semanticCount > 0 ? ` + ${semanticCount} reviewed semantic tile${semanticCount === 1 ? '' : 's'}` : '')
|
|
1914
1953
|
+ (generatedCount > 0 ? ` + ${generatedCount} AI-generated (review required)` : '')
|
|
1915
1954
|
+ (gapCount > 0 ? `; ${gapCount} question${gapCount === 1 ? '' : 's'} still uncovered` : '')
|
|
1916
1955
|
+ '. Review the list and confirm to create the app.'
|
|
@@ -1924,6 +1963,7 @@ export async function startLocalServer(opts) {
|
|
|
1924
1963
|
dashboardId: session.dashboardId,
|
|
1925
1964
|
plan: session.plan,
|
|
1926
1965
|
proposal,
|
|
1966
|
+
proposalHash: session.proposalHash,
|
|
1927
1967
|
warnings: session.warnings,
|
|
1928
1968
|
}, session.appId)] : [],
|
|
1929
1969
|
evaluations: [
|
|
@@ -3385,13 +3425,44 @@ export async function startLocalServer(opts) {
|
|
|
3385
3425
|
}
|
|
3386
3426
|
if (req.method === 'GET' && path === '/api/health') {
|
|
3387
3427
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
3388
|
-
res.end(serializeJSON({ status: 'ok' }));
|
|
3428
|
+
res.end(serializeJSON({ status: 'ok', version: runtimeVersion }));
|
|
3429
|
+
return;
|
|
3430
|
+
}
|
|
3431
|
+
if (req.method === 'GET' && path === '/api/onboarding/launch') {
|
|
3432
|
+
const requestId = apiRequestId('onboarding-launch');
|
|
3433
|
+
const acknowledgedVersion = readUserPrefs(userPrefsPath).setup?.acknowledgedVersion ?? null;
|
|
3434
|
+
const shouldOpen = acknowledgedVersion !== runtimeVersion;
|
|
3435
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
3436
|
+
res.end(serializeJSON({
|
|
3437
|
+
requestId,
|
|
3438
|
+
version: runtimeVersion,
|
|
3439
|
+
acknowledgedVersion,
|
|
3440
|
+
shouldOpen,
|
|
3441
|
+
reason: shouldOpen ? (acknowledgedVersion ? 'version_upgrade' : 'first_install') : null,
|
|
3442
|
+
}));
|
|
3443
|
+
return;
|
|
3444
|
+
}
|
|
3445
|
+
if (req.method === 'POST' && path === '/api/onboarding/acknowledge') {
|
|
3446
|
+
const requestId = apiRequestId('onboarding-acknowledge');
|
|
3447
|
+
const prefs = readUserPrefs(userPrefsPath);
|
|
3448
|
+
prefs.setup = {
|
|
3449
|
+
acknowledgedVersion: runtimeVersion,
|
|
3450
|
+
acknowledgedAt: new Date().toISOString(),
|
|
3451
|
+
};
|
|
3452
|
+
writeUserPrefs(userPrefsPath, prefs);
|
|
3453
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
3454
|
+
res.end(serializeJSON({ requestId, version: runtimeVersion, acknowledged: true }));
|
|
3389
3455
|
return;
|
|
3390
3456
|
}
|
|
3391
3457
|
if (req.method === 'GET' && path === '/api/onboarding/status') {
|
|
3392
3458
|
const requestId = apiRequestId('onboarding-status');
|
|
3393
|
-
const dbtProjectDir =
|
|
3459
|
+
const dbtProjectDir = findDbtProjectPath(projectRoot, projectConfig);
|
|
3394
3460
|
const manifestPath = resolve(dbtProjectDir, projectConfig.dbt?.manifestPath ?? 'target/manifest.json');
|
|
3461
|
+
const configuredDbtSource = projectConfig.dbt?.repoUrl ?? projectConfig.dbt?.projectDir;
|
|
3462
|
+
const dbtConfigured = existsSync(join(dbtProjectDir, 'dbt_project.yml'))
|
|
3463
|
+
|| Boolean(configuredDbtSource && !/\{\{[^}]+\}\}/.test(configuredDbtSource));
|
|
3464
|
+
const configuredProfilesDir = projectConfig.dbt?.profilesDir ? resolve(projectRoot, projectConfig.dbt.profilesDir) : undefined;
|
|
3465
|
+
const profilePath = findDbtProfilePaths(projectRoot, dbtProjectDir, configuredProfilesDir)[0];
|
|
3395
3466
|
const registry = loadDomainPackageRegistry(projectRoot);
|
|
3396
3467
|
const manifestFound = existsSync(manifestPath);
|
|
3397
3468
|
let snapshotId;
|
|
@@ -3409,9 +3480,15 @@ export async function startLocalServer(opts) {
|
|
|
3409
3480
|
requestId,
|
|
3410
3481
|
snapshotId,
|
|
3411
3482
|
dbt: {
|
|
3412
|
-
configured:
|
|
3483
|
+
configured: dbtConfigured,
|
|
3413
3484
|
projectDir: projectConfig.dbt?.projectDir,
|
|
3414
3485
|
manifestPath: projectConfig.dbt?.manifestPath,
|
|
3486
|
+
profilesDir: configuredProfilesDir
|
|
3487
|
+
? portableProjectPath(projectRoot, configuredProfilesDir)
|
|
3488
|
+
: profilePath ? portableProjectPath(projectRoot, dirname(profilePath)) : undefined,
|
|
3489
|
+
repoUrl: projectConfig.dbt?.repoUrl,
|
|
3490
|
+
branch: projectConfig.dbt?.branch,
|
|
3491
|
+
subPath: projectConfig.dbt?.subPath,
|
|
3415
3492
|
projectFound: existsSync(join(dbtProjectDir, 'dbt_project.yml')),
|
|
3416
3493
|
manifestFound,
|
|
3417
3494
|
},
|
|
@@ -3457,7 +3534,9 @@ export async function startLocalServer(opts) {
|
|
|
3457
3534
|
throw error;
|
|
3458
3535
|
const { dbtProjectDir } = onboardingDbtPaths(body);
|
|
3459
3536
|
try {
|
|
3460
|
-
|
|
3537
|
+
const { profilesDir } = onboardingDbtPaths(body);
|
|
3538
|
+
const parseArgs = buildDbtParseArgs(dbtProjectDir, profilesDir);
|
|
3539
|
+
execFileSync('dbt', parseArgs, {
|
|
3461
3540
|
cwd: dbtProjectDir,
|
|
3462
3541
|
timeout: 120_000,
|
|
3463
3542
|
maxBuffer: 1024 * 1024,
|
|
@@ -3483,16 +3562,24 @@ export async function startLocalServer(opts) {
|
|
|
3483
3562
|
...projectConfig,
|
|
3484
3563
|
manifestVersion: 3,
|
|
3485
3564
|
modeling: { mode: 'dbt-first' },
|
|
3486
|
-
dbt: {
|
|
3565
|
+
dbt: {
|
|
3566
|
+
projectDir: preview.projectDir,
|
|
3567
|
+
manifestPath: preview.manifestPath,
|
|
3568
|
+
...(preview.profilesDir ? { profilesDir: preview.profilesDir } : {}),
|
|
3569
|
+
...(preview.repoUrl ? { repoUrl: preview.repoUrl, branch: preview.branch, subPath: preview.subPath } : {}),
|
|
3570
|
+
},
|
|
3487
3571
|
semanticLayer: { provider: 'dbt', projectPath: preview.projectDir },
|
|
3488
3572
|
};
|
|
3489
3573
|
const configPath = join(projectRoot, 'dql.config.json');
|
|
3490
3574
|
const previousConfigSource = readFileSync(configPath, 'utf8');
|
|
3491
3575
|
const previousProjectConfig = projectConfig;
|
|
3576
|
+
const previousConnection = connection;
|
|
3492
3577
|
const tempPath = `${configPath}.tmp-${process.pid}`;
|
|
3493
3578
|
writeFileSync(tempPath, `${JSON.stringify(nextConfig, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
3494
3579
|
renameSync(tempPath, configPath);
|
|
3495
3580
|
projectConfig = nextConfig;
|
|
3581
|
+
if (!configuredConnection)
|
|
3582
|
+
connection = resolveDbtProfileRuntimeConnection(projectRoot, nextConfig);
|
|
3496
3583
|
projectSnapshots.invalidate();
|
|
3497
3584
|
invalidateAgentProjectState(projectRoot);
|
|
3498
3585
|
let snapshotId;
|
|
@@ -3504,6 +3591,7 @@ export async function startLocalServer(opts) {
|
|
|
3504
3591
|
writeFileSync(rollbackPath, previousConfigSource, { encoding: 'utf8', mode: 0o600 });
|
|
3505
3592
|
renameSync(rollbackPath, configPath);
|
|
3506
3593
|
projectConfig = previousProjectConfig;
|
|
3594
|
+
connection = previousConnection;
|
|
3507
3595
|
projectSnapshots.invalidate();
|
|
3508
3596
|
invalidateAgentProjectState(projectRoot);
|
|
3509
3597
|
throw Object.assign(new Error(`dbt-first configuration did not compile and was rolled back: ${error instanceof Error ? error.message : String(error)}`), { code: 'SNAPSHOT_BUILD_FAILED' });
|
|
@@ -3722,11 +3810,64 @@ export async function startLocalServer(opts) {
|
|
|
3722
3810
|
// provenance references and the sparse DQL overlay, never copied dbt YAML.
|
|
3723
3811
|
if (req.method === 'GET' && path === '/api/modeling/dbt-first') {
|
|
3724
3812
|
const requestId = apiRequestId('modeling-dbt-first');
|
|
3725
|
-
const
|
|
3813
|
+
const modelingEnabled = projectConfig.manifestVersion === 3 && projectConfig.modeling?.mode === 'dbt-first';
|
|
3814
|
+
if (!modelingEnabled) {
|
|
3815
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
3816
|
+
res.end(serializeJSON(apiErrorEnvelope({
|
|
3817
|
+
requestId,
|
|
3818
|
+
code: 'DBT_FIRST_NOT_ENABLED',
|
|
3819
|
+
message: 'dbt-first modeling is not enabled. Connect and apply a dbt project from Settings → Project & dbt.',
|
|
3820
|
+
details: {
|
|
3821
|
+
manifestVersion: projectConfig.manifestVersion ?? 2,
|
|
3822
|
+
modelingMode: projectConfig.modeling?.mode ?? null,
|
|
3823
|
+
},
|
|
3824
|
+
nextActions: ['Open Settings → Project & dbt and apply a valid dbt project.'],
|
|
3825
|
+
})));
|
|
3826
|
+
return;
|
|
3827
|
+
}
|
|
3828
|
+
const dbtProjectDir = findDbtProjectPath(projectRoot, projectConfig);
|
|
3829
|
+
const configuredManifestPath = projectConfig.dbt?.manifestPath ?? 'target/manifest.json';
|
|
3830
|
+
const expectedManifestPath = resolve(dbtProjectDir, configuredManifestPath);
|
|
3831
|
+
if (!existsSync(expectedManifestPath)) {
|
|
3832
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
3833
|
+
res.end(serializeJSON(apiErrorEnvelope({
|
|
3834
|
+
requestId,
|
|
3835
|
+
code: 'DBT_MANIFEST_NOT_FOUND',
|
|
3836
|
+
message: `The configured dbt manifest was not found at ${configuredManifestPath}.`,
|
|
3837
|
+
details: {
|
|
3838
|
+
projectDir: projectConfig.dbt?.projectDir ?? portableProjectPath(projectRoot, dbtProjectDir),
|
|
3839
|
+
manifestPath: configuredManifestPath,
|
|
3840
|
+
},
|
|
3841
|
+
nextActions: ['Run dbt parse, dbt compile, or dbt build in the configured dbt project, then refresh Domain Studio.'],
|
|
3842
|
+
})));
|
|
3843
|
+
return;
|
|
3844
|
+
}
|
|
3845
|
+
let snapshot;
|
|
3846
|
+
try {
|
|
3847
|
+
snapshot = projectSnapshot();
|
|
3848
|
+
}
|
|
3849
|
+
catch (error) {
|
|
3850
|
+
res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
3851
|
+
res.end(serializeJSON(apiErrorEnvelope({
|
|
3852
|
+
requestId,
|
|
3853
|
+
code: 'DBT_MANIFEST_COMPILE_FAILED',
|
|
3854
|
+
message: apiErrorMessage(error),
|
|
3855
|
+
details: { manifestPath: configuredManifestPath },
|
|
3856
|
+
nextActions: ['Fix or rebuild the dbt manifest, then refresh Domain Studio.'],
|
|
3857
|
+
})));
|
|
3858
|
+
return;
|
|
3859
|
+
}
|
|
3726
3860
|
const manifest = snapshot.manifest;
|
|
3727
3861
|
if (manifest.manifestVersion !== 3 || !manifest.modeling || !manifest.dbtProvenance) {
|
|
3728
|
-
res.writeHead(
|
|
3729
|
-
res.end(serializeJSON(apiErrorEnvelope({
|
|
3862
|
+
res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
3863
|
+
res.end(serializeJSON(apiErrorEnvelope({
|
|
3864
|
+
requestId,
|
|
3865
|
+
snapshotId: snapshot.snapshotId,
|
|
3866
|
+
code: 'DBT_MANIFEST_COMPILE_FAILED',
|
|
3867
|
+
message: snapshot.error ?? 'DQL could not compile Domain Studio from the configured dbt manifest.',
|
|
3868
|
+
details: { manifestPath: configuredManifestPath },
|
|
3869
|
+
nextActions: ['Review the dbt artifact and DQL compile diagnostics, rebuild the dbt manifest, then refresh Domain Studio.'],
|
|
3870
|
+
})));
|
|
3730
3871
|
return;
|
|
3731
3872
|
}
|
|
3732
3873
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -4446,6 +4587,52 @@ export async function startLocalServer(opts) {
|
|
|
4446
4587
|
// ── Skills (spec 16) — user-authored business context. AI drafts, humans
|
|
4447
4588
|
// certify; skills never carry certification. PROJECT skills (user empty)
|
|
4448
4589
|
// are shared; PERSONAL skills (user set) are user-bound. ────────────────
|
|
4590
|
+
if (req.method === 'GET' && path === '/api/skills/settings') {
|
|
4591
|
+
try {
|
|
4592
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
4593
|
+
res.end(serializeJSON(buildSkillPathSettings(projectRoot)));
|
|
4594
|
+
}
|
|
4595
|
+
catch (error) {
|
|
4596
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
4597
|
+
res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
|
|
4598
|
+
}
|
|
4599
|
+
return;
|
|
4600
|
+
}
|
|
4601
|
+
if (req.method === 'PUT' && path === '/api/skills/settings') {
|
|
4602
|
+
try {
|
|
4603
|
+
const body = (await readJSON(req).catch(() => ({})));
|
|
4604
|
+
const requestedPath = typeof body.path === 'string' ? body.path.trim() : '';
|
|
4605
|
+
if (!requestedPath || requestedPath.includes('\0')) {
|
|
4606
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
4607
|
+
res.end(serializeJSON({ error: 'Provide an existing Skills folder path.' }));
|
|
4608
|
+
return;
|
|
4609
|
+
}
|
|
4610
|
+
const resolvedPath = resolve(projectRoot, requestedPath);
|
|
4611
|
+
if (!existsSync(resolvedPath) || !statSync(resolvedPath).isDirectory()) {
|
|
4612
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
4613
|
+
res.end(serializeJSON({ error: `Skills folder not found: ${resolvedPath}` }));
|
|
4614
|
+
return;
|
|
4615
|
+
}
|
|
4616
|
+
const configPath = join(projectRoot, 'dql.config.json');
|
|
4617
|
+
const raw = existsSync(configPath)
|
|
4618
|
+
? JSON.parse(readFileSync(configPath, 'utf-8'))
|
|
4619
|
+
: {};
|
|
4620
|
+
const layout = raw.layout && typeof raw.layout === 'object' && !Array.isArray(raw.layout)
|
|
4621
|
+
? raw.layout
|
|
4622
|
+
: {};
|
|
4623
|
+
raw.layout = { ...layout, skillsPath: requestedPath };
|
|
4624
|
+
writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, 'utf-8');
|
|
4625
|
+
projectConfig = loadProjectConfig(projectRoot);
|
|
4626
|
+
invalidateAgentProjectState(projectRoot);
|
|
4627
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
4628
|
+
res.end(serializeJSON(buildSkillPathSettings(projectRoot)));
|
|
4629
|
+
}
|
|
4630
|
+
catch (error) {
|
|
4631
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
4632
|
+
res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
|
|
4633
|
+
}
|
|
4634
|
+
return;
|
|
4635
|
+
}
|
|
4449
4636
|
if (req.method === 'GET' && path === '/api/skills') {
|
|
4450
4637
|
try {
|
|
4451
4638
|
const skills = loadSkills(projectRoot).skills
|
|
@@ -5148,6 +5335,33 @@ export async function startLocalServer(opts) {
|
|
|
5148
5335
|
}
|
|
5149
5336
|
return;
|
|
5150
5337
|
}
|
|
5338
|
+
const appStoryRun = path.match(/^\/api\/apps\/([^/]+)\/dashboards\/([^/]+)\/story$/);
|
|
5339
|
+
if (req.method === 'POST' && appStoryRun) {
|
|
5340
|
+
const body = await readJSON(req).catch(() => ({}));
|
|
5341
|
+
const runId = typeof body.runId === 'string' ? body.runId : '';
|
|
5342
|
+
const evidence = dashboardRunEvidence.get(runId);
|
|
5343
|
+
const appId = decodeURIComponent(appStoryRun[1]);
|
|
5344
|
+
const dashboardId = decodeURIComponent(appStoryRun[2]);
|
|
5345
|
+
if (!evidence || evidence.expiresAt < Date.now() || evidence.appId !== appId || evidence.dashboardId !== dashboardId) {
|
|
5346
|
+
dashboardRunEvidence.delete(runId);
|
|
5347
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5348
|
+
res.end(serializeJSON({ error: 'Dashboard run evidence expired or does not match this App. Run the dashboard again.' }));
|
|
5349
|
+
return;
|
|
5350
|
+
}
|
|
5351
|
+
// The endpoint accepts runId only. Optional provider wording can be added
|
|
5352
|
+
// here later; the claim-checked deterministic brief is always available.
|
|
5353
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5354
|
+
res.end(serializeJSON({
|
|
5355
|
+
runId,
|
|
5356
|
+
snapshotId: evidence.snapshotId,
|
|
5357
|
+
filterFingerprint: evidence.filterFingerprint,
|
|
5358
|
+
resultFingerprint: evidence.resultFingerprint,
|
|
5359
|
+
personaFingerprint: evidence.personaFingerprint,
|
|
5360
|
+
facts: evidence.facts,
|
|
5361
|
+
story: evidence.story,
|
|
5362
|
+
}));
|
|
5363
|
+
return;
|
|
5364
|
+
}
|
|
5151
5365
|
const appDashRun = path.match(/^\/api\/apps\/([^/]+)\/dashboards\/([^/]+)\/run$/);
|
|
5152
5366
|
if (req.method === 'POST' && appDashRun) {
|
|
5153
5367
|
try {
|
|
@@ -5160,6 +5374,12 @@ export async function startLocalServer(opts) {
|
|
|
5160
5374
|
res.end(serializeJSON({ error: `Dashboard "${dashboardId}" not found in app "${appId}"` }));
|
|
5161
5375
|
return;
|
|
5162
5376
|
}
|
|
5377
|
+
const activeApp = activePersonaAppId();
|
|
5378
|
+
if (activeApp && activeApp !== appId) {
|
|
5379
|
+
res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5380
|
+
res.end(serializeJSON({ error: `The active persona belongs to App "${activeApp}", not "${appId}".` }));
|
|
5381
|
+
return;
|
|
5382
|
+
}
|
|
5163
5383
|
const manifest = buildManifest({ projectRoot });
|
|
5164
5384
|
const variables = body.variables && typeof body.variables === 'object'
|
|
5165
5385
|
? body.variables
|
|
@@ -5258,6 +5478,70 @@ export async function startLocalServer(opts) {
|
|
|
5258
5478
|
});
|
|
5259
5479
|
continue;
|
|
5260
5480
|
}
|
|
5481
|
+
if (item.semantic) {
|
|
5482
|
+
try {
|
|
5483
|
+
if (!semanticLayer)
|
|
5484
|
+
throw new Error('Semantic layer is not available for this governed query.');
|
|
5485
|
+
const knownMetrics = new Set(semanticLayer.listMetrics().map((metric) => metric.name));
|
|
5486
|
+
const missingMetrics = item.semantic.metrics.filter((metric) => !knownMetrics.has(metric));
|
|
5487
|
+
if (missingMetrics.length)
|
|
5488
|
+
throw new Error(`Semantic definition changed; missing metric(s): ${missingMetrics.join(', ')}`);
|
|
5489
|
+
const knownModels = new Set(semanticLayer.listSemanticModels().map((model) => model.name));
|
|
5490
|
+
const missingModels = item.semantic.semanticModelRefs.filter((model) => !knownModels.has(model));
|
|
5491
|
+
if (missingModels.length)
|
|
5492
|
+
throw new Error(`Semantic definition changed; missing model(s): ${missingModels.join(', ')}`);
|
|
5493
|
+
const targetConnection = requireActiveConnection(isConnectionConfig(body.connection) ? body.connection : connection);
|
|
5494
|
+
const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer);
|
|
5495
|
+
const activeFilters = Object.entries(dashboardVariables)
|
|
5496
|
+
.filter(([field, value]) => item.semantic.dimensions?.includes(field) && value !== undefined && value !== null && value !== '')
|
|
5497
|
+
.map(([dimension, value]) => ({ dimension, operator: 'equals', values: Array.isArray(value) ? value.map(String) : [String(value)] }));
|
|
5498
|
+
const staticFilters = (item.semantic.filters ?? []).map((filter) => ({
|
|
5499
|
+
dimension: filter.field,
|
|
5500
|
+
operator: filter.operator,
|
|
5501
|
+
values: Array.isArray(filter.value) ? filter.value.map(String) : [String(filter.value)],
|
|
5502
|
+
}));
|
|
5503
|
+
const composed = composeRuntimeSemanticQuery({
|
|
5504
|
+
metrics: item.semantic.metrics,
|
|
5505
|
+
dimensions: item.semantic.dimensions ?? [],
|
|
5506
|
+
filters: [...staticFilters, ...activeFilters],
|
|
5507
|
+
...(item.semantic.timeDimension ? { timeDimension: { name: item.semantic.timeDimension, granularity: 'month' } } : {}),
|
|
5508
|
+
orderBy: item.semantic.orderBy?.map((order) => ({ name: order.field, direction: order.direction })),
|
|
5509
|
+
limit: item.semantic.limit,
|
|
5510
|
+
engine: item.semantic.provider,
|
|
5511
|
+
}, semanticLayer, {
|
|
5512
|
+
projectRoot,
|
|
5513
|
+
projectConfig,
|
|
5514
|
+
detectedProvider: semanticDetectedProvider,
|
|
5515
|
+
driver: targetConnection.driver,
|
|
5516
|
+
tableMapping,
|
|
5517
|
+
});
|
|
5518
|
+
if (!composed)
|
|
5519
|
+
throw new Error('The governed semantic query could not be composed.');
|
|
5520
|
+
const prepared = prepareLocalExecution(composed.sql, targetConnection, projectRoot, projectConfig);
|
|
5521
|
+
const result = await executor.executeQuery(prepared.sql, [], {}, prepared.connection);
|
|
5522
|
+
tiles.push({
|
|
5523
|
+
tileId: item.i,
|
|
5524
|
+
status: 'ok',
|
|
5525
|
+
tileType: 'semantic',
|
|
5526
|
+
title: item.title ?? item.semantic.id,
|
|
5527
|
+
viz: item.viz,
|
|
5528
|
+
result: normalizeQueryResult(result),
|
|
5529
|
+
trustState: 'review_required',
|
|
5530
|
+
citation: { kind: 'semantic_query', name: item.semantic.id },
|
|
5531
|
+
});
|
|
5532
|
+
}
|
|
5533
|
+
catch (err) {
|
|
5534
|
+
tiles.push({
|
|
5535
|
+
tileId: item.i,
|
|
5536
|
+
status: 'error',
|
|
5537
|
+
tileType: 'semantic',
|
|
5538
|
+
title: item.title ?? item.semantic.id,
|
|
5539
|
+
error: err instanceof Error ? err.message : String(err),
|
|
5540
|
+
trustState: 'review_required',
|
|
5541
|
+
});
|
|
5542
|
+
}
|
|
5543
|
+
continue;
|
|
5544
|
+
}
|
|
5261
5545
|
const block = resolveDashboardItemBlock(item, manifest);
|
|
5262
5546
|
if (!block) {
|
|
5263
5547
|
tiles.push({
|
|
@@ -5381,12 +5665,55 @@ export async function startLocalServer(opts) {
|
|
|
5381
5665
|
}
|
|
5382
5666
|
}
|
|
5383
5667
|
localApps?.close();
|
|
5668
|
+
const snapshot = projectSnapshot();
|
|
5669
|
+
const filterFingerprint = createHash('sha256').update(JSON.stringify(dashboardVariables)).digest('hex');
|
|
5670
|
+
const resultFingerprint = createHash('sha256').update(JSON.stringify(tiles.map((tile) => ({ tileId: tile.tileId, status: tile.status, result: tile.result })))).digest('hex');
|
|
5671
|
+
const personaFingerprint = createHash('sha256').update(activePersonaAppId() ?? 'global').digest('hex');
|
|
5672
|
+
const runId = `app_run_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
5673
|
+
const storyResult = buildDeterministicDashboardStory({
|
|
5674
|
+
goal: loaded.dashboard.story?.goal ?? loaded.dashboard.metadata.businessOutcome ?? loaded.dashboard.metadata.title,
|
|
5675
|
+
audience: loaded.dashboard.story?.audience ?? loaded.dashboard.metadata.audience,
|
|
5676
|
+
filters: dashboardVariables,
|
|
5677
|
+
tiles: tiles.map((tile) => ({
|
|
5678
|
+
tileId: tile.tileId,
|
|
5679
|
+
title: typeof tile.title === 'string' ? tile.title : tile.tileId,
|
|
5680
|
+
status: tile.status,
|
|
5681
|
+
trustState: (tile.trustState === 'certified' || tile.trustState === 'review_required' || tile.trustState === 'draft_ready'
|
|
5682
|
+
? tile.trustState
|
|
5683
|
+
: tile.certificationStatus === 'certified' ? 'certified' : 'review_required'),
|
|
5684
|
+
result: tile.result,
|
|
5685
|
+
citation: tile.citation,
|
|
5686
|
+
})),
|
|
5687
|
+
eligibleTileIds: loaded.dashboard.story?.eligibleTileIds,
|
|
5688
|
+
driverTileIds: loaded.dashboard.story?.driverTileIds,
|
|
5689
|
+
});
|
|
5690
|
+
dashboardRunEvidence.set(runId, {
|
|
5691
|
+
appId,
|
|
5692
|
+
dashboardId,
|
|
5693
|
+
snapshotId: snapshot.snapshotId,
|
|
5694
|
+
filterFingerprint,
|
|
5695
|
+
resultFingerprint,
|
|
5696
|
+
personaFingerprint,
|
|
5697
|
+
facts: storyResult.facts,
|
|
5698
|
+
story: storyResult.story,
|
|
5699
|
+
expiresAt: Date.now() + 15 * 60_000,
|
|
5700
|
+
});
|
|
5701
|
+
for (const [id, evidence] of dashboardRunEvidence)
|
|
5702
|
+
if (evidence.expiresAt < Date.now())
|
|
5703
|
+
dashboardRunEvidence.delete(id);
|
|
5384
5704
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
5385
5705
|
res.end(serializeJSON({
|
|
5386
5706
|
appId,
|
|
5387
5707
|
dashboardId,
|
|
5388
5708
|
persona: activePersonaAppId() ? { appId: activePersonaAppId() } : null,
|
|
5709
|
+
runId,
|
|
5710
|
+
snapshotId: snapshot.snapshotId,
|
|
5711
|
+
filterFingerprint,
|
|
5712
|
+
resultFingerprint,
|
|
5713
|
+
personaFingerprint,
|
|
5389
5714
|
tiles,
|
|
5715
|
+
facts: storyResult.facts,
|
|
5716
|
+
story: storyResult.story,
|
|
5390
5717
|
}));
|
|
5391
5718
|
}
|
|
5392
5719
|
catch (err) {
|
|
@@ -6691,16 +7018,26 @@ export async function startLocalServer(opts) {
|
|
|
6691
7018
|
res.end(serializeJSON({ error: 'goal is required' }));
|
|
6692
7019
|
return;
|
|
6693
7020
|
}
|
|
6694
|
-
const
|
|
6695
|
-
const
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
7021
|
+
const snapshot = projectSnapshot();
|
|
7022
|
+
const kgPath = defaultKgPath(projectRoot);
|
|
7023
|
+
await reindexProject(projectRoot, { kgPath, manifest: snapshot.manifest });
|
|
7024
|
+
const kg = new KGStore(kgPath);
|
|
7025
|
+
let plan;
|
|
7026
|
+
try {
|
|
7027
|
+
plan = planAppFromPrompt({
|
|
7028
|
+
prompt: goal,
|
|
7029
|
+
kg,
|
|
7030
|
+
domain: typeof body.domain === 'string' ? body.domain : undefined,
|
|
7031
|
+
audience: typeof body.audience === 'string' ? body.audience : undefined,
|
|
7032
|
+
mode: body.mode === 'personal' ? 'personal' : 'stakeholder',
|
|
7033
|
+
snapshotId: snapshot.snapshotId,
|
|
7034
|
+
});
|
|
7035
|
+
}
|
|
7036
|
+
finally {
|
|
7037
|
+
kg.close();
|
|
7038
|
+
}
|
|
6702
7039
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6703
|
-
res.end(serializeJSON({ plan,
|
|
7040
|
+
res.end(serializeJSON({ plan, snapshotId: snapshot.snapshotId }));
|
|
6704
7041
|
}
|
|
6705
7042
|
catch (error) {
|
|
6706
7043
|
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -7595,9 +7932,35 @@ export async function startLocalServer(opts) {
|
|
|
7595
7932
|
?? Object.keys(connections)[0]
|
|
7596
7933
|
?? 'default';
|
|
7597
7934
|
const dbtProfiles = discoverDbtProfileConnections(projectRoot, cfg);
|
|
7935
|
+
const activeDbtProfile = dbtProfiles.find((candidate) => candidate.missingFields.length === 0
|
|
7936
|
+
&& !candidate.warnings.some((warning) => warning.startsWith('DuckDB file not found'))
|
|
7937
|
+
&& !candidate.warnings.some((warning) => warning.startsWith('Not the default dbt target')));
|
|
7938
|
+
const activeConnection = connection ? {
|
|
7939
|
+
source: Object.keys(connections).length > 0 ? 'dql_config' : activeDbtProfile ? 'dbt_profile' : 'runtime',
|
|
7940
|
+
driver: connection.driver,
|
|
7941
|
+
...(Object.keys(connections).length === 0 && activeDbtProfile ? { profileId: activeDbtProfile.id } : {}),
|
|
7942
|
+
} : null;
|
|
7598
7943
|
const connectorStatus = getConnectorInstallStatuses(projectRoot);
|
|
7599
7944
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7600
|
-
res.end(serializeJSON({ default: defaultKey, connections, dbtProfiles, connectorStatus }));
|
|
7945
|
+
res.end(serializeJSON({ default: defaultKey, connections, dbtProfiles, activeConnection, connectorStatus }));
|
|
7946
|
+
return;
|
|
7947
|
+
}
|
|
7948
|
+
if (req.method === 'POST' && path === '/api/connections/dbt-profiles/preview') {
|
|
7949
|
+
const body = await readJSON(req).catch(() => ({}));
|
|
7950
|
+
const profilePath = typeof body.path === 'string' ? body.path.trim() : '';
|
|
7951
|
+
if (!profilePath) {
|
|
7952
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7953
|
+
res.end(serializeJSON({ error: 'Enter a profiles.yml file or folder path.' }));
|
|
7954
|
+
return;
|
|
7955
|
+
}
|
|
7956
|
+
const dbtProfiles = discoverDbtProfileConnections(projectRoot, loadProjectConfig(projectRoot), profilePath);
|
|
7957
|
+
if (dbtProfiles.length === 0) {
|
|
7958
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7959
|
+
res.end(serializeJSON({ error: 'No supported dbt profile targets were found at that path.' }));
|
|
7960
|
+
return;
|
|
7961
|
+
}
|
|
7962
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7963
|
+
res.end(serializeJSON({ dbtProfiles }));
|
|
7601
7964
|
return;
|
|
7602
7965
|
}
|
|
7603
7966
|
if (req.method === "GET" && path === "/api/datasets") {
|
|
@@ -10080,7 +10443,7 @@ export function resolveDefaultLLMProvider(projectRoot) {
|
|
|
10080
10443
|
const preferred = ['openai', 'gemini', 'anthropic', 'custom-openai', 'ollama'];
|
|
10081
10444
|
for (const id of preferred) {
|
|
10082
10445
|
const provider = settings.find((item) => item.id === id);
|
|
10083
|
-
if (provider?.enabled && provider.
|
|
10446
|
+
if (provider?.enabled && provider.configured)
|
|
10084
10447
|
return id;
|
|
10085
10448
|
}
|
|
10086
10449
|
return null;
|
|
@@ -10663,6 +11026,18 @@ export function loadProjectConfig(projectRoot) {
|
|
|
10663
11026
|
}
|
|
10664
11027
|
return config;
|
|
10665
11028
|
}
|
|
11029
|
+
/** Current Git-backed Skills folder and what the loader can read from it. */
|
|
11030
|
+
export function buildSkillPathSettings(projectRoot) {
|
|
11031
|
+
const resolvedPath = skillsDir(projectRoot);
|
|
11032
|
+
const loaded = loadSkills(projectRoot);
|
|
11033
|
+
return {
|
|
11034
|
+
path: configuredSkillsPath(projectRoot),
|
|
11035
|
+
resolvedPath,
|
|
11036
|
+
exists: existsSync(resolvedPath) && statSync(resolvedPath).isDirectory(),
|
|
11037
|
+
skillCount: loaded.skills.filter((skill) => skill.scope === 'project').length,
|
|
11038
|
+
errorCount: loaded.errors.length,
|
|
11039
|
+
};
|
|
11040
|
+
}
|
|
10666
11041
|
function dbtFirstOwnsSemanticAuthoring(config) {
|
|
10667
11042
|
return config.manifestVersion === 3 && config.modeling?.mode === 'dbt-first';
|
|
10668
11043
|
}
|
|
@@ -11502,7 +11877,8 @@ function installConnectorPackage(projectRoot, driver) {
|
|
|
11502
11877
|
description: 'Project-local DQL connector packages',
|
|
11503
11878
|
}, null, 2) + '\n', 'utf-8');
|
|
11504
11879
|
}
|
|
11505
|
-
|
|
11880
|
+
const npm = resolveNpmInvocation();
|
|
11881
|
+
execFileSync(npm.command, [...npm.argsPrefix, 'install', '--prefix', installRoot, '--no-audit', '--no-fund', definition.packageSpec], {
|
|
11506
11882
|
cwd: projectRoot,
|
|
11507
11883
|
encoding: 'utf-8',
|
|
11508
11884
|
stdio: 'pipe',
|
|
@@ -12709,9 +13085,18 @@ function readUserPrefs(userPrefsPath) {
|
|
|
12709
13085
|
return { favorites: [], recentlyUsed: [] };
|
|
12710
13086
|
}
|
|
12711
13087
|
const raw = JSON.parse(readFileSync(userPrefsPath, 'utf-8'));
|
|
13088
|
+
const acknowledgedVersion = typeof raw.setup?.acknowledgedVersion === 'string'
|
|
13089
|
+
? raw.setup.acknowledgedVersion.trim()
|
|
13090
|
+
: '';
|
|
12712
13091
|
return {
|
|
12713
13092
|
favorites: Array.isArray(raw.favorites) ? raw.favorites.map(String) : [],
|
|
12714
13093
|
recentlyUsed: Array.isArray(raw.recentlyUsed) ? raw.recentlyUsed.map(String) : [],
|
|
13094
|
+
...(acknowledgedVersion ? {
|
|
13095
|
+
setup: {
|
|
13096
|
+
acknowledgedVersion,
|
|
13097
|
+
...(typeof raw.setup?.acknowledgedAt === 'string' ? { acknowledgedAt: raw.setup.acknowledgedAt } : {}),
|
|
13098
|
+
},
|
|
13099
|
+
} : {}),
|
|
12715
13100
|
};
|
|
12716
13101
|
}
|
|
12717
13102
|
catch {
|
|
@@ -15187,7 +15572,7 @@ async function createBlockStudioAssistProvider(projectRoot, requestedProvider) {
|
|
|
15187
15572
|
const activeProvider = getActiveProvider(projectRoot);
|
|
15188
15573
|
// Subscription CLI providers (Claude Code / Codex) carry no API key — they're
|
|
15189
15574
|
// usable when enabled; their real "installed + logged in" check runs in available().
|
|
15190
|
-
const isUsable = (provider) => provider.enabled &&
|
|
15575
|
+
const isUsable = (provider) => provider.enabled && provider.configured;
|
|
15191
15576
|
const selected = requestedProvider
|
|
15192
15577
|
? settings.find((provider) => provider.id === requestedProvider && isUsable(provider))
|
|
15193
15578
|
: settings.find((provider) => provider.id === activeProvider && provider.enabled)
|
|
@@ -16202,15 +16587,23 @@ function resolveDbtManifestPath(projectRoot, projectConfig = {}) {
|
|
|
16202
16587
|
candidates.push(join(projectRoot, 'target', 'manifest.json'), join(resolve(projectRoot, '..'), 'target', 'manifest.json'), join(resolve(projectRoot, '../dbt'), 'target', 'manifest.json'), join(resolve(projectRoot, '../../dbt'), 'target', 'manifest.json'));
|
|
16203
16588
|
return candidates.find((candidate, index, list) => list.indexOf(candidate) === index && existsSync(candidate));
|
|
16204
16589
|
}
|
|
16205
|
-
export function discoverDbtProfileConnections(projectRoot, projectConfig) {
|
|
16590
|
+
export function discoverDbtProfileConnections(projectRoot, projectConfig, explicitPath) {
|
|
16206
16591
|
const dbtProjectPath = findDbtProjectPath(projectRoot, projectConfig);
|
|
16207
|
-
const projectProfileName = readDbtProjectProfileName(dbtProjectPath);
|
|
16208
|
-
const
|
|
16592
|
+
const projectProfileName = explicitPath ? null : readDbtProjectProfileName(dbtProjectPath);
|
|
16593
|
+
const configuredProfilesDir = projectConfig.dbt?.profilesDir
|
|
16594
|
+
? resolve(projectRoot, projectConfig.dbt.profilesDir)
|
|
16595
|
+
: undefined;
|
|
16596
|
+
const profilePaths = explicitPath
|
|
16597
|
+
? findExplicitDbtProfilePaths(projectRoot, explicitPath)
|
|
16598
|
+
: findDbtProfilePaths(projectRoot, dbtProjectPath, configuredProfilesDir);
|
|
16209
16599
|
const candidates = [];
|
|
16210
16600
|
for (const profilePath of profilePaths) {
|
|
16211
16601
|
const profiles = readYamlFile(profilePath);
|
|
16212
16602
|
if (!profiles)
|
|
16213
16603
|
continue;
|
|
16604
|
+
const profileRelativeBase = explicitPath && existsSync(join(dirname(profilePath), 'dbt_project.yml'))
|
|
16605
|
+
? dirname(profilePath)
|
|
16606
|
+
: dbtProjectPath;
|
|
16214
16607
|
for (const [profileName, rawProfile] of Object.entries(profiles)) {
|
|
16215
16608
|
if (!rawProfile || typeof rawProfile !== 'object')
|
|
16216
16609
|
continue;
|
|
@@ -16234,7 +16627,7 @@ export function discoverDbtProfileConnections(projectRoot, projectConfig) {
|
|
|
16234
16627
|
mapped.connection.filepath &&
|
|
16235
16628
|
mapped.connection.filepath !== ':memory:') {
|
|
16236
16629
|
if (!isAbsoluteLikePath(mapped.connection.filepath)) {
|
|
16237
|
-
mapped.connection.filepath = resolve(
|
|
16630
|
+
mapped.connection.filepath = resolve(profileRelativeBase, mapped.connection.filepath);
|
|
16238
16631
|
}
|
|
16239
16632
|
if (!existsSync(mapped.connection.filepath)) {
|
|
16240
16633
|
mapped.warnings.push(`DuckDB file not found at ${mapped.connection.filepath} — run \`dbt build\` (or \`dbt seed\`) to create it before querying.`);
|
|
@@ -16259,6 +16652,21 @@ export function discoverDbtProfileConnections(projectRoot, projectConfig) {
|
|
|
16259
16652
|
}
|
|
16260
16653
|
return candidates.slice(0, 20);
|
|
16261
16654
|
}
|
|
16655
|
+
export function buildDbtParseArgs(dbtProjectDir, profilesDir) {
|
|
16656
|
+
return [
|
|
16657
|
+
'parse',
|
|
16658
|
+
'--project-dir', dbtProjectDir,
|
|
16659
|
+
...(profilesDir ? ['--profiles-dir', profilesDir] : []),
|
|
16660
|
+
];
|
|
16661
|
+
}
|
|
16662
|
+
/** CFG-003: use a complete default dbt target when no saved DQL connection exists. */
|
|
16663
|
+
export function resolveDbtProfileRuntimeConnection(projectRoot, projectConfig) {
|
|
16664
|
+
const candidates = discoverDbtProfileConnections(projectRoot, projectConfig);
|
|
16665
|
+
const complete = candidates.filter((candidate) => candidate.missingFields.length === 0
|
|
16666
|
+
&& !candidate.warnings.some((warning) => warning.startsWith('DuckDB file not found')));
|
|
16667
|
+
const defaultTarget = complete.find((candidate) => !candidate.warnings.some((warning) => warning.startsWith('Not the default dbt target')));
|
|
16668
|
+
return defaultTarget?.connection ?? (complete.length === 1 ? complete[0].connection : null);
|
|
16669
|
+
}
|
|
16262
16670
|
function findDbtProjectPath(projectRoot, projectConfig) {
|
|
16263
16671
|
const configuredDbtDir = projectConfig.dbt?.projectDir
|
|
16264
16672
|
? resolve(projectRoot, projectConfig.dbt.projectDir)
|
|
@@ -16279,8 +16687,18 @@ function findDbtProjectPath(projectRoot, projectConfig) {
|
|
|
16279
16687
|
?? semanticDbtDir
|
|
16280
16688
|
?? projectRoot;
|
|
16281
16689
|
}
|
|
16282
|
-
function
|
|
16690
|
+
function portableProjectPath(projectRoot, absolutePath) {
|
|
16691
|
+
const normalizedAbsolute = resolve(absolutePath).replace(/\\/g, '/');
|
|
16692
|
+
const relativePath = relative(projectRoot, absolutePath).replace(/\\/g, '/');
|
|
16693
|
+
return !relativePath
|
|
16694
|
+
? '.'
|
|
16695
|
+
: relativePath === '..' || relativePath.startsWith('../')
|
|
16696
|
+
? normalizedAbsolute
|
|
16697
|
+
: relativePath;
|
|
16698
|
+
}
|
|
16699
|
+
function findDbtProfilePaths(projectRoot, dbtProjectPath, configuredProfilesDir) {
|
|
16283
16700
|
const dirs = [
|
|
16701
|
+
configuredProfilesDir,
|
|
16284
16702
|
process.env.DBT_PROFILES_DIR,
|
|
16285
16703
|
dbtProjectPath,
|
|
16286
16704
|
projectRoot,
|
|
@@ -16288,7 +16706,7 @@ function findDbtProfilePaths(projectRoot, dbtProjectPath) {
|
|
|
16288
16706
|
].filter((value) => Boolean(value));
|
|
16289
16707
|
const paths = [];
|
|
16290
16708
|
for (const dir of dirs) {
|
|
16291
|
-
for (const filename of ['profiles.yml', 'profiles.yaml']) {
|
|
16709
|
+
for (const filename of ['profiles.yml', 'profiles.yaml', 'profile.yml', 'profile.yaml']) {
|
|
16292
16710
|
const profilePath = resolve(dir, filename);
|
|
16293
16711
|
if (existsSync(profilePath) && !paths.includes(profilePath)) {
|
|
16294
16712
|
paths.push(profilePath);
|
|
@@ -16297,6 +16715,24 @@ function findDbtProfilePaths(projectRoot, dbtProjectPath) {
|
|
|
16297
16715
|
}
|
|
16298
16716
|
return paths;
|
|
16299
16717
|
}
|
|
16718
|
+
function findExplicitDbtProfilePaths(projectRoot, input) {
|
|
16719
|
+
const trimmed = input.trim();
|
|
16720
|
+
if (!trimmed)
|
|
16721
|
+
return [];
|
|
16722
|
+
const expanded = trimmed === '~'
|
|
16723
|
+
? homedir()
|
|
16724
|
+
: trimmed.startsWith('~/')
|
|
16725
|
+
? join(homedir(), trimmed.slice(2))
|
|
16726
|
+
: trimmed;
|
|
16727
|
+
const absolute = resolve(projectRoot, expanded);
|
|
16728
|
+
if (!existsSync(absolute))
|
|
16729
|
+
return [];
|
|
16730
|
+
if (!statSync(absolute).isDirectory())
|
|
16731
|
+
return [absolute];
|
|
16732
|
+
return ['profiles.yml', 'profiles.yaml', 'profile.yml', 'profile.yaml']
|
|
16733
|
+
.map((filename) => join(absolute, filename))
|
|
16734
|
+
.filter((profilePath) => existsSync(profilePath));
|
|
16735
|
+
}
|
|
16300
16736
|
function readDbtProjectProfileName(dbtProjectPath) {
|
|
16301
16737
|
const projectFile = join(dbtProjectPath, 'dbt_project.yml');
|
|
16302
16738
|
const projectYaml = readYamlFile(projectFile);
|
|
@@ -16904,7 +17340,12 @@ async function readGitStatus(cwd) {
|
|
|
16904
17340
|
behind = Number(match[0] ?? 0);
|
|
16905
17341
|
ahead = Number(match[1] ?? 0);
|
|
16906
17342
|
}
|
|
16907
|
-
|
|
17343
|
+
// UI-001, E2E-001: the Source Control surface is file-oriented. Git's
|
|
17344
|
+
// "normal" untracked mode collapses a new directory into a single `?? dir/`
|
|
17345
|
+
// record, which prevents the UI from showing or diffing the files inside it.
|
|
17346
|
+
// Enumerate every untracked path so tracked and untracked changes share the
|
|
17347
|
+
// same per-file review contract.
|
|
17348
|
+
const statusRes = await execGit(gitRoot, ['status', '--porcelain=v1', '--untracked-files=all']);
|
|
16908
17349
|
const changes = [];
|
|
16909
17350
|
if (statusRes.code === 0) {
|
|
16910
17351
|
for (const line of statusRes.stdout.split('\n')) {
|
|
@@ -17556,7 +17997,11 @@ async function testProviderConfig(projectRoot, id, overrides) {
|
|
|
17556
17997
|
const base = getEffectiveProviderConfig(projectRoot, id);
|
|
17557
17998
|
// When the user supplies inline values (testing what they typed before saving),
|
|
17558
17999
|
// merge them over the saved config and test reachability regardless of enabled.
|
|
17559
|
-
|
|
18000
|
+
// The test route always passes an override object, even when every draft field
|
|
18001
|
+
// is blank. That still means "test this candidate before save" and must allow
|
|
18002
|
+
// a previously configured-but-disabled provider to be reactivated without
|
|
18003
|
+
// forcing the user to paste its redacted secret again.
|
|
18004
|
+
const inline = overrides !== undefined;
|
|
17560
18005
|
const config = {
|
|
17561
18006
|
...base,
|
|
17562
18007
|
...(overrides?.apiKey ? { apiKey: overrides.apiKey } : {}),
|
|
@@ -17636,15 +18081,19 @@ async function testOpenAIProviderConfig(config, label, details) {
|
|
|
17636
18081
|
return { ok: false, message: `${label} is not configured${details}. Add an API key in Settings or OPENAI_API_KEY.` };
|
|
17637
18082
|
}
|
|
17638
18083
|
try {
|
|
17639
|
-
|
|
17640
|
-
|
|
17641
|
-
|
|
17642
|
-
|
|
17643
|
-
|
|
18084
|
+
// Use the same provider adapter as governed Ask so enterprise gateways are
|
|
18085
|
+
// tested against the runtime path users will actually execute.
|
|
18086
|
+
const provider = new OpenAIProvider({
|
|
18087
|
+
apiKey: config.apiKey,
|
|
18088
|
+
baseUrl: config.baseUrl,
|
|
18089
|
+
model: config.model,
|
|
17644
18090
|
});
|
|
18091
|
+
const response = await provider.generate([
|
|
18092
|
+
{ role: 'user', content: 'Reply with exactly: OK' },
|
|
18093
|
+
], { maxTokens: 16, temperature: 0 });
|
|
17645
18094
|
return {
|
|
17646
18095
|
ok: true,
|
|
17647
|
-
message: `${label}
|
|
18096
|
+
message: `${label} responded through the governed runtime${details}: ${response.trim().slice(0, 80) || 'OK'}`,
|
|
17648
18097
|
};
|
|
17649
18098
|
}
|
|
17650
18099
|
catch (error) {
|
|
@@ -17659,20 +18108,17 @@ async function testAnthropicProviderConfig(config, label, details) {
|
|
|
17659
18108
|
return { ok: false, message: `${label} is not configured${details}. Add an API key in Settings or ANTHROPIC_API_KEY.` };
|
|
17660
18109
|
}
|
|
17661
18110
|
try {
|
|
17662
|
-
const
|
|
18111
|
+
const provider = new ClaudeProvider({
|
|
17663
18112
|
apiKey: config.apiKey,
|
|
17664
|
-
|
|
18113
|
+
baseUrl: config.baseUrl,
|
|
18114
|
+
model: config.model,
|
|
17665
18115
|
});
|
|
17666
|
-
const response = await
|
|
17667
|
-
|
|
17668
|
-
|
|
17669
|
-
temperature: 0,
|
|
17670
|
-
messages: [{ role: 'user', content: 'Reply with exactly: OK' }],
|
|
17671
|
-
});
|
|
17672
|
-
const text = response.content?.filter((block) => block.type === 'text').map((block) => block.text ?? '').join('') ?? '';
|
|
18116
|
+
const response = await provider.generate([
|
|
18117
|
+
{ role: 'user', content: 'Reply with exactly: OK' },
|
|
18118
|
+
], { maxTokens: 16, temperature: 0 });
|
|
17673
18119
|
return {
|
|
17674
18120
|
ok: true,
|
|
17675
|
-
message: `${label}
|
|
18121
|
+
message: `${label} responded through the governed runtime${details}: ${response.trim().slice(0, 80) || 'OK'}`,
|
|
17676
18122
|
};
|
|
17677
18123
|
}
|
|
17678
18124
|
catch (error) {
|