@duckcodeailabs/dql-cli 1.11.10 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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, 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
@@ -12297,9 +12585,9 @@ FROM from_counts, to_counts, joined, unmatched, from_max, to_max`;
12297
12585
  : 'Warehouse evidence does not prove the declared cardinality and safe fanout policy.',
12298
12586
  };
12299
12587
  }
12300
- function relatedProductsForDomain(manifest, domainId) {
12588
+ function relatedProductsForDomain(manifest, domainId, liveNotebookFiles = []) {
12301
12589
  const apps = Object.values(manifest.apps ?? {})
12302
- .filter((app) => app.ownerDomain === domainId || app.usesDomains.includes(domainId))
12590
+ .filter((app) => !domainId || app.ownerDomain === domainId || app.usesDomains.includes(domainId))
12303
12591
  .map((app) => ({
12304
12592
  id: app.id,
12305
12593
  name: app.name,
@@ -12312,8 +12600,8 @@ function relatedProductsForDomain(manifest, domainId) {
12312
12600
  lifecycle: app.lifecycle,
12313
12601
  legacyLayout: app.filePath.startsWith('domains/'),
12314
12602
  }));
12315
- const notebooks = Object.entries(manifest.notebooks)
12316
- .filter(([, notebook]) => notebook.ownerDomain === domainId || notebook.usesDomains.includes(domainId))
12603
+ const compiledNotebooks = Object.entries(manifest.notebooks)
12604
+ .filter(([, notebook]) => !domainId || notebook.ownerDomain === domainId || notebook.usesDomains.includes(domainId))
12317
12605
  .map(([id, notebook]) => ({
12318
12606
  id,
12319
12607
  title: notebook.title,
@@ -12325,6 +12613,29 @@ function relatedProductsForDomain(manifest, domainId) {
12325
12613
  classification: notebook.classification,
12326
12614
  legacyLayout: notebook.filePath.startsWith('domains/'),
12327
12615
  }));
12616
+ const notebooksByPath = new Map(compiledNotebooks.map((notebook) => [String(notebook.filePath), notebook]));
12617
+ for (const file of liveNotebookFiles) {
12618
+ if (file.type !== 'notebook')
12619
+ continue;
12620
+ const usesDomains = file.usesDomains ?? [];
12621
+ if (domainId && file.ownerDomain !== domainId && !usesDomains.includes(domainId))
12622
+ continue;
12623
+ if (notebooksByPath.has(file.path))
12624
+ continue;
12625
+ notebooksByPath.set(file.path, {
12626
+ id: file.path,
12627
+ title: file.name,
12628
+ filePath: file.path,
12629
+ ownerDomain: file.ownerDomain,
12630
+ usesDomains,
12631
+ purpose: undefined,
12632
+ requiredExports: [],
12633
+ classification: undefined,
12634
+ legacyLayout: file.path.startsWith('domains/'),
12635
+ });
12636
+ }
12637
+ const notebooks = [...notebooksByPath.values()]
12638
+ .sort((a, b) => String(a.title).localeCompare(String(b.title)));
12328
12639
  return { apps, notebooks };
12329
12640
  }
12330
12641
  function domainWorkspaceSummary(manifest, domainId) {
@@ -12709,6 +13020,8 @@ export function analyticalFreshnessObservedThrough(result, request) {
12709
13020
  function serializeSkill(skill) {
12710
13021
  return {
12711
13022
  id: skill.id,
13023
+ localId: skill.localId,
13024
+ qualifiedId: skill.qualifiedId,
12712
13025
  scope: 'project',
12713
13026
  user: undefined,
12714
13027
  domain: skill.domain,
@@ -13875,6 +14188,19 @@ function createDqlArtifactExecutionReceipt(source, compiledSql, parameters, resu
13875
14188
  function executionFingerprint(value) {
13876
14189
  return createHash('sha256').update(value).digest('hex');
13877
14190
  }
14191
+ /**
14192
+ * Older clients normalized executable DQL with trim(), while the receipt was
14193
+ * correctly created from the original renderer output ending in one newline.
14194
+ * Accept only that exact legacy line-ending variant; any substantive source
14195
+ * change still fails closed.
14196
+ */
14197
+ function executionSourceFingerprintMatches(expected, source) {
14198
+ return [
14199
+ source,
14200
+ `${source}\n`,
14201
+ `${source}\r\n`,
14202
+ ].some((candidate) => executionFingerprint(candidate) === expected);
14203
+ }
13878
14204
  function stableExecutionValue(value) {
13879
14205
  const seen = new WeakSet();
13880
14206
  return JSON.stringify(value, (_key, item) => {
@@ -13986,6 +14312,66 @@ function getProjectConnectionsForApi(config) {
13986
14312
  }
13987
14313
  return connections;
13988
14314
  }
14315
+ function metadataScopeDbtDefaults(projectRoot, config) {
14316
+ const manifestPath = resolveDbtManifestPath(projectRoot, config);
14317
+ if (!manifestPath || !existsSync(manifestPath))
14318
+ return { relations: [] };
14319
+ try {
14320
+ const source = readFileSync(manifestPath, 'utf-8');
14321
+ const lookup = buildDbtRelationLookup(JSON.parse(source));
14322
+ return {
14323
+ relations: Array.from(new Set([...lookup.models.values(), ...lookup.sources.values()])).sort(),
14324
+ dbtFingerprint: createHash('sha256').update(source).digest('hex'),
14325
+ };
14326
+ }
14327
+ catch {
14328
+ return { relations: [] };
14329
+ }
14330
+ }
14331
+ function configuredMetadataScopeInput(config, connectionId) {
14332
+ const value = config.metadataScopes?.[connectionId];
14333
+ return value && typeof value === 'object' ? value : undefined;
14334
+ }
14335
+ function activeConnectionMetadataScope(projectRoot, config, activeConnection, connectionId = config.defaultConnectionName ?? 'default') {
14336
+ return normalizeConnectionMetadataScope(connectionId, activeConnection, configuredMetadataScopeInput(config, connectionId), metadataScopeDbtDefaults(projectRoot, config));
14337
+ }
14338
+ function optionalActiveConnectionMetadataScope(projectRoot, config, activeConnection, connectionId) {
14339
+ try {
14340
+ return activeConnectionMetadataScope(projectRoot, config, activeConnection, connectionId);
14341
+ }
14342
+ catch {
14343
+ return null;
14344
+ }
14345
+ }
14346
+ function projectConnectionById(projectRoot, config, connectionId) {
14347
+ const stored = getStoredConnections(config);
14348
+ const normalized = normalizeStoredConnection(stored[connectionId]);
14349
+ if (normalized)
14350
+ return normalizeProjectConnection(normalized, projectRoot);
14351
+ if (connectionId === (config.defaultConnectionName ?? 'default') && config.defaultConnection) {
14352
+ return normalizeProjectConnection(config.defaultConnection, projectRoot);
14353
+ }
14354
+ return null;
14355
+ }
14356
+ function persistConnectionMetadataScope(projectRoot, connectionId, scope) {
14357
+ const configPath = join(projectRoot, 'dql.config.json');
14358
+ const raw = existsSync(configPath)
14359
+ ? JSON.parse(readFileSync(configPath, 'utf-8'))
14360
+ : {};
14361
+ const existing = raw.metadataScopes && typeof raw.metadataScopes === 'object' && !Array.isArray(raw.metadataScopes)
14362
+ ? raw.metadataScopes
14363
+ : {};
14364
+ raw.metadataScopes = {
14365
+ ...existing,
14366
+ [connectionId]: {
14367
+ mode: scope.mode,
14368
+ scopes: scope.selectedScopes,
14369
+ relations: scope.relations.length > 0 ? scope.relations : undefined,
14370
+ dbtFingerprint: scope.dbtFingerprint,
14371
+ },
14372
+ };
14373
+ writeFileSync(configPath, JSON.stringify(raw, null, 2) + '\n', 'utf-8');
14374
+ }
13989
14375
  const CONNECTOR_INSTALLS = {
13990
14376
  duckdb: {
13991
14377
  driver: 'duckdb',
@@ -15340,6 +15726,7 @@ function scanNotebookFiles(projectRoot) {
15340
15726
  path: relativePath,
15341
15727
  type,
15342
15728
  folder: relativeDir,
15729
+ ...readNotebookFileProductContext(fullPath, type),
15343
15730
  });
15344
15731
  }
15345
15732
  }
@@ -15364,12 +15751,29 @@ function scanNotebookFiles(projectRoot) {
15364
15751
  path: relativePath,
15365
15752
  type,
15366
15753
  folder: relativeDir.split('/')[0] ?? relativeDir,
15754
+ ...readNotebookFileProductContext(fullPath, type),
15367
15755
  });
15368
15756
  }
15369
15757
  }
15370
15758
  catch { /* skip unreadable dirs */ }
15371
15759
  }
15372
15760
  }
15761
+ function readNotebookFileProductContext(fullPath, type) {
15762
+ if ((type !== 'notebook' && type !== 'workbook') || !fullPath.endsWith('.dqlnb'))
15763
+ return {};
15764
+ try {
15765
+ const metadata = deserializeNotebook(readFileSync(fullPath, 'utf-8')).metadata;
15766
+ return {
15767
+ ...(metadata.ownerDomain ? { ownerDomain: metadata.ownerDomain } : {}),
15768
+ usesDomains: metadata.usesDomains ?? [],
15769
+ };
15770
+ }
15771
+ catch {
15772
+ // A malformed notebook must remain visible in the file list so the user
15773
+ // can open and repair it; it simply falls back to the Project folder.
15774
+ return {};
15775
+ }
15776
+ }
15373
15777
  function inferDqlArtifactName(fullPath, type, fallbackName) {
15374
15778
  if (!fullPath.endsWith('.dql'))
15375
15779
  return fallbackName;
@@ -15573,15 +15977,15 @@ export function openBlockStudioDocument(projectRoot, relativePath, semanticLayer
15573
15977
  throw new Error(`File not found: ${normalizedPath}`);
15574
15978
  }
15575
15979
  const source = readFileSync(absPath, 'utf-8');
15576
- const companionPath = blockCompanionRelativePath(normalizedPath);
15980
+ const companionPath = blockCompanionRelativePath(projectRoot, normalizedPath);
15577
15981
  const companion = companionPath ? readBlockCompanionFile(projectRoot, companionPath) : null;
15578
15982
  const parsedMetadata = parseBlockSourceMetadata(source);
15579
15983
  const fileName = normalizedPath.split('/').pop()?.replace(/\.dql$/, '') ?? 'block';
15580
15984
  const metadata = {
15581
15985
  name: parsedMetadata.name || companion?.name || fileName,
15582
15986
  path: normalizedPath,
15583
- domain: parsedMetadata.domain || companion?.domain || inferBlockStudioPathDomain(normalizedPath) || 'uncategorized',
15584
- folderPath: inferBlockStudioFolderPath(normalizedPath),
15987
+ domain: parsedMetadata.domain || companion?.domain || inferBlockStudioPathDomain(projectRoot, normalizedPath) || 'uncategorized',
15988
+ folderPath: inferBlockStudioFolderPath(projectRoot, normalizedPath),
15585
15989
  description: parsedMetadata.description || companion?.description || '',
15586
15990
  owner: parsedMetadata.owner || companion?.owner || '',
15587
15991
  tags: parsedMetadata.tags.length > 0 ? parsedMetadata.tags : companion?.tags ?? [],
@@ -15640,9 +16044,20 @@ function parseSemanticBlockConfig(source) {
15640
16044
  limit: limitMatch ? Number.parseInt(limitMatch[1], 10) : undefined,
15641
16045
  };
15642
16046
  }
15643
- export async function resolveSemanticTableMapping(executor, connection, semanticLayer) {
16047
+ export async function resolveSemanticTableMapping(executor, connection, semanticLayer, projectRoot) {
15644
16048
  if (!semanticLayer)
15645
16049
  return undefined;
16050
+ if (projectRoot) {
16051
+ const snapshot = latestRuntimeSchemaSnapshotForProject(projectRoot);
16052
+ if (snapshot?.scopeFingerprint && snapshot.tables.length) {
16053
+ return buildSemanticTableMapping(semanticLayer, snapshot.tables.map((table) => ({
16054
+ table_catalog: table.catalogOrDatabase,
16055
+ table_schema: table.schema,
16056
+ table_name: table.name ?? table.relation.split('.').at(-1),
16057
+ table_relation: table.relation,
16058
+ })));
16059
+ }
16060
+ }
15646
16061
  try {
15647
16062
  const tablesResult = await executor.executeQuery(`SELECT table_schema, table_name
15648
16063
  FROM information_schema.tables
@@ -15660,10 +16075,11 @@ export function buildSemanticTableMapping(semanticLayer, rows) {
15660
16075
  for (const row of rows) {
15661
16076
  const schema = String(row['table_schema'] ?? '');
15662
16077
  const name = String(row['table_name'] ?? '');
16078
+ const relation = typeof row['table_relation'] === 'string' ? row['table_relation'] : undefined;
15663
16079
  if (!name)
15664
16080
  continue;
15665
16081
  dbTableNames.add(name);
15666
- schemaQualified.set(name, schema ? `${schema}.${name}` : name);
16082
+ schemaQualified.set(name, relation ?? (schema ? `${schema}.${name}` : name));
15667
16083
  }
15668
16084
  const tableMapping = {};
15669
16085
  const allSemanticTables = new Set();
@@ -16587,7 +17003,7 @@ export function saveBlockStudioArtifacts(projectRoot, options) {
16587
17003
  const safeDomain = (options.domain ?? '')
16588
17004
  .trim()
16589
17005
  .toLowerCase()
16590
- .replace(/[^a-z0-9/_-]+/g, '-')
17006
+ .replace(/[^a-z0-9._/-]+/g, '-')
16591
17007
  .replace(/^\/+|\/+$/g, '') || 'uncategorized';
16592
17008
  const safeFolderPath = normalizeBlockStudioFolderPath(options.folderPath);
16593
17009
  const previousPath = options.currentPath ? normalize(options.currentPath).replace(/^\/+/, '') : null;
@@ -16615,7 +17031,7 @@ export function saveBlockStudioArtifacts(projectRoot, options) {
16615
17031
  const previousAbsPath = join(projectRoot, previousPath);
16616
17032
  if (existsSync(previousAbsPath))
16617
17033
  rmSync(previousAbsPath, { force: true });
16618
- const previousCompanion = blockCompanionRelativePath(previousPath);
17034
+ const previousCompanion = blockCompanionRelativePath(projectRoot, previousPath);
16619
17035
  if (previousCompanion) {
16620
17036
  const previousCompanionPath = join(projectRoot, previousCompanion);
16621
17037
  if (existsSync(previousCompanionPath))
@@ -16643,7 +17059,7 @@ export function deleteBlockStudioArtifacts(projectRoot, relativePath) {
16643
17059
  if (!existsSync(absolutePath)) {
16644
17060
  throw new Error(`File not found: ${normalizedPath}`);
16645
17061
  }
16646
- const companionPath = blockCompanionRelativePath(normalizedPath);
17062
+ const companionPath = blockCompanionRelativePath(projectRoot, normalizedPath);
16647
17063
  rmSync(absolutePath, { force: true });
16648
17064
  if (companionPath) {
16649
17065
  const absoluteCompanionPath = resolve(rootPath, companionPath);
@@ -16701,7 +17117,12 @@ export function saveBlockStudioDraftArtifacts(projectRoot, options) {
16701
17117
  }
16702
17118
  function canonicalBlockRelativePath(projectRoot, safeDomain, safeFolderPath, slug, previousPath) {
16703
17119
  const folder = safeFolderPath ? `${safeFolderPath}/` : '';
16704
- const previousDomainFirst = Boolean(previousPath?.match(/^domains\/[^/]+\/blocks\//));
17120
+ const packageRoot = loadDomainPackageRegistry(projectRoot).get(safeDomain)?.root;
17121
+ if (packageRoot) {
17122
+ const relativePackageRoot = relative(projectRoot, packageRoot).replaceAll('\\', '/');
17123
+ return `${relativePackageRoot}/blocks/${folder}${slug}.dql`;
17124
+ }
17125
+ const previousDomainFirst = Boolean(previousPath?.match(/^domains\/.+\/blocks\//));
16705
17126
  if (previousDomainFirst || existsSync(join(projectRoot, 'domains', safeDomain))) {
16706
17127
  return `domains/${safeDomain}/blocks/${folder}${slug}.dql`;
16707
17128
  }
@@ -16724,19 +17145,23 @@ function isDraftBlockPath(value) {
16724
17145
  if (!value)
16725
17146
  return false;
16726
17147
  const normalized = normalize(value).replace(/^\/+/, '');
16727
- return normalized.startsWith('blocks/_drafts/') || /^domains\/[^/]+\/blocks\/_drafts\//.test(normalized);
17148
+ return normalized.startsWith('blocks/_drafts/') || /^domains\/.+\/blocks\/_drafts\//.test(normalized);
16728
17149
  }
16729
17150
  function isBlockStudioBlockPath(value) {
16730
17151
  if (!value)
16731
17152
  return false;
16732
17153
  const normalized = normalize(value).replace(/^\/+/, '');
16733
- return normalized.startsWith('blocks/') || /^domains\/[^/]+\/blocks\//.test(normalized);
17154
+ return normalized.startsWith('blocks/') || /^domains\/.+\/blocks\//.test(normalized);
16734
17155
  }
16735
- function inferBlockStudioPathDomain(blockPath) {
17156
+ function inferBlockStudioPathDomain(projectRoot, blockPath) {
16736
17157
  const normalized = normalize(blockPath).replace(/^\/+/, '');
16737
17158
  if (normalized.startsWith('blocks/')) {
16738
17159
  return normalized.split('/')[1] ?? '';
16739
17160
  }
17161
+ const resolved = domainBlockPathParts(projectRoot, normalized);
17162
+ if (resolved) {
17163
+ return resolved.subpath.startsWith('_drafts/') ? `_drafts/${resolved.domain}` : resolved.domain;
17164
+ }
16740
17165
  const domainFirst = normalized.match(/^domains\/([^/]+)\/blocks\/(.+)$/);
16741
17166
  if (!domainFirst)
16742
17167
  return '';
@@ -16746,8 +17171,11 @@ function inferBlockStudioPathDomain(blockPath) {
16746
17171
  return `_drafts/${domain}`;
16747
17172
  return domain;
16748
17173
  }
16749
- function inferBlockStudioFolderPath(blockPath) {
17174
+ function inferBlockStudioFolderPath(projectRoot, blockPath) {
16750
17175
  const normalized = normalize(blockPath).replace(/^\/+/, '').replaceAll('\\', '/');
17176
+ const resolved = domainBlockPathParts(projectRoot, normalized);
17177
+ if (resolved)
17178
+ return resolved.subpath.split('/').slice(0, -1).join('/');
16751
17179
  const domainFirst = normalized.match(/^domains\/[^/]+\/blocks\/(.+)$/);
16752
17180
  if (domainFirst)
16753
17181
  return domainFirst[1].split('/').slice(0, -1).join('/');
@@ -16756,12 +17184,20 @@ function inferBlockStudioFolderPath(blockPath) {
16756
17184
  return legacy[1].split('/').slice(0, -1).join('/');
16757
17185
  return '';
16758
17186
  }
16759
- function blockCompanionRelativePath(blockPath) {
17187
+ function blockCompanionRelativePath(projectRoot, blockPath) {
16760
17188
  const normalized = normalize(blockPath).replace(/^\/+/, '');
16761
17189
  if (normalized.startsWith('blocks/')) {
16762
17190
  const withoutRoot = normalized.slice('blocks/'.length).replace(/\.dql$/, '.yaml');
16763
17191
  return join('semantic-layer', 'blocks', withoutRoot).replaceAll('\\', '/');
16764
17192
  }
17193
+ const resolved = domainBlockPathParts(projectRoot, normalized);
17194
+ if (resolved && resolved.subpath.endsWith('.dql')) {
17195
+ const blockPath = resolved.subpath.replace(/\.dql$/, '');
17196
+ const companionBlockPath = blockPath.startsWith('_drafts/')
17197
+ ? join('_drafts', resolved.domain, blockPath.slice('_drafts/'.length))
17198
+ : join(resolved.domain, blockPath);
17199
+ return join('semantic-layer', 'blocks', `${companionBlockPath}.yaml`).replaceAll('\\', '/');
17200
+ }
16765
17201
  const domainFirst = normalized.match(/^domains\/([^/]+)\/blocks\/(.+)\.dql$/);
16766
17202
  if (domainFirst) {
16767
17203
  const domain = domainFirst[1];
@@ -16773,6 +17209,19 @@ function blockCompanionRelativePath(blockPath) {
16773
17209
  }
16774
17210
  return null;
16775
17211
  }
17212
+ function domainBlockPathParts(projectRoot, blockPath) {
17213
+ const normalized = normalize(blockPath).replace(/^\/+/, '').replaceAll('\\', '/');
17214
+ if (!normalized.startsWith('domains/'))
17215
+ return null;
17216
+ const absolute = join(projectRoot, normalized);
17217
+ const pkg = loadDomainPackageRegistry(projectRoot).packageForPath(absolute);
17218
+ if (!pkg)
17219
+ return null;
17220
+ const local = relative(pkg.root, absolute).replaceAll('\\', '/');
17221
+ if (!local.startsWith('blocks/'))
17222
+ return null;
17223
+ return { domain: pkg.id, subpath: local.slice('blocks/'.length) };
17224
+ }
16776
17225
  function readBlockCompanionFile(projectRoot, relativePath) {
16777
17226
  const absPath = join(projectRoot, relativePath);
16778
17227
  if (!existsSync(absPath))
@@ -17085,7 +17534,7 @@ export function setBlockStudioStatus(projectRoot, blockPath, newStatus) {
17085
17534
  throw new Error('Block file not found');
17086
17535
  const source = setBlockStudioStatusInSource(readFileSync(absPath, 'utf-8'), newStatus);
17087
17536
  writeFileSync(absPath, source, 'utf-8');
17088
- const companionPath = blockCompanionRelativePath(normalizedPath);
17537
+ const companionPath = blockCompanionRelativePath(projectRoot, normalizedPath);
17089
17538
  if (!companionPath)
17090
17539
  return;
17091
17540
  const absCompanionPath = join(projectRoot, companionPath);
@@ -18254,16 +18703,18 @@ function buildConversationSystemPrompt(kind, isGeneralKnowledge, catalogContext,
18254
18703
  return lines.join('\n');
18255
18704
  }
18256
18705
  function renderConversationMemoryForPrompt(context) {
18706
+ const envelope = renderConversationEnvelopeForPrompt(context);
18257
18707
  const recap = buildConversationContextRecap(context);
18258
- if (!recap)
18708
+ if (!recap && !envelope)
18259
18709
  return undefined;
18260
18710
  return [
18261
18711
  'Current conversation context:',
18712
+ envelope,
18262
18713
  recap,
18263
18714
  'If the user asks what this conversation is about, summarize this context directly. Do not query data or invent values.',
18264
- ].join('\n');
18715
+ ].filter(Boolean).join('\n');
18265
18716
  }
18266
- function buildConversationContextRecap(context) {
18717
+ export function buildConversationContextRecap(context) {
18267
18718
  if (!context)
18268
18719
  return undefined;
18269
18720
  const turnRecap = buildConversationTurnsRecap(context);
@@ -18273,10 +18724,7 @@ function buildConversationContextRecap(context) {
18273
18724
  const sourceAnswerSummary = agentRunString(context.sourceAnswerSummary);
18274
18725
  const columns = agentRunStringArray(context.resultColumns ?? context.outputColumns).slice(0, 8);
18275
18726
  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];
18727
+ const firstRow = firstConversationResultRow(context.resultRowsSample, columns);
18280
18728
  const leadParts = buildPriorValueLeadParts(values);
18281
18729
  const rowFacts = firstRow
18282
18730
  ? columns
@@ -18308,16 +18756,19 @@ function buildConversationTurnsRecap(context) {
18308
18756
  const active = activeTurnId
18309
18757
  ? turns.find((turn) => agentRunString(turn.id) === activeTurnId)
18310
18758
  : undefined;
18311
- const selected = active ?? turns[turns.length - 1];
18759
+ // A recap turn is persisted like every other turn, but it must not become the
18760
+ // subject of the next recap. Prefer the newest analytical/authoring turn and
18761
+ // fall back to the active turn only when the thread contains conversation alone.
18762
+ const substantiveTurns = turns.filter((turn) => agentRunString(turn.route) !== 'conversation');
18763
+ const selected = active && agentRunString(active.route) !== 'conversation'
18764
+ ? active
18765
+ : substantiveTurns[substantiveTurns.length - 1] ?? active ?? turns[turns.length - 1];
18312
18766
  if (!selected)
18313
18767
  return undefined;
18314
18768
  const result = agentRunRecord(selected.result);
18315
18769
  const values = agentRunRecord(result?.dimensionValues);
18316
18770
  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];
18771
+ const firstRow = firstConversationResultRow(result?.rowsSample, columns);
18321
18772
  const leadParts = buildPriorValueLeadParts(values);
18322
18773
  const rowFacts = firstRow
18323
18774
  ? columns
@@ -18329,7 +18780,7 @@ function buildConversationTurnsRecap(context) {
18329
18780
  .filter(Boolean)
18330
18781
  : [];
18331
18782
  const previous = turns
18332
- .filter((turn) => turn !== selected)
18783
+ .filter((turn) => turn !== selected && agentRunString(turn.route) !== 'conversation')
18333
18784
  .map((turn) => agentRunString(turn.question))
18334
18785
  .filter((value) => Boolean(value))
18335
18786
  .slice(-2);
@@ -18343,6 +18794,17 @@ function buildConversationTurnsRecap(context) {
18343
18794
  ].filter(Boolean);
18344
18795
  return bits.length > 0 ? `We were talking about ${bits.join('. ')}.` : undefined;
18345
18796
  }
18797
+ function firstConversationResultRow(value, columns) {
18798
+ if (!Array.isArray(value) || value.length === 0)
18799
+ return undefined;
18800
+ const first = value[0];
18801
+ const record = agentRunRecord(first);
18802
+ if (record)
18803
+ return record;
18804
+ if (!Array.isArray(first))
18805
+ return undefined;
18806
+ return Object.fromEntries(columns.map((column, index) => [column, first[index]]));
18807
+ }
18346
18808
  function agentRunStringArray(value) {
18347
18809
  if (!Array.isArray(value))
18348
18810
  return [];
@@ -18526,12 +18988,19 @@ export function readGitMetadata(projectRoot) {
18526
18988
  return null;
18527
18989
  }
18528
18990
  }
18529
- function resolveBlockWriteTarget(projectRoot, safeDomain, slug) {
18991
+ function resolveBlockWriteTarget(projectRoot, safeDomain, safeFolderPath, slug) {
18992
+ const folder = safeFolderPath ? `${safeFolderPath}/` : '';
18993
+ const packageRoot = safeDomain ? loadDomainPackageRegistry(projectRoot).get(safeDomain)?.root : undefined;
18994
+ if (packageRoot) {
18995
+ const relativePackageRoot = relative(projectRoot, packageRoot).replaceAll('\\', '/');
18996
+ const relativePath = `${relativePackageRoot}/blocks/${folder}${slug}.dql`;
18997
+ return { relativePath, absPath: join(projectRoot, relativePath) };
18998
+ }
18530
18999
  if (safeDomain && existsSync(join(projectRoot, 'domains', safeDomain))) {
18531
- const relativePath = `domains/${safeDomain}/blocks/${slug}.dql`;
19000
+ const relativePath = `domains/${safeDomain}/blocks/${folder}${slug}.dql`;
18532
19001
  return { relativePath, absPath: join(projectRoot, relativePath) };
18533
19002
  }
18534
- const relativePath = safeDomain ? `blocks/${safeDomain}/${slug}.dql` : `blocks/${slug}.dql`;
19003
+ const relativePath = safeDomain ? `blocks/${safeDomain}/${folder}${slug}.dql` : `blocks/${folder}${slug}.dql`;
18535
19004
  return { relativePath, absPath: join(projectRoot, relativePath) };
18536
19005
  }
18537
19006
  export function createBlockArtifacts(projectRoot, options) {
@@ -18539,9 +19008,10 @@ export function createBlockArtifacts(projectRoot, options) {
18539
19008
  const safeDomain = (options.domain ?? '')
18540
19009
  .trim()
18541
19010
  .toLowerCase()
18542
- .replace(/[^a-z0-9/_-]+/g, '-')
19011
+ .replace(/[^a-z0-9._/-]+/g, '-')
18543
19012
  .replace(/^\/+|\/+$/g, '');
18544
- const target = resolveBlockWriteTarget(projectRoot, safeDomain, slug);
19013
+ const safeFolderPath = normalizeBlockStudioFolderPath(options.folderPath);
19014
+ const target = resolveBlockWriteTarget(projectRoot, safeDomain, safeFolderPath, slug);
18545
19015
  mkdirSync(dirname(target.absPath), { recursive: true });
18546
19016
  const blockPath = target.absPath;
18547
19017
  if (existsSync(blockPath)) {
@@ -18575,6 +19045,7 @@ export function createBlockArtifacts(projectRoot, options) {
18575
19045
  slug,
18576
19046
  name: options.name,
18577
19047
  domain: safeDomain || 'uncategorized',
19048
+ folderPath: safeFolderPath,
18578
19049
  owner: options.owner,
18579
19050
  description: options.description,
18580
19051
  tags: options.tags,
@@ -18594,9 +19065,9 @@ export function createSemanticBuilderBlock(projectRoot, options) {
18594
19065
  const safeDomain = (options.domain ?? '')
18595
19066
  .trim()
18596
19067
  .toLowerCase()
18597
- .replace(/[^a-z0-9/_-]+/g, '-')
19068
+ .replace(/[^a-z0-9._/-]+/g, '-')
18598
19069
  .replace(/^\/+|\/+$/g, '') || 'uncategorized';
18599
- const target = resolveBlockWriteTarget(projectRoot, safeDomain, slug);
19070
+ const target = resolveBlockWriteTarget(projectRoot, safeDomain, '', slug);
18600
19071
  mkdirSync(dirname(target.absPath), { recursive: true });
18601
19072
  const blockPath = target.absPath;
18602
19073
  if (existsSync(blockPath)) {
@@ -18922,7 +19393,7 @@ function yamlScalar(value) {
18922
19393
  return value;
18923
19394
  return JSON.stringify(value);
18924
19395
  }
18925
- function buildNotebookTemplate(title, template) {
19396
+ function buildNotebookTemplate(title, template, productContext = {}) {
18926
19397
  const id = () => Math.random().toString(36).slice(2, 10);
18927
19398
  let cells;
18928
19399
  if (template === "analysis") {
@@ -19035,7 +19506,18 @@ function buildNotebookTemplate(title, template) {
19035
19506
  else {
19036
19507
  cells = [];
19037
19508
  }
19038
- return JSON.stringify({ dqlnbVersion: 2, version: 1, title, cells }, null, 2);
19509
+ return JSON.stringify({
19510
+ dqlnbVersion: 2,
19511
+ version: 1,
19512
+ title,
19513
+ metadata: {
19514
+ createdWith: 'dql',
19515
+ ...(productContext.ownerDomain ? { ownerDomain: productContext.ownerDomain } : {}),
19516
+ usesDomains: productContext.usesDomains ?? (productContext.ownerDomain ? [productContext.ownerDomain] : []),
19517
+ requiredExports: [],
19518
+ },
19519
+ cells,
19520
+ }, null, 2);
19039
19521
  }
19040
19522
  /** Build a lineage graph from the project's blocks and semantic layer. */
19041
19523
  // Cache per project + source fingerprint. A process can serve different roots,
@@ -20081,33 +20563,8 @@ function ensureGitignoreEntry(projectRoot, pattern) {
20081
20563
  // Best-effort; failure to write .gitignore shouldn't fail the snapshot.
20082
20564
  }
20083
20565
  }
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
20566
  export function ensureLocalRuntimeGitignore(projectRoot) {
20109
- for (const rule of LOCAL_RUNTIME_GITIGNORE_RULES)
20110
- ensureGitignoreEntry(projectRoot, rule);
20567
+ ensureDqlGitignore(projectRoot);
20111
20568
  }
20112
20569
  async function readGitDiff(cwd, filePath, staged = false) {
20113
20570
  const gitRoot = await resolveGitRoot(cwd);
@@ -20458,8 +20915,11 @@ async function readGitRemote(cwd) {
20458
20915
  const url = await execGit(gitRoot, ['remote', 'get-url', name]);
20459
20916
  return { inRepo: true, url: url.code === 0 ? url.stdout.trim() : null, name };
20460
20917
  }
20461
- function collectGovernedSourcePaths(root, folder, predicate) {
20462
- const start = join(root, folder);
20918
+ function collectGovernedSourcePaths(root, folder, predicate = () => true) {
20919
+ // `layout.skillsPath` may point at a sibling folder in the same Git
20920
+ // repository. Resolve both project-relative and absolute source roots so the
20921
+ // governed inventory follows the loader instead of assuming `./skills`.
20922
+ const start = resolve(root, folder);
20463
20923
  if (!existsSync(start))
20464
20924
  return [];
20465
20925
  const paths = [];
@@ -20472,13 +20932,16 @@ function collectGovernedSourcePaths(root, folder, predicate) {
20472
20932
  const absolute = join(dir, entry.name);
20473
20933
  if (entry.isDirectory())
20474
20934
  walk(absolute);
20475
- else if (entry.isFile() && predicate(entry.name))
20935
+ else if (entry.isFile() && predicate(entry.name, relative(root, absolute).replace(/\\/g, '/')))
20476
20936
  paths.push(absolute);
20477
20937
  }
20478
20938
  };
20479
20939
  walk(start);
20480
20940
  return paths;
20481
20941
  }
20942
+ function uniqueGovernedSourcePaths(paths) {
20943
+ return [...new Set(paths.map((path) => resolve(path)))].sort();
20944
+ }
20482
20945
  async function gitGovernedFileState(gitRoot, path) {
20483
20946
  const tracked = await execGit(gitRoot, ['ls-files', '--error-unmatch', '--', path]);
20484
20947
  const status = await execGit(gitRoot, ['status', '--porcelain=v1', '--untracked-files=all', '--', path]);
@@ -20490,13 +20953,23 @@ async function gitGovernedFileState(gitRoot, path) {
20490
20953
  async function readGitGovernedContext(cwd) {
20491
20954
  const gitRoot = await resolveGitRoot(cwd);
20492
20955
  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() };
20956
+ if (!gitRoot) {
20957
+ return {
20958
+ inRepo: false,
20959
+ trackingReady: false,
20960
+ legacyBroadIgnore: false,
20961
+ domains: empty(),
20962
+ skills: empty(),
20963
+ artifacts: empty(),
20964
+ learning: empty(),
20965
+ };
20966
+ }
20495
20967
  const summarize = async (files) => {
20496
20968
  const group = empty();
20497
- for (const absolute of files) {
20498
- const path = relative(gitRoot, absolute).replace(/\\/g, '/');
20499
- const state = await gitGovernedFileState(gitRoot, path);
20969
+ for (const absolute of uniqueGovernedSourcePaths(files)) {
20970
+ const gitPath = relative(gitRoot, absolute).replace(/\\/g, '/');
20971
+ const path = relative(cwd, absolute).replace(/\\/g, '/');
20972
+ const state = await gitGovernedFileState(gitRoot, gitPath);
20500
20973
  group.total += 1;
20501
20974
  group[state] += 1;
20502
20975
  if (group.paths.length < 24)
@@ -20504,33 +20977,57 @@ async function readGitGovernedContext(cwd) {
20504
20977
  }
20505
20978
  return group;
20506
20979
  };
20507
- const [domains, skills] = await Promise.all([
20508
- summarize(collectGovernedSourcePaths(cwd, 'domains', (name) => name === 'domain.dql')),
20980
+ const rootArtifacts = [
20981
+ 'blocks',
20982
+ 'terms',
20983
+ 'business-views',
20984
+ 'notebooks',
20985
+ 'workbooks',
20986
+ 'apps',
20987
+ 'dashboards',
20988
+ 'semantic-layer',
20989
+ 'tests',
20990
+ ].flatMap((folder) => collectGovernedSourcePaths(cwd, folder));
20991
+ const configArtifacts = ['dql.config.json', 'package.json']
20992
+ .map((path) => join(cwd, path))
20993
+ .filter(existsSync);
20994
+ const [domains, skills, artifacts, learning] = await Promise.all([
20995
+ summarize(collectGovernedSourcePaths(cwd, 'domains', (name, path) => !name.endsWith('.skill.md') && isGovernedSourceFile(path))),
20509
20996
  summarize([
20510
- ...collectGovernedSourcePaths(cwd, 'skills', (name) => name.endsWith('.skill.md')),
20997
+ ...collectGovernedSourcePaths(cwd, skillsDir(cwd), (name) => name.endsWith('.skill.md')),
20998
+ ...collectGovernedSourcePaths(cwd, 'domains', (name) => name.endsWith('.skill.md')),
20511
20999
  ...collectGovernedSourcePaths(cwd, '.dql/skills', (name) => name.endsWith('.skill.md')),
20512
21000
  ]),
21001
+ summarize([...rootArtifacts.filter((path) => isGovernedSourceFile(relative(cwd, path))), ...configArtifacts]),
21002
+ summarize([
21003
+ ...collectGovernedSourcePaths(cwd, '.dql/hints'),
21004
+ ...collectGovernedSourcePaths(cwd, '.dql/traces'),
21005
+ ...collectGovernedSourcePaths(cwd, '.dql/evaluations'),
21006
+ ...collectGovernedSourcePaths(cwd, '.dql/reviews'),
21007
+ ]),
20513
21008
  ]);
20514
- return { inRepo: true, trackingReady: domains.ignored === 0 && skills.ignored === 0, domains, skills };
21009
+ const gitignorePath = join(cwd, '.gitignore');
21010
+ const legacyBroadIgnore = existsSync(gitignorePath)
21011
+ && readFileSync(gitignorePath, 'utf-8').split(/\r?\n/).some(isLegacyBroadDqlIgnore);
21012
+ const groups = [domains, skills, artifacts, learning];
21013
+ return {
21014
+ inRepo: true,
21015
+ trackingReady: !legacyBroadIgnore && groups.every((group) => group.ignored === 0),
21016
+ legacyBroadIgnore,
21017
+ domains,
21018
+ skills,
21019
+ artifacts,
21020
+ learning,
21021
+ };
20515
21022
  }
20516
21023
  /** Replace only the legacy broad `.dql/` ignore with cache/runtime-specific rules. */
20517
21024
  async function enableGitGovernedContextTracking(cwd) {
20518
21025
  const gitRoot = await resolveGitRoot(cwd);
20519
21026
  if (!gitRoot)
20520
21027
  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');
21028
+ const gitignore = ensureDqlGitignore(cwd);
20532
21029
  const migration = migrateLegacySkills(cwd);
20533
- return { ok: true, changed: rendered !== existing || migration.moved.length > 0 };
21030
+ return { ok: true, changed: gitignore.changed || migration.moved.length > 0 };
20534
21031
  }
20535
21032
  async function gitCreateBranch(cwd, name, checkout) {
20536
21033
  const gitRoot = await resolveGitRoot(cwd);