@duckcodeailabs/dql-cli 1.9.5 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@ 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
12
  import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, hasStandaloneSemanticRef, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
13
- import { loadSemanticLayerFromDir, normalizeDqlArtifactReference, serializeMetricDefinitionToYaml, resolveSemanticLayerAsync, resolveRepoSource, getDialect, Parser, NodeKind, blockParameterDefinitions, buildLineageGraph, buildManifest, collectInputFiles, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryBusiness360, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, domainFolderSlug, previewModelingChange, applyModelingChange, previewDbtSourcePatch, applyDbtSourcePatch, loadDomainPackageRegistry, loadDbtNodeAuthoringDetail, relationshipValidationProofFingerprint, discoverDbtDomains, renderDomainDeclaration, ProjectSnapshotService, analyzeSqlReferences, } from '@duckcodeailabs/dql-core';
13
+ import { loadSemanticLayerFromDir, normalizeDqlArtifactReference, serializeMetricDefinitionToYaml, resolveSemanticLayerAsync, resolveRepoSource, getDialect, Parser, NodeKind, blockParameterDefinitions, buildLineageGraph, buildManifest, collectInputFiles, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryBusiness360, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, domainFolderSlug, previewModelingChange, applyModelingChange, previewDbtSourcePatch, applyDbtSourcePatch, loadDomainPackageRegistry, loadDbtNodeAuthoringDetail, normalizeAnalyticalFailureV1, relationshipValidationProofFingerprint, discoverDbtDomains, renderDomainDeclaration, ProjectSnapshotService, analyzeSqlReferences, } from '@duckcodeailabs/dql-core';
14
14
  import { load as loadYaml } from 'js-yaml';
15
15
  import { listBlockTemplates } from './block-templates.js';
16
16
  import { getRunner as getLLMRunner } from './llm/index.js';
@@ -19,7 +19,7 @@ import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from '
19
19
  import { resolveRetrievalHealthStatus } from './retrieval-health.js';
20
20
  import { createDqlAgentProviderRunner, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
21
21
  import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
22
- import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, recallRelevantTurns, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, aggregationIntegrityIssuesForSql, buildLocalContextPack, toAgentRetrievalEvidence, defaultConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, propose, proposePlan, recordCorrectionTrace, emitCorrectionEvalCase, mineJoinPatterns, reviewHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, } from '@duckcodeailabs/dql-agent';
22
+ import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, recallRelevantTurns, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, aggregationIntegrityIssuesForSql, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, defaultConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, propose, proposePlan, recordCorrectionTrace, emitCorrectionEvalCase, mineJoinPatterns, reviewHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, } from '@duckcodeailabs/dql-agent';
23
23
  import { gatherProposeEnrichment } from './propose-enrich.js';
24
24
  import { handleAppsApi, proposeAppAiBuild, recommendVisualization } from './apps-api.js';
25
25
  import { getActiveProvider, getEffectiveProviderConfig, isProviderSettingsId, listProviderSettings, saveProviderSettings, } from './settings/provider-settings.js';
@@ -279,6 +279,53 @@ export function agentAnswerHasExecutionFailure(governedAnswer) {
279
279
  return typeof governedAnswer.executionError === 'string'
280
280
  && governedAnswer.executionError.trim().length > 0;
281
281
  }
282
+ /** Rebuild the immutable failed-run input from the artifact retained by API-007. */
283
+ export function analyticalFailedRunFromAgentRun(run) {
284
+ for (const artifact of run.artifacts) {
285
+ const payload = agentRunRecord(artifact.payload);
286
+ if (!payload)
287
+ continue;
288
+ const failure = normalizeAnalyticalFailureV1(payload.analyticalFailure);
289
+ const plan = agentRunRecord(payload.resolvedAnalyticalPlan);
290
+ const planFingerprint = agentRunString(plan?.fingerprint) ?? failure?.planFingerprint;
291
+ if (!failure || !planFingerprint)
292
+ continue;
293
+ const dqlArtifact = normalizeDqlArtifactReference(payload.dqlArtifact);
294
+ const dqlSource = dqlArtifact?.source;
295
+ const compiledSql = agentRunString(payload.sql)
296
+ ?? agentRunString(payload.proposedSql)
297
+ ?? dqlArtifact?.compiledSql;
298
+ const graph = agentRunRecord(payload.analyticalExecutionGraph);
299
+ const rawRoute = agentRunString(graph?.route) ?? agentRunString(plan?.recommendedRoute);
300
+ const route = rawRoute === 'certified'
301
+ ? 'certified'
302
+ : rawRoute === 'semantic'
303
+ ? 'semantic'
304
+ : rawRoute === 'governed_sql'
305
+ ? 'governed_sql'
306
+ : 'generated_sql';
307
+ const rawTrust = dqlArtifact?.trustState ?? agentRunString(payload.reviewStatus) ?? artifact.trustState;
308
+ const trustState = rawTrust === 'certified'
309
+ ? 'certified'
310
+ : rawTrust === 'governed'
311
+ ? 'governed'
312
+ : 'review_required';
313
+ return {
314
+ version: 1,
315
+ runId: failure.runId,
316
+ snapshotId: failure.snapshotId,
317
+ route,
318
+ trustState,
319
+ planFingerprint,
320
+ ...(dqlSource ? { dqlSource } : {}),
321
+ ...(failure.dqlFingerprint ? { dqlFingerprint: failure.dqlFingerprint } : {}),
322
+ ...(compiledSql ? { compiledSql } : {}),
323
+ ...(failure.sqlFingerprint ? { sqlFingerprint: failure.sqlFingerprint } : {}),
324
+ failure,
325
+ };
326
+ }
327
+ return undefined;
328
+ }
282
329
  function businessNarrativeGaps(warnings) {
283
330
  const businessRelevant = (warnings ?? []).filter((warning) => !(/missing requested output column/i.test(warning)
284
331
  || /\bquery plan\b/i.test(warning)
@@ -1220,6 +1267,57 @@ export async function startLocalServer(opts) {
1220
1267
  },
1221
1268
  ...(semanticDriver ? { semanticDriver } : {}),
1222
1269
  ...(semanticTableMapping ? { semanticTableMapping } : {}),
1270
+ ...(semanticLayer && semanticConnection ? {
1271
+ resolveAnalyticalFreshness: async (freshnessRequest) => {
1272
+ const binding = freshnessRequest.authorizedAdapterRequest;
1273
+ if (!binding || binding.route !== 'semantic') {
1274
+ throw Object.assign(new Error('The analytical freshness request is not bound to an authorized semantic route.'), {
1275
+ code: 'FRESHNESS_BINDING_REQUIRED',
1276
+ });
1277
+ }
1278
+ if (freshnessRequest.snapshotId !== routeDecision?.resolvedAnalyticalPlan?.snapshotId) {
1279
+ throw Object.assign(new Error('The analytical freshness request does not match the routed metadata snapshot.'), {
1280
+ code: 'SNAPSHOT_DRIFT',
1281
+ });
1282
+ }
1283
+ projectSnapshot();
1284
+ projectSnapshots.assertCurrent(runProjectSnapshot.snapshotId);
1285
+ const compiled = await compileSemanticRuntimeQuery({
1286
+ metrics: [binding.metric],
1287
+ dimensions: [],
1288
+ timeDimension: {
1289
+ name: binding.timeDimension,
1290
+ granularity: binding.granularity,
1291
+ },
1292
+ orderBy: [{ name: binding.outputField, direction: 'desc' }],
1293
+ limit: 1,
1294
+ }, {
1295
+ projectRoot,
1296
+ projectConfig,
1297
+ detectedProvider: semanticDetectedProvider,
1298
+ semanticLayer: semanticLayer,
1299
+ driver: semanticDriver,
1300
+ tableMapping: semanticTableMapping,
1301
+ });
1302
+ if (!compiled) {
1303
+ throw Object.assign(new Error('The authorized semantic freshness query could not be compiled.'), {
1304
+ code: 'COMPILATION_FAILED',
1305
+ });
1306
+ }
1307
+ const prepared = prepareLocalExecution(compiled.sql, semanticConnection, projectRoot, projectConfig);
1308
+ const result = await executor.executeQuery(prepared.sql, [], runtimeVariables({}), prepared.connection);
1309
+ projectSnapshot();
1310
+ projectSnapshots.assertCurrent(runProjectSnapshot.snapshotId);
1311
+ return {
1312
+ version: 1,
1313
+ snapshotId: freshnessRequest.snapshotId,
1314
+ metricId: freshnessRequest.metricId,
1315
+ timeDimensionId: freshnessRequest.timeDimensionId,
1316
+ observedThrough: analyticalFreshnessObservedThrough(result, freshnessRequest),
1317
+ observedAt: new Date().toISOString(),
1318
+ };
1319
+ },
1320
+ } : {}),
1223
1321
  ...(semanticLayer ? {
1224
1322
  semanticQueryCompiler: async (selection) => {
1225
1323
  const compiled = await compileSemanticRuntimeQuery({
@@ -1259,6 +1357,10 @@ export async function startLocalServer(opts) {
1259
1357
  ...(routeDecision?.meaningResolution?.recommendedExecutionId
1260
1358
  ? { preferredExecutionId: routeDecision.meaningResolution.recommendedExecutionId }
1261
1359
  : {}),
1360
+ ...(routeDecision?.resolvedAnalyticalPlan
1361
+ ? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
1362
+ : {}),
1363
+ analyticalReferenceInstant: new Date().toISOString(),
1262
1364
  executeCertifiedBlock: executeCertifiedBlockForAgent,
1263
1365
  executeGeneratedSql: (sql, artifact) => executeGeneratedArtifactForAgent(request.question, sql, artifact),
1264
1366
  executeDqlArtifact: (artifact) => executeArtifactReferenceForAgent(artifact, request.question),
@@ -1619,6 +1721,8 @@ export async function startLocalServer(opts) {
1619
1721
  && (governedAnswer.refusalCode === 'grounding_gap' || governedAnswer.refusalCode === 'modeling_gap')
1620
1722
  && !isExploratory;
1621
1723
  const isProviderError = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'provider_error';
1724
+ const isAnalyticalFailure = Boolean(governedAnswer.analyticalFailure);
1725
+ const isTerminalFailure = isProviderError || isExecutionFailure || isAnalyticalFailure;
1622
1726
  // AGT-004: a rejected attribution/export/proof policy is a deliberate
1623
1727
  // governance boundary, not an ambiguous user question and not a repairable
1624
1728
  // retrieval miss. Keep the precise policy detail visible, but never burn two
@@ -1678,14 +1782,14 @@ export async function startLocalServer(opts) {
1678
1782
  synthesizedAnswer = undefined;
1679
1783
  }
1680
1784
  }
1681
- const status = isProviderError || isExecutionFailure
1785
+ const status = isTerminalFailure
1682
1786
  ? 'blocked'
1683
1787
  : needsClarification
1684
1788
  ? 'needs_clarification'
1685
1789
  : isCertified || isSemantic
1686
1790
  ? 'completed'
1687
1791
  : 'needs_review';
1688
- const trustState = isProviderError || isExecutionFailure
1792
+ const trustState = isTerminalFailure
1689
1793
  ? 'blocked'
1690
1794
  : needsClarification
1691
1795
  ? 'not_applicable'
@@ -1694,7 +1798,7 @@ export async function startLocalServer(opts) {
1694
1798
  : isSemantic
1695
1799
  ? 'governed'
1696
1800
  : 'review_required';
1697
- const stopReason = isProviderError || isExecutionFailure
1801
+ const stopReason = isTerminalFailure
1698
1802
  ? 'blocked'
1699
1803
  : needsClarification
1700
1804
  ? 'needs_clarification'
@@ -1703,10 +1807,23 @@ export async function startLocalServer(opts) {
1703
1807
  : isSemantic
1704
1808
  ? 'governed_semantic_answer'
1705
1809
  : 'human_review_required';
1810
+ const terminalFailureActions = governedAnswer.analyticalFailure
1811
+ ? governedAnswer.analyticalFailure.code === 'PERMISSION_DENIED'
1812
+ ? [{ id: 'repair-access', label: 'Change connection or request access', route: 'blocked' }]
1813
+ : governedAnswer.analyticalFailure.code === 'TIMEOUT'
1814
+ ? [{ id: 'retry-same-plan', label: 'Retry the same bounded plan', route: 'generated_answer' }]
1815
+ : governedAnswer.analyticalFailure.code === 'SNAPSHOT_DRIFT'
1816
+ || governedAnswer.analyticalFailure.code === 'COLUMN_NOT_FOUND'
1817
+ || governedAnswer.analyticalFailure.code === 'RELATION_NOT_FOUND'
1818
+ ? [{ id: 'refresh-snapshot', label: 'Refresh the governed snapshot before retrying', route: 'blocked' }]
1819
+ : [{ id: 'review-analytical-failure', label: 'Review the plan, DQL, SQL, and safe repair actions', route: 'blocked' }]
1820
+ : isProviderError
1821
+ ? [{ id: 'retry-after-provider', label: 'Retry after fixing the AI provider', route: 'generated_answer' }]
1822
+ : [{ id: 'retry-after-connection', label: 'Retry after fixing the database connection', route: 'generated_answer' }];
1706
1823
  const nextActions = needsClarification
1707
1824
  ? [{ id: 'clarify', label: 'Clarify question', route: 'generated_answer' }]
1708
- : isExecutionFailure
1709
- ? [{ id: 'retry-after-connection', label: 'Retry after fixing the database connection', route: 'generated_answer' }]
1825
+ : isTerminalFailure
1826
+ ? terminalFailureActions
1710
1827
  : isGroundingGap
1711
1828
  ? [{ id: 'research-gap', label: 'Research missing metadata coverage', route: 'research', artifactKind: 'research_run' }]
1712
1829
  : [
@@ -1723,15 +1840,15 @@ export async function startLocalServer(opts) {
1723
1840
  resolvedRoute,
1724
1841
  answerRefusalCode: governedAnswer.kind === 'no_answer' ? governedAnswer.refusalCode : undefined,
1725
1842
  answerTier: governedAnswer.route?.tier,
1726
- summary: isExecutionFailure
1843
+ summary: isTerminalFailure
1727
1844
  ? 'The governed query could not be executed.'
1728
1845
  : governedAnswer.route?.label ?? (isCertified ? 'Answered from certified DQL context.' : isExploratory ? 'Exploratory DBT-grounded analysis requires review.' : 'Answered with review-required generated analysis.'),
1729
1846
  answer: synthesizedAnswer ?? governedAnswer.answer ?? governedAnswer.text,
1730
1847
  status,
1731
1848
  trustState,
1732
1849
  stopReason,
1733
- artifacts: isExecutionFailure
1734
- ? []
1850
+ artifacts: isTerminalFailure
1851
+ ? [agentRunArtifact('answer', 'Failed governed analytical run', governedAnswer, governedAnswer.sourceCertifiedBlock ?? governedAnswer.block?.name, 'blocked')]
1735
1852
  : governedAnswer.kind === 'no_answer'
1736
1853
  // A refusal still keeps the DQL draft the answer loop produced (when any),
1737
1854
  // so the "Review DQL draft" next-action isn't a dead link and the user can
@@ -1880,6 +1997,7 @@ export async function startLocalServer(opts) {
1880
1997
  history: request.history,
1881
1998
  // When the user explicitly picked research, investigate — don't collapse to one step.
1882
1999
  forceInvestigate: request.requestedMode === 'research',
2000
+ rootPlan: routeDecision?.resolvedAnalyticalPlan,
1883
2001
  });
1884
2002
  // Direct governed answer ("the metric answers this directly"): don't wrap it in
1885
2003
  // a review-required research dossier — run the query through the answer loop and
@@ -2400,54 +2518,23 @@ export async function startLocalServer(opts) {
2400
2518
  const meaningEvidence = pack.retrievalDiagnostics.meaningEvidence;
2401
2519
  if (!meaningEvidence) {
2402
2520
  return {
2403
- snapshotId: pack.id,
2521
+ snapshotId: pack.knowledgeLens.snapshotId,
2404
2522
  sourceFingerprint: pack.freshness.fingerprint ?? undefined,
2523
+ knowledgeLens: pack.knowledgeLens,
2405
2524
  candidates: [],
2406
2525
  diagnostics: { durationMs: Date.now() - startedAt },
2407
2526
  };
2408
2527
  }
2409
2528
  const evidence = toAgentRetrievalEvidence(meaningEvidence, pack.questionPlan, {
2410
- snapshotId: pack.id,
2529
+ snapshotId: pack.knowledgeLens.snapshotId,
2411
2530
  sourceFingerprint: pack.freshness.fingerprint ?? undefined,
2531
+ knowledgeLens: pack.knowledgeLens,
2532
+ analyticalPolicies: pack.skills.flatMap((skill) => skill.analyticalPolicy ? [skill.analyticalPolicy] : []),
2533
+ contextObjects: pack.objects,
2412
2534
  durationMs: Date.now() - startedAt,
2413
2535
  truncated: pack.retrievalDiagnostics.topRejected.length > 0,
2414
2536
  });
2415
- const certifiedFits = new Map(pack.retrievalDiagnostics.certifiedCandidateFits.map((fit) => [fit.objectKey, fit]));
2416
- const semanticEvidence = new Set(pack.routeDecision.selectedEvidence
2417
- .filter((item) => item.role === 'semantic_metric')
2418
- .map((item) => item.objectKey));
2419
- return {
2420
- ...evidence,
2421
- candidates: evidence.candidates.map((candidate) => {
2422
- if (candidate.kind === 'certified_block') {
2423
- const fit = certifiedFits.get(candidate.id);
2424
- return {
2425
- ...candidate,
2426
- compatibility: fit?.action === 'certified_answer'
2427
- ? 'compatible'
2428
- : fit?.action === 'rejected_for_fit'
2429
- ? 'incompatible'
2430
- : 'partial',
2431
- };
2432
- }
2433
- if ((candidate.kind === 'semantic_metric' || candidate.kind === 'semantic_member')
2434
- && (semanticEvidence.has(candidate.id) || request.selectedEvidenceId === candidate.id)
2435
- && (request.selectedEvidenceId === candidate.id
2436
- || (pack.routeDecision.route !== 'clarify' && pack.routeDecision.route !== 'conflict'))) {
2437
- const requestedDimensions = pack.questionPlan.requestedShape.dimensions.map((dimension) => dimension.toLowerCase());
2438
- const availableDimensions = (candidate.dimensions ?? []).map((dimension) => dimension.toLowerCase());
2439
- const dimensionsFit = requestedDimensions.length === 0 || requestedDimensions.every((requested) => availableDimensions.some((available) => available === requested || available.endsWith(`.${requested}`)));
2440
- const requestedTimeGrain = pack.questionPlan.timeTerms[0]?.toLowerCase();
2441
- const availableTimeGrains = (candidate.timeGrains ?? []).map((grain) => grain.toLowerCase());
2442
- const timeGrainFits = !requestedTimeGrain
2443
- || availableTimeGrains.includes(requestedTimeGrain);
2444
- return { ...candidate, compatibility: dimensionsFit && timeGrainFits ? 'compatible' : 'partial' };
2445
- }
2446
- if (candidate.trustTier === 'governed_sql')
2447
- return { ...candidate, compatibility: 'partial' };
2448
- return candidate;
2449
- }),
2450
- };
2537
+ return applyContextPackCompatibility(evidence, pack, request.selectedEvidenceId);
2451
2538
  };
2452
2539
  const buildRankedAgentRunCatalogContext = async (request) => {
2453
2540
  const evidence = await buildAgentRunEvidence(request);
@@ -4025,13 +4112,25 @@ export async function startLocalServer(opts) {
4025
4112
  }
4026
4113
  if (req.method === 'GET' && path === '/api/onboarding/launch') {
4027
4114
  const requestId = apiRequestId('onboarding-launch');
4028
- const acknowledgedVersion = readUserPrefs(userPrefsPath).setup?.acknowledgedVersion ?? null;
4115
+ const prefs = readUserPrefs(userPrefsPath);
4116
+ const acknowledgedVersion = prefs.setup?.acknowledgedVersion ?? null;
4117
+ const dbtAppliedVersion = prefs.setup?.dbtAppliedVersion ?? null;
4029
4118
  const shouldOpen = acknowledgedVersion !== runtimeVersion;
4119
+ const configuredDbtSource = projectConfig.dbt?.repoUrl ?? projectConfig.dbt?.projectDir;
4120
+ const configuredDbtRoot = findDbtProjectPath(projectRoot, projectConfig);
4121
+ const dbtConfigured = existsSync(join(configuredDbtRoot, 'dbt_project.yml'))
4122
+ || Boolean(configuredDbtSource && !/\{\{[^}]+\}\}/.test(configuredDbtSource));
4123
+ const requiresDbtReapply = Boolean(shouldOpen
4124
+ && acknowledgedVersion
4125
+ && dbtConfigured
4126
+ && dbtAppliedVersion !== runtimeVersion);
4030
4127
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
4031
4128
  res.end(serializeJSON({
4032
4129
  requestId,
4033
4130
  version: runtimeVersion,
4034
4131
  acknowledgedVersion,
4132
+ dbtAppliedVersion,
4133
+ requiresDbtReapply,
4035
4134
  shouldOpen,
4036
4135
  reason: shouldOpen ? (acknowledgedVersion ? 'version_upgrade' : 'first_install') : null,
4037
4136
  }));
@@ -4040,7 +4139,24 @@ export async function startLocalServer(opts) {
4040
4139
  if (req.method === 'POST' && path === '/api/onboarding/acknowledge') {
4041
4140
  const requestId = apiRequestId('onboarding-acknowledge');
4042
4141
  const prefs = readUserPrefs(userPrefsPath);
4142
+ const configuredDbtSource = projectConfig.dbt?.repoUrl ?? projectConfig.dbt?.projectDir;
4143
+ const configuredDbtRoot = findDbtProjectPath(projectRoot, projectConfig);
4144
+ const dbtConfigured = existsSync(join(configuredDbtRoot, 'dbt_project.yml'))
4145
+ || Boolean(configuredDbtSource && !/\{\{[^}]+\}\}/.test(configuredDbtSource));
4146
+ const isVersionUpgrade = Boolean(prefs.setup?.acknowledgedVersion
4147
+ && prefs.setup.acknowledgedVersion !== runtimeVersion);
4148
+ if (isVersionUpgrade && dbtConfigured && prefs.setup?.dbtAppliedVersion !== runtimeVersion) {
4149
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
4150
+ res.end(serializeJSON(apiErrorEnvelope({
4151
+ requestId,
4152
+ code: 'DBT_REAPPLY_REQUIRED',
4153
+ message: `Reapply the saved dbt project with DQL ${runtimeVersion} before completing the update review.`,
4154
+ nextActions: ['Open Project & dbt, preview the prefilled project, then choose Reapply project.'],
4155
+ })));
4156
+ return;
4157
+ }
4043
4158
  prefs.setup = {
4159
+ ...prefs.setup,
4044
4160
  acknowledgedVersion: runtimeVersion,
4045
4161
  acknowledgedAt: new Date().toISOString(),
4046
4162
  };
@@ -4227,6 +4343,16 @@ export async function startLocalServer(opts) {
4227
4343
  await reloadSemanticLayer();
4228
4344
  throw Object.assign(new Error(`dbt-first configuration did not compile and was rolled back: ${error instanceof Error ? error.message : String(error)}`), { code: 'SNAPSHOT_BUILD_FAILED' });
4229
4345
  }
4346
+ // An OSS version upgrade is acknowledged only after the user explicitly
4347
+ // previews and reapplies the saved dbt project. Record that successful
4348
+ // user-controlled compile without replacing their other preferences.
4349
+ const prefs = readUserPrefs(userPrefsPath);
4350
+ prefs.setup = {
4351
+ ...prefs.setup,
4352
+ dbtAppliedVersion: runtimeVersion,
4353
+ dbtAppliedAt: new Date().toISOString(),
4354
+ };
4355
+ writeUserPrefs(userPrefsPath, prefs);
4230
4356
  const preparation = startDbtPreparationJob({
4231
4357
  kind: 'dbt_prepare',
4232
4358
  snapshotId,
@@ -4798,6 +4924,50 @@ export async function startLocalServer(opts) {
4798
4924
  res.end(serializeJSON({ runs, total: agentRunStore.list().length, limit }));
4799
4925
  return;
4800
4926
  }
4927
+ if (req.method === 'POST' && /^\/api\/agent-runs\/[^/]+\/analytical-repair$/.test(path)) {
4928
+ const match = path.match(/^\/api\/agent-runs\/([^/]+)\/analytical-repair$/);
4929
+ const id = decodeURIComponent(match?.[1] ?? '');
4930
+ const run = await agentRunStore.get(id);
4931
+ if (!run) {
4932
+ res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
4933
+ res.end(serializeJSON({ error: 'Agent run not found.' }));
4934
+ return;
4935
+ }
4936
+ const source = analyticalFailedRunFromAgentRun(run);
4937
+ if (!source) {
4938
+ res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
4939
+ res.end(serializeJSON({ error: 'This run has no retained analytical failure artifact.' }));
4940
+ return;
4941
+ }
4942
+ const body = await readJSON(req).catch(() => null);
4943
+ const record = agentRunRecord(body);
4944
+ const repair = (agentRunRecord(record?.repair) ?? record);
4945
+ if (!repair) {
4946
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
4947
+ res.end(serializeJSON({ error: 'A v1 analytical repair request is required.' }));
4948
+ return;
4949
+ }
4950
+ // API-007: snapshot refresh is resolved by the runtime, not trusted from
4951
+ // browser input. projectSnapshot() recompiles when governed sources have
4952
+ // changed and gives the immutable identity used by the derivation.
4953
+ const repairWithRuntimeContext = repair.action === 'refresh_snapshot'
4954
+ ? { ...repair, refreshedSnapshotId: projectSnapshot().snapshotId }
4955
+ : repair;
4956
+ const result = deriveAnalyticalRepair(source, repairWithRuntimeContext);
4957
+ if (result.status === 'blocked') {
4958
+ const status = result.code === 'SOURCE_FINGERPRINT_MISMATCH'
4959
+ ? 409
4960
+ : result.code === 'PERMISSION_FAILURE_TERMINAL' || result.code === 'ACTION_NOT_ALLOWED'
4961
+ ? 403
4962
+ : 400;
4963
+ res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
4964
+ res.end(serializeJSON(result));
4965
+ return;
4966
+ }
4967
+ res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' });
4968
+ res.end(serializeJSON(result));
4969
+ return;
4970
+ }
4801
4971
  if (req.method === 'POST' && /^\/api\/agent-runs\/[^/]+\/cancel$/.test(path)) {
4802
4972
  const match = path.match(/^\/api\/agent-runs\/([^/]+)\/cancel$/);
4803
4973
  const id = decodeURIComponent(match?.[1] ?? '');
@@ -11452,6 +11622,48 @@ export function serializeJSON(value) {
11452
11622
  return current;
11453
11623
  });
11454
11624
  }
11625
+ /**
11626
+ * Extract the single time value returned by an authorized semantic freshness
11627
+ * lookup. Adapter aliases may differ by warehouse, so typed date metadata is
11628
+ * the only fallback; arbitrary string columns are never treated as freshness.
11629
+ * Acceptance: AGT-019, SEC-004.
11630
+ */
11631
+ export function analyticalFreshnessObservedThrough(result, request) {
11632
+ const binding = request.authorizedAdapterRequest;
11633
+ if (!binding || binding.route !== 'semantic') {
11634
+ throw Object.assign(new Error('The freshness lookup has no authorized semantic adapter binding.'), {
11635
+ code: 'FRESHNESS_BINDING_REQUIRED',
11636
+ });
11637
+ }
11638
+ const row = result.rows[0];
11639
+ if (!row) {
11640
+ throw Object.assign(new Error('The authorized freshness lookup returned no rows.'), {
11641
+ code: 'FRESHNESS_OBSERVATION_MISSING',
11642
+ });
11643
+ }
11644
+ const normalize = (value) => value.replace(/[^a-zA-Z0-9]+/g, '_').toLowerCase();
11645
+ const target = normalize(binding.outputField);
11646
+ const exact = result.columns.find((column) => normalize(column.name) === target);
11647
+ const typedDates = result.columns.filter((column) => column.type === 'date' || column.type === 'datetime');
11648
+ const column = exact ?? (typedDates.length === 1 ? typedDates[0] : undefined);
11649
+ if (!column) {
11650
+ throw Object.assign(new Error('The authorized freshness lookup did not return one identifiable time field.'), {
11651
+ code: 'FRESHNESS_OBSERVATION_INVALID',
11652
+ });
11653
+ }
11654
+ const value = row[column.name];
11655
+ const instant = value instanceof Date
11656
+ ? value
11657
+ : typeof value === 'string' && value.trim()
11658
+ ? new Date(value)
11659
+ : undefined;
11660
+ if (!instant || Number.isNaN(instant.getTime())) {
11661
+ throw Object.assign(new Error('The authorized freshness value is not a valid instant.'), {
11662
+ code: 'FRESHNESS_OBSERVATION_INVALID',
11663
+ });
11664
+ }
11665
+ return instant.toISOString();
11666
+ }
11455
11667
  /** Serialize a Skill to the shared API contract shape (spec 16). */
11456
11668
  function serializeSkill(skill) {
11457
11669
  return {
@@ -14123,15 +14335,21 @@ function readUserPrefs(userPrefsPath) {
14123
14335
  const acknowledgedVersion = typeof raw.setup?.acknowledgedVersion === 'string'
14124
14336
  ? raw.setup.acknowledgedVersion.trim()
14125
14337
  : '';
14338
+ const dbtAppliedVersion = typeof raw.setup?.dbtAppliedVersion === 'string'
14339
+ ? raw.setup.dbtAppliedVersion.trim()
14340
+ : '';
14341
+ const setup = acknowledgedVersion || dbtAppliedVersion
14342
+ ? {
14343
+ ...(acknowledgedVersion ? { acknowledgedVersion } : {}),
14344
+ ...(typeof raw.setup?.acknowledgedAt === 'string' ? { acknowledgedAt: raw.setup.acknowledgedAt } : {}),
14345
+ ...(dbtAppliedVersion ? { dbtAppliedVersion } : {}),
14346
+ ...(typeof raw.setup?.dbtAppliedAt === 'string' ? { dbtAppliedAt: raw.setup.dbtAppliedAt } : {}),
14347
+ }
14348
+ : undefined;
14126
14349
  return {
14127
14350
  favorites: Array.isArray(raw.favorites) ? raw.favorites.map(String) : [],
14128
14351
  recentlyUsed: Array.isArray(raw.recentlyUsed) ? raw.recentlyUsed.map(String) : [],
14129
- ...(acknowledgedVersion ? {
14130
- setup: {
14131
- acknowledgedVersion,
14132
- ...(typeof raw.setup?.acknowledgedAt === 'string' ? { acknowledgedAt: raw.setup.acknowledgedAt } : {}),
14133
- },
14134
- } : {}),
14352
+ ...(setup ? { setup } : {}),
14135
14353
  };
14136
14354
  }
14137
14355
  catch {