@duckcodeailabs/dql-cli 1.11.10 → 1.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/dql-notebook/assets/{index-C8miSLDI.js → index-CNQTSBBg.js} +407 -383
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/commands/certify.js +1 -1
- package/dist/commands/certify.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +102 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +6 -13
- package/dist/commands/init.js.map +1 -1
- package/dist/git-contract.d.ts +18 -0
- package/dist/git-contract.d.ts.map +1 -0
- package/dist/git-contract.js +64 -0
- package/dist/git-contract.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +31 -15
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/llm/types.d.ts +2 -0
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/local-runtime.d.ts +5 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +786 -265
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- package/dist/warehouse-metadata.d.ts +60 -0
- package/dist/warehouse-metadata.d.ts.map +1 -0
- package/dist/warehouse-metadata.js +390 -0
- package/dist/warehouse-metadata.js.map +1 -0
- package/package.json +10 -10
package/dist/local-runtime.js
CHANGED
|
@@ -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,
|
|
23
|
+
import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, prepareConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, propose, proposePlan, recordGovernedCorrection, HintStore, defaultHintIndexPath, ensureHintIndexFresh, listHintsFromGit, getHintEvaluationFromGit, getCorrectionTraceFromGit, inspectGovernedHint, editGovernedHintCandidate, reopenGovernedHint, retireHint, supersedeHint, hintsConflict, mineJoinPatterns, reviewGovernedHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, internalRelationIdsInSql, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, projectEmbeddingProvider, isHashedEmbeddingProvider, clearProjectEmbeddingCache, upgradeVectorIndexForProject, openMetadataCatalog, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, } from '@duckcodeailabs/dql-agent';
|
|
23
24
|
import { gatherProposeEnrichment } from './propose-enrich.js';
|
|
24
25
|
import { handleAppsApi, proposeAppAiBuild, recommendVisualization } from './apps-api.js';
|
|
25
26
|
import { getActiveProvider, getEffectiveProviderConfig, isProviderSettingsId, listProviderSettings, saveProviderSettings, } from './settings/provider-settings.js';
|
|
@@ -39,6 +40,7 @@ import { observeWarehouseTargetIdentity } from './semantic-execution/connection-
|
|
|
39
40
|
import { configuredWarehouseTargetIdentity } from './semantic-execution/connection-identity.js';
|
|
40
41
|
import { executeTargetBoundSemanticQuery, semanticExecutionFailureCode, semanticExecutionFailureDetails, } from './semantic-execution/execution-gateway.js';
|
|
41
42
|
import { buildSemanticTargetBinding } from './semantic-execution/target-binding.js';
|
|
43
|
+
import { normalizeConnectionMetadataScope, syncWarehouseMetadata, warehouseMetadataStatus, } from './warehouse-metadata.js';
|
|
42
44
|
import { NotebookDatasetWorkspace, } from "./notebook-datasets.js";
|
|
43
45
|
import { prepareBlockInvocation } from './block-invocation.js';
|
|
44
46
|
const NOTEBOOK_EXECUTE_PREVIEW_ROW_LIMIT = 500;
|
|
@@ -418,6 +420,8 @@ async function conversationContextFromThread(store, threadId, clientContext, que
|
|
|
418
420
|
sourceCertifiedBlock: turn.sourceCertifiedBlock,
|
|
419
421
|
route: turn.route,
|
|
420
422
|
trustLabel: turn.trustLabel,
|
|
423
|
+
runStatus: turn.runStatus,
|
|
424
|
+
stopReason: turn.stopReason,
|
|
421
425
|
certification: turn.certification,
|
|
422
426
|
contextPackId: turn.contextPackId,
|
|
423
427
|
dqlArtifact: turn.dqlArtifact,
|
|
@@ -453,6 +457,7 @@ async function conversationContextFromThread(store, threadId, clientContext, que
|
|
|
453
457
|
...(clientContext ?? {}),
|
|
454
458
|
conversationStateVersion: 1,
|
|
455
459
|
threadId,
|
|
460
|
+
...(serverSnapshot ? { conversationEnvelope: serverSnapshot } : {}),
|
|
456
461
|
...(serverSnapshot ? { serverSnapshot } : {}),
|
|
457
462
|
...(thread?.rollingSummary ? { conversationSummary: thread.rollingSummary } : {}),
|
|
458
463
|
...(turns.length > 0 ? { turns, activeTurnId: turns[turns.length - 1].id } : {}),
|
|
@@ -509,6 +514,8 @@ export function conversationTurnInputFromRun(run) {
|
|
|
509
514
|
answerText: run.answer,
|
|
510
515
|
route: run.route,
|
|
511
516
|
trustLabel: agentRunString(payload?.trustLabel) ?? run.trustState,
|
|
517
|
+
runStatus: run.status,
|
|
518
|
+
stopReason: run.stopReason,
|
|
512
519
|
certification: agentRunString(payload?.certification),
|
|
513
520
|
sourceCertifiedBlock: agentRunString(payload?.sourceCertifiedBlock)
|
|
514
521
|
?? (artifact?.kind === 'answer' ? agentRunString(artifact.ref) : undefined),
|
|
@@ -640,6 +647,17 @@ export async function startLocalServer(opts) {
|
|
|
640
647
|
ensureLocalRuntimeGitignore(projectRoot);
|
|
641
648
|
let projectConfig = loadProjectConfig(projectRoot);
|
|
642
649
|
recordAgentRuntimeVersion(projectRoot, runtimeVersion);
|
|
650
|
+
// A clone carries governed Hint Graph files in Git, never the rebuildable
|
|
651
|
+
// `.dql/cache` database. Materialize that projection as soon as the Notebook
|
|
652
|
+
// opens so Agent Learning and the first retrieval see the same shared truth.
|
|
653
|
+
// Malformed/local cache state must not prevent the rest of the OSS notebook
|
|
654
|
+
// from starting; normal review/reindex surfaces can still report and repair it.
|
|
655
|
+
try {
|
|
656
|
+
ensureHintIndexFresh(projectRoot);
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
// Best-effort bootstrap, consistent with the existing agent reindex path.
|
|
660
|
+
}
|
|
643
661
|
const configuredConnection = rawConnection
|
|
644
662
|
? normalizeProjectConnection(rawConnection, projectRoot)
|
|
645
663
|
: projectConfig.defaultConnection
|
|
@@ -1304,7 +1322,7 @@ export async function startLocalServer(opts) {
|
|
|
1304
1322
|
semanticDriver = undefined;
|
|
1305
1323
|
}
|
|
1306
1324
|
const semanticTableMapping = semanticLayer && semanticConnection
|
|
1307
|
-
? await resolveSemanticTableMapping(executor, semanticConnection, semanticLayer)
|
|
1325
|
+
? await resolveSemanticTableMapping(executor, semanticConnection, semanticLayer, projectRoot)
|
|
1308
1326
|
: undefined;
|
|
1309
1327
|
const requestedDomain = normalizeAgentRunDomain(agentRunWorkspaceValue(request, 'domain'));
|
|
1310
1328
|
const requestedPurpose = agentRunWorkspaceValue(request, 'purpose');
|
|
@@ -2188,13 +2206,16 @@ export async function startLocalServer(opts) {
|
|
|
2188
2206
|
message: 'Building catalog-grounded research plan.',
|
|
2189
2207
|
route: 'research',
|
|
2190
2208
|
});
|
|
2209
|
+
const conversationHistory = request.history?.length
|
|
2210
|
+
? request.history
|
|
2211
|
+
: conversationHistoryFromContext(request.conversationContext);
|
|
2191
2212
|
const plan = await planResearch({
|
|
2192
2213
|
question: request.question,
|
|
2193
2214
|
metrics,
|
|
2194
2215
|
blocks,
|
|
2195
2216
|
intent: request.intent,
|
|
2196
|
-
isFollowUp:
|
|
2197
|
-
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?.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
6592
|
-
//
|
|
6593
|
-
//
|
|
6594
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
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
|
-
//
|
|
6761
|
-
//
|
|
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 } =
|
|
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
|
-
//
|
|
6782
|
-
//
|
|
6783
|
-
//
|
|
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({
|
|
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
|
|
8080
|
-
?
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
|
12107
|
+
if (expectedReceipt
|
|
12108
|
+
&& !sourceRefreshedFromDisk
|
|
12109
|
+
&& !executionSourceFingerprintMatches(expectedReceipt.sourceFingerprint, source)) {
|
|
11822
12110
|
throw new Error('DQL artifact source changed after the answer was produced. Refresh the answer before applying it.');
|
|
11823
12111
|
}
|
|
11824
12112
|
const parameters = body.parameters && typeof body.parameters === 'object' && !Array.isArray(body.parameters)
|
|
@@ -11958,7 +12246,7 @@ export async function startLocalServer(opts) {
|
|
|
11958
12246
|
}
|
|
11959
12247
|
const cellConnection = await resolveExecutionConnection(body);
|
|
11960
12248
|
const tableMapping = needsSemanticTableMapping(executableCell)
|
|
11961
|
-
? await resolveSemanticTableMapping(executor, cellConnection, semanticLayer)
|
|
12249
|
+
? await resolveSemanticTableMapping(executor, cellConnection, semanticLayer, projectRoot)
|
|
11962
12250
|
: undefined;
|
|
11963
12251
|
const semanticCompose = executableCell.type === 'dql'
|
|
11964
12252
|
&& semanticLayer
|
|
@@ -11988,6 +12276,9 @@ export async function startLocalServer(opts) {
|
|
|
11988
12276
|
res.end(serializeJSON({ cellType: cell.type, result: null }));
|
|
11989
12277
|
return;
|
|
11990
12278
|
}
|
|
12279
|
+
const internalRelationError = internalDqlRelationIdValidationError(executableSql, cell.type === 'dql' ? 'DQL query' : 'Notebook query');
|
|
12280
|
+
if (internalRelationError)
|
|
12281
|
+
throw new DqlInternalRelationIdError(internalRelationError);
|
|
11991
12282
|
const prepared = prepareLocalExecution(executableSql, cellConnection, projectRoot, projectConfig);
|
|
11992
12283
|
const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
|
|
11993
12284
|
assertAppAccess({ app, domain: resolved.domain ?? app?.domain, level: 'execute' });
|
|
@@ -12101,24 +12392,33 @@ export async function startLocalServer(opts) {
|
|
|
12101
12392
|
error: error instanceof Error ? error.message : String(error),
|
|
12102
12393
|
});
|
|
12103
12394
|
}
|
|
12395
|
+
const internalRelationCode = error instanceof DqlInternalRelationIdError
|
|
12396
|
+
? 'DQL_INTERNAL_RELATION_ID'
|
|
12397
|
+
: undefined;
|
|
12104
12398
|
const semanticCode = semanticExecutionFailureCode(error) ?? semanticRuntimeErrorCode(error);
|
|
12105
|
-
const code = semanticCode ?? classifyAnalyticalFailure(error);
|
|
12399
|
+
const code = internalRelationCode ?? semanticCode ?? classifyAnalyticalFailure(error);
|
|
12106
12400
|
const semanticDetails = semanticExecutionFailureDetails(error) ?? semanticRuntimeErrorDetails(error);
|
|
12107
12401
|
const details = semanticDetails && typeof semanticDetails === 'object' && !Array.isArray(semanticDetails)
|
|
12108
12402
|
? semanticDetails
|
|
12109
12403
|
: {};
|
|
12110
|
-
const status =
|
|
12111
|
-
?
|
|
12112
|
-
:
|
|
12113
|
-
?
|
|
12114
|
-
:
|
|
12404
|
+
const status = internalRelationCode
|
|
12405
|
+
? 400
|
|
12406
|
+
: code === 'EXECUTION_TARGET_MISMATCH' || code === 'SEMANTIC_SOURCE_DRIFT'
|
|
12407
|
+
? 409
|
|
12408
|
+
: semanticCode
|
|
12409
|
+
? 400
|
|
12410
|
+
: 500;
|
|
12115
12411
|
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
12116
12412
|
res.end(serializeJSON({
|
|
12117
12413
|
error: error instanceof Error ? error.message : String(error),
|
|
12118
12414
|
code,
|
|
12119
12415
|
details: {
|
|
12120
12416
|
...details,
|
|
12121
|
-
phase: typeof details.phase === 'string'
|
|
12417
|
+
phase: typeof details.phase === 'string'
|
|
12418
|
+
? details.phase
|
|
12419
|
+
: internalRelationCode || semanticCode
|
|
12420
|
+
? 'compilation'
|
|
12421
|
+
: 'execution',
|
|
12122
12422
|
executionContext: notebookExecutionIdentity(execContext, body?.cell?.id),
|
|
12123
12423
|
executionTarget: executionTargetDescriptor(body && typeof body === 'object' ? body : {}),
|
|
12124
12424
|
},
|
|
@@ -12297,9 +12597,9 @@ FROM from_counts, to_counts, joined, unmatched, from_max, to_max`;
|
|
|
12297
12597
|
: 'Warehouse evidence does not prove the declared cardinality and safe fanout policy.',
|
|
12298
12598
|
};
|
|
12299
12599
|
}
|
|
12300
|
-
function relatedProductsForDomain(manifest, domainId) {
|
|
12600
|
+
function relatedProductsForDomain(manifest, domainId, liveNotebookFiles = []) {
|
|
12301
12601
|
const apps = Object.values(manifest.apps ?? {})
|
|
12302
|
-
.filter((app) => app.ownerDomain === domainId || app.usesDomains.includes(domainId))
|
|
12602
|
+
.filter((app) => !domainId || app.ownerDomain === domainId || app.usesDomains.includes(domainId))
|
|
12303
12603
|
.map((app) => ({
|
|
12304
12604
|
id: app.id,
|
|
12305
12605
|
name: app.name,
|
|
@@ -12312,8 +12612,8 @@ function relatedProductsForDomain(manifest, domainId) {
|
|
|
12312
12612
|
lifecycle: app.lifecycle,
|
|
12313
12613
|
legacyLayout: app.filePath.startsWith('domains/'),
|
|
12314
12614
|
}));
|
|
12315
|
-
const
|
|
12316
|
-
.filter(([, notebook]) => notebook.ownerDomain === domainId || notebook.usesDomains.includes(domainId))
|
|
12615
|
+
const compiledNotebooks = Object.entries(manifest.notebooks)
|
|
12616
|
+
.filter(([, notebook]) => !domainId || notebook.ownerDomain === domainId || notebook.usesDomains.includes(domainId))
|
|
12317
12617
|
.map(([id, notebook]) => ({
|
|
12318
12618
|
id,
|
|
12319
12619
|
title: notebook.title,
|
|
@@ -12325,6 +12625,29 @@ function relatedProductsForDomain(manifest, domainId) {
|
|
|
12325
12625
|
classification: notebook.classification,
|
|
12326
12626
|
legacyLayout: notebook.filePath.startsWith('domains/'),
|
|
12327
12627
|
}));
|
|
12628
|
+
const notebooksByPath = new Map(compiledNotebooks.map((notebook) => [String(notebook.filePath), notebook]));
|
|
12629
|
+
for (const file of liveNotebookFiles) {
|
|
12630
|
+
if (file.type !== 'notebook')
|
|
12631
|
+
continue;
|
|
12632
|
+
const usesDomains = file.usesDomains ?? [];
|
|
12633
|
+
if (domainId && file.ownerDomain !== domainId && !usesDomains.includes(domainId))
|
|
12634
|
+
continue;
|
|
12635
|
+
if (notebooksByPath.has(file.path))
|
|
12636
|
+
continue;
|
|
12637
|
+
notebooksByPath.set(file.path, {
|
|
12638
|
+
id: file.path,
|
|
12639
|
+
title: file.name,
|
|
12640
|
+
filePath: file.path,
|
|
12641
|
+
ownerDomain: file.ownerDomain,
|
|
12642
|
+
usesDomains,
|
|
12643
|
+
purpose: undefined,
|
|
12644
|
+
requiredExports: [],
|
|
12645
|
+
classification: undefined,
|
|
12646
|
+
legacyLayout: file.path.startsWith('domains/'),
|
|
12647
|
+
});
|
|
12648
|
+
}
|
|
12649
|
+
const notebooks = [...notebooksByPath.values()]
|
|
12650
|
+
.sort((a, b) => String(a.title).localeCompare(String(b.title)));
|
|
12328
12651
|
return { apps, notebooks };
|
|
12329
12652
|
}
|
|
12330
12653
|
function domainWorkspaceSummary(manifest, domainId) {
|
|
@@ -12709,6 +13032,8 @@ export function analyticalFreshnessObservedThrough(result, request) {
|
|
|
12709
13032
|
function serializeSkill(skill) {
|
|
12710
13033
|
return {
|
|
12711
13034
|
id: skill.id,
|
|
13035
|
+
localId: skill.localId,
|
|
13036
|
+
qualifiedId: skill.qualifiedId,
|
|
12712
13037
|
scope: 'project',
|
|
12713
13038
|
user: undefined,
|
|
12714
13039
|
domain: skill.domain,
|
|
@@ -13875,6 +14200,19 @@ function createDqlArtifactExecutionReceipt(source, compiledSql, parameters, resu
|
|
|
13875
14200
|
function executionFingerprint(value) {
|
|
13876
14201
|
return createHash('sha256').update(value).digest('hex');
|
|
13877
14202
|
}
|
|
14203
|
+
/**
|
|
14204
|
+
* Older clients normalized executable DQL with trim(), while the receipt was
|
|
14205
|
+
* correctly created from the original renderer output ending in one newline.
|
|
14206
|
+
* Accept only that exact legacy line-ending variant; any substantive source
|
|
14207
|
+
* change still fails closed.
|
|
14208
|
+
*/
|
|
14209
|
+
function executionSourceFingerprintMatches(expected, source) {
|
|
14210
|
+
return [
|
|
14211
|
+
source,
|
|
14212
|
+
`${source}\n`,
|
|
14213
|
+
`${source}\r\n`,
|
|
14214
|
+
].some((candidate) => executionFingerprint(candidate) === expected);
|
|
14215
|
+
}
|
|
13878
14216
|
function stableExecutionValue(value) {
|
|
13879
14217
|
const seen = new WeakSet();
|
|
13880
14218
|
return JSON.stringify(value, (_key, item) => {
|
|
@@ -13986,6 +14324,66 @@ function getProjectConnectionsForApi(config) {
|
|
|
13986
14324
|
}
|
|
13987
14325
|
return connections;
|
|
13988
14326
|
}
|
|
14327
|
+
function metadataScopeDbtDefaults(projectRoot, config) {
|
|
14328
|
+
const manifestPath = resolveDbtManifestPath(projectRoot, config);
|
|
14329
|
+
if (!manifestPath || !existsSync(manifestPath))
|
|
14330
|
+
return { relations: [] };
|
|
14331
|
+
try {
|
|
14332
|
+
const source = readFileSync(manifestPath, 'utf-8');
|
|
14333
|
+
const lookup = buildDbtRelationLookup(JSON.parse(source));
|
|
14334
|
+
return {
|
|
14335
|
+
relations: Array.from(new Set([...lookup.models.values(), ...lookup.sources.values()])).sort(),
|
|
14336
|
+
dbtFingerprint: createHash('sha256').update(source).digest('hex'),
|
|
14337
|
+
};
|
|
14338
|
+
}
|
|
14339
|
+
catch {
|
|
14340
|
+
return { relations: [] };
|
|
14341
|
+
}
|
|
14342
|
+
}
|
|
14343
|
+
function configuredMetadataScopeInput(config, connectionId) {
|
|
14344
|
+
const value = config.metadataScopes?.[connectionId];
|
|
14345
|
+
return value && typeof value === 'object' ? value : undefined;
|
|
14346
|
+
}
|
|
14347
|
+
function activeConnectionMetadataScope(projectRoot, config, activeConnection, connectionId = config.defaultConnectionName ?? 'default') {
|
|
14348
|
+
return normalizeConnectionMetadataScope(connectionId, activeConnection, configuredMetadataScopeInput(config, connectionId), metadataScopeDbtDefaults(projectRoot, config));
|
|
14349
|
+
}
|
|
14350
|
+
function optionalActiveConnectionMetadataScope(projectRoot, config, activeConnection, connectionId) {
|
|
14351
|
+
try {
|
|
14352
|
+
return activeConnectionMetadataScope(projectRoot, config, activeConnection, connectionId);
|
|
14353
|
+
}
|
|
14354
|
+
catch {
|
|
14355
|
+
return null;
|
|
14356
|
+
}
|
|
14357
|
+
}
|
|
14358
|
+
function projectConnectionById(projectRoot, config, connectionId) {
|
|
14359
|
+
const stored = getStoredConnections(config);
|
|
14360
|
+
const normalized = normalizeStoredConnection(stored[connectionId]);
|
|
14361
|
+
if (normalized)
|
|
14362
|
+
return normalizeProjectConnection(normalized, projectRoot);
|
|
14363
|
+
if (connectionId === (config.defaultConnectionName ?? 'default') && config.defaultConnection) {
|
|
14364
|
+
return normalizeProjectConnection(config.defaultConnection, projectRoot);
|
|
14365
|
+
}
|
|
14366
|
+
return null;
|
|
14367
|
+
}
|
|
14368
|
+
function persistConnectionMetadataScope(projectRoot, connectionId, scope) {
|
|
14369
|
+
const configPath = join(projectRoot, 'dql.config.json');
|
|
14370
|
+
const raw = existsSync(configPath)
|
|
14371
|
+
? JSON.parse(readFileSync(configPath, 'utf-8'))
|
|
14372
|
+
: {};
|
|
14373
|
+
const existing = raw.metadataScopes && typeof raw.metadataScopes === 'object' && !Array.isArray(raw.metadataScopes)
|
|
14374
|
+
? raw.metadataScopes
|
|
14375
|
+
: {};
|
|
14376
|
+
raw.metadataScopes = {
|
|
14377
|
+
...existing,
|
|
14378
|
+
[connectionId]: {
|
|
14379
|
+
mode: scope.mode,
|
|
14380
|
+
scopes: scope.selectedScopes,
|
|
14381
|
+
relations: scope.relations.length > 0 ? scope.relations : undefined,
|
|
14382
|
+
dbtFingerprint: scope.dbtFingerprint,
|
|
14383
|
+
},
|
|
14384
|
+
};
|
|
14385
|
+
writeFileSync(configPath, JSON.stringify(raw, null, 2) + '\n', 'utf-8');
|
|
14386
|
+
}
|
|
13989
14387
|
const CONNECTOR_INSTALLS = {
|
|
13990
14388
|
duckdb: {
|
|
13991
14389
|
driver: 'duckdb',
|
|
@@ -15017,6 +15415,9 @@ function summarizeExploratoryJoinProbe(rows, leftRelation, leftColumn, rightRela
|
|
|
15017
15415
|
return `${leftRelation}.${leftColumn} ↔ ${rightRelation}.${rightColumn}: sample left=${number('left_sample_rows')}, right=${number('right_sample_rows')}, joined=${number('joined_rows')}, unmatched left/right=${number('unmatched_left_sample_rows')}/${number('unmatched_right_sample_rows')}, max matches left/right=${number('max_matches_per_left_key')}/${number('max_matches_per_right_key')}`;
|
|
15018
15416
|
}
|
|
15019
15417
|
function readOnlySqlValidationError(sql, subject) {
|
|
15418
|
+
const internalRelationError = internalDqlRelationIdValidationError(sql, subject);
|
|
15419
|
+
if (internalRelationError)
|
|
15420
|
+
return internalRelationError;
|
|
15020
15421
|
const scanSql = stripSqlStringsAndComments(sql).trim();
|
|
15021
15422
|
if (!/^(select|with)\b/i.test(scanSql)) {
|
|
15022
15423
|
return `${subject} only supports read-only SELECT or WITH queries.`;
|
|
@@ -15031,6 +15432,15 @@ function readOnlySqlValidationError(sql, subject) {
|
|
|
15031
15432
|
}
|
|
15032
15433
|
return null;
|
|
15033
15434
|
}
|
|
15435
|
+
function internalDqlRelationIdValidationError(sql, subject) {
|
|
15436
|
+
const internalIds = internalRelationIdsInSql(stripSqlStringsAndComments(sql));
|
|
15437
|
+
if (internalIds.length === 0)
|
|
15438
|
+
return null;
|
|
15439
|
+
return `${subject} contains internal DQL graph relation identifier${internalIds.length === 1 ? '' : 's'} ${internalIds.join(', ')}. Use the physical database.schema.table relation instead, or choose Ask AI to fix from the notebook error.`;
|
|
15440
|
+
}
|
|
15441
|
+
class DqlInternalRelationIdError extends Error {
|
|
15442
|
+
name = 'DqlInternalRelationIdError';
|
|
15443
|
+
}
|
|
15034
15444
|
function stripSqlStringsAndComments(sql) {
|
|
15035
15445
|
let output = '';
|
|
15036
15446
|
for (let index = 0; index < sql.length; index += 1) {
|
|
@@ -15340,6 +15750,7 @@ function scanNotebookFiles(projectRoot) {
|
|
|
15340
15750
|
path: relativePath,
|
|
15341
15751
|
type,
|
|
15342
15752
|
folder: relativeDir,
|
|
15753
|
+
...readNotebookFileProductContext(fullPath, type),
|
|
15343
15754
|
});
|
|
15344
15755
|
}
|
|
15345
15756
|
}
|
|
@@ -15364,12 +15775,29 @@ function scanNotebookFiles(projectRoot) {
|
|
|
15364
15775
|
path: relativePath,
|
|
15365
15776
|
type,
|
|
15366
15777
|
folder: relativeDir.split('/')[0] ?? relativeDir,
|
|
15778
|
+
...readNotebookFileProductContext(fullPath, type),
|
|
15367
15779
|
});
|
|
15368
15780
|
}
|
|
15369
15781
|
}
|
|
15370
15782
|
catch { /* skip unreadable dirs */ }
|
|
15371
15783
|
}
|
|
15372
15784
|
}
|
|
15785
|
+
function readNotebookFileProductContext(fullPath, type) {
|
|
15786
|
+
if ((type !== 'notebook' && type !== 'workbook') || !fullPath.endsWith('.dqlnb'))
|
|
15787
|
+
return {};
|
|
15788
|
+
try {
|
|
15789
|
+
const metadata = deserializeNotebook(readFileSync(fullPath, 'utf-8')).metadata;
|
|
15790
|
+
return {
|
|
15791
|
+
...(metadata.ownerDomain ? { ownerDomain: metadata.ownerDomain } : {}),
|
|
15792
|
+
usesDomains: metadata.usesDomains ?? [],
|
|
15793
|
+
};
|
|
15794
|
+
}
|
|
15795
|
+
catch {
|
|
15796
|
+
// A malformed notebook must remain visible in the file list so the user
|
|
15797
|
+
// can open and repair it; it simply falls back to the Project folder.
|
|
15798
|
+
return {};
|
|
15799
|
+
}
|
|
15800
|
+
}
|
|
15373
15801
|
function inferDqlArtifactName(fullPath, type, fallbackName) {
|
|
15374
15802
|
if (!fullPath.endsWith('.dql'))
|
|
15375
15803
|
return fallbackName;
|
|
@@ -15573,15 +16001,15 @@ export function openBlockStudioDocument(projectRoot, relativePath, semanticLayer
|
|
|
15573
16001
|
throw new Error(`File not found: ${normalizedPath}`);
|
|
15574
16002
|
}
|
|
15575
16003
|
const source = readFileSync(absPath, 'utf-8');
|
|
15576
|
-
const companionPath = blockCompanionRelativePath(normalizedPath);
|
|
16004
|
+
const companionPath = blockCompanionRelativePath(projectRoot, normalizedPath);
|
|
15577
16005
|
const companion = companionPath ? readBlockCompanionFile(projectRoot, companionPath) : null;
|
|
15578
16006
|
const parsedMetadata = parseBlockSourceMetadata(source);
|
|
15579
16007
|
const fileName = normalizedPath.split('/').pop()?.replace(/\.dql$/, '') ?? 'block';
|
|
15580
16008
|
const metadata = {
|
|
15581
16009
|
name: parsedMetadata.name || companion?.name || fileName,
|
|
15582
16010
|
path: normalizedPath,
|
|
15583
|
-
domain: parsedMetadata.domain || companion?.domain || inferBlockStudioPathDomain(normalizedPath) || 'uncategorized',
|
|
15584
|
-
folderPath: inferBlockStudioFolderPath(normalizedPath),
|
|
16011
|
+
domain: parsedMetadata.domain || companion?.domain || inferBlockStudioPathDomain(projectRoot, normalizedPath) || 'uncategorized',
|
|
16012
|
+
folderPath: inferBlockStudioFolderPath(projectRoot, normalizedPath),
|
|
15585
16013
|
description: parsedMetadata.description || companion?.description || '',
|
|
15586
16014
|
owner: parsedMetadata.owner || companion?.owner || '',
|
|
15587
16015
|
tags: parsedMetadata.tags.length > 0 ? parsedMetadata.tags : companion?.tags ?? [],
|
|
@@ -15640,9 +16068,20 @@ function parseSemanticBlockConfig(source) {
|
|
|
15640
16068
|
limit: limitMatch ? Number.parseInt(limitMatch[1], 10) : undefined,
|
|
15641
16069
|
};
|
|
15642
16070
|
}
|
|
15643
|
-
export async function resolveSemanticTableMapping(executor, connection, semanticLayer) {
|
|
16071
|
+
export async function resolveSemanticTableMapping(executor, connection, semanticLayer, projectRoot) {
|
|
15644
16072
|
if (!semanticLayer)
|
|
15645
16073
|
return undefined;
|
|
16074
|
+
if (projectRoot) {
|
|
16075
|
+
const snapshot = latestRuntimeSchemaSnapshotForProject(projectRoot);
|
|
16076
|
+
if (snapshot?.scopeFingerprint && snapshot.tables.length) {
|
|
16077
|
+
return buildSemanticTableMapping(semanticLayer, snapshot.tables.map((table) => ({
|
|
16078
|
+
table_catalog: table.catalogOrDatabase,
|
|
16079
|
+
table_schema: table.schema,
|
|
16080
|
+
table_name: table.name ?? table.relation.split('.').at(-1),
|
|
16081
|
+
table_relation: table.relation,
|
|
16082
|
+
})));
|
|
16083
|
+
}
|
|
16084
|
+
}
|
|
15646
16085
|
try {
|
|
15647
16086
|
const tablesResult = await executor.executeQuery(`SELECT table_schema, table_name
|
|
15648
16087
|
FROM information_schema.tables
|
|
@@ -15660,10 +16099,11 @@ export function buildSemanticTableMapping(semanticLayer, rows) {
|
|
|
15660
16099
|
for (const row of rows) {
|
|
15661
16100
|
const schema = String(row['table_schema'] ?? '');
|
|
15662
16101
|
const name = String(row['table_name'] ?? '');
|
|
16102
|
+
const relation = typeof row['table_relation'] === 'string' ? row['table_relation'] : undefined;
|
|
15663
16103
|
if (!name)
|
|
15664
16104
|
continue;
|
|
15665
16105
|
dbTableNames.add(name);
|
|
15666
|
-
schemaQualified.set(name, schema ? `${schema}.${name}` : name);
|
|
16106
|
+
schemaQualified.set(name, relation ?? (schema ? `${schema}.${name}` : name));
|
|
15667
16107
|
}
|
|
15668
16108
|
const tableMapping = {};
|
|
15669
16109
|
const allSemanticTables = new Set();
|
|
@@ -16587,7 +17027,7 @@ export function saveBlockStudioArtifacts(projectRoot, options) {
|
|
|
16587
17027
|
const safeDomain = (options.domain ?? '')
|
|
16588
17028
|
.trim()
|
|
16589
17029
|
.toLowerCase()
|
|
16590
|
-
.replace(/[^a-z0-9
|
|
17030
|
+
.replace(/[^a-z0-9._/-]+/g, '-')
|
|
16591
17031
|
.replace(/^\/+|\/+$/g, '') || 'uncategorized';
|
|
16592
17032
|
const safeFolderPath = normalizeBlockStudioFolderPath(options.folderPath);
|
|
16593
17033
|
const previousPath = options.currentPath ? normalize(options.currentPath).replace(/^\/+/, '') : null;
|
|
@@ -16615,7 +17055,7 @@ export function saveBlockStudioArtifacts(projectRoot, options) {
|
|
|
16615
17055
|
const previousAbsPath = join(projectRoot, previousPath);
|
|
16616
17056
|
if (existsSync(previousAbsPath))
|
|
16617
17057
|
rmSync(previousAbsPath, { force: true });
|
|
16618
|
-
const previousCompanion = blockCompanionRelativePath(previousPath);
|
|
17058
|
+
const previousCompanion = blockCompanionRelativePath(projectRoot, previousPath);
|
|
16619
17059
|
if (previousCompanion) {
|
|
16620
17060
|
const previousCompanionPath = join(projectRoot, previousCompanion);
|
|
16621
17061
|
if (existsSync(previousCompanionPath))
|
|
@@ -16643,7 +17083,7 @@ export function deleteBlockStudioArtifacts(projectRoot, relativePath) {
|
|
|
16643
17083
|
if (!existsSync(absolutePath)) {
|
|
16644
17084
|
throw new Error(`File not found: ${normalizedPath}`);
|
|
16645
17085
|
}
|
|
16646
|
-
const companionPath = blockCompanionRelativePath(normalizedPath);
|
|
17086
|
+
const companionPath = blockCompanionRelativePath(projectRoot, normalizedPath);
|
|
16647
17087
|
rmSync(absolutePath, { force: true });
|
|
16648
17088
|
if (companionPath) {
|
|
16649
17089
|
const absoluteCompanionPath = resolve(rootPath, companionPath);
|
|
@@ -16701,7 +17141,12 @@ export function saveBlockStudioDraftArtifacts(projectRoot, options) {
|
|
|
16701
17141
|
}
|
|
16702
17142
|
function canonicalBlockRelativePath(projectRoot, safeDomain, safeFolderPath, slug, previousPath) {
|
|
16703
17143
|
const folder = safeFolderPath ? `${safeFolderPath}/` : '';
|
|
16704
|
-
const
|
|
17144
|
+
const packageRoot = loadDomainPackageRegistry(projectRoot).get(safeDomain)?.root;
|
|
17145
|
+
if (packageRoot) {
|
|
17146
|
+
const relativePackageRoot = relative(projectRoot, packageRoot).replaceAll('\\', '/');
|
|
17147
|
+
return `${relativePackageRoot}/blocks/${folder}${slug}.dql`;
|
|
17148
|
+
}
|
|
17149
|
+
const previousDomainFirst = Boolean(previousPath?.match(/^domains\/.+\/blocks\//));
|
|
16705
17150
|
if (previousDomainFirst || existsSync(join(projectRoot, 'domains', safeDomain))) {
|
|
16706
17151
|
return `domains/${safeDomain}/blocks/${folder}${slug}.dql`;
|
|
16707
17152
|
}
|
|
@@ -16724,19 +17169,23 @@ function isDraftBlockPath(value) {
|
|
|
16724
17169
|
if (!value)
|
|
16725
17170
|
return false;
|
|
16726
17171
|
const normalized = normalize(value).replace(/^\/+/, '');
|
|
16727
|
-
return normalized.startsWith('blocks/_drafts/') || /^domains
|
|
17172
|
+
return normalized.startsWith('blocks/_drafts/') || /^domains\/.+\/blocks\/_drafts\//.test(normalized);
|
|
16728
17173
|
}
|
|
16729
17174
|
function isBlockStudioBlockPath(value) {
|
|
16730
17175
|
if (!value)
|
|
16731
17176
|
return false;
|
|
16732
17177
|
const normalized = normalize(value).replace(/^\/+/, '');
|
|
16733
|
-
return normalized.startsWith('blocks/') || /^domains
|
|
17178
|
+
return normalized.startsWith('blocks/') || /^domains\/.+\/blocks\//.test(normalized);
|
|
16734
17179
|
}
|
|
16735
|
-
function inferBlockStudioPathDomain(blockPath) {
|
|
17180
|
+
function inferBlockStudioPathDomain(projectRoot, blockPath) {
|
|
16736
17181
|
const normalized = normalize(blockPath).replace(/^\/+/, '');
|
|
16737
17182
|
if (normalized.startsWith('blocks/')) {
|
|
16738
17183
|
return normalized.split('/')[1] ?? '';
|
|
16739
17184
|
}
|
|
17185
|
+
const resolved = domainBlockPathParts(projectRoot, normalized);
|
|
17186
|
+
if (resolved) {
|
|
17187
|
+
return resolved.subpath.startsWith('_drafts/') ? `_drafts/${resolved.domain}` : resolved.domain;
|
|
17188
|
+
}
|
|
16740
17189
|
const domainFirst = normalized.match(/^domains\/([^/]+)\/blocks\/(.+)$/);
|
|
16741
17190
|
if (!domainFirst)
|
|
16742
17191
|
return '';
|
|
@@ -16746,8 +17195,11 @@ function inferBlockStudioPathDomain(blockPath) {
|
|
|
16746
17195
|
return `_drafts/${domain}`;
|
|
16747
17196
|
return domain;
|
|
16748
17197
|
}
|
|
16749
|
-
function inferBlockStudioFolderPath(blockPath) {
|
|
17198
|
+
function inferBlockStudioFolderPath(projectRoot, blockPath) {
|
|
16750
17199
|
const normalized = normalize(blockPath).replace(/^\/+/, '').replaceAll('\\', '/');
|
|
17200
|
+
const resolved = domainBlockPathParts(projectRoot, normalized);
|
|
17201
|
+
if (resolved)
|
|
17202
|
+
return resolved.subpath.split('/').slice(0, -1).join('/');
|
|
16751
17203
|
const domainFirst = normalized.match(/^domains\/[^/]+\/blocks\/(.+)$/);
|
|
16752
17204
|
if (domainFirst)
|
|
16753
17205
|
return domainFirst[1].split('/').slice(0, -1).join('/');
|
|
@@ -16756,12 +17208,20 @@ function inferBlockStudioFolderPath(blockPath) {
|
|
|
16756
17208
|
return legacy[1].split('/').slice(0, -1).join('/');
|
|
16757
17209
|
return '';
|
|
16758
17210
|
}
|
|
16759
|
-
function blockCompanionRelativePath(blockPath) {
|
|
17211
|
+
function blockCompanionRelativePath(projectRoot, blockPath) {
|
|
16760
17212
|
const normalized = normalize(blockPath).replace(/^\/+/, '');
|
|
16761
17213
|
if (normalized.startsWith('blocks/')) {
|
|
16762
17214
|
const withoutRoot = normalized.slice('blocks/'.length).replace(/\.dql$/, '.yaml');
|
|
16763
17215
|
return join('semantic-layer', 'blocks', withoutRoot).replaceAll('\\', '/');
|
|
16764
17216
|
}
|
|
17217
|
+
const resolved = domainBlockPathParts(projectRoot, normalized);
|
|
17218
|
+
if (resolved && resolved.subpath.endsWith('.dql')) {
|
|
17219
|
+
const blockPath = resolved.subpath.replace(/\.dql$/, '');
|
|
17220
|
+
const companionBlockPath = blockPath.startsWith('_drafts/')
|
|
17221
|
+
? join('_drafts', resolved.domain, blockPath.slice('_drafts/'.length))
|
|
17222
|
+
: join(resolved.domain, blockPath);
|
|
17223
|
+
return join('semantic-layer', 'blocks', `${companionBlockPath}.yaml`).replaceAll('\\', '/');
|
|
17224
|
+
}
|
|
16765
17225
|
const domainFirst = normalized.match(/^domains\/([^/]+)\/blocks\/(.+)\.dql$/);
|
|
16766
17226
|
if (domainFirst) {
|
|
16767
17227
|
const domain = domainFirst[1];
|
|
@@ -16773,6 +17233,19 @@ function blockCompanionRelativePath(blockPath) {
|
|
|
16773
17233
|
}
|
|
16774
17234
|
return null;
|
|
16775
17235
|
}
|
|
17236
|
+
function domainBlockPathParts(projectRoot, blockPath) {
|
|
17237
|
+
const normalized = normalize(blockPath).replace(/^\/+/, '').replaceAll('\\', '/');
|
|
17238
|
+
if (!normalized.startsWith('domains/'))
|
|
17239
|
+
return null;
|
|
17240
|
+
const absolute = join(projectRoot, normalized);
|
|
17241
|
+
const pkg = loadDomainPackageRegistry(projectRoot).packageForPath(absolute);
|
|
17242
|
+
if (!pkg)
|
|
17243
|
+
return null;
|
|
17244
|
+
const local = relative(pkg.root, absolute).replaceAll('\\', '/');
|
|
17245
|
+
if (!local.startsWith('blocks/'))
|
|
17246
|
+
return null;
|
|
17247
|
+
return { domain: pkg.id, subpath: local.slice('blocks/'.length) };
|
|
17248
|
+
}
|
|
16776
17249
|
function readBlockCompanionFile(projectRoot, relativePath) {
|
|
16777
17250
|
const absPath = join(projectRoot, relativePath);
|
|
16778
17251
|
if (!existsSync(absPath))
|
|
@@ -17085,7 +17558,7 @@ export function setBlockStudioStatus(projectRoot, blockPath, newStatus) {
|
|
|
17085
17558
|
throw new Error('Block file not found');
|
|
17086
17559
|
const source = setBlockStudioStatusInSource(readFileSync(absPath, 'utf-8'), newStatus);
|
|
17087
17560
|
writeFileSync(absPath, source, 'utf-8');
|
|
17088
|
-
const companionPath = blockCompanionRelativePath(normalizedPath);
|
|
17561
|
+
const companionPath = blockCompanionRelativePath(projectRoot, normalizedPath);
|
|
17089
17562
|
if (!companionPath)
|
|
17090
17563
|
return;
|
|
17091
17564
|
const absCompanionPath = join(projectRoot, companionPath);
|
|
@@ -18254,16 +18727,18 @@ function buildConversationSystemPrompt(kind, isGeneralKnowledge, catalogContext,
|
|
|
18254
18727
|
return lines.join('\n');
|
|
18255
18728
|
}
|
|
18256
18729
|
function renderConversationMemoryForPrompt(context) {
|
|
18730
|
+
const envelope = renderConversationEnvelopeForPrompt(context);
|
|
18257
18731
|
const recap = buildConversationContextRecap(context);
|
|
18258
|
-
if (!recap)
|
|
18732
|
+
if (!recap && !envelope)
|
|
18259
18733
|
return undefined;
|
|
18260
18734
|
return [
|
|
18261
18735
|
'Current conversation context:',
|
|
18736
|
+
envelope,
|
|
18262
18737
|
recap,
|
|
18263
18738
|
'If the user asks what this conversation is about, summarize this context directly. Do not query data or invent values.',
|
|
18264
|
-
].join('\n');
|
|
18739
|
+
].filter(Boolean).join('\n');
|
|
18265
18740
|
}
|
|
18266
|
-
function buildConversationContextRecap(context) {
|
|
18741
|
+
export function buildConversationContextRecap(context) {
|
|
18267
18742
|
if (!context)
|
|
18268
18743
|
return undefined;
|
|
18269
18744
|
const turnRecap = buildConversationTurnsRecap(context);
|
|
@@ -18273,10 +18748,7 @@ function buildConversationContextRecap(context) {
|
|
|
18273
18748
|
const sourceAnswerSummary = agentRunString(context.sourceAnswerSummary);
|
|
18274
18749
|
const columns = agentRunStringArray(context.resultColumns ?? context.outputColumns).slice(0, 8);
|
|
18275
18750
|
const values = agentRunRecord(context.resultDimensionValues);
|
|
18276
|
-
const
|
|
18277
|
-
? context.resultRowsSample.map(agentRunRecord).filter((row) => Boolean(row)).slice(0, 1)
|
|
18278
|
-
: [];
|
|
18279
|
-
const firstRow = rows[0];
|
|
18751
|
+
const firstRow = firstConversationResultRow(context.resultRowsSample, columns);
|
|
18280
18752
|
const leadParts = buildPriorValueLeadParts(values);
|
|
18281
18753
|
const rowFacts = firstRow
|
|
18282
18754
|
? columns
|
|
@@ -18308,16 +18780,19 @@ function buildConversationTurnsRecap(context) {
|
|
|
18308
18780
|
const active = activeTurnId
|
|
18309
18781
|
? turns.find((turn) => agentRunString(turn.id) === activeTurnId)
|
|
18310
18782
|
: undefined;
|
|
18311
|
-
|
|
18783
|
+
// A recap turn is persisted like every other turn, but it must not become the
|
|
18784
|
+
// subject of the next recap. Prefer the newest analytical/authoring turn and
|
|
18785
|
+
// fall back to the active turn only when the thread contains conversation alone.
|
|
18786
|
+
const substantiveTurns = turns.filter((turn) => agentRunString(turn.route) !== 'conversation');
|
|
18787
|
+
const selected = active && agentRunString(active.route) !== 'conversation'
|
|
18788
|
+
? active
|
|
18789
|
+
: substantiveTurns[substantiveTurns.length - 1] ?? active ?? turns[turns.length - 1];
|
|
18312
18790
|
if (!selected)
|
|
18313
18791
|
return undefined;
|
|
18314
18792
|
const result = agentRunRecord(selected.result);
|
|
18315
18793
|
const values = agentRunRecord(result?.dimensionValues);
|
|
18316
18794
|
const columns = agentRunStringArray(result?.columns).slice(0, 8);
|
|
18317
|
-
const
|
|
18318
|
-
? result.rowsSample.map(agentRunRecord).filter((row) => Boolean(row)).slice(0, 1)
|
|
18319
|
-
: [];
|
|
18320
|
-
const firstRow = rows[0];
|
|
18795
|
+
const firstRow = firstConversationResultRow(result?.rowsSample, columns);
|
|
18321
18796
|
const leadParts = buildPriorValueLeadParts(values);
|
|
18322
18797
|
const rowFacts = firstRow
|
|
18323
18798
|
? columns
|
|
@@ -18329,7 +18804,7 @@ function buildConversationTurnsRecap(context) {
|
|
|
18329
18804
|
.filter(Boolean)
|
|
18330
18805
|
: [];
|
|
18331
18806
|
const previous = turns
|
|
18332
|
-
.filter((turn) => turn !== selected)
|
|
18807
|
+
.filter((turn) => turn !== selected && agentRunString(turn.route) !== 'conversation')
|
|
18333
18808
|
.map((turn) => agentRunString(turn.question))
|
|
18334
18809
|
.filter((value) => Boolean(value))
|
|
18335
18810
|
.slice(-2);
|
|
@@ -18343,6 +18818,17 @@ function buildConversationTurnsRecap(context) {
|
|
|
18343
18818
|
].filter(Boolean);
|
|
18344
18819
|
return bits.length > 0 ? `We were talking about ${bits.join('. ')}.` : undefined;
|
|
18345
18820
|
}
|
|
18821
|
+
function firstConversationResultRow(value, columns) {
|
|
18822
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
18823
|
+
return undefined;
|
|
18824
|
+
const first = value[0];
|
|
18825
|
+
const record = agentRunRecord(first);
|
|
18826
|
+
if (record)
|
|
18827
|
+
return record;
|
|
18828
|
+
if (!Array.isArray(first))
|
|
18829
|
+
return undefined;
|
|
18830
|
+
return Object.fromEntries(columns.map((column, index) => [column, first[index]]));
|
|
18831
|
+
}
|
|
18346
18832
|
function agentRunStringArray(value) {
|
|
18347
18833
|
if (!Array.isArray(value))
|
|
18348
18834
|
return [];
|
|
@@ -18526,12 +19012,19 @@ export function readGitMetadata(projectRoot) {
|
|
|
18526
19012
|
return null;
|
|
18527
19013
|
}
|
|
18528
19014
|
}
|
|
18529
|
-
function resolveBlockWriteTarget(projectRoot, safeDomain, slug) {
|
|
19015
|
+
function resolveBlockWriteTarget(projectRoot, safeDomain, safeFolderPath, slug) {
|
|
19016
|
+
const folder = safeFolderPath ? `${safeFolderPath}/` : '';
|
|
19017
|
+
const packageRoot = safeDomain ? loadDomainPackageRegistry(projectRoot).get(safeDomain)?.root : undefined;
|
|
19018
|
+
if (packageRoot) {
|
|
19019
|
+
const relativePackageRoot = relative(projectRoot, packageRoot).replaceAll('\\', '/');
|
|
19020
|
+
const relativePath = `${relativePackageRoot}/blocks/${folder}${slug}.dql`;
|
|
19021
|
+
return { relativePath, absPath: join(projectRoot, relativePath) };
|
|
19022
|
+
}
|
|
18530
19023
|
if (safeDomain && existsSync(join(projectRoot, 'domains', safeDomain))) {
|
|
18531
|
-
const relativePath = `domains/${safeDomain}/blocks/${slug}.dql`;
|
|
19024
|
+
const relativePath = `domains/${safeDomain}/blocks/${folder}${slug}.dql`;
|
|
18532
19025
|
return { relativePath, absPath: join(projectRoot, relativePath) };
|
|
18533
19026
|
}
|
|
18534
|
-
const relativePath = safeDomain ? `blocks/${safeDomain}/${slug}.dql` : `blocks/${slug}.dql`;
|
|
19027
|
+
const relativePath = safeDomain ? `blocks/${safeDomain}/${folder}${slug}.dql` : `blocks/${folder}${slug}.dql`;
|
|
18535
19028
|
return { relativePath, absPath: join(projectRoot, relativePath) };
|
|
18536
19029
|
}
|
|
18537
19030
|
export function createBlockArtifacts(projectRoot, options) {
|
|
@@ -18539,9 +19032,10 @@ export function createBlockArtifacts(projectRoot, options) {
|
|
|
18539
19032
|
const safeDomain = (options.domain ?? '')
|
|
18540
19033
|
.trim()
|
|
18541
19034
|
.toLowerCase()
|
|
18542
|
-
.replace(/[^a-z0-9
|
|
19035
|
+
.replace(/[^a-z0-9._/-]+/g, '-')
|
|
18543
19036
|
.replace(/^\/+|\/+$/g, '');
|
|
18544
|
-
const
|
|
19037
|
+
const safeFolderPath = normalizeBlockStudioFolderPath(options.folderPath);
|
|
19038
|
+
const target = resolveBlockWriteTarget(projectRoot, safeDomain, safeFolderPath, slug);
|
|
18545
19039
|
mkdirSync(dirname(target.absPath), { recursive: true });
|
|
18546
19040
|
const blockPath = target.absPath;
|
|
18547
19041
|
if (existsSync(blockPath)) {
|
|
@@ -18575,6 +19069,7 @@ export function createBlockArtifacts(projectRoot, options) {
|
|
|
18575
19069
|
slug,
|
|
18576
19070
|
name: options.name,
|
|
18577
19071
|
domain: safeDomain || 'uncategorized',
|
|
19072
|
+
folderPath: safeFolderPath,
|
|
18578
19073
|
owner: options.owner,
|
|
18579
19074
|
description: options.description,
|
|
18580
19075
|
tags: options.tags,
|
|
@@ -18594,9 +19089,9 @@ export function createSemanticBuilderBlock(projectRoot, options) {
|
|
|
18594
19089
|
const safeDomain = (options.domain ?? '')
|
|
18595
19090
|
.trim()
|
|
18596
19091
|
.toLowerCase()
|
|
18597
|
-
.replace(/[^a-z0-9
|
|
19092
|
+
.replace(/[^a-z0-9._/-]+/g, '-')
|
|
18598
19093
|
.replace(/^\/+|\/+$/g, '') || 'uncategorized';
|
|
18599
|
-
const target = resolveBlockWriteTarget(projectRoot, safeDomain, slug);
|
|
19094
|
+
const target = resolveBlockWriteTarget(projectRoot, safeDomain, '', slug);
|
|
18600
19095
|
mkdirSync(dirname(target.absPath), { recursive: true });
|
|
18601
19096
|
const blockPath = target.absPath;
|
|
18602
19097
|
if (existsSync(blockPath)) {
|
|
@@ -18922,7 +19417,7 @@ function yamlScalar(value) {
|
|
|
18922
19417
|
return value;
|
|
18923
19418
|
return JSON.stringify(value);
|
|
18924
19419
|
}
|
|
18925
|
-
function buildNotebookTemplate(title, template) {
|
|
19420
|
+
function buildNotebookTemplate(title, template, productContext = {}) {
|
|
18926
19421
|
const id = () => Math.random().toString(36).slice(2, 10);
|
|
18927
19422
|
let cells;
|
|
18928
19423
|
if (template === "analysis") {
|
|
@@ -19035,7 +19530,18 @@ function buildNotebookTemplate(title, template) {
|
|
|
19035
19530
|
else {
|
|
19036
19531
|
cells = [];
|
|
19037
19532
|
}
|
|
19038
|
-
return JSON.stringify({
|
|
19533
|
+
return JSON.stringify({
|
|
19534
|
+
dqlnbVersion: 2,
|
|
19535
|
+
version: 1,
|
|
19536
|
+
title,
|
|
19537
|
+
metadata: {
|
|
19538
|
+
createdWith: 'dql',
|
|
19539
|
+
...(productContext.ownerDomain ? { ownerDomain: productContext.ownerDomain } : {}),
|
|
19540
|
+
usesDomains: productContext.usesDomains ?? (productContext.ownerDomain ? [productContext.ownerDomain] : []),
|
|
19541
|
+
requiredExports: [],
|
|
19542
|
+
},
|
|
19543
|
+
cells,
|
|
19544
|
+
}, null, 2);
|
|
19039
19545
|
}
|
|
19040
19546
|
/** Build a lineage graph from the project's blocks and semantic layer. */
|
|
19041
19547
|
// Cache per project + source fingerprint. A process can serve different roots,
|
|
@@ -20081,33 +20587,8 @@ function ensureGitignoreEntry(projectRoot, pattern) {
|
|
|
20081
20587
|
// Best-effort; failure to write .gitignore shouldn't fail the snapshot.
|
|
20082
20588
|
}
|
|
20083
20589
|
}
|
|
20084
|
-
/**
|
|
20085
|
-
* UI-001, SEC-001, E2E-001: local runtime, credential, and generated artifacts must never be offered as
|
|
20086
|
-
* shareable source-control changes. Project skills are intentionally absent:
|
|
20087
|
-
* `.dql/skills` remains readable during migration and can be governed in Git.
|
|
20088
|
-
*/
|
|
20089
|
-
const LOCAL_RUNTIME_GITIGNORE_RULES = [
|
|
20090
|
-
'dql-manifest.json',
|
|
20091
|
-
'*.duckdb',
|
|
20092
|
-
'*.duckdb.wal',
|
|
20093
|
-
'*.run.json',
|
|
20094
|
-
'**/.dql/runs/',
|
|
20095
|
-
'**/.dql/cache/',
|
|
20096
|
-
'**/.dql/imports/',
|
|
20097
|
-
'**/.dql/local/',
|
|
20098
|
-
'**/.dql/runtimes/',
|
|
20099
|
-
'**/.dql/connectors/',
|
|
20100
|
-
'**/.dql/memory/',
|
|
20101
|
-
'**/.dql/migration-staging/',
|
|
20102
|
-
'**/.dql/docker-starter/',
|
|
20103
|
-
'**/.dql/oauth-credentials.json',
|
|
20104
|
-
'**/.dql/provider-settings.json',
|
|
20105
|
-
'**/.dql/mcp-servers.json',
|
|
20106
|
-
'**/.dql-user-prefs.json',
|
|
20107
|
-
];
|
|
20108
20590
|
export function ensureLocalRuntimeGitignore(projectRoot) {
|
|
20109
|
-
|
|
20110
|
-
ensureGitignoreEntry(projectRoot, rule);
|
|
20591
|
+
ensureDqlGitignore(projectRoot);
|
|
20111
20592
|
}
|
|
20112
20593
|
async function readGitDiff(cwd, filePath, staged = false) {
|
|
20113
20594
|
const gitRoot = await resolveGitRoot(cwd);
|
|
@@ -20458,8 +20939,11 @@ async function readGitRemote(cwd) {
|
|
|
20458
20939
|
const url = await execGit(gitRoot, ['remote', 'get-url', name]);
|
|
20459
20940
|
return { inRepo: true, url: url.code === 0 ? url.stdout.trim() : null, name };
|
|
20460
20941
|
}
|
|
20461
|
-
function collectGovernedSourcePaths(root, folder, predicate) {
|
|
20462
|
-
|
|
20942
|
+
function collectGovernedSourcePaths(root, folder, predicate = () => true) {
|
|
20943
|
+
// `layout.skillsPath` may point at a sibling folder in the same Git
|
|
20944
|
+
// repository. Resolve both project-relative and absolute source roots so the
|
|
20945
|
+
// governed inventory follows the loader instead of assuming `./skills`.
|
|
20946
|
+
const start = resolve(root, folder);
|
|
20463
20947
|
if (!existsSync(start))
|
|
20464
20948
|
return [];
|
|
20465
20949
|
const paths = [];
|
|
@@ -20472,13 +20956,16 @@ function collectGovernedSourcePaths(root, folder, predicate) {
|
|
|
20472
20956
|
const absolute = join(dir, entry.name);
|
|
20473
20957
|
if (entry.isDirectory())
|
|
20474
20958
|
walk(absolute);
|
|
20475
|
-
else if (entry.isFile() && predicate(entry.name))
|
|
20959
|
+
else if (entry.isFile() && predicate(entry.name, relative(root, absolute).replace(/\\/g, '/')))
|
|
20476
20960
|
paths.push(absolute);
|
|
20477
20961
|
}
|
|
20478
20962
|
};
|
|
20479
20963
|
walk(start);
|
|
20480
20964
|
return paths;
|
|
20481
20965
|
}
|
|
20966
|
+
function uniqueGovernedSourcePaths(paths) {
|
|
20967
|
+
return [...new Set(paths.map((path) => resolve(path)))].sort();
|
|
20968
|
+
}
|
|
20482
20969
|
async function gitGovernedFileState(gitRoot, path) {
|
|
20483
20970
|
const tracked = await execGit(gitRoot, ['ls-files', '--error-unmatch', '--', path]);
|
|
20484
20971
|
const status = await execGit(gitRoot, ['status', '--porcelain=v1', '--untracked-files=all', '--', path]);
|
|
@@ -20490,13 +20977,23 @@ async function gitGovernedFileState(gitRoot, path) {
|
|
|
20490
20977
|
async function readGitGovernedContext(cwd) {
|
|
20491
20978
|
const gitRoot = await resolveGitRoot(cwd);
|
|
20492
20979
|
const empty = () => ({ total: 0, tracked: 0, changed: 0, untracked: 0, ignored: 0, paths: [] });
|
|
20493
|
-
if (!gitRoot)
|
|
20494
|
-
return {
|
|
20980
|
+
if (!gitRoot) {
|
|
20981
|
+
return {
|
|
20982
|
+
inRepo: false,
|
|
20983
|
+
trackingReady: false,
|
|
20984
|
+
legacyBroadIgnore: false,
|
|
20985
|
+
domains: empty(),
|
|
20986
|
+
skills: empty(),
|
|
20987
|
+
artifacts: empty(),
|
|
20988
|
+
learning: empty(),
|
|
20989
|
+
};
|
|
20990
|
+
}
|
|
20495
20991
|
const summarize = async (files) => {
|
|
20496
20992
|
const group = empty();
|
|
20497
|
-
for (const absolute of files) {
|
|
20498
|
-
const
|
|
20499
|
-
const
|
|
20993
|
+
for (const absolute of uniqueGovernedSourcePaths(files)) {
|
|
20994
|
+
const gitPath = relative(gitRoot, absolute).replace(/\\/g, '/');
|
|
20995
|
+
const path = relative(cwd, absolute).replace(/\\/g, '/');
|
|
20996
|
+
const state = await gitGovernedFileState(gitRoot, gitPath);
|
|
20500
20997
|
group.total += 1;
|
|
20501
20998
|
group[state] += 1;
|
|
20502
20999
|
if (group.paths.length < 24)
|
|
@@ -20504,33 +21001,57 @@ async function readGitGovernedContext(cwd) {
|
|
|
20504
21001
|
}
|
|
20505
21002
|
return group;
|
|
20506
21003
|
};
|
|
20507
|
-
const
|
|
20508
|
-
|
|
21004
|
+
const rootArtifacts = [
|
|
21005
|
+
'blocks',
|
|
21006
|
+
'terms',
|
|
21007
|
+
'business-views',
|
|
21008
|
+
'notebooks',
|
|
21009
|
+
'workbooks',
|
|
21010
|
+
'apps',
|
|
21011
|
+
'dashboards',
|
|
21012
|
+
'semantic-layer',
|
|
21013
|
+
'tests',
|
|
21014
|
+
].flatMap((folder) => collectGovernedSourcePaths(cwd, folder));
|
|
21015
|
+
const configArtifacts = ['dql.config.json', 'package.json']
|
|
21016
|
+
.map((path) => join(cwd, path))
|
|
21017
|
+
.filter(existsSync);
|
|
21018
|
+
const [domains, skills, artifacts, learning] = await Promise.all([
|
|
21019
|
+
summarize(collectGovernedSourcePaths(cwd, 'domains', (name, path) => !name.endsWith('.skill.md') && isGovernedSourceFile(path))),
|
|
20509
21020
|
summarize([
|
|
20510
|
-
...collectGovernedSourcePaths(cwd,
|
|
21021
|
+
...collectGovernedSourcePaths(cwd, skillsDir(cwd), (name) => name.endsWith('.skill.md')),
|
|
21022
|
+
...collectGovernedSourcePaths(cwd, 'domains', (name) => name.endsWith('.skill.md')),
|
|
20511
21023
|
...collectGovernedSourcePaths(cwd, '.dql/skills', (name) => name.endsWith('.skill.md')),
|
|
20512
21024
|
]),
|
|
21025
|
+
summarize([...rootArtifacts.filter((path) => isGovernedSourceFile(relative(cwd, path))), ...configArtifacts]),
|
|
21026
|
+
summarize([
|
|
21027
|
+
...collectGovernedSourcePaths(cwd, '.dql/hints'),
|
|
21028
|
+
...collectGovernedSourcePaths(cwd, '.dql/traces'),
|
|
21029
|
+
...collectGovernedSourcePaths(cwd, '.dql/evaluations'),
|
|
21030
|
+
...collectGovernedSourcePaths(cwd, '.dql/reviews'),
|
|
21031
|
+
]),
|
|
20513
21032
|
]);
|
|
20514
|
-
|
|
21033
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
21034
|
+
const legacyBroadIgnore = existsSync(gitignorePath)
|
|
21035
|
+
&& readFileSync(gitignorePath, 'utf-8').split(/\r?\n/).some(isLegacyBroadDqlIgnore);
|
|
21036
|
+
const groups = [domains, skills, artifacts, learning];
|
|
21037
|
+
return {
|
|
21038
|
+
inRepo: true,
|
|
21039
|
+
trackingReady: !legacyBroadIgnore && groups.every((group) => group.ignored === 0),
|
|
21040
|
+
legacyBroadIgnore,
|
|
21041
|
+
domains,
|
|
21042
|
+
skills,
|
|
21043
|
+
artifacts,
|
|
21044
|
+
learning,
|
|
21045
|
+
};
|
|
20515
21046
|
}
|
|
20516
21047
|
/** Replace only the legacy broad `.dql/` ignore with cache/runtime-specific rules. */
|
|
20517
21048
|
async function enableGitGovernedContextTracking(cwd) {
|
|
20518
21049
|
const gitRoot = await resolveGitRoot(cwd);
|
|
20519
21050
|
if (!gitRoot)
|
|
20520
21051
|
return { ok: false, error: 'Not a git repository' };
|
|
20521
|
-
const
|
|
20522
|
-
const existing = existsSync(ignorePath) ? readFileSync(ignorePath, 'utf-8') : '';
|
|
20523
|
-
const lines = existing.split(/\r?\n/);
|
|
20524
|
-
const next = lines.filter((line) => !/^\s*\/?\.dql\/\s*$/.test(line));
|
|
20525
|
-
for (const rule of LOCAL_RUNTIME_GITIGNORE_RULES) {
|
|
20526
|
-
if (!next.some((line) => line.trim() === rule))
|
|
20527
|
-
next.push(rule);
|
|
20528
|
-
}
|
|
20529
|
-
const rendered = `${next.join('\n').replace(/\n+$/, '')}\n`;
|
|
20530
|
-
if (rendered !== existing)
|
|
20531
|
-
writeFileSync(ignorePath, rendered, 'utf-8');
|
|
21052
|
+
const gitignore = ensureDqlGitignore(cwd);
|
|
20532
21053
|
const migration = migrateLegacySkills(cwd);
|
|
20533
|
-
return { ok: true, changed:
|
|
21054
|
+
return { ok: true, changed: gitignore.changed || migration.moved.length > 0 };
|
|
20534
21055
|
}
|
|
20535
21056
|
async function gitCreateBranch(cwd, name, checkout) {
|
|
20536
21057
|
const gitRoot = await resolveGitRoot(cwd);
|