@duckcodeailabs/dql-cli 1.10.0 → 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, 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, 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({
@@ -1262,6 +1360,7 @@ export async function startLocalServer(opts) {
1262
1360
  ...(routeDecision?.resolvedAnalyticalPlan
1263
1361
  ? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
1264
1362
  : {}),
1363
+ analyticalReferenceInstant: new Date().toISOString(),
1265
1364
  executeCertifiedBlock: executeCertifiedBlockForAgent,
1266
1365
  executeGeneratedSql: (sql, artifact) => executeGeneratedArtifactForAgent(request.question, sql, artifact),
1267
1366
  executeDqlArtifact: (artifact) => executeArtifactReferenceForAgent(artifact, request.question),
@@ -1622,6 +1721,8 @@ export async function startLocalServer(opts) {
1622
1721
  && (governedAnswer.refusalCode === 'grounding_gap' || governedAnswer.refusalCode === 'modeling_gap')
1623
1722
  && !isExploratory;
1624
1723
  const isProviderError = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'provider_error';
1724
+ const isAnalyticalFailure = Boolean(governedAnswer.analyticalFailure);
1725
+ const isTerminalFailure = isProviderError || isExecutionFailure || isAnalyticalFailure;
1625
1726
  // AGT-004: a rejected attribution/export/proof policy is a deliberate
1626
1727
  // governance boundary, not an ambiguous user question and not a repairable
1627
1728
  // retrieval miss. Keep the precise policy detail visible, but never burn two
@@ -1681,14 +1782,14 @@ export async function startLocalServer(opts) {
1681
1782
  synthesizedAnswer = undefined;
1682
1783
  }
1683
1784
  }
1684
- const status = isProviderError || isExecutionFailure
1785
+ const status = isTerminalFailure
1685
1786
  ? 'blocked'
1686
1787
  : needsClarification
1687
1788
  ? 'needs_clarification'
1688
1789
  : isCertified || isSemantic
1689
1790
  ? 'completed'
1690
1791
  : 'needs_review';
1691
- const trustState = isProviderError || isExecutionFailure
1792
+ const trustState = isTerminalFailure
1692
1793
  ? 'blocked'
1693
1794
  : needsClarification
1694
1795
  ? 'not_applicable'
@@ -1697,7 +1798,7 @@ export async function startLocalServer(opts) {
1697
1798
  : isSemantic
1698
1799
  ? 'governed'
1699
1800
  : 'review_required';
1700
- const stopReason = isProviderError || isExecutionFailure
1801
+ const stopReason = isTerminalFailure
1701
1802
  ? 'blocked'
1702
1803
  : needsClarification
1703
1804
  ? 'needs_clarification'
@@ -1706,10 +1807,23 @@ export async function startLocalServer(opts) {
1706
1807
  : isSemantic
1707
1808
  ? 'governed_semantic_answer'
1708
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' }];
1709
1823
  const nextActions = needsClarification
1710
1824
  ? [{ id: 'clarify', label: 'Clarify question', route: 'generated_answer' }]
1711
- : isExecutionFailure
1712
- ? [{ id: 'retry-after-connection', label: 'Retry after fixing the database connection', route: 'generated_answer' }]
1825
+ : isTerminalFailure
1826
+ ? terminalFailureActions
1713
1827
  : isGroundingGap
1714
1828
  ? [{ id: 'research-gap', label: 'Research missing metadata coverage', route: 'research', artifactKind: 'research_run' }]
1715
1829
  : [
@@ -1726,15 +1840,15 @@ export async function startLocalServer(opts) {
1726
1840
  resolvedRoute,
1727
1841
  answerRefusalCode: governedAnswer.kind === 'no_answer' ? governedAnswer.refusalCode : undefined,
1728
1842
  answerTier: governedAnswer.route?.tier,
1729
- summary: isExecutionFailure
1843
+ summary: isTerminalFailure
1730
1844
  ? 'The governed query could not be executed.'
1731
1845
  : governedAnswer.route?.label ?? (isCertified ? 'Answered from certified DQL context.' : isExploratory ? 'Exploratory DBT-grounded analysis requires review.' : 'Answered with review-required generated analysis.'),
1732
1846
  answer: synthesizedAnswer ?? governedAnswer.answer ?? governedAnswer.text,
1733
1847
  status,
1734
1848
  trustState,
1735
1849
  stopReason,
1736
- artifacts: isExecutionFailure
1737
- ? []
1850
+ artifacts: isTerminalFailure
1851
+ ? [agentRunArtifact('answer', 'Failed governed analytical run', governedAnswer, governedAnswer.sourceCertifiedBlock ?? governedAnswer.block?.name, 'blocked')]
1738
1852
  : governedAnswer.kind === 'no_answer'
1739
1853
  // A refusal still keeps the DQL draft the answer loop produced (when any),
1740
1854
  // so the "Review DQL draft" next-action isn't a dead link and the user can
@@ -2415,6 +2529,8 @@ export async function startLocalServer(opts) {
2415
2529
  snapshotId: pack.knowledgeLens.snapshotId,
2416
2530
  sourceFingerprint: pack.freshness.fingerprint ?? undefined,
2417
2531
  knowledgeLens: pack.knowledgeLens,
2532
+ analyticalPolicies: pack.skills.flatMap((skill) => skill.analyticalPolicy ? [skill.analyticalPolicy] : []),
2533
+ contextObjects: pack.objects,
2418
2534
  durationMs: Date.now() - startedAt,
2419
2535
  truncated: pack.retrievalDiagnostics.topRejected.length > 0,
2420
2536
  });
@@ -4808,6 +4924,50 @@ export async function startLocalServer(opts) {
4808
4924
  res.end(serializeJSON({ runs, total: agentRunStore.list().length, limit }));
4809
4925
  return;
4810
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
+ }
4811
4971
  if (req.method === 'POST' && /^\/api\/agent-runs\/[^/]+\/cancel$/.test(path)) {
4812
4972
  const match = path.match(/^\/api\/agent-runs\/([^/]+)\/cancel$/);
4813
4973
  const id = decodeURIComponent(match?.[1] ?? '');
@@ -11462,6 +11622,48 @@ export function serializeJSON(value) {
11462
11622
  return current;
11463
11623
  });
11464
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
+ }
11465
11667
  /** Serialize a Skill to the shared API contract shape (spec 16). */
11466
11668
  function serializeSkill(skill) {
11467
11669
  return {