@duckcodeailabs/dql-cli 1.11.10 → 1.12.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.
@@ -9,6 +9,7 @@ import { dirname, extname, join, normalize, relative, resolve, sep, } from "node
9
9
  import { Readable } from "node:stream";
10
10
  import { buildMixedSourceWarehouseFallbackSql, findMentionedNotebookDataset, planMixedSourceNotebookSql, planMixedSourceSql, } from "./mixed-source-sql.js";
11
11
  import { resolveNpmInvocation } from './npm-runtime.js';
12
+ import { ensureDqlGitignore, isGovernedSourceFile, isLegacyBroadDqlIgnore, } from './git-contract.js';
12
13
  import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, hasStandaloneSemanticRef, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
13
14
  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';
14
15
  import { load as loadYaml } from 'js-yaml';
@@ -19,7 +20,7 @@ import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from '
19
20
  import { resolveRetrievalHealthStatus } from './retrieval-health.js';
20
21
  import { createDqlAgentProviderRunner, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
21
22
  import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
22
- import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, recallRelevantTurns, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, defaultConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, propose, proposePlan, recordCorrectionTrace, listHintsFromGit, evaluateHint, emitCorrectionEvalCase, mineJoinPatterns, reviewHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, projectEmbeddingProvider, isHashedEmbeddingProvider, clearProjectEmbeddingCache, upgradeVectorIndexForProject, openMetadataCatalog, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, } from '@duckcodeailabs/dql-agent';
23
+ 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, 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, } from '@duckcodeailabs/dql-agent';
23
24
  import { gatherProposeEnrichment } from './propose-enrich.js';
24
25
  import { handleAppsApi, proposeAppAiBuild, recommendVisualization } from './apps-api.js';
25
26
  import { getActiveProvider, getEffectiveProviderConfig, isProviderSettingsId, listProviderSettings, saveProviderSettings, } from './settings/provider-settings.js';
@@ -39,6 +40,7 @@ import { observeWarehouseTargetIdentity } from './semantic-execution/connection-
39
40
  import { configuredWarehouseTargetIdentity } from './semantic-execution/connection-identity.js';
40
41
  import { executeTargetBoundSemanticQuery, semanticExecutionFailureCode, semanticExecutionFailureDetails, } from './semantic-execution/execution-gateway.js';
41
42
  import { buildSemanticTargetBinding } from './semantic-execution/target-binding.js';
43
+ import { normalizeConnectionMetadataScope, syncWarehouseMetadata, warehouseMetadataStatus, } from './warehouse-metadata.js';
42
44
  import { NotebookDatasetWorkspace, } from "./notebook-datasets.js";
43
45
  import { prepareBlockInvocation } from './block-invocation.js';
44
46
  const NOTEBOOK_EXECUTE_PREVIEW_ROW_LIMIT = 500;
@@ -418,6 +420,8 @@ async function conversationContextFromThread(store, threadId, clientContext, que
418
420
  sourceCertifiedBlock: turn.sourceCertifiedBlock,
419
421
  route: turn.route,
420
422
  trustLabel: turn.trustLabel,
423
+ runStatus: turn.runStatus,
424
+ stopReason: turn.stopReason,
421
425
  certification: turn.certification,
422
426
  contextPackId: turn.contextPackId,
423
427
  dqlArtifact: turn.dqlArtifact,
@@ -453,6 +457,7 @@ async function conversationContextFromThread(store, threadId, clientContext, que
453
457
  ...(clientContext ?? {}),
454
458
  conversationStateVersion: 1,
455
459
  threadId,
460
+ ...(serverSnapshot ? { conversationEnvelope: serverSnapshot } : {}),
456
461
  ...(serverSnapshot ? { serverSnapshot } : {}),
457
462
  ...(thread?.rollingSummary ? { conversationSummary: thread.rollingSummary } : {}),
458
463
  ...(turns.length > 0 ? { turns, activeTurnId: turns[turns.length - 1].id } : {}),
@@ -509,6 +514,8 @@ export function conversationTurnInputFromRun(run) {
509
514
  answerText: run.answer,
510
515
  route: run.route,
511
516
  trustLabel: agentRunString(payload?.trustLabel) ?? run.trustState,
517
+ runStatus: run.status,
518
+ stopReason: run.stopReason,
512
519
  certification: agentRunString(payload?.certification),
513
520
  sourceCertifiedBlock: agentRunString(payload?.sourceCertifiedBlock)
514
521
  ?? (artifact?.kind === 'answer' ? agentRunString(artifact.ref) : undefined),
@@ -640,6 +647,17 @@ export async function startLocalServer(opts) {
640
647
  ensureLocalRuntimeGitignore(projectRoot);
641
648
  let projectConfig = loadProjectConfig(projectRoot);
642
649
  recordAgentRuntimeVersion(projectRoot, runtimeVersion);
650
+ // A clone carries governed Hint Graph files in Git, never the rebuildable
651
+ // `.dql/cache` database. Materialize that projection as soon as the Notebook
652
+ // opens so Agent Learning and the first retrieval see the same shared truth.
653
+ // Malformed/local cache state must not prevent the rest of the OSS notebook
654
+ // from starting; normal review/reindex surfaces can still report and repair it.
655
+ try {
656
+ ensureHintIndexFresh(projectRoot);
657
+ }
658
+ catch {
659
+ // Best-effort bootstrap, consistent with the existing agent reindex path.
660
+ }
643
661
  const configuredConnection = rawConnection
644
662
  ? normalizeProjectConnection(rawConnection, projectRoot)
645
663
  : projectConfig.defaultConnection
@@ -1304,7 +1322,7 @@ export async function startLocalServer(opts) {
1304
1322
  semanticDriver = undefined;
1305
1323
  }
1306
1324
  const semanticTableMapping = semanticLayer && semanticConnection
1307
- ? await resolveSemanticTableMapping(executor, semanticConnection, semanticLayer)
1325
+ ? await resolveSemanticTableMapping(executor, semanticConnection, semanticLayer, projectRoot)
1308
1326
  : undefined;
1309
1327
  const requestedDomain = normalizeAgentRunDomain(agentRunWorkspaceValue(request, 'domain'));
1310
1328
  const requestedPurpose = agentRunWorkspaceValue(request, 'purpose');
@@ -2188,13 +2206,16 @@ export async function startLocalServer(opts) {
2188
2206
  message: 'Building catalog-grounded research plan.',
2189
2207
  route: 'research',
2190
2208
  });
2209
+ const conversationHistory = request.history?.length
2210
+ ? request.history
2211
+ : conversationHistoryFromContext(request.conversationContext);
2191
2212
  const plan = await planResearch({
2192
2213
  question: request.question,
2193
2214
  metrics,
2194
2215
  blocks,
2195
2216
  intent: request.intent,
2196
- isFollowUp: Boolean(request.history?.length),
2197
- history: request.history,
2217
+ isFollowUp: conversationHistory.length > 0,
2218
+ history: conversationHistory,
2198
2219
  // When the user explicitly picked research, investigate — don't collapse to one step.
2199
2220
  forceInvestigate: request.requestedMode === 'research',
2200
2221
  rootPlan: routeDecision?.resolvedAnalyticalPlan,
@@ -2663,7 +2684,8 @@ export async function startLocalServer(opts) {
2663
2684
  // this only inside the provider adapter allowed stale catalog matches to win
2664
2685
  // before "they" / "this amount" became customer-scoped context.
2665
2686
  const followUp = resolveAgentFollowUpContext(request.conversationContext, request.question);
2666
- const serverSnapshot = agentRunRecord(request.conversationContext?.serverSnapshot);
2687
+ const serverSnapshot = agentRunRecord(request.conversationContext?.conversationEnvelope
2688
+ ?? request.conversationContext?.serverSnapshot);
2667
2689
  const topicRelation = agentRunString(serverSnapshot?.topicRelation);
2668
2690
  // The readiness marker is source-versioned. When it matches, pass the
2669
2691
  // already-built metadata identity into retrieval so buildLocalContextPack
@@ -2815,6 +2837,7 @@ export async function startLocalServer(opts) {
2815
2837
  path: defaultAgentRunSqlitePath(projectRoot),
2816
2838
  legacyJsonPath: defaultAgentRunStorePath(projectRoot),
2817
2839
  });
2840
+ const conversationStorePath = await prepareConversationPath(projectRoot);
2818
2841
  // A run may outlive its streaming browser connection, so cancellation is
2819
2842
  // server-owned and keyed by run id rather than relying on fetch abort alone.
2820
2843
  const activeAgentRunControllers = new Map();
@@ -2826,7 +2849,7 @@ export async function startLocalServer(opts) {
2826
2849
  if (conversationStoreInstance !== undefined)
2827
2850
  return conversationStoreInstance;
2828
2851
  try {
2829
- conversationStoreInstance = new ConversationStore(defaultConversationPath(projectRoot));
2852
+ conversationStoreInstance = new ConversationStore(conversationStorePath);
2830
2853
  }
2831
2854
  catch {
2832
2855
  conversationStoreInstance = null;
@@ -2882,7 +2905,7 @@ export async function startLocalServer(opts) {
2882
2905
  };
2883
2906
  const resolved = resolveNotebookBlockReferenceCell(cell, projectRoot);
2884
2907
  const activeConnection = requireActiveConnection();
2885
- const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer);
2908
+ const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer, projectRoot);
2886
2909
  const semanticCompose = resolved.cell.type === 'dql'
2887
2910
  && semanticLayer
2888
2911
  && /\btype\s*=\s*"semantic"/i.test(resolved.cell.source)
@@ -2969,7 +2992,7 @@ export async function startLocalServer(opts) {
2969
2992
  const activeConnection = requireActiveConnection();
2970
2993
  const precompiledSemanticQuery = Boolean(extractBlockStudioSql(source));
2971
2994
  const tableMapping = !precompiledSemanticQuery
2972
- ? await resolveSemanticTableMapping(executor, activeConnection, semanticLayer)
2995
+ ? await resolveSemanticTableMapping(executor, activeConnection, semanticLayer, projectRoot)
2973
2996
  : undefined;
2974
2997
  const semanticCompose = semanticLayer
2975
2998
  ? await composeSemanticBlockSqlForRuntime(source, semanticLayer, {
@@ -3231,6 +3254,13 @@ export async function startLocalServer(opts) {
3231
3254
  };
3232
3255
  };
3233
3256
  const catalogContext = await buildAgentSchemaContextFromCatalog(projectRoot, question, preparedContextPack).catch(() => []);
3257
+ const activeScope = connection
3258
+ ? optionalActiveConnectionMetadataScope(projectRoot, projectConfig, connection, projectConfig.defaultConnectionName ?? 'default')
3259
+ : null;
3260
+ const activeRuntimeSnapshot = latestRuntimeSchemaSnapshotForProject(projectRoot);
3261
+ const hasActivatedMetadataGeneration = Boolean(activeScope
3262
+ && activeRuntimeSnapshot?.scopeFingerprint === activeScope.scopeFingerprint
3263
+ && activeRuntimeSnapshot.status !== 'stale');
3234
3264
  const valueGrounding = resolveAgentRuntimeValueGrounding(projectConfig);
3235
3265
  if (catalogContext.length > 0) {
3236
3266
  if (!connection || valueGrounding.mode !== 'safe_automatic')
@@ -3239,9 +3269,17 @@ export async function startLocalServer(opts) {
3239
3269
  // row value must not turn a warm Ask into an information_schema scan over
3240
3270
  // thousands of enterprise tables; only field-scoped value probes are live.
3241
3271
  const enriched = await enrichAgentSchemaContextWithValueMatches(question, catalogContext, executor, connection, valueGrounding.searchSafeColumns);
3242
- recordAgentRuntimeSchemaSnapshot(projectRoot, catalogContext, 'catalog runtime schema');
3272
+ if (!hasActivatedMetadataGeneration) {
3273
+ recordAgentRuntimeSchemaSnapshot(projectRoot, catalogContext, 'catalog runtime schema');
3274
+ }
3243
3275
  return enriched;
3244
3276
  }
3277
+ // An activated, scope-qualified warehouse generation is the complete warm
3278
+ // metadata authority. A lexical miss is not permission to rescan every
3279
+ // visible schema for each question; the governed planner may clarify or the
3280
+ // user may explicitly refresh the selected scope.
3281
+ if (hasActivatedMetadataGeneration)
3282
+ return [];
3245
3283
  if (!connection)
3246
3284
  return [];
3247
3285
  try {
@@ -4039,15 +4077,7 @@ export async function startLocalServer(opts) {
4039
4077
  const activeConnection = requireActiveConnection(targetConnection);
4040
4078
  let tableMapping;
4041
4079
  if (semanticLayer) {
4042
- try {
4043
- const tablesResult = await executor.executeQuery(`SELECT table_schema, table_name
4044
- FROM information_schema.tables
4045
- WHERE UPPER(table_schema) NOT IN ('INFORMATION_SCHEMA', 'PG_CATALOG')`, [], {}, activeConnection);
4046
- tableMapping = buildSemanticTableMapping(semanticLayer, tablesResult.rows);
4047
- }
4048
- catch {
4049
- tableMapping = undefined;
4050
- }
4080
+ tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer, projectRoot);
4051
4081
  }
4052
4082
  const invocation = prepareBlockInvocation({ source, parameters, surface: 'block_studio' });
4053
4083
  if (invocation.errors.length > 0)
@@ -4091,7 +4121,7 @@ export async function startLocalServer(opts) {
4091
4121
  const runBlockStudioTestSummary = async (source, targetConnection) => {
4092
4122
  const activeConnection = requireActiveConnection(targetConnection);
4093
4123
  const start = Date.now();
4094
- const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer);
4124
+ const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer, projectRoot);
4095
4125
  // Run tests against the SAME SQL the preview runs: for a semantic block with a
4096
4126
  // pre-compiled query, that's the query (not a recompiled metric), so the test's
4097
4127
  // output columns match the block's declared outputs.
@@ -4810,6 +4840,18 @@ export async function startLocalServer(opts) {
4810
4840
  }));
4811
4841
  return;
4812
4842
  }
4843
+ if (req.method === 'GET' && path === '/api/domain-workspaces/related-products') {
4844
+ const requestId = apiRequestId('domain-related-products');
4845
+ const snapshot = projectSnapshot();
4846
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
4847
+ res.end(serializeJSON({
4848
+ requestId,
4849
+ domain: null,
4850
+ ...relatedProductsForDomain(snapshot.manifest, undefined, scanNotebookFiles(projectRoot)),
4851
+ snapshotId: snapshot.snapshotId,
4852
+ }));
4853
+ return;
4854
+ }
4813
4855
  if (req.method === 'GET' && path.startsWith('/api/domain-workspaces/')) {
4814
4856
  const requestId = apiRequestId('domain-workspace');
4815
4857
  const suffix = decodeURIComponent(path.slice('/api/domain-workspaces/'.length));
@@ -4842,7 +4884,7 @@ export async function startLocalServer(opts) {
4842
4884
  }
4843
4885
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
4844
4886
  res.end(serializeJSON(suffix.endsWith(relatedSuffix)
4845
- ? { requestId, domain: domainId, ...relatedProductsForDomain(manifest, domainId), snapshotId: snapshot.snapshotId }
4887
+ ? { requestId, domain: domainId, ...relatedProductsForDomain(manifest, domainId, scanNotebookFiles(projectRoot)), snapshotId: snapshot.snapshotId }
4846
4888
  : {
4847
4889
  requestId,
4848
4890
  snapshotId: snapshot.snapshotId,
@@ -6588,32 +6630,180 @@ export async function startLocalServer(opts) {
6588
6630
  res.end(serializeJSON({ ok: true, files }));
6589
6631
  return;
6590
6632
  }
6591
- // Local learning loop (OSS): record an analyst's wrong→right correction as a
6592
- // scope-matched Hint-Graph hint plus an advisory memory, so future similar
6593
- // questions avoid the same mistake. Single-user self-serve the correction IS
6594
- // the approval, so the derived candidate is approved immediately unless the
6595
- // caller opts out. Advisory only: never overrides certified routing. The
6596
- // multi-tenant review workflow + automated distillation stay a cloud feature.
6597
- // Hint review queue. Corrections are captured as CANDIDATES; a hint only
6598
- // starts shaping other people's answers once a human approves it here.
6633
+ // CTX-005 / API-003 / E2E-001 local-first Hint Graph review queue.
6634
+ // Corrections are captured as
6635
+ // candidates; only the shared governed lifecycle can evaluate and approve
6636
+ // them for future advisory retrieval.
6599
6637
  if (req.method === 'GET' && path === '/api/agent/hints') {
6600
6638
  try {
6601
6639
  const status = url.searchParams.get('status');
6602
6640
  const all = listHintsFromGit(projectRoot);
6603
- const hints = status ? all.filter((hint) => hint.status === status) : all;
6641
+ const selected = (status ? all.filter((hint) => hint.status === status) : all)
6642
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
6643
+ .slice(0, 200);
6644
+ const hintIndex = new HintStore(defaultHintIndexPath(projectRoot));
6645
+ const graphEdgesByHint = new Map(selected.map((hint) => [hint.id, hintIndex.edgesForHint(hint.id)]));
6646
+ hintIndex.close();
6647
+ const snapshotId = projectSnapshot().snapshotId;
6648
+ const approved = all.filter((hint) => hint.status === 'approved');
6649
+ const inspectionById = new Map(await Promise.all(all
6650
+ .filter((hint) => hint.status === 'candidate' || hint.status === 'approved')
6651
+ .map(async (hint) => [
6652
+ hint.id,
6653
+ await inspectGovernedHint(projectRoot, { hintId: hint.id, snapshotId }),
6654
+ ])));
6655
+ // Match retrieval truth: stale/invalid approved hints cannot create a
6656
+ // conflict or supersede another otherwise-current hint.
6657
+ const eligibleApproved = approved.filter((hint) => inspectionById.get(hint.id)?.state === 'current');
6658
+ const conflictIds = new Map();
6659
+ for (let left = 0; left < eligibleApproved.length; left += 1) {
6660
+ for (let right = left + 1; right < eligibleApproved.length; right += 1) {
6661
+ if (!hintsConflict(eligibleApproved[left], eligibleApproved[right]))
6662
+ continue;
6663
+ conflictIds.set(eligibleApproved[left].id, [...(conflictIds.get(eligibleApproved[left].id) ?? []), eligibleApproved[right].id]);
6664
+ conflictIds.set(eligibleApproved[right].id, [...(conflictIds.get(eligibleApproved[right].id) ?? []), eligibleApproved[left].id]);
6665
+ }
6666
+ }
6667
+ const supersededIds = new Map(eligibleApproved.filter((hint) => hint.supersedes).map((hint) => [hint.supersedes, hint.id]));
6668
+ const hints = await Promise.all(selected.map(async (hint) => {
6669
+ const inspection = inspectionById.get(hint.id) ?? null;
6670
+ const exclusions = [
6671
+ ...(inspection && inspection.state !== 'current'
6672
+ ? [{ reason: inspection.state, detail: inspection.checks.filter((check) => !check.passed).map((check) => check.evidence).join(' ') }]
6673
+ : []),
6674
+ ...(supersededIds.has(hint.id)
6675
+ ? [{ reason: 'superseded', detail: `Superseded by ${supersededIds.get(hint.id)}.`, relatedHintId: supersededIds.get(hint.id) }]
6676
+ : []),
6677
+ ...((conflictIds.get(hint.id) ?? []).map((relatedHintId) => ({
6678
+ reason: 'conflict',
6679
+ detail: `Conflicts with approved hint ${relatedHintId}.`,
6680
+ relatedHintId,
6681
+ }))),
6682
+ ];
6683
+ return {
6684
+ ...hint,
6685
+ graphEdges: graphEdgesByHint.get(hint.id) ?? [],
6686
+ trace: hint.traceId ? getCorrectionTraceFromGit(projectRoot, hint.traceId) : undefined,
6687
+ evaluation: hint.evaluationId ? getHintEvaluationFromGit(projectRoot, hint.evaluationId) : undefined,
6688
+ inspection: inspection ? {
6689
+ currentSnapshotId: inspection.currentSnapshotId,
6690
+ snapshotCurrent: inspection.snapshotCurrent,
6691
+ dependenciesCurrent: inspection.dependenciesCurrent,
6692
+ driftedDependencies: inspection.driftedDependencies,
6693
+ checks: inspection.checks,
6694
+ state: inspection.state,
6695
+ } : undefined,
6696
+ exclusions,
6697
+ };
6698
+ }));
6604
6699
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
6605
6700
  res.end(serializeJSON({
6606
- hints: hints.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).slice(0, 200),
6701
+ hints,
6607
6702
  counts: {
6608
6703
  candidate: all.filter((hint) => hint.status === 'candidate').length,
6609
6704
  approved: all.filter((hint) => hint.status === 'approved').length,
6610
6705
  rejected: all.filter((hint) => hint.status === 'rejected').length,
6706
+ retired: all.filter((hint) => hint.status === 'retired').length,
6611
6707
  },
6612
6708
  }));
6613
6709
  }
6614
6710
  catch (error) {
6615
6711
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
6616
- res.end(serializeJSON({ hints: [], counts: { candidate: 0, approved: 0, rejected: 0 }, error: error instanceof Error ? error.message : String(error) }));
6712
+ res.end(serializeJSON({ hints: [], counts: { candidate: 0, approved: 0, rejected: 0, retired: 0 }, error: error instanceof Error ? error.message : String(error) }));
6713
+ }
6714
+ return;
6715
+ }
6716
+ const hintEditMatch = /^\/api\/agent\/hints\/([^/]+)$/.exec(path);
6717
+ if (req.method === 'PATCH' && hintEditMatch) {
6718
+ try {
6719
+ const hintId = decodeURIComponent(hintEditMatch[1]);
6720
+ const body = await readJSON(req).catch(() => null);
6721
+ if (!body) {
6722
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
6723
+ res.end(serializeJSON({ ok: false, error: 'Candidate fields are required.' }));
6724
+ return;
6725
+ }
6726
+ const rawScope = body.scope && typeof body.scope === 'object' ? body.scope : undefined;
6727
+ const scopeValue = (key) => {
6728
+ const value = rawScope?.[key];
6729
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
6730
+ };
6731
+ const hint = await editGovernedHintCandidate(projectRoot, {
6732
+ hintId,
6733
+ title: typeof body.title === 'string' ? body.title : undefined,
6734
+ guidance: typeof body.guidance === 'string' ? body.guidance : undefined,
6735
+ correctedSql: typeof body.correctedSql === 'string' ? body.correctedSql : undefined,
6736
+ scope: rawScope ? {
6737
+ metric: scopeValue('metric'),
6738
+ dbtModel: scopeValue('dbtModel'),
6739
+ domain: scopeValue('domain'),
6740
+ dialect: scopeValue('dialect'),
6741
+ term: scopeValue('term'),
6742
+ block: scopeValue('block'),
6743
+ } : undefined,
6744
+ snapshotId: projectSnapshot().snapshotId,
6745
+ });
6746
+ if (!hint) {
6747
+ res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
6748
+ res.end(serializeJSON({ ok: false, error: 'Hint not found.' }));
6749
+ return;
6750
+ }
6751
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
6752
+ res.end(serializeJSON({ ok: true, hint }));
6753
+ }
6754
+ catch (error) {
6755
+ res.writeHead(error instanceof Error && error.name === 'HintLifecycleError' ? 409 : 500, { 'Content-Type': 'application/json; charset=utf-8' });
6756
+ res.end(serializeJSON({ ok: false, error: error instanceof Error ? error.message : String(error) }));
6757
+ }
6758
+ return;
6759
+ }
6760
+ const hintLifecycleMatch = /^\/api\/agent\/hints\/([^/]+)\/lifecycle$/.exec(path);
6761
+ if (req.method === 'POST' && hintLifecycleMatch) {
6762
+ try {
6763
+ const hintId = decodeURIComponent(hintLifecycleMatch[1]);
6764
+ const body = await readJSON(req).catch(() => null);
6765
+ const action = body?.action;
6766
+ const reviewer = typeof body?.reviewer === 'string' && body.reviewer.trim()
6767
+ ? body.reviewer.trim()
6768
+ : (resolveLocalOwner(projectRoot) ?? 'local');
6769
+ const note = typeof body?.note === 'string' && body.note.trim() ? body.note.trim() : undefined;
6770
+ let result;
6771
+ if (action === 'retire') {
6772
+ result = retireHint(projectRoot, { hintId, reviewer, note });
6773
+ }
6774
+ else if (action === 'reopen') {
6775
+ result = await reopenGovernedHint(projectRoot, {
6776
+ hintId,
6777
+ reviewer,
6778
+ note,
6779
+ snapshotId: projectSnapshot().snapshotId,
6780
+ });
6781
+ }
6782
+ else if (action === 'supersede') {
6783
+ const targetHintId = typeof body?.targetHintId === 'string' ? body.targetHintId.trim() : '';
6784
+ if (!targetHintId) {
6785
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
6786
+ res.end(serializeJSON({ ok: false, error: 'targetHintId is required for supersede.' }));
6787
+ return;
6788
+ }
6789
+ result = supersedeHint(projectRoot, { hintId, targetHintId, reviewer, note });
6790
+ }
6791
+ else {
6792
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
6793
+ res.end(serializeJSON({ ok: false, error: "action must be 'retire', 'reopen', or 'supersede'." }));
6794
+ return;
6795
+ }
6796
+ if (!result) {
6797
+ res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
6798
+ res.end(serializeJSON({ ok: false, error: 'Hint not found.' }));
6799
+ return;
6800
+ }
6801
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
6802
+ res.end(serializeJSON({ ok: true, hint: result.hint, review: result.review }));
6803
+ }
6804
+ catch (error) {
6805
+ res.writeHead(error instanceof Error && error.name === 'HintLifecycleError' ? 409 : 500, { 'Content-Type': 'application/json; charset=utf-8' });
6806
+ res.end(serializeJSON({ ok: false, error: error instanceof Error ? error.message : String(error) }));
6617
6807
  }
6618
6808
  return;
6619
6809
  }
@@ -6633,85 +6823,30 @@ export async function startLocalServer(opts) {
6633
6823
  : (resolveLocalOwner(projectRoot) ?? 'local');
6634
6824
  const note = typeof body?.note === 'string' && body.note.trim() ? body.note.trim() : undefined;
6635
6825
  const snapshotId = projectSnapshot().snapshotId;
6636
- // dbt-first v3 requires a PASSED evaluation artifact recorded against the
6637
- // same snapshot before a candidate may be approved. Approving from the
6638
- // review queue therefore runs the hint's required evaluation first: the
6639
- // corrected SQL is re-validated against the current project so a hint can
6640
- // never be promoted on evidence that has gone stale.
6641
- if (decision === 'approved') {
6642
- const pending = listHintsFromGit(projectRoot).find((entry) => entry.id === hintId);
6643
- if (pending?.status === 'candidate' && pending.requiredEvaluation && !pending.evaluationId) {
6644
- // The evaluation must be a real check, not a rubber stamp: the
6645
- // corrected SQL has to parse and reference at least one relation
6646
- // against the CURRENT project. A correction that no longer analyses
6647
- // cannot be promoted into everyone's prompt.
6648
- const validation = (() => {
6649
- const sql = pending.correctedSql?.trim();
6650
- if (!sql)
6651
- return { ok: false, error: 'The correction carries no SQL to verify.' };
6652
- try {
6653
- const analysis = analyzeSqlReferences(sql);
6654
- if (!analysis.tables.length)
6655
- return { ok: false, error: 'Corrected SQL references no known relation.' };
6656
- return { ok: true };
6657
- }
6658
- catch (error) {
6659
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
6660
- }
6661
- })();
6662
- try {
6663
- evaluateHint(projectRoot, {
6664
- hintId,
6665
- snapshotId,
6666
- evaluator: reviewer,
6667
- evaluation: pending.requiredEvaluation,
6668
- checks: [{
6669
- name: 'corrected-sql-validates',
6670
- passed: validation.ok,
6671
- evidence: validation.ok ? 'Corrected SQL passed local context validation.' : validation.error,
6672
- }],
6673
- evidence: [`snapshot: ${snapshotId}`, `reviewer: ${reviewer}`],
6674
- });
6675
- }
6676
- catch (evaluationError) {
6677
- res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
6678
- res.end(serializeJSON({ ok: false, error: evaluationError instanceof Error ? evaluationError.message : String(evaluationError) }));
6679
- return;
6680
- }
6681
- }
6682
- }
6683
- const reviewed = reviewHint(projectRoot, { hintId, decision, reviewer, snapshotId, ...(note ? { note } : {}) });
6826
+ const reviewed = await reviewGovernedHint(projectRoot, {
6827
+ hintId,
6828
+ decision,
6829
+ reviewer,
6830
+ snapshotId,
6831
+ ...(note ? { note } : {}),
6832
+ ...(decision === 'approved' ? {
6833
+ executeSql: async (sql, execution) => executeGeneratedSqlForAgent(sql, {
6834
+ ...generatedSqlArtifactContract(execution.question, sql),
6835
+ limit: execution.rowLimit,
6836
+ }),
6837
+ } : {}),
6838
+ });
6684
6839
  if (!reviewed) {
6685
6840
  res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
6686
6841
  res.end(serializeJSON({ ok: false, error: 'Hint not found.' }));
6687
6842
  return;
6688
6843
  }
6689
6844
  const hint = reviewed.hint;
6690
- // Keep the mirrored advisory memory in step with the decision, so an
6691
- // approved lesson is recalled and a rejected one stops being recalled.
6692
- try {
6693
- const memory = new MemoryStore(defaultMemoryPath(projectRoot));
6694
- memory.upsert({
6695
- id: `mem_${hint.id}`,
6696
- scope: 'project',
6697
- title: hint.title,
6698
- content: hint.guidance,
6699
- tags: [hint.scope.metric, hint.scope.domain, hint.scope.dbtModel].filter((x) => Boolean(x)),
6700
- source: 'correction',
6701
- confidence: 0.9,
6702
- importance: 0.85,
6703
- enabled: decision === 'approved',
6704
- });
6705
- memory.close();
6706
- }
6707
- catch {
6708
- /* best-effort */
6709
- }
6710
6845
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
6711
- res.end(serializeJSON({ ok: true, hint }));
6846
+ res.end(serializeJSON({ ok: true, hint, evaluation: reviewed.evaluation }));
6712
6847
  }
6713
6848
  catch (error) {
6714
- res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
6849
+ res.writeHead(error instanceof Error && error.name === 'HintLifecycleError' ? 409 : 500, { 'Content-Type': 'application/json; charset=utf-8' });
6715
6850
  res.end(serializeJSON({ ok: false, error: error instanceof Error ? error.message : String(error) }));
6716
6851
  }
6717
6852
  return;
@@ -6757,12 +6892,12 @@ export async function startLocalServer(opts) {
6757
6892
  const correctionFailedRoute = typeof body.failedRoute === 'string' && body.failedRoute.trim()
6758
6893
  ? body.failedRoute.trim()
6759
6894
  : 'generated_answer';
6760
- // Must match the name emitCorrectionEvalCase writes, so the required
6761
- // evaluation and the emitted regression case are the same artifact.
6895
+ // Stable name linked to the Git evaluation artifacts created during
6896
+ // review. Capture itself does not silently write a regression source.
6762
6897
  const correctionEvaluation = typeof body.requiredEvaluation === 'string' && body.requiredEvaluation.trim()
6763
6898
  ? body.requiredEvaluation.trim()
6764
6899
  : `correction: ${question.slice(0, 60)}`;
6765
- const { trace, hint } = recordCorrectionTrace(projectRoot, {
6900
+ const { trace, hint } = await recordGovernedCorrection(projectRoot, {
6766
6901
  question,
6767
6902
  scope,
6768
6903
  wrongAnswer: wrongSql || '(no prior SQL captured)',
@@ -6778,49 +6913,19 @@ export async function startLocalServer(opts) {
6778
6913
  hintGuidance: typeof body.guidance === 'string' && body.guidance.trim() ? body.guidance.trim() : undefined,
6779
6914
  tags: Array.isArray(body.tags) ? body.tags.map(String) : undefined,
6780
6915
  });
6781
- // Capture always, approve deliberately. A hint is injected into the
6782
- // generation prompt as a "human-approved correction" and shapes every
6783
- // future answer in scope one analyst's local fix must not silently
6784
- // become a team-wide rule across thousands of models. The trace and the
6785
- // candidate are always recorded, so nothing is lost; approval is a
6786
- // one-click review in Settings. Callers may still opt in explicitly.
6787
- let approvedHint = hint;
6788
- if (body.approve === true) {
6789
- reviewHint(projectRoot, { hintId: hint.id, decision: 'approved', reviewer: author ?? 'local', note: 'Approved on capture.' });
6790
- approvedHint = { ...hint, status: 'approved' };
6791
- }
6792
- // W4.3 — turn the correction into a durable regression eval case so the wrong
6793
- // answer can never silently return. Best-effort; never blocks the correction.
6794
- try {
6795
- emitCorrectionEvalCase(projectRoot, { question, correctedSql, name: correctionEvaluation });
6796
- }
6797
- catch {
6798
- /* best-effort */
6799
- }
6800
- // Plain-language advisory memory mirroring the lesson, for transparency + recall.
6801
- try {
6802
- const memory = new MemoryStore(defaultMemoryPath(projectRoot));
6803
- memory.upsert({
6804
- id: `mem_${hint.id}`,
6805
- scope: 'project',
6806
- title: approvedHint.title,
6807
- content: `${approvedHint.guidance}${rationale ? ` (${rationale})` : ''}`,
6808
- tags: [scope.metric, scope.domain, scope.dbtModel].filter((x) => Boolean(x)),
6809
- source: 'correction',
6810
- confidence: 0.9,
6811
- importance: 0.85,
6812
- // A candidate is evidence, not a rule. Mirroring it as an ENABLED
6813
- // memory would inject it into prompts through the side door and
6814
- // defeat the approval gate; it is enabled when the hint is approved.
6815
- enabled: approvedHint.status === 'approved',
6816
- });
6817
- memory.close();
6818
- }
6819
- catch {
6820
- /* best-effort */
6821
- }
6916
+ // `approve` is intentionally ignored: capture never promotes a hint.
6917
+ // Review must use /api/agent/hints/:id/review so the same evidence gates
6918
+ // run for notebook, CLI runtime, and MCP callers.
6822
6919
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
6823
- res.end(serializeJSON({ ok: true, trace, hint: approvedHint }));
6920
+ res.end(serializeJSON({
6921
+ ok: true,
6922
+ trace,
6923
+ hint,
6924
+ approvalRequested: body.approve === true,
6925
+ note: body.approve === true
6926
+ ? 'Captured as a candidate. Automatic approval is not permitted; use the review queue.'
6927
+ : undefined,
6928
+ }));
6824
6929
  }
6825
6930
  catch (error) {
6826
6931
  res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
@@ -6984,7 +7089,7 @@ export async function startLocalServer(opts) {
6984
7089
  if (missingModels.length)
6985
7090
  throw new Error(`Semantic definition changed; missing model(s): ${missingModels.join(', ')}`);
6986
7091
  const targetConnection = requireActiveConnection(isConnectionConfig(body.connection) ? body.connection : connection);
6987
- const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer);
7092
+ const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer, projectRoot);
6988
7093
  const activeFilters = Object.entries(dashboardVariables)
6989
7094
  .filter(([field, value]) => item.semantic.dimensions?.includes(field) && value !== undefined && value !== null && value !== '')
6990
7095
  .map(([dimension, value]) => ({ dimension, operator: 'equals', values: Array.isArray(value) ? value.map(String) : [String(value)] }));
@@ -7054,7 +7159,7 @@ export async function startLocalServer(opts) {
7054
7159
  const absBlockPath = join(projectRoot, block.filePath);
7055
7160
  const source = readFileSync(absBlockPath, 'utf-8');
7056
7161
  const targetConnection = isConnectionConfig(body.connection) ? body.connection : connection;
7057
- const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer);
7162
+ const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer, projectRoot);
7058
7163
  const boundParameters = dashboardTileParameterValues({
7059
7164
  item,
7060
7165
  dashboardValues: dashboardVariables,
@@ -7303,12 +7408,31 @@ export async function startLocalServer(opts) {
7303
7408
  if (req.method === 'POST' && path === '/api/notebooks') {
7304
7409
  try {
7305
7410
  const body = await readJSON(req);
7306
- const { name, template } = body;
7411
+ const { name, template, ownerDomain, usesDomains } = body;
7307
7412
  if (!name || typeof name !== 'string') {
7308
7413
  res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
7309
7414
  res.end(serializeJSON({ error: 'Missing notebook name' }));
7310
7415
  return;
7311
7416
  }
7417
+ const normalizedOwnerDomain = typeof ownerDomain === 'string' && ownerDomain.trim()
7418
+ ? ownerDomain.trim()
7419
+ : undefined;
7420
+ if (normalizedOwnerDomain && !loadDomainPackageRegistry(projectRoot).get(normalizedOwnerDomain)) {
7421
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
7422
+ res.end(serializeJSON({ error: `Unknown domain: ${normalizedOwnerDomain}` }));
7423
+ return;
7424
+ }
7425
+ const normalizedUsesDomains = Array.from(new Set([
7426
+ ...(normalizedOwnerDomain ? [normalizedOwnerDomain] : []),
7427
+ ...(Array.isArray(usesDomains) ? usesDomains : []),
7428
+ ].map((domain) => typeof domain === 'string' ? domain.trim() : '').filter(Boolean)));
7429
+ const registry = loadDomainPackageRegistry(projectRoot);
7430
+ const unknownUsedDomain = normalizedUsesDomains.find((domain) => !registry.get(domain));
7431
+ if (unknownUsedDomain) {
7432
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
7433
+ res.end(serializeJSON({ error: `Unknown domain: ${unknownUsedDomain}` }));
7434
+ return;
7435
+ }
7312
7436
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'notebook';
7313
7437
  const nbDir = join(projectRoot, 'notebooks');
7314
7438
  mkdirSync(nbDir, { recursive: true });
@@ -7318,7 +7442,10 @@ export async function startLocalServer(opts) {
7318
7442
  res.end(serializeJSON({ error: 'Notebook already exists' }));
7319
7443
  return;
7320
7444
  }
7321
- const content = buildNotebookTemplate(name, template ?? 'blank');
7445
+ const content = buildNotebookTemplate(name, template ?? 'blank', {
7446
+ ownerDomain: normalizedOwnerDomain,
7447
+ usesDomains: normalizedUsesDomains,
7448
+ });
7322
7449
  writeFileSync(nbPath, content, 'utf-8');
7323
7450
  res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' });
7324
7451
  res.end(serializeJSON({ path: `notebooks/${slug}.dqlnb`, content }));
@@ -8020,6 +8147,15 @@ export async function startLocalServer(opts) {
8020
8147
  if (req.method === 'POST' && path === '/api/git/pull') {
8021
8148
  try {
8022
8149
  const result = await gitPull(projectRoot);
8150
+ if (result.ok) {
8151
+ try {
8152
+ await refreshUnifiedProjectIndexes(projectRoot);
8153
+ }
8154
+ catch (error) {
8155
+ const refreshWarning = `Git pull succeeded, but local DQL indexes could not be refreshed: ${error instanceof Error ? error.message : String(error)}`;
8156
+ result.output = [result.output, refreshWarning].filter(Boolean).join('\n');
8157
+ }
8158
+ }
8023
8159
  res.writeHead(result.ok ? 200 : 400, { 'Content-Type': 'application/json; charset=utf-8' });
8024
8160
  res.end(serializeJSON(result));
8025
8161
  }
@@ -8033,6 +8169,9 @@ export async function startLocalServer(opts) {
8033
8169
  try {
8034
8170
  const body = (await readJSON(req));
8035
8171
  const result = await gitCreateBranch(projectRoot, body.name ?? '', body.checkout !== false);
8172
+ if (result.ok && body.checkout !== false) {
8173
+ await refreshUnifiedProjectIndexes(projectRoot).catch(() => undefined);
8174
+ }
8036
8175
  res.writeHead(result.ok ? 200 : 400, { 'Content-Type': 'application/json; charset=utf-8' });
8037
8176
  res.end(serializeJSON(result));
8038
8177
  }
@@ -8046,6 +8185,8 @@ export async function startLocalServer(opts) {
8046
8185
  try {
8047
8186
  const body = (await readJSON(req));
8048
8187
  const result = await gitCheckout(projectRoot, body.name ?? '');
8188
+ if (result.ok)
8189
+ await refreshUnifiedProjectIndexes(projectRoot).catch(() => undefined);
8049
8190
  res.writeHead(result.ok ? 200 : 400, { 'Content-Type': 'application/json; charset=utf-8' });
8050
8191
  res.end(serializeJSON(result));
8051
8192
  }
@@ -8076,13 +8217,45 @@ export async function startLocalServer(opts) {
8076
8217
  const schemaLimit = Math.min(500, Math.max(1, Number(url.searchParams.get('limit')) || 200));
8077
8218
  const schemaOffset = Math.max(0, Number(url.searchParams.get('offset')) || 0);
8078
8219
  const schemaSearch = (url.searchParams.get('q') ?? '').trim();
8079
- const { tables, columnsByPath } = connection
8080
- ? await introspectSchema(executor, connection, {
8081
- limit: schemaLimit,
8082
- offset: schemaOffset,
8083
- search: schemaSearch,
8220
+ const activeScope = connection
8221
+ ? optionalActiveConnectionMetadataScope(projectRoot, projectConfig, connection, projectConfig.defaultConnectionName ?? 'default')
8222
+ : null;
8223
+ const cachedSnapshot = latestRuntimeSchemaSnapshotForProject(projectRoot);
8224
+ const cachedScopeMatches = Boolean(cachedSnapshot
8225
+ && activeScope
8226
+ && cachedSnapshot.scopeFingerprint === activeScope.scopeFingerprint);
8227
+ const cachedTables = cachedScopeMatches
8228
+ ? cachedSnapshot.tables
8229
+ .filter((table) => {
8230
+ if (!schemaSearch)
8231
+ return true;
8232
+ const needle = schemaSearch.toLowerCase();
8233
+ return table.relation.toLowerCase().includes(needle)
8234
+ || table.name?.toLowerCase().includes(needle)
8235
+ || table.schema?.toLowerCase().includes(needle);
8084
8236
  })
8085
- : { tables: [], columnsByPath: new Map() };
8237
+ .slice(schemaOffset, schemaOffset + schemaLimit)
8238
+ : [];
8239
+ const { tables, columnsByPath } = cachedScopeMatches
8240
+ ? {
8241
+ tables: cachedTables.map((table) => ({
8242
+ schema: table.schema ?? '',
8243
+ name: table.name ?? table.relation.split('.').at(-1) ?? table.relation,
8244
+ path: table.relation,
8245
+ type: 'TABLE',
8246
+ })),
8247
+ columnsByPath: new Map(cachedTables.map((table) => [
8248
+ table.relation,
8249
+ table.columns.map((column) => ({ name: column.name, type: column.type ?? '' })),
8250
+ ])),
8251
+ }
8252
+ : connection
8253
+ ? await introspectSchema(executor, connection, {
8254
+ limit: schemaLimit,
8255
+ offset: schemaOffset,
8256
+ search: schemaSearch,
8257
+ })
8258
+ : { tables: [], columnsByPath: new Map() };
8086
8259
  const dbTables = tables.map((t) => ({
8087
8260
  name: t.path,
8088
8261
  path: t.path,
@@ -8133,7 +8306,7 @@ export async function startLocalServer(opts) {
8133
8306
  if (req.method === 'POST' && path === '/api/blocks') {
8134
8307
  try {
8135
8308
  const body = await readJSON(req);
8136
- const { name, domain, content, description, tags, metricRefs, template, blockType, } = body;
8309
+ const { name, domain, content, description, tags, folderPath, metricRefs, template, blockType, } = body;
8137
8310
  if (!name || typeof name !== 'string') {
8138
8311
  res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
8139
8312
  res.end(serializeJSON({ error: 'Missing block name' }));
@@ -8150,6 +8323,7 @@ export async function startLocalServer(opts) {
8150
8323
  content,
8151
8324
  description,
8152
8325
  tags,
8326
+ folderPath,
8153
8327
  metricRefs,
8154
8328
  template,
8155
8329
  blockType,
@@ -8301,7 +8475,7 @@ export async function startLocalServer(opts) {
8301
8475
  const llmMatch = /llmContext\s*=\s*"((?:[^"\\]|\\.)*)"/.exec(source);
8302
8476
  blocks.push({
8303
8477
  name: nameMatch?.[1] ?? entry.name.replace('.dql', ''),
8304
- domain: (domainMatch?.[1] ?? inferBlockStudioPathDomain(relPath)) || 'uncategorized',
8478
+ domain: (domainMatch?.[1] ?? inferBlockStudioPathDomain(projectRoot, relPath)) || 'uncategorized',
8305
8479
  status: statusMatch?.[1] ?? 'draft',
8306
8480
  owner: ownerMatch?.[1] ?? null,
8307
8481
  tags: parsedTags,
@@ -8523,7 +8697,7 @@ export async function startLocalServer(opts) {
8523
8697
  return;
8524
8698
  }
8525
8699
  const activeConnection = requireActiveConnection();
8526
- const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer);
8700
+ const tableMapping = await resolveSemanticTableMapping(executor, activeConnection, semanticLayer, projectRoot);
8527
8701
  const semanticCompose = semanticLayer
8528
8702
  ? await composeSemanticBlockSqlForRuntime(source, semanticLayer, {
8529
8703
  driver: activeConnection.driver,
@@ -9445,7 +9619,7 @@ export async function startLocalServer(opts) {
9445
9619
  driver = undefined;
9446
9620
  }
9447
9621
  const tableMapping = composeConnection
9448
- ? await resolveSemanticTableMapping(executor, composeConnection, semanticLayer)
9622
+ ? await resolveSemanticTableMapping(executor, composeConnection, semanticLayer, projectRoot)
9449
9623
  : undefined;
9450
9624
  const composed = composeSemanticQueryForQuestion({
9451
9625
  semanticLayer,
@@ -9649,6 +9823,105 @@ export async function startLocalServer(opts) {
9649
9823
  }
9650
9824
  return;
9651
9825
  }
9826
+ const metadataScopePreviewMatch = path.match(/^\/api\/connections\/([^/]+)\/metadata-scope\/preview$/);
9827
+ const metadataScopeMatch = path.match(/^\/api\/connections\/([^/]+)\/metadata-scope$/);
9828
+ const metadataSyncMatch = path.match(/^\/api\/connections\/([^/]+)\/metadata-sync$/);
9829
+ if (metadataScopePreviewMatch && req.method === 'POST') {
9830
+ try {
9831
+ const connectionId = decodeURIComponent(metadataScopePreviewMatch[1]);
9832
+ const cfg = loadProjectConfig(projectRoot);
9833
+ const selectedConnection = projectConnectionById(projectRoot, cfg, connectionId);
9834
+ if (!selectedConnection)
9835
+ throw new Error(`Unknown connection "${connectionId}".`);
9836
+ const body = await readJSON(req).catch(() => ({}));
9837
+ const scope = normalizeConnectionMetadataScope(connectionId, selectedConnection, body, metadataScopeDbtDefaults(projectRoot, cfg));
9838
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
9839
+ res.end(serializeJSON({
9840
+ scope,
9841
+ status: warehouseMetadataStatus(projectRoot, scope.scopeFingerprint),
9842
+ }));
9843
+ }
9844
+ catch (error) {
9845
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
9846
+ res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
9847
+ }
9848
+ return;
9849
+ }
9850
+ if (metadataScopeMatch && req.method === 'GET') {
9851
+ try {
9852
+ const connectionId = decodeURIComponent(metadataScopeMatch[1]);
9853
+ const cfg = loadProjectConfig(projectRoot);
9854
+ const selectedConnection = projectConnectionById(projectRoot, cfg, connectionId);
9855
+ if (!selectedConnection)
9856
+ throw new Error(`Unknown connection "${connectionId}".`);
9857
+ const scope = normalizeConnectionMetadataScope(connectionId, selectedConnection, configuredMetadataScopeInput(cfg, connectionId), metadataScopeDbtDefaults(projectRoot, cfg));
9858
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
9859
+ res.end(serializeJSON({
9860
+ scope,
9861
+ status: warehouseMetadataStatus(projectRoot, scope.scopeFingerprint),
9862
+ }));
9863
+ }
9864
+ catch (error) {
9865
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
9866
+ res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
9867
+ }
9868
+ return;
9869
+ }
9870
+ if (metadataScopeMatch && req.method === 'PUT') {
9871
+ try {
9872
+ const connectionId = decodeURIComponent(metadataScopeMatch[1]);
9873
+ const cfg = loadProjectConfig(projectRoot);
9874
+ const selectedConnection = projectConnectionById(projectRoot, cfg, connectionId);
9875
+ if (!selectedConnection)
9876
+ throw new Error(`Unknown connection "${connectionId}".`);
9877
+ const body = await readJSON(req);
9878
+ const scope = normalizeConnectionMetadataScope(connectionId, selectedConnection, body, metadataScopeDbtDefaults(projectRoot, cfg));
9879
+ const synced = await syncWarehouseMetadata({
9880
+ projectRoot,
9881
+ executor,
9882
+ connection: selectedConnection,
9883
+ scope,
9884
+ });
9885
+ persistConnectionMetadataScope(projectRoot, connectionId, scope);
9886
+ projectConfig = loadProjectConfig(projectRoot);
9887
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
9888
+ res.end(serializeJSON({
9889
+ scope: synced.scope,
9890
+ status: warehouseMetadataStatus(projectRoot, synced.scope.scopeFingerprint),
9891
+ }));
9892
+ }
9893
+ catch (error) {
9894
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
9895
+ res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
9896
+ }
9897
+ return;
9898
+ }
9899
+ if (metadataSyncMatch && req.method === 'POST') {
9900
+ try {
9901
+ const connectionId = decodeURIComponent(metadataSyncMatch[1]);
9902
+ const cfg = loadProjectConfig(projectRoot);
9903
+ const selectedConnection = projectConnectionById(projectRoot, cfg, connectionId);
9904
+ if (!selectedConnection)
9905
+ throw new Error(`Unknown connection "${connectionId}".`);
9906
+ const scope = normalizeConnectionMetadataScope(connectionId, selectedConnection, configuredMetadataScopeInput(cfg, connectionId), metadataScopeDbtDefaults(projectRoot, cfg));
9907
+ const synced = await syncWarehouseMetadata({
9908
+ projectRoot,
9909
+ executor,
9910
+ connection: selectedConnection,
9911
+ scope,
9912
+ });
9913
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
9914
+ res.end(serializeJSON({
9915
+ scope: synced.scope,
9916
+ status: warehouseMetadataStatus(projectRoot, synced.scope.scopeFingerprint),
9917
+ }));
9918
+ }
9919
+ catch (error) {
9920
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
9921
+ res.end(serializeJSON({ error: error instanceof Error ? error.message : String(error) }));
9922
+ }
9923
+ return;
9924
+ }
9652
9925
  if (req.method === 'GET' && path === '/api/connections') {
9653
9926
  const cfg = loadProjectConfig(projectRoot);
9654
9927
  const connections = getProjectConnectionsForApi(cfg);
@@ -9665,8 +9938,21 @@ export async function startLocalServer(opts) {
9665
9938
  ...(Object.keys(connections).length === 0 && activeDbtProfile ? { profileId: activeDbtProfile.id } : {}),
9666
9939
  } : null;
9667
9940
  const connectorStatus = getConnectorInstallStatuses(projectRoot);
9941
+ const metadataScope = connection
9942
+ ? optionalActiveConnectionMetadataScope(projectRoot, cfg, connection, defaultKey)
9943
+ : null;
9668
9944
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
9669
- res.end(serializeJSON({ default: defaultKey, connections, dbtProfiles, activeConnection, connectorStatus }));
9945
+ res.end(serializeJSON({
9946
+ default: defaultKey,
9947
+ connections,
9948
+ dbtProfiles,
9949
+ activeConnection,
9950
+ connectorStatus,
9951
+ metadataScope,
9952
+ metadataStatus: metadataScope
9953
+ ? warehouseMetadataStatus(projectRoot, metadataScope.scopeFingerprint)
9954
+ : warehouseMetadataStatus(projectRoot),
9955
+ }));
9670
9956
  return;
9671
9957
  }
9672
9958
  if (req.method === 'POST' && path === '/api/connections/dbt-profiles/preview') {
@@ -11113,7 +11399,7 @@ export async function startLocalServer(opts) {
11113
11399
  }
11114
11400
  const executionConnection = await resolveExecutionConnection(body);
11115
11401
  const tableMapping = hasStandaloneSemanticRef(body.sql)
11116
- ? await resolveSemanticTableMapping(executor, executionConnection, semanticLayer)
11402
+ ? await resolveSemanticTableMapping(executor, executionConnection, semanticLayer, projectRoot)
11117
11403
  : undefined;
11118
11404
  const semantic = prepareSemanticSql(body.sql, semanticLayer, { tableMapping });
11119
11405
  if (semantic.unresolvedRefs.length > 0) {
@@ -11243,7 +11529,7 @@ export async function startLocalServer(opts) {
11243
11529
  metricFlow,
11244
11530
  compile: async () => {
11245
11531
  const tableMapping = plannedAdapter === 'native'
11246
- ? await resolveSemanticTableMapping(executor, targetConnection, semanticLayer)
11532
+ ? await resolveSemanticTableMapping(executor, targetConnection, semanticLayer, projectRoot)
11247
11533
  : undefined;
11248
11534
  return composeRuntimeSemanticQuery({
11249
11535
  metrics,
@@ -11346,7 +11632,7 @@ export async function startLocalServer(opts) {
11346
11632
  metricFlow,
11347
11633
  compile: async () => {
11348
11634
  const tableMapping = plannedAdapter === 'native'
11349
- ? await resolveSemanticTableMapping(executor, targetConnection, semanticLayer)
11635
+ ? await resolveSemanticTableMapping(executor, targetConnection, semanticLayer, projectRoot)
11350
11636
  : undefined;
11351
11637
  return composeRuntimeSemanticQuery({
11352
11638
  metrics,
@@ -11441,7 +11727,7 @@ export async function startLocalServer(opts) {
11441
11727
  return;
11442
11728
  }
11443
11729
  const targetConnection = await resolveExecutionConnection(body);
11444
- const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer);
11730
+ const tableMapping = await resolveSemanticTableMapping(executor, targetConnection, semanticLayer, projectRoot);
11445
11731
  const composed = await composeRuntimeSemanticQuery({
11446
11732
  metrics,
11447
11733
  dimensions,
@@ -11818,7 +12104,9 @@ export async function startLocalServer(opts) {
11818
12104
  const sourceRefreshedFromDisk = Boolean(requestedBlockName)
11819
12105
  && receiptSource.trim().length > 0
11820
12106
  && receiptSource !== source;
11821
- if (expectedReceipt && !sourceRefreshedFromDisk && expectedReceipt.sourceFingerprint !== executionFingerprint(source)) {
12107
+ if (expectedReceipt
12108
+ && !sourceRefreshedFromDisk
12109
+ && !executionSourceFingerprintMatches(expectedReceipt.sourceFingerprint, source)) {
11822
12110
  throw new Error('DQL artifact source changed after the answer was produced. Refresh the answer before applying it.');
11823
12111
  }
11824
12112
  const parameters = body.parameters && typeof body.parameters === 'object' && !Array.isArray(body.parameters)
@@ -11958,7 +12246,7 @@ export async function startLocalServer(opts) {
11958
12246
  }
11959
12247
  const cellConnection = await resolveExecutionConnection(body);
11960
12248
  const tableMapping = needsSemanticTableMapping(executableCell)
11961
- ? await resolveSemanticTableMapping(executor, cellConnection, semanticLayer)
12249
+ ? await resolveSemanticTableMapping(executor, cellConnection, semanticLayer, projectRoot)
11962
12250
  : undefined;
11963
12251
  const semanticCompose = executableCell.type === 'dql'
11964
12252
  && semanticLayer
@@ -11988,6 +12276,9 @@ export async function startLocalServer(opts) {
11988
12276
  res.end(serializeJSON({ cellType: cell.type, result: null }));
11989
12277
  return;
11990
12278
  }
12279
+ const internalRelationError = internalDqlRelationIdValidationError(executableSql, cell.type === 'dql' ? 'DQL query' : 'Notebook query');
12280
+ if (internalRelationError)
12281
+ throw new DqlInternalRelationIdError(internalRelationError);
11991
12282
  const prepared = prepareLocalExecution(executableSql, cellConnection, projectRoot, projectConfig);
11992
12283
  const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
11993
12284
  assertAppAccess({ app, domain: resolved.domain ?? app?.domain, level: 'execute' });
@@ -12101,24 +12392,33 @@ export async function startLocalServer(opts) {
12101
12392
  error: error instanceof Error ? error.message : String(error),
12102
12393
  });
12103
12394
  }
12395
+ const internalRelationCode = error instanceof DqlInternalRelationIdError
12396
+ ? 'DQL_INTERNAL_RELATION_ID'
12397
+ : undefined;
12104
12398
  const semanticCode = semanticExecutionFailureCode(error) ?? semanticRuntimeErrorCode(error);
12105
- const code = semanticCode ?? classifyAnalyticalFailure(error);
12399
+ const code = internalRelationCode ?? semanticCode ?? classifyAnalyticalFailure(error);
12106
12400
  const semanticDetails = semanticExecutionFailureDetails(error) ?? semanticRuntimeErrorDetails(error);
12107
12401
  const details = semanticDetails && typeof semanticDetails === 'object' && !Array.isArray(semanticDetails)
12108
12402
  ? semanticDetails
12109
12403
  : {};
12110
- const status = code === 'EXECUTION_TARGET_MISMATCH' || code === 'SEMANTIC_SOURCE_DRIFT'
12111
- ? 409
12112
- : semanticCode
12113
- ? 400
12114
- : 500;
12404
+ const status = internalRelationCode
12405
+ ? 400
12406
+ : code === 'EXECUTION_TARGET_MISMATCH' || code === 'SEMANTIC_SOURCE_DRIFT'
12407
+ ? 409
12408
+ : semanticCode
12409
+ ? 400
12410
+ : 500;
12115
12411
  res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
12116
12412
  res.end(serializeJSON({
12117
12413
  error: error instanceof Error ? error.message : String(error),
12118
12414
  code,
12119
12415
  details: {
12120
12416
  ...details,
12121
- phase: typeof details.phase === 'string' ? details.phase : semanticCode ? 'compilation' : 'execution',
12417
+ phase: typeof details.phase === 'string'
12418
+ ? details.phase
12419
+ : internalRelationCode || semanticCode
12420
+ ? 'compilation'
12421
+ : 'execution',
12122
12422
  executionContext: notebookExecutionIdentity(execContext, body?.cell?.id),
12123
12423
  executionTarget: executionTargetDescriptor(body && typeof body === 'object' ? body : {}),
12124
12424
  },
@@ -12297,9 +12597,9 @@ FROM from_counts, to_counts, joined, unmatched, from_max, to_max`;
12297
12597
  : 'Warehouse evidence does not prove the declared cardinality and safe fanout policy.',
12298
12598
  };
12299
12599
  }
12300
- function relatedProductsForDomain(manifest, domainId) {
12600
+ function relatedProductsForDomain(manifest, domainId, liveNotebookFiles = []) {
12301
12601
  const apps = Object.values(manifest.apps ?? {})
12302
- .filter((app) => app.ownerDomain === domainId || app.usesDomains.includes(domainId))
12602
+ .filter((app) => !domainId || app.ownerDomain === domainId || app.usesDomains.includes(domainId))
12303
12603
  .map((app) => ({
12304
12604
  id: app.id,
12305
12605
  name: app.name,
@@ -12312,8 +12612,8 @@ function relatedProductsForDomain(manifest, domainId) {
12312
12612
  lifecycle: app.lifecycle,
12313
12613
  legacyLayout: app.filePath.startsWith('domains/'),
12314
12614
  }));
12315
- const notebooks = Object.entries(manifest.notebooks)
12316
- .filter(([, notebook]) => notebook.ownerDomain === domainId || notebook.usesDomains.includes(domainId))
12615
+ const compiledNotebooks = Object.entries(manifest.notebooks)
12616
+ .filter(([, notebook]) => !domainId || notebook.ownerDomain === domainId || notebook.usesDomains.includes(domainId))
12317
12617
  .map(([id, notebook]) => ({
12318
12618
  id,
12319
12619
  title: notebook.title,
@@ -12325,6 +12625,29 @@ function relatedProductsForDomain(manifest, domainId) {
12325
12625
  classification: notebook.classification,
12326
12626
  legacyLayout: notebook.filePath.startsWith('domains/'),
12327
12627
  }));
12628
+ const notebooksByPath = new Map(compiledNotebooks.map((notebook) => [String(notebook.filePath), notebook]));
12629
+ for (const file of liveNotebookFiles) {
12630
+ if (file.type !== 'notebook')
12631
+ continue;
12632
+ const usesDomains = file.usesDomains ?? [];
12633
+ if (domainId && file.ownerDomain !== domainId && !usesDomains.includes(domainId))
12634
+ continue;
12635
+ if (notebooksByPath.has(file.path))
12636
+ continue;
12637
+ notebooksByPath.set(file.path, {
12638
+ id: file.path,
12639
+ title: file.name,
12640
+ filePath: file.path,
12641
+ ownerDomain: file.ownerDomain,
12642
+ usesDomains,
12643
+ purpose: undefined,
12644
+ requiredExports: [],
12645
+ classification: undefined,
12646
+ legacyLayout: file.path.startsWith('domains/'),
12647
+ });
12648
+ }
12649
+ const notebooks = [...notebooksByPath.values()]
12650
+ .sort((a, b) => String(a.title).localeCompare(String(b.title)));
12328
12651
  return { apps, notebooks };
12329
12652
  }
12330
12653
  function domainWorkspaceSummary(manifest, domainId) {
@@ -12709,6 +13032,8 @@ export function analyticalFreshnessObservedThrough(result, request) {
12709
13032
  function serializeSkill(skill) {
12710
13033
  return {
12711
13034
  id: skill.id,
13035
+ localId: skill.localId,
13036
+ qualifiedId: skill.qualifiedId,
12712
13037
  scope: 'project',
12713
13038
  user: undefined,
12714
13039
  domain: skill.domain,
@@ -13875,6 +14200,19 @@ function createDqlArtifactExecutionReceipt(source, compiledSql, parameters, resu
13875
14200
  function executionFingerprint(value) {
13876
14201
  return createHash('sha256').update(value).digest('hex');
13877
14202
  }
14203
+ /**
14204
+ * Older clients normalized executable DQL with trim(), while the receipt was
14205
+ * correctly created from the original renderer output ending in one newline.
14206
+ * Accept only that exact legacy line-ending variant; any substantive source
14207
+ * change still fails closed.
14208
+ */
14209
+ function executionSourceFingerprintMatches(expected, source) {
14210
+ return [
14211
+ source,
14212
+ `${source}\n`,
14213
+ `${source}\r\n`,
14214
+ ].some((candidate) => executionFingerprint(candidate) === expected);
14215
+ }
13878
14216
  function stableExecutionValue(value) {
13879
14217
  const seen = new WeakSet();
13880
14218
  return JSON.stringify(value, (_key, item) => {
@@ -13986,6 +14324,66 @@ function getProjectConnectionsForApi(config) {
13986
14324
  }
13987
14325
  return connections;
13988
14326
  }
14327
+ function metadataScopeDbtDefaults(projectRoot, config) {
14328
+ const manifestPath = resolveDbtManifestPath(projectRoot, config);
14329
+ if (!manifestPath || !existsSync(manifestPath))
14330
+ return { relations: [] };
14331
+ try {
14332
+ const source = readFileSync(manifestPath, 'utf-8');
14333
+ const lookup = buildDbtRelationLookup(JSON.parse(source));
14334
+ return {
14335
+ relations: Array.from(new Set([...lookup.models.values(), ...lookup.sources.values()])).sort(),
14336
+ dbtFingerprint: createHash('sha256').update(source).digest('hex'),
14337
+ };
14338
+ }
14339
+ catch {
14340
+ return { relations: [] };
14341
+ }
14342
+ }
14343
+ function configuredMetadataScopeInput(config, connectionId) {
14344
+ const value = config.metadataScopes?.[connectionId];
14345
+ return value && typeof value === 'object' ? value : undefined;
14346
+ }
14347
+ function activeConnectionMetadataScope(projectRoot, config, activeConnection, connectionId = config.defaultConnectionName ?? 'default') {
14348
+ return normalizeConnectionMetadataScope(connectionId, activeConnection, configuredMetadataScopeInput(config, connectionId), metadataScopeDbtDefaults(projectRoot, config));
14349
+ }
14350
+ function optionalActiveConnectionMetadataScope(projectRoot, config, activeConnection, connectionId) {
14351
+ try {
14352
+ return activeConnectionMetadataScope(projectRoot, config, activeConnection, connectionId);
14353
+ }
14354
+ catch {
14355
+ return null;
14356
+ }
14357
+ }
14358
+ function projectConnectionById(projectRoot, config, connectionId) {
14359
+ const stored = getStoredConnections(config);
14360
+ const normalized = normalizeStoredConnection(stored[connectionId]);
14361
+ if (normalized)
14362
+ return normalizeProjectConnection(normalized, projectRoot);
14363
+ if (connectionId === (config.defaultConnectionName ?? 'default') && config.defaultConnection) {
14364
+ return normalizeProjectConnection(config.defaultConnection, projectRoot);
14365
+ }
14366
+ return null;
14367
+ }
14368
+ function persistConnectionMetadataScope(projectRoot, connectionId, scope) {
14369
+ const configPath = join(projectRoot, 'dql.config.json');
14370
+ const raw = existsSync(configPath)
14371
+ ? JSON.parse(readFileSync(configPath, 'utf-8'))
14372
+ : {};
14373
+ const existing = raw.metadataScopes && typeof raw.metadataScopes === 'object' && !Array.isArray(raw.metadataScopes)
14374
+ ? raw.metadataScopes
14375
+ : {};
14376
+ raw.metadataScopes = {
14377
+ ...existing,
14378
+ [connectionId]: {
14379
+ mode: scope.mode,
14380
+ scopes: scope.selectedScopes,
14381
+ relations: scope.relations.length > 0 ? scope.relations : undefined,
14382
+ dbtFingerprint: scope.dbtFingerprint,
14383
+ },
14384
+ };
14385
+ writeFileSync(configPath, JSON.stringify(raw, null, 2) + '\n', 'utf-8');
14386
+ }
13989
14387
  const CONNECTOR_INSTALLS = {
13990
14388
  duckdb: {
13991
14389
  driver: 'duckdb',
@@ -15017,6 +15415,9 @@ function summarizeExploratoryJoinProbe(rows, leftRelation, leftColumn, rightRela
15017
15415
  return `${leftRelation}.${leftColumn} ↔ ${rightRelation}.${rightColumn}: sample left=${number('left_sample_rows')}, right=${number('right_sample_rows')}, joined=${number('joined_rows')}, unmatched left/right=${number('unmatched_left_sample_rows')}/${number('unmatched_right_sample_rows')}, max matches left/right=${number('max_matches_per_left_key')}/${number('max_matches_per_right_key')}`;
15018
15416
  }
15019
15417
  function readOnlySqlValidationError(sql, subject) {
15418
+ const internalRelationError = internalDqlRelationIdValidationError(sql, subject);
15419
+ if (internalRelationError)
15420
+ return internalRelationError;
15020
15421
  const scanSql = stripSqlStringsAndComments(sql).trim();
15021
15422
  if (!/^(select|with)\b/i.test(scanSql)) {
15022
15423
  return `${subject} only supports read-only SELECT or WITH queries.`;
@@ -15031,6 +15432,15 @@ function readOnlySqlValidationError(sql, subject) {
15031
15432
  }
15032
15433
  return null;
15033
15434
  }
15435
+ function internalDqlRelationIdValidationError(sql, subject) {
15436
+ const internalIds = internalRelationIdsInSql(stripSqlStringsAndComments(sql));
15437
+ if (internalIds.length === 0)
15438
+ return null;
15439
+ return `${subject} contains internal DQL graph relation identifier${internalIds.length === 1 ? '' : 's'} ${internalIds.join(', ')}. Use the physical database.schema.table relation instead, or choose Ask AI to fix from the notebook error.`;
15440
+ }
15441
+ class DqlInternalRelationIdError extends Error {
15442
+ name = 'DqlInternalRelationIdError';
15443
+ }
15034
15444
  function stripSqlStringsAndComments(sql) {
15035
15445
  let output = '';
15036
15446
  for (let index = 0; index < sql.length; index += 1) {
@@ -15340,6 +15750,7 @@ function scanNotebookFiles(projectRoot) {
15340
15750
  path: relativePath,
15341
15751
  type,
15342
15752
  folder: relativeDir,
15753
+ ...readNotebookFileProductContext(fullPath, type),
15343
15754
  });
15344
15755
  }
15345
15756
  }
@@ -15364,12 +15775,29 @@ function scanNotebookFiles(projectRoot) {
15364
15775
  path: relativePath,
15365
15776
  type,
15366
15777
  folder: relativeDir.split('/')[0] ?? relativeDir,
15778
+ ...readNotebookFileProductContext(fullPath, type),
15367
15779
  });
15368
15780
  }
15369
15781
  }
15370
15782
  catch { /* skip unreadable dirs */ }
15371
15783
  }
15372
15784
  }
15785
+ function readNotebookFileProductContext(fullPath, type) {
15786
+ if ((type !== 'notebook' && type !== 'workbook') || !fullPath.endsWith('.dqlnb'))
15787
+ return {};
15788
+ try {
15789
+ const metadata = deserializeNotebook(readFileSync(fullPath, 'utf-8')).metadata;
15790
+ return {
15791
+ ...(metadata.ownerDomain ? { ownerDomain: metadata.ownerDomain } : {}),
15792
+ usesDomains: metadata.usesDomains ?? [],
15793
+ };
15794
+ }
15795
+ catch {
15796
+ // A malformed notebook must remain visible in the file list so the user
15797
+ // can open and repair it; it simply falls back to the Project folder.
15798
+ return {};
15799
+ }
15800
+ }
15373
15801
  function inferDqlArtifactName(fullPath, type, fallbackName) {
15374
15802
  if (!fullPath.endsWith('.dql'))
15375
15803
  return fallbackName;
@@ -15573,15 +16001,15 @@ export function openBlockStudioDocument(projectRoot, relativePath, semanticLayer
15573
16001
  throw new Error(`File not found: ${normalizedPath}`);
15574
16002
  }
15575
16003
  const source = readFileSync(absPath, 'utf-8');
15576
- const companionPath = blockCompanionRelativePath(normalizedPath);
16004
+ const companionPath = blockCompanionRelativePath(projectRoot, normalizedPath);
15577
16005
  const companion = companionPath ? readBlockCompanionFile(projectRoot, companionPath) : null;
15578
16006
  const parsedMetadata = parseBlockSourceMetadata(source);
15579
16007
  const fileName = normalizedPath.split('/').pop()?.replace(/\.dql$/, '') ?? 'block';
15580
16008
  const metadata = {
15581
16009
  name: parsedMetadata.name || companion?.name || fileName,
15582
16010
  path: normalizedPath,
15583
- domain: parsedMetadata.domain || companion?.domain || inferBlockStudioPathDomain(normalizedPath) || 'uncategorized',
15584
- folderPath: inferBlockStudioFolderPath(normalizedPath),
16011
+ domain: parsedMetadata.domain || companion?.domain || inferBlockStudioPathDomain(projectRoot, normalizedPath) || 'uncategorized',
16012
+ folderPath: inferBlockStudioFolderPath(projectRoot, normalizedPath),
15585
16013
  description: parsedMetadata.description || companion?.description || '',
15586
16014
  owner: parsedMetadata.owner || companion?.owner || '',
15587
16015
  tags: parsedMetadata.tags.length > 0 ? parsedMetadata.tags : companion?.tags ?? [],
@@ -15640,9 +16068,20 @@ function parseSemanticBlockConfig(source) {
15640
16068
  limit: limitMatch ? Number.parseInt(limitMatch[1], 10) : undefined,
15641
16069
  };
15642
16070
  }
15643
- export async function resolveSemanticTableMapping(executor, connection, semanticLayer) {
16071
+ export async function resolveSemanticTableMapping(executor, connection, semanticLayer, projectRoot) {
15644
16072
  if (!semanticLayer)
15645
16073
  return undefined;
16074
+ if (projectRoot) {
16075
+ const snapshot = latestRuntimeSchemaSnapshotForProject(projectRoot);
16076
+ if (snapshot?.scopeFingerprint && snapshot.tables.length) {
16077
+ return buildSemanticTableMapping(semanticLayer, snapshot.tables.map((table) => ({
16078
+ table_catalog: table.catalogOrDatabase,
16079
+ table_schema: table.schema,
16080
+ table_name: table.name ?? table.relation.split('.').at(-1),
16081
+ table_relation: table.relation,
16082
+ })));
16083
+ }
16084
+ }
15646
16085
  try {
15647
16086
  const tablesResult = await executor.executeQuery(`SELECT table_schema, table_name
15648
16087
  FROM information_schema.tables
@@ -15660,10 +16099,11 @@ export function buildSemanticTableMapping(semanticLayer, rows) {
15660
16099
  for (const row of rows) {
15661
16100
  const schema = String(row['table_schema'] ?? '');
15662
16101
  const name = String(row['table_name'] ?? '');
16102
+ const relation = typeof row['table_relation'] === 'string' ? row['table_relation'] : undefined;
15663
16103
  if (!name)
15664
16104
  continue;
15665
16105
  dbTableNames.add(name);
15666
- schemaQualified.set(name, schema ? `${schema}.${name}` : name);
16106
+ schemaQualified.set(name, relation ?? (schema ? `${schema}.${name}` : name));
15667
16107
  }
15668
16108
  const tableMapping = {};
15669
16109
  const allSemanticTables = new Set();
@@ -16587,7 +17027,7 @@ export function saveBlockStudioArtifacts(projectRoot, options) {
16587
17027
  const safeDomain = (options.domain ?? '')
16588
17028
  .trim()
16589
17029
  .toLowerCase()
16590
- .replace(/[^a-z0-9/_-]+/g, '-')
17030
+ .replace(/[^a-z0-9._/-]+/g, '-')
16591
17031
  .replace(/^\/+|\/+$/g, '') || 'uncategorized';
16592
17032
  const safeFolderPath = normalizeBlockStudioFolderPath(options.folderPath);
16593
17033
  const previousPath = options.currentPath ? normalize(options.currentPath).replace(/^\/+/, '') : null;
@@ -16615,7 +17055,7 @@ export function saveBlockStudioArtifacts(projectRoot, options) {
16615
17055
  const previousAbsPath = join(projectRoot, previousPath);
16616
17056
  if (existsSync(previousAbsPath))
16617
17057
  rmSync(previousAbsPath, { force: true });
16618
- const previousCompanion = blockCompanionRelativePath(previousPath);
17058
+ const previousCompanion = blockCompanionRelativePath(projectRoot, previousPath);
16619
17059
  if (previousCompanion) {
16620
17060
  const previousCompanionPath = join(projectRoot, previousCompanion);
16621
17061
  if (existsSync(previousCompanionPath))
@@ -16643,7 +17083,7 @@ export function deleteBlockStudioArtifacts(projectRoot, relativePath) {
16643
17083
  if (!existsSync(absolutePath)) {
16644
17084
  throw new Error(`File not found: ${normalizedPath}`);
16645
17085
  }
16646
- const companionPath = blockCompanionRelativePath(normalizedPath);
17086
+ const companionPath = blockCompanionRelativePath(projectRoot, normalizedPath);
16647
17087
  rmSync(absolutePath, { force: true });
16648
17088
  if (companionPath) {
16649
17089
  const absoluteCompanionPath = resolve(rootPath, companionPath);
@@ -16701,7 +17141,12 @@ export function saveBlockStudioDraftArtifacts(projectRoot, options) {
16701
17141
  }
16702
17142
  function canonicalBlockRelativePath(projectRoot, safeDomain, safeFolderPath, slug, previousPath) {
16703
17143
  const folder = safeFolderPath ? `${safeFolderPath}/` : '';
16704
- const previousDomainFirst = Boolean(previousPath?.match(/^domains\/[^/]+\/blocks\//));
17144
+ const packageRoot = loadDomainPackageRegistry(projectRoot).get(safeDomain)?.root;
17145
+ if (packageRoot) {
17146
+ const relativePackageRoot = relative(projectRoot, packageRoot).replaceAll('\\', '/');
17147
+ return `${relativePackageRoot}/blocks/${folder}${slug}.dql`;
17148
+ }
17149
+ const previousDomainFirst = Boolean(previousPath?.match(/^domains\/.+\/blocks\//));
16705
17150
  if (previousDomainFirst || existsSync(join(projectRoot, 'domains', safeDomain))) {
16706
17151
  return `domains/${safeDomain}/blocks/${folder}${slug}.dql`;
16707
17152
  }
@@ -16724,19 +17169,23 @@ function isDraftBlockPath(value) {
16724
17169
  if (!value)
16725
17170
  return false;
16726
17171
  const normalized = normalize(value).replace(/^\/+/, '');
16727
- return normalized.startsWith('blocks/_drafts/') || /^domains\/[^/]+\/blocks\/_drafts\//.test(normalized);
17172
+ return normalized.startsWith('blocks/_drafts/') || /^domains\/.+\/blocks\/_drafts\//.test(normalized);
16728
17173
  }
16729
17174
  function isBlockStudioBlockPath(value) {
16730
17175
  if (!value)
16731
17176
  return false;
16732
17177
  const normalized = normalize(value).replace(/^\/+/, '');
16733
- return normalized.startsWith('blocks/') || /^domains\/[^/]+\/blocks\//.test(normalized);
17178
+ return normalized.startsWith('blocks/') || /^domains\/.+\/blocks\//.test(normalized);
16734
17179
  }
16735
- function inferBlockStudioPathDomain(blockPath) {
17180
+ function inferBlockStudioPathDomain(projectRoot, blockPath) {
16736
17181
  const normalized = normalize(blockPath).replace(/^\/+/, '');
16737
17182
  if (normalized.startsWith('blocks/')) {
16738
17183
  return normalized.split('/')[1] ?? '';
16739
17184
  }
17185
+ const resolved = domainBlockPathParts(projectRoot, normalized);
17186
+ if (resolved) {
17187
+ return resolved.subpath.startsWith('_drafts/') ? `_drafts/${resolved.domain}` : resolved.domain;
17188
+ }
16740
17189
  const domainFirst = normalized.match(/^domains\/([^/]+)\/blocks\/(.+)$/);
16741
17190
  if (!domainFirst)
16742
17191
  return '';
@@ -16746,8 +17195,11 @@ function inferBlockStudioPathDomain(blockPath) {
16746
17195
  return `_drafts/${domain}`;
16747
17196
  return domain;
16748
17197
  }
16749
- function inferBlockStudioFolderPath(blockPath) {
17198
+ function inferBlockStudioFolderPath(projectRoot, blockPath) {
16750
17199
  const normalized = normalize(blockPath).replace(/^\/+/, '').replaceAll('\\', '/');
17200
+ const resolved = domainBlockPathParts(projectRoot, normalized);
17201
+ if (resolved)
17202
+ return resolved.subpath.split('/').slice(0, -1).join('/');
16751
17203
  const domainFirst = normalized.match(/^domains\/[^/]+\/blocks\/(.+)$/);
16752
17204
  if (domainFirst)
16753
17205
  return domainFirst[1].split('/').slice(0, -1).join('/');
@@ -16756,12 +17208,20 @@ function inferBlockStudioFolderPath(blockPath) {
16756
17208
  return legacy[1].split('/').slice(0, -1).join('/');
16757
17209
  return '';
16758
17210
  }
16759
- function blockCompanionRelativePath(blockPath) {
17211
+ function blockCompanionRelativePath(projectRoot, blockPath) {
16760
17212
  const normalized = normalize(blockPath).replace(/^\/+/, '');
16761
17213
  if (normalized.startsWith('blocks/')) {
16762
17214
  const withoutRoot = normalized.slice('blocks/'.length).replace(/\.dql$/, '.yaml');
16763
17215
  return join('semantic-layer', 'blocks', withoutRoot).replaceAll('\\', '/');
16764
17216
  }
17217
+ const resolved = domainBlockPathParts(projectRoot, normalized);
17218
+ if (resolved && resolved.subpath.endsWith('.dql')) {
17219
+ const blockPath = resolved.subpath.replace(/\.dql$/, '');
17220
+ const companionBlockPath = blockPath.startsWith('_drafts/')
17221
+ ? join('_drafts', resolved.domain, blockPath.slice('_drafts/'.length))
17222
+ : join(resolved.domain, blockPath);
17223
+ return join('semantic-layer', 'blocks', `${companionBlockPath}.yaml`).replaceAll('\\', '/');
17224
+ }
16765
17225
  const domainFirst = normalized.match(/^domains\/([^/]+)\/blocks\/(.+)\.dql$/);
16766
17226
  if (domainFirst) {
16767
17227
  const domain = domainFirst[1];
@@ -16773,6 +17233,19 @@ function blockCompanionRelativePath(blockPath) {
16773
17233
  }
16774
17234
  return null;
16775
17235
  }
17236
+ function domainBlockPathParts(projectRoot, blockPath) {
17237
+ const normalized = normalize(blockPath).replace(/^\/+/, '').replaceAll('\\', '/');
17238
+ if (!normalized.startsWith('domains/'))
17239
+ return null;
17240
+ const absolute = join(projectRoot, normalized);
17241
+ const pkg = loadDomainPackageRegistry(projectRoot).packageForPath(absolute);
17242
+ if (!pkg)
17243
+ return null;
17244
+ const local = relative(pkg.root, absolute).replaceAll('\\', '/');
17245
+ if (!local.startsWith('blocks/'))
17246
+ return null;
17247
+ return { domain: pkg.id, subpath: local.slice('blocks/'.length) };
17248
+ }
16776
17249
  function readBlockCompanionFile(projectRoot, relativePath) {
16777
17250
  const absPath = join(projectRoot, relativePath);
16778
17251
  if (!existsSync(absPath))
@@ -17085,7 +17558,7 @@ export function setBlockStudioStatus(projectRoot, blockPath, newStatus) {
17085
17558
  throw new Error('Block file not found');
17086
17559
  const source = setBlockStudioStatusInSource(readFileSync(absPath, 'utf-8'), newStatus);
17087
17560
  writeFileSync(absPath, source, 'utf-8');
17088
- const companionPath = blockCompanionRelativePath(normalizedPath);
17561
+ const companionPath = blockCompanionRelativePath(projectRoot, normalizedPath);
17089
17562
  if (!companionPath)
17090
17563
  return;
17091
17564
  const absCompanionPath = join(projectRoot, companionPath);
@@ -18254,16 +18727,18 @@ function buildConversationSystemPrompt(kind, isGeneralKnowledge, catalogContext,
18254
18727
  return lines.join('\n');
18255
18728
  }
18256
18729
  function renderConversationMemoryForPrompt(context) {
18730
+ const envelope = renderConversationEnvelopeForPrompt(context);
18257
18731
  const recap = buildConversationContextRecap(context);
18258
- if (!recap)
18732
+ if (!recap && !envelope)
18259
18733
  return undefined;
18260
18734
  return [
18261
18735
  'Current conversation context:',
18736
+ envelope,
18262
18737
  recap,
18263
18738
  'If the user asks what this conversation is about, summarize this context directly. Do not query data or invent values.',
18264
- ].join('\n');
18739
+ ].filter(Boolean).join('\n');
18265
18740
  }
18266
- function buildConversationContextRecap(context) {
18741
+ export function buildConversationContextRecap(context) {
18267
18742
  if (!context)
18268
18743
  return undefined;
18269
18744
  const turnRecap = buildConversationTurnsRecap(context);
@@ -18273,10 +18748,7 @@ function buildConversationContextRecap(context) {
18273
18748
  const sourceAnswerSummary = agentRunString(context.sourceAnswerSummary);
18274
18749
  const columns = agentRunStringArray(context.resultColumns ?? context.outputColumns).slice(0, 8);
18275
18750
  const values = agentRunRecord(context.resultDimensionValues);
18276
- const rows = Array.isArray(context.resultRowsSample)
18277
- ? context.resultRowsSample.map(agentRunRecord).filter((row) => Boolean(row)).slice(0, 1)
18278
- : [];
18279
- const firstRow = rows[0];
18751
+ const firstRow = firstConversationResultRow(context.resultRowsSample, columns);
18280
18752
  const leadParts = buildPriorValueLeadParts(values);
18281
18753
  const rowFacts = firstRow
18282
18754
  ? columns
@@ -18308,16 +18780,19 @@ function buildConversationTurnsRecap(context) {
18308
18780
  const active = activeTurnId
18309
18781
  ? turns.find((turn) => agentRunString(turn.id) === activeTurnId)
18310
18782
  : undefined;
18311
- const selected = active ?? turns[turns.length - 1];
18783
+ // A recap turn is persisted like every other turn, but it must not become the
18784
+ // subject of the next recap. Prefer the newest analytical/authoring turn and
18785
+ // fall back to the active turn only when the thread contains conversation alone.
18786
+ const substantiveTurns = turns.filter((turn) => agentRunString(turn.route) !== 'conversation');
18787
+ const selected = active && agentRunString(active.route) !== 'conversation'
18788
+ ? active
18789
+ : substantiveTurns[substantiveTurns.length - 1] ?? active ?? turns[turns.length - 1];
18312
18790
  if (!selected)
18313
18791
  return undefined;
18314
18792
  const result = agentRunRecord(selected.result);
18315
18793
  const values = agentRunRecord(result?.dimensionValues);
18316
18794
  const columns = agentRunStringArray(result?.columns).slice(0, 8);
18317
- const rows = Array.isArray(result?.rowsSample)
18318
- ? result.rowsSample.map(agentRunRecord).filter((row) => Boolean(row)).slice(0, 1)
18319
- : [];
18320
- const firstRow = rows[0];
18795
+ const firstRow = firstConversationResultRow(result?.rowsSample, columns);
18321
18796
  const leadParts = buildPriorValueLeadParts(values);
18322
18797
  const rowFacts = firstRow
18323
18798
  ? columns
@@ -18329,7 +18804,7 @@ function buildConversationTurnsRecap(context) {
18329
18804
  .filter(Boolean)
18330
18805
  : [];
18331
18806
  const previous = turns
18332
- .filter((turn) => turn !== selected)
18807
+ .filter((turn) => turn !== selected && agentRunString(turn.route) !== 'conversation')
18333
18808
  .map((turn) => agentRunString(turn.question))
18334
18809
  .filter((value) => Boolean(value))
18335
18810
  .slice(-2);
@@ -18343,6 +18818,17 @@ function buildConversationTurnsRecap(context) {
18343
18818
  ].filter(Boolean);
18344
18819
  return bits.length > 0 ? `We were talking about ${bits.join('. ')}.` : undefined;
18345
18820
  }
18821
+ function firstConversationResultRow(value, columns) {
18822
+ if (!Array.isArray(value) || value.length === 0)
18823
+ return undefined;
18824
+ const first = value[0];
18825
+ const record = agentRunRecord(first);
18826
+ if (record)
18827
+ return record;
18828
+ if (!Array.isArray(first))
18829
+ return undefined;
18830
+ return Object.fromEntries(columns.map((column, index) => [column, first[index]]));
18831
+ }
18346
18832
  function agentRunStringArray(value) {
18347
18833
  if (!Array.isArray(value))
18348
18834
  return [];
@@ -18526,12 +19012,19 @@ export function readGitMetadata(projectRoot) {
18526
19012
  return null;
18527
19013
  }
18528
19014
  }
18529
- function resolveBlockWriteTarget(projectRoot, safeDomain, slug) {
19015
+ function resolveBlockWriteTarget(projectRoot, safeDomain, safeFolderPath, slug) {
19016
+ const folder = safeFolderPath ? `${safeFolderPath}/` : '';
19017
+ const packageRoot = safeDomain ? loadDomainPackageRegistry(projectRoot).get(safeDomain)?.root : undefined;
19018
+ if (packageRoot) {
19019
+ const relativePackageRoot = relative(projectRoot, packageRoot).replaceAll('\\', '/');
19020
+ const relativePath = `${relativePackageRoot}/blocks/${folder}${slug}.dql`;
19021
+ return { relativePath, absPath: join(projectRoot, relativePath) };
19022
+ }
18530
19023
  if (safeDomain && existsSync(join(projectRoot, 'domains', safeDomain))) {
18531
- const relativePath = `domains/${safeDomain}/blocks/${slug}.dql`;
19024
+ const relativePath = `domains/${safeDomain}/blocks/${folder}${slug}.dql`;
18532
19025
  return { relativePath, absPath: join(projectRoot, relativePath) };
18533
19026
  }
18534
- const relativePath = safeDomain ? `blocks/${safeDomain}/${slug}.dql` : `blocks/${slug}.dql`;
19027
+ const relativePath = safeDomain ? `blocks/${safeDomain}/${folder}${slug}.dql` : `blocks/${folder}${slug}.dql`;
18535
19028
  return { relativePath, absPath: join(projectRoot, relativePath) };
18536
19029
  }
18537
19030
  export function createBlockArtifacts(projectRoot, options) {
@@ -18539,9 +19032,10 @@ export function createBlockArtifacts(projectRoot, options) {
18539
19032
  const safeDomain = (options.domain ?? '')
18540
19033
  .trim()
18541
19034
  .toLowerCase()
18542
- .replace(/[^a-z0-9/_-]+/g, '-')
19035
+ .replace(/[^a-z0-9._/-]+/g, '-')
18543
19036
  .replace(/^\/+|\/+$/g, '');
18544
- const target = resolveBlockWriteTarget(projectRoot, safeDomain, slug);
19037
+ const safeFolderPath = normalizeBlockStudioFolderPath(options.folderPath);
19038
+ const target = resolveBlockWriteTarget(projectRoot, safeDomain, safeFolderPath, slug);
18545
19039
  mkdirSync(dirname(target.absPath), { recursive: true });
18546
19040
  const blockPath = target.absPath;
18547
19041
  if (existsSync(blockPath)) {
@@ -18575,6 +19069,7 @@ export function createBlockArtifacts(projectRoot, options) {
18575
19069
  slug,
18576
19070
  name: options.name,
18577
19071
  domain: safeDomain || 'uncategorized',
19072
+ folderPath: safeFolderPath,
18578
19073
  owner: options.owner,
18579
19074
  description: options.description,
18580
19075
  tags: options.tags,
@@ -18594,9 +19089,9 @@ export function createSemanticBuilderBlock(projectRoot, options) {
18594
19089
  const safeDomain = (options.domain ?? '')
18595
19090
  .trim()
18596
19091
  .toLowerCase()
18597
- .replace(/[^a-z0-9/_-]+/g, '-')
19092
+ .replace(/[^a-z0-9._/-]+/g, '-')
18598
19093
  .replace(/^\/+|\/+$/g, '') || 'uncategorized';
18599
- const target = resolveBlockWriteTarget(projectRoot, safeDomain, slug);
19094
+ const target = resolveBlockWriteTarget(projectRoot, safeDomain, '', slug);
18600
19095
  mkdirSync(dirname(target.absPath), { recursive: true });
18601
19096
  const blockPath = target.absPath;
18602
19097
  if (existsSync(blockPath)) {
@@ -18922,7 +19417,7 @@ function yamlScalar(value) {
18922
19417
  return value;
18923
19418
  return JSON.stringify(value);
18924
19419
  }
18925
- function buildNotebookTemplate(title, template) {
19420
+ function buildNotebookTemplate(title, template, productContext = {}) {
18926
19421
  const id = () => Math.random().toString(36).slice(2, 10);
18927
19422
  let cells;
18928
19423
  if (template === "analysis") {
@@ -19035,7 +19530,18 @@ function buildNotebookTemplate(title, template) {
19035
19530
  else {
19036
19531
  cells = [];
19037
19532
  }
19038
- return JSON.stringify({ dqlnbVersion: 2, version: 1, title, cells }, null, 2);
19533
+ return JSON.stringify({
19534
+ dqlnbVersion: 2,
19535
+ version: 1,
19536
+ title,
19537
+ metadata: {
19538
+ createdWith: 'dql',
19539
+ ...(productContext.ownerDomain ? { ownerDomain: productContext.ownerDomain } : {}),
19540
+ usesDomains: productContext.usesDomains ?? (productContext.ownerDomain ? [productContext.ownerDomain] : []),
19541
+ requiredExports: [],
19542
+ },
19543
+ cells,
19544
+ }, null, 2);
19039
19545
  }
19040
19546
  /** Build a lineage graph from the project's blocks and semantic layer. */
19041
19547
  // Cache per project + source fingerprint. A process can serve different roots,
@@ -20081,33 +20587,8 @@ function ensureGitignoreEntry(projectRoot, pattern) {
20081
20587
  // Best-effort; failure to write .gitignore shouldn't fail the snapshot.
20082
20588
  }
20083
20589
  }
20084
- /**
20085
- * UI-001, SEC-001, E2E-001: local runtime, credential, and generated artifacts must never be offered as
20086
- * shareable source-control changes. Project skills are intentionally absent:
20087
- * `.dql/skills` remains readable during migration and can be governed in Git.
20088
- */
20089
- const LOCAL_RUNTIME_GITIGNORE_RULES = [
20090
- 'dql-manifest.json',
20091
- '*.duckdb',
20092
- '*.duckdb.wal',
20093
- '*.run.json',
20094
- '**/.dql/runs/',
20095
- '**/.dql/cache/',
20096
- '**/.dql/imports/',
20097
- '**/.dql/local/',
20098
- '**/.dql/runtimes/',
20099
- '**/.dql/connectors/',
20100
- '**/.dql/memory/',
20101
- '**/.dql/migration-staging/',
20102
- '**/.dql/docker-starter/',
20103
- '**/.dql/oauth-credentials.json',
20104
- '**/.dql/provider-settings.json',
20105
- '**/.dql/mcp-servers.json',
20106
- '**/.dql-user-prefs.json',
20107
- ];
20108
20590
  export function ensureLocalRuntimeGitignore(projectRoot) {
20109
- for (const rule of LOCAL_RUNTIME_GITIGNORE_RULES)
20110
- ensureGitignoreEntry(projectRoot, rule);
20591
+ ensureDqlGitignore(projectRoot);
20111
20592
  }
20112
20593
  async function readGitDiff(cwd, filePath, staged = false) {
20113
20594
  const gitRoot = await resolveGitRoot(cwd);
@@ -20458,8 +20939,11 @@ async function readGitRemote(cwd) {
20458
20939
  const url = await execGit(gitRoot, ['remote', 'get-url', name]);
20459
20940
  return { inRepo: true, url: url.code === 0 ? url.stdout.trim() : null, name };
20460
20941
  }
20461
- function collectGovernedSourcePaths(root, folder, predicate) {
20462
- const start = join(root, folder);
20942
+ function collectGovernedSourcePaths(root, folder, predicate = () => true) {
20943
+ // `layout.skillsPath` may point at a sibling folder in the same Git
20944
+ // repository. Resolve both project-relative and absolute source roots so the
20945
+ // governed inventory follows the loader instead of assuming `./skills`.
20946
+ const start = resolve(root, folder);
20463
20947
  if (!existsSync(start))
20464
20948
  return [];
20465
20949
  const paths = [];
@@ -20472,13 +20956,16 @@ function collectGovernedSourcePaths(root, folder, predicate) {
20472
20956
  const absolute = join(dir, entry.name);
20473
20957
  if (entry.isDirectory())
20474
20958
  walk(absolute);
20475
- else if (entry.isFile() && predicate(entry.name))
20959
+ else if (entry.isFile() && predicate(entry.name, relative(root, absolute).replace(/\\/g, '/')))
20476
20960
  paths.push(absolute);
20477
20961
  }
20478
20962
  };
20479
20963
  walk(start);
20480
20964
  return paths;
20481
20965
  }
20966
+ function uniqueGovernedSourcePaths(paths) {
20967
+ return [...new Set(paths.map((path) => resolve(path)))].sort();
20968
+ }
20482
20969
  async function gitGovernedFileState(gitRoot, path) {
20483
20970
  const tracked = await execGit(gitRoot, ['ls-files', '--error-unmatch', '--', path]);
20484
20971
  const status = await execGit(gitRoot, ['status', '--porcelain=v1', '--untracked-files=all', '--', path]);
@@ -20490,13 +20977,23 @@ async function gitGovernedFileState(gitRoot, path) {
20490
20977
  async function readGitGovernedContext(cwd) {
20491
20978
  const gitRoot = await resolveGitRoot(cwd);
20492
20979
  const empty = () => ({ total: 0, tracked: 0, changed: 0, untracked: 0, ignored: 0, paths: [] });
20493
- if (!gitRoot)
20494
- return { inRepo: false, trackingReady: false, domains: empty(), skills: empty() };
20980
+ if (!gitRoot) {
20981
+ return {
20982
+ inRepo: false,
20983
+ trackingReady: false,
20984
+ legacyBroadIgnore: false,
20985
+ domains: empty(),
20986
+ skills: empty(),
20987
+ artifacts: empty(),
20988
+ learning: empty(),
20989
+ };
20990
+ }
20495
20991
  const summarize = async (files) => {
20496
20992
  const group = empty();
20497
- for (const absolute of files) {
20498
- const path = relative(gitRoot, absolute).replace(/\\/g, '/');
20499
- const state = await gitGovernedFileState(gitRoot, path);
20993
+ for (const absolute of uniqueGovernedSourcePaths(files)) {
20994
+ const gitPath = relative(gitRoot, absolute).replace(/\\/g, '/');
20995
+ const path = relative(cwd, absolute).replace(/\\/g, '/');
20996
+ const state = await gitGovernedFileState(gitRoot, gitPath);
20500
20997
  group.total += 1;
20501
20998
  group[state] += 1;
20502
20999
  if (group.paths.length < 24)
@@ -20504,33 +21001,57 @@ async function readGitGovernedContext(cwd) {
20504
21001
  }
20505
21002
  return group;
20506
21003
  };
20507
- const [domains, skills] = await Promise.all([
20508
- summarize(collectGovernedSourcePaths(cwd, 'domains', (name) => name === 'domain.dql')),
21004
+ const rootArtifacts = [
21005
+ 'blocks',
21006
+ 'terms',
21007
+ 'business-views',
21008
+ 'notebooks',
21009
+ 'workbooks',
21010
+ 'apps',
21011
+ 'dashboards',
21012
+ 'semantic-layer',
21013
+ 'tests',
21014
+ ].flatMap((folder) => collectGovernedSourcePaths(cwd, folder));
21015
+ const configArtifacts = ['dql.config.json', 'package.json']
21016
+ .map((path) => join(cwd, path))
21017
+ .filter(existsSync);
21018
+ const [domains, skills, artifacts, learning] = await Promise.all([
21019
+ summarize(collectGovernedSourcePaths(cwd, 'domains', (name, path) => !name.endsWith('.skill.md') && isGovernedSourceFile(path))),
20509
21020
  summarize([
20510
- ...collectGovernedSourcePaths(cwd, 'skills', (name) => name.endsWith('.skill.md')),
21021
+ ...collectGovernedSourcePaths(cwd, skillsDir(cwd), (name) => name.endsWith('.skill.md')),
21022
+ ...collectGovernedSourcePaths(cwd, 'domains', (name) => name.endsWith('.skill.md')),
20511
21023
  ...collectGovernedSourcePaths(cwd, '.dql/skills', (name) => name.endsWith('.skill.md')),
20512
21024
  ]),
21025
+ summarize([...rootArtifacts.filter((path) => isGovernedSourceFile(relative(cwd, path))), ...configArtifacts]),
21026
+ summarize([
21027
+ ...collectGovernedSourcePaths(cwd, '.dql/hints'),
21028
+ ...collectGovernedSourcePaths(cwd, '.dql/traces'),
21029
+ ...collectGovernedSourcePaths(cwd, '.dql/evaluations'),
21030
+ ...collectGovernedSourcePaths(cwd, '.dql/reviews'),
21031
+ ]),
20513
21032
  ]);
20514
- return { inRepo: true, trackingReady: domains.ignored === 0 && skills.ignored === 0, domains, skills };
21033
+ const gitignorePath = join(cwd, '.gitignore');
21034
+ const legacyBroadIgnore = existsSync(gitignorePath)
21035
+ && readFileSync(gitignorePath, 'utf-8').split(/\r?\n/).some(isLegacyBroadDqlIgnore);
21036
+ const groups = [domains, skills, artifacts, learning];
21037
+ return {
21038
+ inRepo: true,
21039
+ trackingReady: !legacyBroadIgnore && groups.every((group) => group.ignored === 0),
21040
+ legacyBroadIgnore,
21041
+ domains,
21042
+ skills,
21043
+ artifacts,
21044
+ learning,
21045
+ };
20515
21046
  }
20516
21047
  /** Replace only the legacy broad `.dql/` ignore with cache/runtime-specific rules. */
20517
21048
  async function enableGitGovernedContextTracking(cwd) {
20518
21049
  const gitRoot = await resolveGitRoot(cwd);
20519
21050
  if (!gitRoot)
20520
21051
  return { ok: false, error: 'Not a git repository' };
20521
- const ignorePath = join(cwd, '.gitignore');
20522
- const existing = existsSync(ignorePath) ? readFileSync(ignorePath, 'utf-8') : '';
20523
- const lines = existing.split(/\r?\n/);
20524
- const next = lines.filter((line) => !/^\s*\/?\.dql\/\s*$/.test(line));
20525
- for (const rule of LOCAL_RUNTIME_GITIGNORE_RULES) {
20526
- if (!next.some((line) => line.trim() === rule))
20527
- next.push(rule);
20528
- }
20529
- const rendered = `${next.join('\n').replace(/\n+$/, '')}\n`;
20530
- if (rendered !== existing)
20531
- writeFileSync(ignorePath, rendered, 'utf-8');
21052
+ const gitignore = ensureDqlGitignore(cwd);
20532
21053
  const migration = migrateLegacySkills(cwd);
20533
- return { ok: true, changed: rendered !== existing || migration.moved.length > 0 };
21054
+ return { ok: true, changed: gitignore.changed || migration.moved.length > 0 };
20534
21055
  }
20535
21056
  async function gitCreateBranch(cwd, name, checkout) {
20536
21057
  const gitRoot = await resolveGitRoot(cwd);