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