@duckcodeailabs/dql-cli 1.14.0 → 1.14.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.
Files changed (88) hide show
  1. package/dist/args.d.ts +15 -0
  2. package/dist/args.d.ts.map +1 -1
  3. package/dist/args.js +25 -0
  4. package/dist/args.js.map +1 -1
  5. package/dist/assets/dql-notebook/assets/{AgentLogPage-Ch7VK20X.js → AgentLogPage-BPz-UWFh.js} +1 -1
  6. package/dist/assets/dql-notebook/assets/{AiBuildDialog-DBr5TmyM.js → AiBuildDialog-BEl53WA_.js} +1 -1
  7. package/dist/assets/dql-notebook/assets/{AiBuildResult-jLPzQO7O.js → AiBuildResult-B4yfGTTZ.js} +1 -1
  8. package/dist/assets/dql-notebook/assets/{AiSidePanel-CdlGsiVC.js → AiSidePanel-CSZAAvuD.js} +1 -1
  9. package/dist/assets/dql-notebook/assets/{AnalyticsHome-C0DXbOwY.js → AnalyticsHome-D5P6Ujwi.js} +1 -1
  10. package/dist/assets/dql-notebook/assets/{AppsView-CcpwjApv.js → AppsView-DQOwU9Cg.js} +4 -4
  11. package/dist/assets/dql-notebook/assets/{BlockStudio-CFYxafw-.js → BlockStudio-B4ap0GdY.js} +1 -1
  12. package/dist/assets/dql-notebook/assets/{BusinessArtifactView-C0kLYg2p.js → BusinessArtifactView-BIfNI-0S.js} +1 -1
  13. package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-CMwElXD_.js → DbtFirstModelingPage-D72byb2g.js} +1 -1
  14. package/dist/assets/dql-notebook/assets/{GitPage-JjhRDeWY.js → GitPage-lQb1uXH2.js} +1 -1
  15. package/dist/assets/dql-notebook/assets/{GlobalAiRail-DakE4NdR.js → GlobalAiRail-CVECf6Xj.js} +1 -1
  16. package/dist/assets/dql-notebook/assets/{GovernedContextPage-BokDqG6a.js → GovernedContextPage-trOyMCY6.js} +1 -1
  17. package/dist/assets/dql-notebook/assets/{HelpDocsPage-CjOv6_gz.js → HelpDocsPage-D8hLS5lE.js} +1 -1
  18. package/dist/assets/dql-notebook/assets/{HomePage-nGaNcwdw.js → HomePage-eIkfBIep.js} +1 -1
  19. package/dist/assets/dql-notebook/assets/{LineageDAG-CO6CFJRg.js → LineageDAG-CSqcbDrE.js} +1 -1
  20. package/dist/assets/dql-notebook/assets/{LineageDetailView-BnGb7OF7.js → LineageDetailView-DJZZjZu-.js} +1 -1
  21. package/dist/assets/dql-notebook/assets/{LineageDrawer-C5Y0Ht0b.js → LineageDrawer-BcIipIc3.js} +1 -1
  22. package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-Cgh3GR3F.js → LineagePathBreadcrumb-CviIf8PN.js} +1 -1
  23. package/dist/assets/dql-notebook/assets/{MiniLineageGraph-Kla9PYuj.js → MiniLineageGraph-vH_MY_Ju.js} +1 -1
  24. package/dist/assets/dql-notebook/assets/{NewBlockModal-BR2SnmPT.js → NewBlockModal-DbCQg-pj.js} +1 -1
  25. package/dist/assets/dql-notebook/assets/{NewNotebookModal-BaCHMYeB.js → NewNotebookModal-5Vp6XiuK.js} +1 -1
  26. package/dist/assets/dql-notebook/assets/{NotebookEditor-CBLqY8cE.js → NotebookEditor-DjqJS44s.js} +1 -1
  27. package/dist/assets/dql-notebook/assets/{ReadinessPage-CiN0IWSS.js → ReadinessPage-BHGrC5ho.js} +1 -1
  28. package/dist/assets/dql-notebook/assets/{SetupOnboarding-BhiCsYF-.js → SetupOnboarding-BfS9Tdx-.js} +1 -1
  29. package/dist/assets/dql-notebook/assets/{SkillsPage-CCSf8VMm.js → SkillsPage-BFH21wSj.js} +1 -1
  30. package/dist/assets/dql-notebook/assets/{TrustBadge-BgQmFe_x.js → TrustBadge-zm6g_SxZ.js} +1 -1
  31. package/dist/assets/dql-notebook/assets/{UnifiedAgentRunPanel-Bw5AXNDB.js → UnifiedAgentRunPanel--oxmjlgr.js} +21 -21
  32. package/dist/assets/dql-notebook/assets/{answer-to-notebook-AeDYUDla.js → answer-to-notebook-DPhxIEzF.js} +1 -1
  33. package/dist/assets/dql-notebook/assets/{arrow-left-C55x2hq_.js → arrow-left-DNEb86Xc.js} +1 -1
  34. package/dist/assets/dql-notebook/assets/{arrow-right-C1cJrhOm.js → arrow-right-DpPWbwaD.js} +1 -1
  35. package/dist/assets/dql-notebook/assets/{book-open-text-CQf_sdv2.js → book-open-text-D7s5jo4X.js} +1 -1
  36. package/dist/assets/dql-notebook/assets/{circle-x-X8-Z2yLY.js → circle-x-Db3dXKg7.js} +1 -1
  37. package/dist/assets/dql-notebook/assets/{dagre.esm-BjjNYKyY.js → dagre.esm-C7pppQ1a.js} +1 -1
  38. package/dist/assets/dql-notebook/assets/{external-link-C2zrz5DH.js → external-link-BxwXitO_.js} +1 -1
  39. package/dist/assets/dql-notebook/assets/{grip-vertical-qGV_PYGU.js → grip-vertical-Dt4lkWRi.js} +1 -1
  40. package/dist/assets/dql-notebook/assets/{index-ByTDPDaH.js → index-DKo-bwNw.js} +2 -2
  41. package/dist/assets/dql-notebook/assets/{link-2-VpyOxQXG.js → link-2-Dfo2P6wi.js} +1 -1
  42. package/dist/assets/dql-notebook/assets/{list-tree-CH2Jhwms.js → list-tree-DiTmIWAL.js} +1 -1
  43. package/dist/assets/dql-notebook/assets/{minimize-2-B2TZJ8BT.js → minimize-2-CMTAkPzL.js} +1 -1
  44. package/dist/assets/dql-notebook/assets/{panel-right-open-BunF88lt.js → panel-right-open-DwYr7FW4.js} +1 -1
  45. package/dist/assets/dql-notebook/assets/{play-DAFVF4_G.js → play-BXhHYQ4x.js} +1 -1
  46. package/dist/assets/dql-notebook/assets/{rotate-ccw-DGCrrqtY.js → rotate-ccw-BNi6F8pl.js} +1 -1
  47. package/dist/assets/dql-notebook/assets/{semantic-fields-Ci-9QL9F.js → semantic-fields-CNOGysAy.js} +1 -1
  48. package/dist/assets/dql-notebook/assets/{sliders-horizontal-BOAlXXbn.js → sliders-horizontal-Ec5MUMUW.js} +1 -1
  49. package/dist/assets/dql-notebook/assets/{star-CkksZXHt.js → star-B9leDkp_.js} +1 -1
  50. package/dist/assets/dql-notebook/assets/{triangle-alert-D3mjyJZE.js → triangle-alert-BefTYCzx.js} +1 -1
  51. package/dist/assets/dql-notebook/assets/{upload-CTNOAVEO.js → upload-SPiOM2tQ.js} +1 -1
  52. package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-DiQjc7x-.js → usePersistedAgentThreadId-C4foXeiQ.js} +1 -1
  53. package/dist/assets/dql-notebook/assets/{user-round-BWd5tQRg.js → user-round-comGmyw-.js} +1 -1
  54. package/dist/assets/dql-notebook/assets/{wand-sparkles-CqsAv8P-.js → wand-sparkles-BffR4dF8.js} +1 -1
  55. package/dist/assets/dql-notebook/assets/{workflow-CSqsj-sC.js → workflow-ChmPTEzH.js} +1 -1
  56. package/dist/assets/dql-notebook/assets/{wrench-DWqzqlX8.js → wrench-DovaG_ze.js} +1 -1
  57. package/dist/assets/dql-notebook/assets/{x-65M5rLCE.js → x-nRx91AgW.js} +1 -1
  58. package/dist/assets/dql-notebook/index.html +1 -1
  59. package/dist/commands/agent-eval-cassette.d.ts +73 -0
  60. package/dist/commands/agent-eval-cassette.d.ts.map +1 -0
  61. package/dist/commands/agent-eval-cassette.js +170 -0
  62. package/dist/commands/agent-eval-cassette.js.map +1 -0
  63. package/dist/commands/agent-eval-runtime.d.ts +97 -0
  64. package/dist/commands/agent-eval-runtime.d.ts.map +1 -0
  65. package/dist/commands/agent-eval-runtime.js +155 -0
  66. package/dist/commands/agent-eval-runtime.js.map +1 -0
  67. package/dist/commands/agent.d.ts +115 -1
  68. package/dist/commands/agent.d.ts.map +1 -1
  69. package/dist/commands/agent.js +289 -62
  70. package/dist/commands/agent.js.map +1 -1
  71. package/dist/index.js +4 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/llm/analyst-loop-tools.d.ts +19 -0
  74. package/dist/llm/analyst-loop-tools.d.ts.map +1 -0
  75. package/dist/llm/analyst-loop-tools.js +56 -0
  76. package/dist/llm/analyst-loop-tools.js.map +1 -0
  77. package/dist/llm/providers/dql-agent-provider.d.ts +24 -1
  78. package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
  79. package/dist/llm/providers/dql-agent-provider.js +306 -7
  80. package/dist/llm/providers/dql-agent-provider.js.map +1 -1
  81. package/dist/llm/types.d.ts +19 -1
  82. package/dist/llm/types.d.ts.map +1 -1
  83. package/dist/local-runtime.d.ts +108 -1
  84. package/dist/local-runtime.d.ts.map +1 -1
  85. package/dist/local-runtime.js +626 -110
  86. package/dist/local-runtime.js.map +1 -1
  87. package/dist/package.json +10 -10
  88. package/package.json +10 -10
@@ -23,9 +23,10 @@ import { getRunner as getLLMRunner } from './llm/index.js';
23
23
  import { rethrowIfCancelled } from './llm/cancellation.js';
24
24
  import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from './version-status.js';
25
25
  import { resolveRetrievalHealthStatus } from './retrieval-health.js';
26
- import { createDqlAgentProviderRunner, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
26
+ import { applyFinding, createResearchState, narrationMaxTokensForFacts, nextHypothesis, rerankCandidates, synthesizeResearchNarrative, AgenticExecutionCapabilityGate, mintFinalSqlAuthorization, verifyAgenticSqlExecutionCapability, qualifyAuthorizationReferences, validateSqlAgainstLocalContext as validateAuthorizedSqlReferences, verifyFinalSql, } from '@duckcodeailabs/dql-agent';
27
+ import { applyEvalCassette, createDqlAgentProviderRunner, createGovernedTextProvider, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
27
28
  import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
28
- import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildAggregationSafetyProof, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, prepareConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, normalizeWarehouseSqlFailure, parseProposal, propose, proposePlan, recordGovernedCorrection, HintStore, defaultHintIndexPath, ensureHintIndexFresh, listHintsFromGit, getHintEvaluationFromGit, getCorrectionTraceFromGit, inspectGovernedHint, editGovernedHintCandidate, reopenGovernedHint, retireHint, supersedeHint, hintsConflict, mineJoinPatterns, reviewGovernedHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, internalRelationIdsInSql, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, previewSkillChange, buildContextAuthoringProposal, contextAuthoringDependencyClosure, FileContextAuthoringProposalStore, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, projectEmbeddingProvider, isHashedEmbeddingProvider, clearProjectEmbeddingCache, upgradeVectorIndexForProject, openMetadataCatalog, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, createAgentRunBudget, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, isTrustedConversationTurn, resolveInternalRelationIds, analyticalError, tagAnalyticalError, withAnalyticalErrorOrigin, withAnalyticalErrorOriginSync, assertProviderPayloadAllowed, createProviderDispatchEgressReceipt, prepareProviderWireEnvelopeForDispatch, markProviderMetadataArray, createProviderEgressReceipt, redactProviderResultRows, composeVerifiedAnalyticalNarrative, buildCoverageGap, capResearchBranches, buildResearchEvidenceLedger, buildAnalyticalTurnPlan, DEFAULT_ASK_ROW_EGRESS_POLICY, ZERO_ROW_EGRESS_POLICY, resolveProviderResultRowEgressPolicy, normalizeCanonicalQueryResult, normalizeAnalyticalExecutionFingerprint, normalizeAnalyticalExecutionReceipt, createAgentRunCancellationError, } from '@duckcodeailabs/dql-agent';
29
+ import { composeBusinessExplanation, ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildAggregationSafetyProof, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, prepareConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, normalizeWarehouseSqlFailure, parseProposal, propose, proposePlan, recordGovernedCorrection, HintStore, defaultHintIndexPath, ensureHintIndexFresh, listHintsFromGit, getHintEvaluationFromGit, getCorrectionTraceFromGit, inspectGovernedHint, editGovernedHintCandidate, reopenGovernedHint, retireHint, supersedeHint, hintsConflict, mineJoinPatterns, reviewGovernedHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, internalRelationIdsInSql, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, previewSkillChange, buildContextAuthoringProposal, contextAuthoringDependencyClosure, FileContextAuthoringProposalStore, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, projectEmbeddingProvider, isHashedEmbeddingProvider, clearProjectEmbeddingCache, upgradeVectorIndexForProject, openMetadataCatalog, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, createAgentRunBudget, isProbeSafeColumn, deadlineScale, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, isTrustedConversationTurn, resolveInternalRelationIds, analyticalError, tagAnalyticalError, withAnalyticalErrorOrigin, withAnalyticalErrorOriginSync, assertProviderPayloadAllowed, createProviderDispatchEgressReceipt, prepareProviderWireEnvelopeForDispatch, markProviderMetadataArray, createProviderEgressReceipt, redactProviderResultRows, composeVerifiedAnalyticalNarrative, buildCoverageGap, capResearchBranches, buildResearchEvidenceLedger, buildAnalyticalTurnPlan, resolveTopRankedRegionDependency, DEFAULT_ASK_ROW_EGRESS_POLICY, ZERO_ROW_EGRESS_POLICY, resolveProviderResultRowEgressPolicy, normalizeCanonicalQueryResult, normalizeAnalyticalExecutionFingerprint, normalizeAnalyticalExecutionReceipt, createAgentRunCancellationError, } from '@duckcodeailabs/dql-agent';
29
30
  import { addSqlResultFilter, dashboardFilterableResultColumns, filterableResultColumns, replaceBlockStudioSql } from './sql-result-filter.js';
30
31
  import { gatherProposeEnrichment } from './propose-enrich.js';
31
32
  import { handleAppsApi, proposeAppAiBuild, recommendVisualization, } from './apps-api.js';
@@ -328,6 +329,10 @@ const CLIENT_PLAN_AUTHORITY_KEYS = new Set([
328
329
  'priorResolvedAnalyticalPlan',
329
330
  'resolvedAnalyticalPlan',
330
331
  'analyticalFrame',
332
+ // Only the local compound executor may inject this after it has derived a
333
+ // canonical parent result binding. A browser-provided lookalike cannot become
334
+ // a child filter or skip ordinary member validation.
335
+ 'analyticalTaskDependencyBinding',
331
336
  ]);
332
337
  /**
333
338
  * Browser/embedding context is useful retrieval and history input, but it is
@@ -378,6 +383,52 @@ export function agentRunDeadlineMs(request, env = process.env, activeProviderId)
378
383
  ? AGENT_RESEARCH_DEADLINE_MS
379
384
  : AGENT_LOOKUP_DEADLINE_MS;
380
385
  }
386
+ /**
387
+ * Run ready independent compound clauses concurrently, but wait for a typed
388
+ * parent result before executing a declared dependent clause. The scheduler
389
+ * itself has no authority to query or filter; callers supply both execution and
390
+ * a dependency resolver so immutable-plan and SQL guards remain unchanged.
391
+ */
392
+ export async function scheduleCompoundAnalyticalTasks(input) {
393
+ const pending = [...input.tasks];
394
+ const settled = new Map();
395
+ while (pending.length > 0) {
396
+ const ready = pending.filter((task) => task.dependencies.every((dependencyId) => settled.has(dependencyId)));
397
+ if (ready.length === 0) {
398
+ for (const task of pending.splice(0)) {
399
+ settled.set(task.id, {
400
+ task,
401
+ error: 'The compound task dependency graph could not be resolved.',
402
+ dependencyError: {
403
+ ok: false,
404
+ code: 'RESULT_CONTRACT_MISMATCH',
405
+ message: 'The dependent task could not run because its parent dependency was unresolved.',
406
+ },
407
+ });
408
+ }
409
+ break;
410
+ }
411
+ for (const task of ready)
412
+ pending.splice(pending.indexOf(task), 1);
413
+ const batch = await Promise.all(ready.map(async (task) => {
414
+ if (!task.dependency || task.dependency.kind !== 'top_ranked_region')
415
+ return input.runTask(task);
416
+ const parent = settled.get(task.dependency.sourceTaskId);
417
+ const resolution = input.resolveDependency(task, parent);
418
+ if (!resolution.ok) {
419
+ const dependencyError = resolution;
420
+ return { task, error: dependencyError.message, dependencyError };
421
+ }
422
+ return input.runTask(task, resolution.binding);
423
+ }));
424
+ for (const result of batch)
425
+ settled.set(result.task.id, result);
426
+ }
427
+ return input.tasks.map((task) => settled.get(task.id) ?? {
428
+ task,
429
+ error: 'The compound task did not produce an outcome.',
430
+ });
431
+ }
381
432
  /**
382
433
  * Decide how a settled answer gets its business-facing prose.
383
434
  *
@@ -420,11 +471,64 @@ export function shouldSynthesizeAgentRunAnswer(governedAnswer, requestedMode = '
420
471
  rowEgress: DEFAULT_ASK_ROW_EGRESS_POLICY,
421
472
  }).mode !== 'skip';
422
473
  }
474
+ /**
475
+ * A receipt may be rendered in the local inspector and exported into evaluation
476
+ * output. Keep only stable validation codes there; provider error messages can
477
+ * contain a prompt excerpt, result value, or connector detail and must not
478
+ * become user-visible durable data.
479
+ */
480
+ function narrationIntegrityFailureCodes(failures) {
481
+ return [...new Set(failures.map((failure) => {
482
+ const match = failure.trim().match(/^([A-Z][A-Z0-9_]{1,80})/);
483
+ return match?.[1] ?? 'NARRATION_VALIDATION_FAILED';
484
+ }).filter(Boolean))].slice(0, 8);
485
+ }
423
486
  /**
424
487
  * AGT-010 — the semantic route label is descriptive, while the exact
425
488
  * route-specific aggregation proof is authoritative for governed trust.
426
489
  * Missing proof remains blocked for legacy or malformed results.
427
490
  */
491
+ /**
492
+ * Trust for ONE answer, by the same rule the single-answer path uses: a route
493
+ * label is not authority, and a semantic route earns `governed` only when its
494
+ * aggregation proof actually passed.
495
+ */
496
+ export function trustStateForAgentAnswer(answer) {
497
+ if (answer.certification === 'certified' || answer.kind === 'certified')
498
+ return 'certified';
499
+ return semanticAnswerHasPassedAggregationProof(answer) ? 'governed' : 'review_required';
500
+ }
501
+ const TRUST_RANK = {
502
+ certified: 3,
503
+ governed: 2,
504
+ grounded: 1,
505
+ review_required: 0,
506
+ };
507
+ /**
508
+ * A compound answer is exactly as trustworthy as its WEAKEST successful child.
509
+ *
510
+ * The previous rule was `every child completed ? 'governed' : 'review_required'`,
511
+ * which stamped `governed` on a parent whose children were review-required
512
+ * generated SQL — completion is not proof. That is a governance violation and
513
+ * the worst possible failure for this product: the reader is told a number
514
+ * carries governed authority when nothing proved it.
515
+ *
516
+ * `certified` is deliberately NOT reachable here. Certified trust is granted
517
+ * only by executing the exact certified artifact; a parent that merely
518
+ * assembled certified children did not execute one, so it caps at `governed`.
519
+ */
520
+ export function compoundTrustState(childTrust) {
521
+ if (childTrust.length === 0)
522
+ return 'review_required';
523
+ const weakest = childTrust.reduce((low, current) => (TRUST_RANK[current] ?? 0) < (TRUST_RANK[low] ?? 0) ? current : low);
524
+ return weakest === 'certified' ? 'governed' : weakest;
525
+ }
526
+ /** Neutral parent outcome: governed only when every child completed governed. */
527
+ export function compoundStopReason(completedCount, childCount, trustState) {
528
+ return completedCount === childCount && childCount > 0 && trustState === 'governed'
529
+ ? 'governed_compound_answer'
530
+ : 'human_review_required';
531
+ }
428
532
  export function semanticAnswerHasPassedAggregationProof(governedAnswer) {
429
533
  return governedAnswer.route?.tier === 'semantic_metric'
430
534
  && governedAnswer.aggregationSafetyProof?.status === 'safe';
@@ -1039,6 +1143,7 @@ export function conversationTurnInputFromRun(run) {
1039
1143
  sql: agentRunString(payload?.proposedSql) ?? agentRunString(payload?.sql),
1040
1144
  dqlArtifact: agentRunRecord(payload?.dqlArtifact),
1041
1145
  cascade: agentRunRecord(payload?.cascade),
1146
+ narrationIntegrityReceipt: run.narrationIntegrityReceipt,
1042
1147
  result: columns.length > 0 || rows.length > 0
1043
1148
  ? {
1044
1149
  columns,
@@ -1166,12 +1271,7 @@ export class RunScopedProviderDispatchEvidence {
1166
1271
  * from what it already has.
1167
1272
  */
1168
1273
  expectedDispatchMs() {
1169
- if (this.observedDispatchDurations.length === 0)
1170
- return ASSUMED_PROVIDER_DISPATCH_MS;
1171
- const sorted = [...this.observedDispatchDurations].sort((left, right) => left - right);
1172
- // The slowest observed call is the honest predictor: an optimistic median
1173
- // still admits a dispatch that the deadline then kills.
1174
- return sorted[sorted.length - 1];
1274
+ return predictDispatchMs(this.observedDispatchDurations);
1175
1275
  }
1176
1276
  /** True when the remaining wall clock cannot fit another provider call. */
1177
1277
  cannotFitAnotherDispatch() {
@@ -1364,6 +1464,50 @@ function mergeRunScopedProviderDispatchEvidence(run, evidence) {
1364
1464
  diagnosticReceiptV2,
1365
1465
  };
1366
1466
  }
1467
+ /**
1468
+ * Final physical generated-SQL boundary.
1469
+ *
1470
+ * This receives the exact prepared statement immediately before the connector
1471
+ * callback. It intentionally validates before invoking `execute`: a bad
1472
+ * capability or unproven prepared reference must result in zero warehouse
1473
+ * calls, not a post-execution warning. It is module-exported only for the
1474
+ * local-runtime boundary harness; it is never an HTTP API or durable artifact.
1475
+ *
1476
+ * @internal
1477
+ */
1478
+ export async function executePreparedAgenticSqlBoundary(input) {
1479
+ const capability = input.capability;
1480
+ if (capability) {
1481
+ const authorization = mintFinalSqlAuthorization({
1482
+ sql: input.preparedSql,
1483
+ proven: capability.provenIdentifiers.map((identifier) => ({
1484
+ identifier,
1485
+ evidence: capability.evidence[identifier] ?? 'catalog',
1486
+ })),
1487
+ runId: capability.runId,
1488
+ executionId: capability.executionId,
1489
+ snapshotId: capability.snapshotId,
1490
+ planId: capability.planId,
1491
+ targetFingerprint: capability.targetFingerprint,
1492
+ bindings: input.bindings,
1493
+ });
1494
+ const validation = validateAuthorizedSqlReferences(input.preparedSql, undefined);
1495
+ const verdict = verifyFinalSql(authorization, input.preparedSql, qualifyAuthorizationReferences(input.preparedSql, {
1496
+ relations: validation.referencedRelations ?? [],
1497
+ columns: validation.referencedColumns ?? [],
1498
+ }), {
1499
+ ...input.scope,
1500
+ bindings: input.bindings,
1501
+ });
1502
+ if (process.env.DQL_ORCHESTRATOR_TRACE) {
1503
+ console.warn(`[dql] execution authorization: ${verdict.ok ? 'admitted' : 'REFUSED'} proven=${authorization.provenIdentifiers.length}${verdict.ok ? '' : ` reason=${verdict.reason}`}`);
1504
+ }
1505
+ if (!verdict.ok) {
1506
+ throw analyticalError(verdict.reason ?? 'The statement was not authorized for execution.', { origin: 'governance_gate', stage: 'execute', code: 'unauthorized_sql' });
1507
+ }
1508
+ }
1509
+ return input.execute();
1510
+ }
1367
1511
  export async function startLocalServer(opts) {
1368
1512
  const { rootDir, executor, connection: rawConnection, preferredPort, projectRoot = process.cwd() } = opts;
1369
1513
  const bindHost = opts.host ?? process.env.DQL_HOST ?? '127.0.0.1';
@@ -2061,6 +2205,9 @@ export async function startLocalServer(opts) {
2061
2205
  });
2062
2206
  };
2063
2207
  async function runGovernedAgentAnswerForRun(request, repair, route = 'generated_answer', onProgress, routeDecision) {
2208
+ return runGovernedAgentAnswerForRunInner(request, repair, route, onProgress, routeDecision);
2209
+ }
2210
+ async function runGovernedAgentAnswerForRunInner(request, repair, route = 'generated_answer', onProgress, routeDecision) {
2064
2211
  const governed = resolveGovernedAnswerRunner(projectRoot);
2065
2212
  let resolvedProvider = governed?.provider ?? null;
2066
2213
  let runner = governed?.runner ?? null;
@@ -2163,6 +2310,9 @@ export async function startLocalServer(opts) {
2163
2310
  snapshotId: runProjectSnapshot.snapshotId,
2164
2311
  })
2165
2312
  : undefined;
2313
+ // Local to this exact answer invocation. Compound children each enter this
2314
+ // function separately, so no child can consume another child's capability.
2315
+ const agenticExecutionCapabilityGate = new AgenticExecutionCapabilityGate();
2166
2316
  await runner.run({
2167
2317
  provider: resolvedProvider,
2168
2318
  ...(agentRunProviderEvidenceContext.getStore()
@@ -2187,9 +2337,13 @@ export async function startLocalServer(opts) {
2187
2337
  },
2188
2338
  reasoningEffort,
2189
2339
  ...(analysisDepth ? { analysisDepth } : {}),
2340
+ orchestrationMode: route === 'research' ? 'research' : 'ask',
2190
2341
  allowProviderSemanticMemberSelection: route === 'research',
2191
2342
  researchResultRowsOptIn: route === 'research' && request.researchResultRowsOptIn === true,
2192
2343
  projectRoot,
2344
+ // Keys the execution authorization, so the proofs the analyst loop
2345
+ // gathers can be checked against the statement this run executes.
2346
+ ...(request.runId ? { agentRunId: request.runId } : {}),
2193
2347
  preparedContextPack: preparedAgentContextPacks.get(request),
2194
2348
  domainContext,
2195
2349
  projectSnapshot: { snapshotId: runProjectSnapshot.snapshotId, manifest: runProjectSnapshot.manifest },
@@ -2357,6 +2511,20 @@ export async function startLocalServer(opts) {
2357
2511
  analyticalReferenceInstant: new Date().toISOString(),
2358
2512
  executeCertifiedBlock: (node, invocation) => executeCertifiedBlockForAgent(node, invocation, semanticConnection, semanticConnectionName),
2359
2513
  executeGeneratedSql: (sql, artifact) => executeGeneratedArtifactForAgent(request.question, sql, artifact, semanticConnection, semanticConnectionName),
2514
+ executeAgenticGeneratedSql: async (capability, sql, artifact) => {
2515
+ if (!agenticExecutionCapabilityGate.consume(capability)) {
2516
+ throw analyticalError('This analyst execution capability was already consumed; DQL did not retry it with stale proof.', {
2517
+ origin: 'governance_gate', stage: 'execute', code: 'unauthorized_sql',
2518
+ });
2519
+ }
2520
+ return executeGeneratedArtifactForAgent(request.question, sql, artifact, semanticConnection, semanticConnectionName, capability, {
2521
+ runId: request.runId,
2522
+ executionId: capability.executionId,
2523
+ snapshotId: runProjectSnapshot.snapshotId,
2524
+ planId: routeDecision?.resolvedAnalyticalPlan?.planId,
2525
+ targetFingerprint: generatedProposalTargetIdentity?.identityFingerprint,
2526
+ });
2527
+ },
2360
2528
  executeDqlArtifact: (artifact) => executeArtifactReferenceForAgent(artifact, request.question, semanticConnection, semanticConnectionName),
2361
2529
  getSchemaContext: (question, preparedContextPack) => getSchemaContextForAgent(question, preparedContextPack, semanticConnection, request.executionTarget?.target === 'connection'
2362
2530
  ? request.executionTarget.connectionName
@@ -2629,6 +2797,7 @@ export async function startLocalServer(opts) {
2629
2797
  const runStartedAtMs = Date.now();
2630
2798
  const turnPlan = buildAnalyticalTurnPlan({
2631
2799
  question: request.question,
2800
+ mode: route === 'research' ? 'research' : 'ask',
2632
2801
  turnId: request.runId,
2633
2802
  candidateIds: routeDecision?.retrievalEvidence?.candidateIds ?? [],
2634
2803
  frozen: routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative',
@@ -2641,18 +2810,27 @@ export async function startLocalServer(opts) {
2641
2810
  // is copied into several labels. Independent children share the parent's
2642
2811
  // signal/deadline and return truthful partial success.
2643
2812
  if (turnPlan.tasks.length > 1 && !childTurn && (attempt ?? 0) === 0) {
2644
- const childResults = await Promise.all(turnPlan.tasks.slice(0, 6).map(async (task) => {
2813
+ const runChildTask = async (task, dependencyBinding) => {
2645
2814
  if (request.signal?.aborted)
2646
2815
  rethrowIfCancelled(request.signal.reason, request.signal);
2647
2816
  try {
2648
2817
  const childRequest = {
2649
2818
  ...request,
2650
2819
  question: task.question,
2820
+ ...(dependencyBinding ? {
2821
+ conversationContext: {
2822
+ ...(request.conversationContext ?? {}),
2823
+ // This is the complete parent-to-child data boundary: no
2824
+ // parent prose, SQL, or rows cross into a dependent clause.
2825
+ analyticalTaskDependencyBinding: dependencyBinding,
2826
+ },
2827
+ } : {}),
2651
2828
  workspaceContext: {
2652
2829
  ...(request.workspaceContext && typeof request.workspaceContext === 'object' ? request.workspaceContext : {}),
2653
2830
  analyticalTaskChild: true,
2654
2831
  analyticalParentRunId: request.runId,
2655
2832
  analyticalTaskId: task.id,
2833
+ ...(dependencyBinding ? { analyticalTaskDependencyBinding: dependencyBinding } : {}),
2656
2834
  },
2657
2835
  };
2658
2836
  const answer = await runGovernedAgentAnswerForRun(childRequest, { attempt: 0, repairHint }, route, (message) => emit({ type: 'executor.started', message: `Task ${task.id}: ${message}`, route }), undefined);
@@ -2679,6 +2857,42 @@ export async function startLocalServer(opts) {
2679
2857
  rethrowIfCancelled(error, request.signal);
2680
2858
  return { task, error: error instanceof Error ? error.message : String(error) };
2681
2859
  }
2860
+ };
2861
+ const scheduledChildren = await scheduleCompoundAnalyticalTasks({
2862
+ tasks: turnPlan.tasks.slice(0, 6),
2863
+ runTask: async (task, binding) => {
2864
+ const child = await runChildTask(task, binding);
2865
+ return { task, value: child.answer, error: child.error };
2866
+ },
2867
+ resolveDependency: (task, parent) => {
2868
+ const sourceTaskId = task.dependency?.sourceTaskId ?? '';
2869
+ return resolveTopRankedRegionDependency(sourceTaskId, parent?.value?.result
2870
+ ? normalizeCanonicalQueryResult({
2871
+ ...parent.value.result,
2872
+ resultFingerprint: parent.value.result.resultFingerprint ?? parent.value.result.executionReceipt?.resultFingerprint,
2873
+ executionReceipt: parent.value.result.executionReceipt,
2874
+ answerTier: parent.value.route?.tier ?? parent.value.sourceTier,
2875
+ })
2876
+ : undefined, parent?.task);
2877
+ },
2878
+ });
2879
+ const childResults = scheduledChildren.map(({ task, value, error, dependencyError }) => ({
2880
+ task,
2881
+ answer: value,
2882
+ error,
2883
+ ...(dependencyError ? {
2884
+ dependencyGap: buildCoverageGap({
2885
+ code: dependencyError.code,
2886
+ phase: 'planning',
2887
+ message: dependencyError.message,
2888
+ searchedSources: routeDecision?.retrievalEvidence?.candidateIds ?? [],
2889
+ attemptedRoutes: ['certified', 'semantic', 'governed_relational', 'generated'],
2890
+ missing: ['unambiguous_top_region'],
2891
+ recoverable: true,
2892
+ planFrozen: turnPlan.frozen,
2893
+ nextActions: ['Ask for a single top region or review the parent result before retrying the customer task.'],
2894
+ }),
2895
+ } : {}),
2682
2896
  }));
2683
2897
  const outcomes = childResults.map(({ task, answer, error }) => ({
2684
2898
  version: 1,
@@ -2687,7 +2901,7 @@ export async function startLocalServer(opts) {
2687
2901
  ...(answer?.answer || answer?.text ? { summary: answer.answer ?? answer.text } : {}),
2688
2902
  ...(answer?.result?.resultFingerprint ? { resultFingerprint: answer.result.resultFingerprint } : {}),
2689
2903
  ...(error || answer?.kind === 'no_answer' ? {
2690
- gap: buildCoverageGap({
2904
+ gap: childResults.find((candidate) => candidate.task.id === task.id)?.dependencyGap ?? buildCoverageGap({
2691
2905
  code: answer?.refusalCode === 'ambiguous' ? 'AMBIGUOUS_MEANING' : 'EXECUTION_FAILED',
2692
2906
  phase: answer?.executionError ? 'execution' : 'meaning',
2693
2907
  message: error ?? answer?.answer ?? answer?.text ?? 'The task did not produce an accepted analytical result.',
@@ -2706,14 +2920,23 @@ export async function startLocalServer(opts) {
2706
2920
  status: outcomes.find((outcome) => outcome.taskId === task.id)?.status === 'completed' ? 'completed' : 'gap',
2707
2921
  }));
2708
2922
  const answerText = childResults.map(({ task, answer, error }) => `${task.question}: ${error ?? answer?.answer ?? answer?.text ?? 'No accepted result was produced.'}`).join('\n\n');
2923
+ const compoundTrust = compoundTrustState(childResults
2924
+ .filter(({ answer, error }) => !error && answer && answer.kind !== 'no_answer')
2925
+ .map(({ answer }) => trustStateForAgentAnswer(answer)));
2709
2926
  return {
2710
2927
  summary: completedCount === outcomes.length
2711
- ? `Answered ${completedCount} independent analytical clauses.`
2928
+ ? `Answered ${completedCount} analytical clauses.`
2712
2929
  : `Answered ${completedCount} of ${outcomes.length} analytical clauses; the remaining clauses need review.`,
2713
2930
  answer: answerText,
2714
2931
  status: completedCount === outcomes.length ? 'completed' : completedCount > 0 ? 'needs_review' : 'needs_clarification',
2715
- trustState: completedCount === outcomes.length ? 'governed' : 'review_required',
2716
- stopReason: completedCount === outcomes.length ? 'governed_semantic_answer' : 'human_review_required',
2932
+ // The parent is only as trustworthy as its weakest SUCCESSFUL child.
2933
+ // Completion is not proof: the previous rule stamped `governed` on a
2934
+ // parent assembled from review-required generated SQL.
2935
+ trustState: compoundTrust,
2936
+ // The stop reason has to agree with the trust it reports. Claiming a
2937
+ // governed semantic answer over generated/certified children
2938
+ // misrepresents provenance as much as the trust label does.
2939
+ stopReason: compoundStopReason(completedCount, outcomes.length, compoundTrust),
2717
2940
  artifacts: childResults.map(({ task, answer, error }) => agentRunArtifact('answer', `Task: ${task.question}`, {
2718
2941
  taskId: task.id,
2719
2942
  question: task.question,
@@ -3062,7 +3285,34 @@ export async function startLocalServer(opts) {
3062
3285
  ? { ...preview, rows: redactProviderResultRows(preview.rows, narrationMaxRows) }
3063
3286
  : undefined;
3064
3287
  let narrationSource;
3065
- let narrationValidationFailures = [];
3288
+ // The receipt is the durable source of truth for evaluation and inspector
3289
+ // display. Do not reconstruct this later from rows or the reader-facing
3290
+ // fallback sentence: both are presentation artifacts, not evidence that a
3291
+ // fact-grounded narrator actually ran.
3292
+ const verifiedFactNarration = narrationPlan.mode === 'verified_facts'
3293
+ && Boolean(governedAnswer.analyticalFacts && governedAnswer.resolvedAnalyticalPlan?.analyticalFrame);
3294
+ let narrationIntegrityReceipt = narrationPlan.mode === 'skip'
3295
+ ? {
3296
+ version: 1,
3297
+ mode: 'skip',
3298
+ outcome: 'skipped',
3299
+ attempted: false,
3300
+ factCount: 0,
3301
+ maxRows: 0,
3302
+ validationFailures: [],
3303
+ skipReason: narrationPlan.reason,
3304
+ }
3305
+ : {
3306
+ version: 1,
3307
+ mode: verifiedFactNarration ? 'verified_facts' : 'preview_grounded',
3308
+ // This is deliberately pessimistic until a narration outcome is
3309
+ // observed, so an exception cannot be persisted as a silent skip.
3310
+ outcome: 'error',
3311
+ attempted: true,
3312
+ factCount: verifiedFactNarration ? governedAnswer.analyticalFacts?.facts.length ?? 0 : 0,
3313
+ maxRows: narrationPlan.maxRows,
3314
+ validationFailures: [],
3315
+ };
3066
3316
  if (narrationPlan.mode !== 'skip' && narrationProvider) {
3067
3317
  const narrationStartedAtMs = Date.now();
3068
3318
  const draft = governedAnswer.answer ?? governedAnswer.text;
@@ -3075,8 +3325,16 @@ export async function startLocalServer(opts) {
3075
3325
  columnCount: providerPreview?.columns.length ?? 0,
3076
3326
  },
3077
3327
  });
3078
- const narrationDispatchOptions = () => ({
3079
- maxTokens: 350,
3328
+ const narrationDispatchOptions = (factCount = 0) => ({
3329
+ // The ceiling has to grow with the result. Every claim must echo the
3330
+ // fact ids it rests on, and a fact id is a long hex string that
3331
+ // tokenizes badly — ten of them consume most of the budget before a
3332
+ // word of prose is written. At a flat 350 a ten-row answer was
3333
+ // truncated mid-sentence ("...Elizabeth Shea (875), and Dyl"), so the
3334
+ // JSON never closed, BOTH attempts failed as UNPARSEABLE_CLAIMS, and
3335
+ // the reader got "Verified narration was unavailable" above a robot
3336
+ // dump of the very rows the model had just described correctly.
3337
+ maxTokens: narrationMaxTokensForFacts(factCount),
3080
3338
  temperature: 0.3,
3081
3339
  maxProviderDispatches: 2,
3082
3340
  ...(agentRunProviderEvidenceContext.getStore()
@@ -3093,10 +3351,19 @@ export async function startLocalServer(opts) {
3093
3351
  factSet: governedAnswer.analyticalFacts,
3094
3352
  question: request.question,
3095
3353
  maxRows: narrationPlan.maxRows,
3096
- complete: async ({ system, user }) => streamOrGenerate(narrationProvider, [{ role: 'system', content: system }, { role: 'user', content: user }], narrationDispatchOptions(), () => { }),
3354
+ complete: async ({ system, user }) => streamOrGenerate(narrationProvider, [{ role: 'system', content: system }, { role: 'user', content: user }], narrationDispatchOptions(governedAnswer.analyticalFacts?.facts.length ?? 0), () => { }),
3097
3355
  });
3098
3356
  narrationSource = composed.source;
3099
- narrationValidationFailures = composed.validationFailures;
3357
+ narrationIntegrityReceipt = {
3358
+ ...narrationIntegrityReceipt,
3359
+ outcome: composed.source === 'llm' ? 'success' : 'deterministic_fallback',
3360
+ validationFailures: narrationIntegrityFailureCodes(composed.validationFailures),
3361
+ };
3362
+ if (process.env.DQL_ORCHESTRATOR_TRACE) {
3363
+ console.warn(`[dql] narration: source=${composed.source}${narrationIntegrityReceipt.validationFailures.length > 0
3364
+ ? ` rejected=${narrationIntegrityReceipt.validationFailures.join(',')}`
3365
+ : ''}`);
3366
+ }
3100
3367
  synthesizedAnswer = composed.source === 'llm'
3101
3368
  ? composed.narrative.text
3102
3369
  // A verification failure is not silent: the deterministic join is a
@@ -3126,11 +3393,22 @@ export async function startLocalServer(opts) {
3126
3393
  narrationSource = result.source;
3127
3394
  if (result.text)
3128
3395
  synthesizedAnswer = result.text;
3396
+ narrationIntegrityReceipt = {
3397
+ ...narrationIntegrityReceipt,
3398
+ outcome: result.source === 'llm' ? 'success' : 'deterministic_fallback',
3399
+ validationFailures: [],
3400
+ };
3129
3401
  }
3130
3402
  }
3131
3403
  catch {
3132
3404
  // Keep the governed draft on any narration failure.
3133
3405
  synthesizedAnswer = undefined;
3406
+ narrationIntegrityReceipt = {
3407
+ ...narrationIntegrityReceipt,
3408
+ outcome: 'error',
3409
+ validationFailures: [],
3410
+ errorCode: 'narration_error',
3411
+ };
3134
3412
  }
3135
3413
  finally {
3136
3414
  narrationDurationMs = Date.now() - narrationStartedAtMs;
@@ -3270,7 +3548,11 @@ export async function startLocalServer(opts) {
3270
3548
  trustState,
3271
3549
  stopReason,
3272
3550
  artifacts: isTerminalFailure
3273
- ? [agentRunArtifact('answer', 'Failed governed analytical run', governedAnswer, governedAnswer.sourceCertifiedBlock ?? governedAnswer.block?.name, 'blocked')]
3551
+ ? [agentRunArtifact('answer',
3552
+ // Was 'Failed governed analytical run' — internal orchestration state
3553
+ // used as the card heading a user reads. It names our pipeline, not
3554
+ // what happened to their question.
3555
+ terminalFailureTitle(governedAnswer), governedAnswer, governedAnswer.sourceCertifiedBlock ?? governedAnswer.block?.name, 'blocked')]
3274
3556
  : governedAnswer.kind === 'no_answer'
3275
3557
  // A refusal still keeps the DQL draft the answer loop produced (when any),
3276
3558
  // so the "Review DQL draft" next-action isn't a dead link and the user can
@@ -3367,20 +3649,38 @@ export async function startLocalServer(opts) {
3367
3649
  ...(governedAnswer.executionError ? [
3368
3650
  agentRunEvaluation('execution-error', 'Execution error', false, 'warning', governedAnswer.executionError),
3369
3651
  ] : []),
3370
- // A rejected narration must say WHY it was rejected. The failures were
3371
- // computed and then dropped, so a reader saw "Verified narration was
3372
- // unavailable" with no way to tell whether the model invented a number,
3373
- // cited a fact id that does not exist, or the provider simply failed.
3374
- // The fallback itself stays: this only makes its reason inspectable.
3375
- ...(narrationSource === 'deterministic' && narrationValidationFailures.length > 0 ? [
3376
- agentRunEvaluation('narration-verification', 'Narration verification', false, 'warning', `The drafted narration was rejected against the result fact set, so the deterministic record was shown instead: ${narrationValidationFailures.join('; ')}`, { narrationSource, validationFailures: narrationValidationFailures }),
3652
+ // Keep only the content-free receipt codes in the durable inspection
3653
+ // record. Raw verifier prose can contain a result value, prompt excerpt,
3654
+ // or provider error and is not safe evidence to surface or persist.
3655
+ ...(narrationIntegrityReceipt.outcome === 'deterministic_fallback'
3656
+ && narrationIntegrityReceipt.validationFailures.length > 0 ? [
3657
+ agentRunEvaluation('narration-verification', 'Narration verification', false, 'warning', `The drafted narration was rejected against the result fact set, so the deterministic record was shown instead: ${narrationIntegrityReceipt.validationFailures.join(', ')}.`, { narrationSource, validationFailures: narrationIntegrityReceipt.validationFailures }),
3377
3658
  ] : []),
3378
3659
  ],
3379
3660
  nextActions,
3380
3661
  providerEgressReceipts: finalProviderEgressReceipts,
3381
3662
  telemetry: finalTelemetry,
3663
+ narrationIntegrityReceipt,
3382
3664
  };
3383
3665
  };
3666
+ /**
3667
+ * A heading for a run that ended without an answer, in the user's terms.
3668
+ *
3669
+ * Says WHICH stage stopped, because "it failed" and "it was stopped before
3670
+ * running" call for different next moves: one is worth retrying, the other
3671
+ * needs the question or the model changed.
3672
+ */
3673
+ const terminalFailureTitle = (answer) => {
3674
+ switch (answer.refusalCode) {
3675
+ case 'policy_blocked': return 'Blocked by a governance policy';
3676
+ case 'modeling_gap': return 'Not modeled yet';
3677
+ case 'grounding_gap': return 'Not enough context to answer safely';
3678
+ case 'model_declined': return 'The assistant declined to answer';
3679
+ case 'provider_error': return 'The AI provider did not respond';
3680
+ case 'ambiguous': return 'Needs one detail before running';
3681
+ default: return 'No answer was produced';
3682
+ }
3683
+ };
3384
3684
  const conversationRunExecutor = async ({ request, routeDecision, emitAnswerDelta }) => {
3385
3685
  const kind = routeDecision?.conversationalKind ?? 'smalltalk';
3386
3686
  const isGeneralKnowledge = routeDecision?.category === 'general_knowledge';
@@ -3397,6 +3697,15 @@ export async function startLocalServer(opts) {
3397
3697
  let text = kind === 'answer_explanation'
3398
3698
  ? buildPriorAnswerExplanation(request.question, request.conversationContext)
3399
3699
  : undefined;
3700
+ // A definitional question that NAMES a governed artifact is answerable from
3701
+ // the catalog: the description, domain, and dimensions are already recorded.
3702
+ // Reaching for a provider to paraphrase facts we hold can only add drift, and
3703
+ // the generic conversational reply this replaces used none of them.
3704
+ //
3705
+ // Returns undefined unless the question names something real, so a turn that
3706
+ // does not match keeps today's behaviour exactly.
3707
+ if (!text)
3708
+ text = buildGovernedObjectExplanation(request.question);
3400
3709
  if (text) {
3401
3710
  emitAnswerDelta?.(text);
3402
3711
  }
@@ -3863,10 +4172,18 @@ export async function startLocalServer(opts) {
3863
4172
  const conversationHistory = request.history?.length
3864
4173
  ? request.history
3865
4174
  : conversationHistoryFromContext(request.conversationContext);
4175
+ // The provider that will plan the investigation as hypotheses. Absent or
4176
+ // unreachable, `planResearch` keeps its deterministic template, so
4177
+ // research never depends on a model being available.
4178
+ const researchPlanner = resolveGovernedAnswerRunner(projectRoot);
4179
+ const researchPlannerProvider = researchPlanner
4180
+ ? createGovernedTextProvider(researchPlanner.provider, projectRoot)
4181
+ : undefined;
3866
4182
  const plan = await planResearch({
3867
4183
  question: request.question,
3868
4184
  metrics,
3869
4185
  blocks,
4186
+ ...(researchPlannerProvider ? { provider: researchPlannerProvider } : {}),
3870
4187
  intent: request.intent,
3871
4188
  isFollowUp: conversationHistory.length > 0,
3872
4189
  history: conversationHistory,
@@ -3949,10 +4266,32 @@ export async function startLocalServer(opts) {
3949
4266
  expectation: 'Whether the frozen context contains enough evidence for a bounded answer.',
3950
4267
  };
3951
4268
  const branches = capResearchBranches(plan.steps.length > 0 ? plan.steps : [fallbackBranch], 6);
4269
+ // The replan edge. Each branch tests one hypothesis; folding its
4270
+ // outcome back into the state is what lets the investigation stop
4271
+ // when the question is settled instead of grinding through a plan
4272
+ // frozen before any observation. `nextHypothesis` returning
4273
+ // undefined is how the loop learns to stop — it enforces the hop
4274
+ // budget and reports when nothing is open.
4275
+ let researchState = createResearchState(request.question, branches.map((branch, position) => ({
4276
+ id: `h${position + 1}`,
4277
+ statement: branch.thought,
4278
+ priorConfidence: 1 - position / (branches.length + 1),
4279
+ })));
3952
4280
  for (let index = 0; index < branches.length; index += 1) {
3953
4281
  const step = branches[index];
3954
4282
  if (request.signal?.aborted)
3955
4283
  rethrowIfCancelled(request.signal.reason, request.signal);
4284
+ // A hypothesis an earlier finding already closed is not
4285
+ // re-investigated, and an exhausted hop budget stops the run.
4286
+ const stillOpen = nextHypothesis(researchState);
4287
+ if (!stillOpen) {
4288
+ emit({
4289
+ type: 'executor.started',
4290
+ message: `Stopping early: ${researchState.hopsUsed} of ${branches.length} branches settled what could be settled.`,
4291
+ route: 'research',
4292
+ });
4293
+ break;
4294
+ }
3956
4295
  const branchId = `${step.action.kind}:${step.action.target}`;
3957
4296
  const branchQuestion = `${request.question}\nResearch branch ${index + 1} (${branchId}): ${step.expectation}`;
3958
4297
  const childId = `${created.id}:research:${index + 1}`;
@@ -4007,7 +4346,27 @@ export async function startLocalServer(opts) {
4007
4346
  baselineDqlArtifact: researchSource?.dqlArtifact,
4008
4347
  baselineRunId: agentRunString(researchSource?.runId),
4009
4348
  });
4010
- researchRuns.push(withNotebookResearchChecklist(executed));
4349
+ const branchRun = withNotebookResearchChecklist(executed);
4350
+ researchRuns.push(branchRun);
4351
+ // Observe, then decide. A branch that produced rows is evidence
4352
+ // for its hypothesis; one that did not is inconclusive, which is
4353
+ // a real outcome and not a failure.
4354
+ // Rows are not support. A branch that returned data has been
4355
+ // OBSERVED, not confirmed — deciding whether the observation
4356
+ // matches what the hypothesis predicted needs the expectation,
4357
+ // and nothing available at this layer can judge it. Recording
4358
+ // rows as `supports` would let the dossier report a driver the
4359
+ // evidence never established, which is the failure mode the
4360
+ // whole verified-fact chain exists to prevent.
4361
+ researchState = applyFinding(researchState, {
4362
+ id: `f${index + 1}`,
4363
+ hypothesisId: `h${index + 1}`,
4364
+ verdict: 'inconclusive',
4365
+ summary: branchRun.summary ?? '',
4366
+ strength: (branchRun.resultPreview?.rows?.length ?? 0) > 0
4367
+ ? 0.5
4368
+ : 0.1,
4369
+ });
4011
4370
  }
4012
4371
  catch (error) {
4013
4372
  // A child is a real durable run even when cancellation stops the
@@ -4023,6 +4382,13 @@ export async function startLocalServer(opts) {
4023
4382
  const stopped = storage.getRun(child.id);
4024
4383
  if (stopped)
4025
4384
  researchRuns.push(withNotebookResearchChecklist(stopped));
4385
+ researchState = applyFinding(researchState, {
4386
+ id: `f${index + 1}`,
4387
+ hypothesisId: `h${index + 1}`,
4388
+ verdict: 'inconclusive',
4389
+ summary: message,
4390
+ strength: 0,
4391
+ });
4026
4392
  rethrowIfCancelled(error, request.signal);
4027
4393
  }
4028
4394
  }
@@ -4121,20 +4487,40 @@ export async function startLocalServer(opts) {
4121
4487
  reviewRequired: true,
4122
4488
  }, request.researchResultRowsOptIn === true)
4123
4489
  : undefined;
4490
+ // The cross-branch story. Every branch tested a hypothesis and produced a
4491
+ // finding; narrating only the one result the executor happened to carry
4492
+ // reported a single fact and discarded the rest, which is the visible
4493
+ // half of "research answers one question instead of telling a story".
4494
+ const researchStory = !needsClarification && plan.steps.length > 0
4495
+ ? synthesizeResearchNarrative({
4496
+ question: request.question,
4497
+ branches: researchRuns.map((branch, index) => ({
4498
+ statement: plan.steps[index]?.thought ?? branch.question ?? '',
4499
+ produced: branch.status === 'ready'
4500
+ && (branch.resultPreview?.rows?.length ?? 0) > 0,
4501
+ ...(branch.summary ? { summary: branch.summary } : {}),
4502
+ ...(branch.status ? { status: branch.status } : {}),
4503
+ })),
4504
+ })
4505
+ : undefined;
4124
4506
  const summary = needsClarification
4125
4507
  ? 'Needs clarification before running deeper research.'
4126
- : narration?.summary
4127
- ?? (researchZeroRows
4128
- ? 'The query executed cleanly against real data and matched 0 rows.'
4129
- : researchRun?.status === 'ready'
4130
- ? 'Saved a grounded research dossier with context evidence and next review actions.'
4131
- : researchRun?.status === 'error'
4132
- ? 'Saved a research dossier, but the preview needs review before promotion.'
4133
- : researchWorkspaceError
4134
- ? 'Prepared a grounded research plan; durable research storage is unavailable in this runtime.'
4135
- : plan.done
4136
- ? 'Prepared a direct grounded-answer plan.'
4137
- : 'Prepared a grounded research plan over real DQL assets.');
4508
+ // The story leads; the verified-fact narration follows it, so the
4509
+ // numbers still come from the narrator that checks them.
4510
+ : researchStory
4511
+ ? `${researchStory}${narration?.summary ? `\n\n${narration.summary}` : ''}`
4512
+ : narration?.summary
4513
+ ?? (researchZeroRows
4514
+ ? 'The query executed cleanly against real data and matched 0 rows.'
4515
+ : researchRun?.status === 'ready'
4516
+ ? 'Saved a grounded research dossier with context evidence and next review actions.'
4517
+ : researchRun?.status === 'error'
4518
+ ? 'Saved a research dossier, but the preview needs review before promotion.'
4519
+ : researchWorkspaceError
4520
+ ? 'Prepared a grounded research plan; durable research storage is unavailable in this runtime.'
4521
+ : plan.done
4522
+ ? 'Prepared a direct grounded-answer plan.'
4523
+ : 'Prepared a grounded research plan over real DQL assets.');
4138
4524
  return {
4139
4525
  summary,
4140
4526
  answer: plan.followUp?.question ?? narration?.summary
@@ -4482,6 +4868,22 @@ export async function startLocalServer(opts) {
4482
4868
  // lookup, and governed execution for the lifetime of a request. This removes
4483
4869
  // both positional catalog truncation and the previous duplicate retrieval pass.
4484
4870
  const preparedAgentContextPacks = new WeakMap();
4871
+ /**
4872
+ * Cross-encoder pass over the fused candidates, when a provider is available.
4873
+ * Advisory throughout: it may only reorder ids retrieval returned, and any
4874
+ * failure leaves retrieval's own ordering in place.
4875
+ */
4876
+ const agentRerankCandidates = (() => {
4877
+ const governed = resolveGovernedAnswerRunner(projectRoot);
4878
+ const provider = governed
4879
+ ? createGovernedTextProvider(governed.provider, projectRoot)
4880
+ : undefined;
4881
+ if (!provider)
4882
+ return undefined;
4883
+ return (question, candidates) => rerankCandidates(provider, question, candidates, {
4884
+ timeoutMs: Math.round(2_500 * deadlineScale()),
4885
+ });
4886
+ })();
4485
4887
  const pendingAgentContextPacks = new WeakMap();
4486
4888
  const buildAgentRunContextPack = async (request) => {
4487
4889
  const prepared = preparedAgentContextPacks.get(request);
@@ -4549,6 +4951,10 @@ export async function startLocalServer(opts) {
4549
4951
  },
4550
4952
  strictness: request.analysisDepth === 'deep' ? 'exploratory' : 'balanced',
4551
4953
  limit: request.analysisDepth === 'deep' ? 120 : 80,
4954
+ // The runtime PRE-BUILDS this pack, so wiring the reranker only at the
4955
+ // provider's own `buildLocalContextPack` left it unreachable on the
4956
+ // common path — the prepared pack is used and that call never happens.
4957
+ ...(agentRerankCandidates ? { rerankCandidates: agentRerankCandidates } : {}),
4552
4958
  domainContext: requestedDomain
4553
4959
  ? resolveUiDomainContext({
4554
4960
  manifest: snapshot.manifest,
@@ -4604,6 +5010,56 @@ export async function startLocalServer(opts) {
4604
5010
  };
4605
5011
  // Compact fallback used only for plain conversational replies. Analytical
4606
5012
  // turns use the structured, question-ranked evidence path above.
5013
+ /**
5014
+ * Explain a governed artifact the question names, from catalog metadata alone.
5015
+ *
5016
+ * Certified blocks are offered first: when a concept exists both as a
5017
+ * certified block and a raw model, the certified one is the authored
5018
+ * definition and the other is an implementation detail.
5019
+ */
5020
+ const buildGovernedObjectExplanation = (question) => {
5021
+ try {
5022
+ const blocks = collectPlanBlocks(projectRoot, { certifiedOnly: true });
5023
+ const certifiedNames = new Set(blocks.map((block) => block.name));
5024
+ const all = [
5025
+ ...blocks.map((block) => ({ block, status: 'certified' })),
5026
+ ...collectPlanBlocks(projectRoot, { certifiedOnly: false })
5027
+ .filter((block) => !certifiedNames.has(block.name))
5028
+ .map((block) => ({ block, status: 'draft' })),
5029
+ ];
5030
+ // Metrics as well as blocks. "How is revenue defined here?" names a
5031
+ // semantic metric, not a block, and answering it from the metric's own
5032
+ // description is the whole point of holding one.
5033
+ const metricObjects = loadSemanticMetrics(projectRoot).map((metric) => ({
5034
+ objectKey: `semantic:metric:${metric.name}`,
5035
+ objectType: 'semantic_metric',
5036
+ name: metric.name,
5037
+ ...(metric.description ? { description: metric.description } : {}),
5038
+ ...(metric.domain ? { domain: metric.domain } : {}),
5039
+ status: 'governed',
5040
+ payload: {},
5041
+ }));
5042
+ const explanation = composeBusinessExplanation(question, [
5043
+ ...all.map(({ block, status }) => ({
5044
+ objectKey: `dql:block:${block.name}`,
5045
+ objectType: 'dql_block',
5046
+ name: block.name,
5047
+ ...(block.description ? { description: block.description } : {}),
5048
+ ...(block.domain ? { domain: block.domain } : {}),
5049
+ status,
5050
+ payload: {
5051
+ ...(block.dimensions?.length ? { dimensions: block.dimensions } : {}),
5052
+ },
5053
+ })),
5054
+ ...metricObjects,
5055
+ ]);
5056
+ return explanation?.text;
5057
+ }
5058
+ catch {
5059
+ // Never let an explanation attempt break a conversational turn.
5060
+ return undefined;
5061
+ }
5062
+ };
4607
5063
  const buildAgentRunCatalogContext = () => {
4608
5064
  try {
4609
5065
  const blocks = collectPlanBlocks(projectRoot, { certifiedOnly: true });
@@ -5155,12 +5611,43 @@ export async function startLocalServer(opts) {
5155
5611
  * string, so the only remaining reasons to differ are the connection and the
5156
5612
  * governance gates, both of which report themselves honestly.
5157
5613
  */
5158
- const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection, bindings, executionConnectionName) => {
5614
+ const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection, bindings, executionConnectionName, agenticCapability, agenticScope) => {
5159
5615
  const activeConnection = requireActiveConnection(executionConnection);
5160
5616
  const rowBound = clampAnalyticalRowBound(seed?.limit ?? 200);
5161
5617
  const trimmed = sql.trim().replace(/;\s*$/, '').trim();
5162
5618
  if (!trimmed)
5163
5619
  throw analyticalError('The generated SQL was empty.', { origin: 'host', stage: 'execute' });
5620
+ const bindingValue = {
5621
+ sqlParams: bindings?.sqlParams ?? [],
5622
+ variables: bindings?.variables ?? {},
5623
+ };
5624
+ if (agenticCapability) {
5625
+ // The proposal itself is immutable. A changed literal, comment, or
5626
+ // whitespace is drift here rather than a benign formatting change.
5627
+ const currentSnapshotId = projectSnapshot().snapshotId;
5628
+ let targetFingerprint;
5629
+ if (agenticCapability.targetFingerprint) {
5630
+ try {
5631
+ targetFingerprint = (await observeWarehouseTargetIdentity(executor, activeConnection)).identityFingerprint;
5632
+ }
5633
+ catch {
5634
+ throw analyticalError('DQL could not re-confirm the selected execution target, so the analyst-approved query was not run.', {
5635
+ origin: 'governance_gate', stage: 'execute', code: 'unauthorized_sql',
5636
+ });
5637
+ }
5638
+ }
5639
+ const capabilityVerdict = verifyAgenticSqlExecutionCapability(agenticCapability, sql, {
5640
+ ...agenticScope,
5641
+ bindings: bindingValue,
5642
+ snapshotId: currentSnapshotId,
5643
+ ...(targetFingerprint ? { targetFingerprint } : {}),
5644
+ });
5645
+ if (!capabilityVerdict.ok) {
5646
+ throw analyticalError(capabilityVerdict.reason ?? 'The analyst-approved SQL no longer matches this execution.', {
5647
+ origin: 'governance_gate', stage: 'execute', code: 'unauthorized_sql',
5648
+ });
5649
+ }
5650
+ }
5164
5651
  // RESOLVE FIRST, VALIDATE SECOND. Executing verbatim means executing the
5165
5652
  // statement the notebook would execute — and the notebook resolves
5166
5653
  // `@metric()` / `@dim()` refs and dbt macros before it runs anything.
@@ -5185,8 +5672,6 @@ export async function startLocalServer(opts) {
5185
5672
  // release denied; a governance boundary must not move as a side effect.
5186
5673
  const sourceDomain = seed?.source?.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1] ?? 'uncategorized';
5187
5674
  assertAppAccess({ app, domain: sourceDomain, level: 'execute' });
5188
- // A semantic query the loop already compiled (MetricFlow / dbt Cloud) must
5189
- // execute through its pinned target binding, not as loose SQL.
5190
5675
  const semanticExecutionHolder = { value: null };
5191
5676
  const execution = await analyticalExecutionService.execute({
5192
5677
  sql: semantic.sql,
@@ -5198,27 +5683,46 @@ export async function startLocalServer(opts) {
5198
5683
  variables: bindings?.variables,
5199
5684
  semanticRefs: semantic.semanticRefs,
5200
5685
  executePrepared: async (preparation) => {
5201
- const pinnedSemanticCompile = compiledSemanticQueries.get(executionFingerprint(preparation.preparedSql))
5202
- ?? compiledSemanticQueries.get(executionFingerprint(trimmed));
5203
- if (pinnedSemanticCompile) {
5204
- const semanticExecution = await executeTargetBoundSemanticQuery({
5205
- executor,
5206
- connection: activeConnection,
5207
- projectRoot,
5208
- plannedAdapter: pinnedSemanticCompile.engine,
5209
- metricFlow: pinnedSemanticCompile.engine === 'metricflow-cli'
5210
- ? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
5211
- : undefined,
5212
- compile: async () => pinnedSemanticCompile,
5213
- prepareSql: () => ({ sql: preparation.executedSql, connection: preparation.connection }),
5214
- rowBound,
5215
- });
5216
- if (semanticExecution) {
5217
- semanticExecutionHolder.value = semanticExecution;
5218
- return semanticExecution.result;
5686
+ // Both the native executor and the target-bound semantic adapter can
5687
+ // reach the warehouse from this callback. Admit the exact prepared
5688
+ // bytes before either path so a matching cached semantic compile cannot
5689
+ // bypass the generated proposal's capability.
5690
+ return executePreparedAgenticSqlBoundary({
5691
+ capability: agenticCapability,
5692
+ preparedSql: preparation.executedSql,
5693
+ bindings: bindingValue,
5694
+ scope: {
5695
+ ...agenticScope,
5696
+ snapshotId: projectSnapshot().snapshotId,
5697
+ ...(agenticCapability ? { targetFingerprint: agenticCapability.targetFingerprint } : {}),
5698
+ },
5699
+ execute: async () => {
5700
+ // A semantic query the loop already compiled (MetricFlow / dbt
5701
+ // Cloud) must execute through its pinned target binding, not as
5702
+ // loose SQL.
5703
+ const pinnedSemanticCompile = compiledSemanticQueries.get(executionFingerprint(preparation.preparedSql))
5704
+ ?? compiledSemanticQueries.get(executionFingerprint(trimmed));
5705
+ if (pinnedSemanticCompile) {
5706
+ const semanticExecution = await executeTargetBoundSemanticQuery({
5707
+ executor,
5708
+ connection: activeConnection,
5709
+ projectRoot,
5710
+ plannedAdapter: pinnedSemanticCompile.engine,
5711
+ metricFlow: pinnedSemanticCompile.engine === 'metricflow-cli'
5712
+ ? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
5713
+ : undefined,
5714
+ compile: async () => pinnedSemanticCompile,
5715
+ prepareSql: () => ({ sql: preparation.executedSql, connection: preparation.connection }),
5716
+ rowBound,
5717
+ });
5718
+ if (semanticExecution) {
5719
+ semanticExecutionHolder.value = semanticExecution;
5720
+ return semanticExecution.result;
5721
+ }
5722
+ }
5723
+ return executor.executeQuery(preparation.executedSql, bindings?.sqlParams ?? [], runtimeVariables(bindings?.variables ?? {}), preparation.connection);
5219
5724
  }
5220
- }
5221
- return executor.executeQuery(preparation.executedSql, bindings?.sqlParams ?? [], runtimeVariables(bindings?.variables ?? {}), preparation.connection);
5725
+ });
5222
5726
  },
5223
5727
  });
5224
5728
  const semanticExecution = semanticExecutionHolder.value;
@@ -5276,14 +5780,19 @@ export async function startLocalServer(opts) {
5276
5780
  executableArtifact,
5277
5781
  };
5278
5782
  };
5279
- const executeGeneratedArtifactForAgent = async (question, sql, seed, executionConnection, executionConnectionName) => {
5783
+ const executeGeneratedArtifactForAgent = async (question, sql, seed, executionConnection, executionConnectionName, agenticCapability, agenticScope) => {
5280
5784
  // A seed that is already a certified/saved artifact keeps the DQL-first
5281
5785
  // path: there the `.dql` source IS the contract, and its parameters and
5282
5786
  // semantic refs must be compiled, not bypassed.
5283
5787
  if (seed && seed.kind !== 'sql_block') {
5788
+ if (agenticCapability) {
5789
+ throw analyticalError('The analyst-approved SQL cannot be redirected through a saved artifact.', {
5790
+ origin: 'governance_gate', stage: 'execute', code: 'unauthorized_sql',
5791
+ });
5792
+ }
5284
5793
  return executeArtifactReferenceForAgent({ ...seed, limit: seed.limit ?? 200 }, question, executionConnection, executionConnectionName);
5285
5794
  }
5286
- return executeGeneratedSqlDirect(question, sql, seed, executionConnection, undefined, executionConnectionName);
5795
+ return executeGeneratedSqlDirect(question, sql, seed, executionConnection, undefined, executionConnectionName, agenticCapability, agenticScope);
5287
5796
  };
5288
5797
  /**
5289
5798
  * EXP-001: execution host for the deliberately narrow non-governed lane.
@@ -5891,7 +6400,10 @@ export async function startLocalServer(opts) {
5891
6400
  let invocation;
5892
6401
  let plan;
5893
6402
  try {
5894
- const program = new Parser(repairedSource, '<bounded-dql-repair>').parse();
6403
+ // The source label is echoed into parse errors, which reach the user.
6404
+ // `<bounded-dql-repair>` is an internal artifact id and tells them nothing;
6405
+ // it appeared verbatim in a reported failure card.
6406
+ const program = new Parser(repairedSource, 'repaired query').parse();
5895
6407
  const blocks = program.statements.filter((statement) => statement.kind === NodeKind.BlockDecl);
5896
6408
  if (blocks.length !== 1 || program.statements.length !== 1) {
5897
6409
  throw new Error('The repaired source must contain exactly one DQL block.');
@@ -25821,7 +26333,13 @@ async function createBlockStudioAssistProvider(projectRoot, requestedProvider) {
25821
26333
  default:
25822
26334
  return null;
25823
26335
  }
25824
- return await provider.available() ? provider : null;
26336
+ // Route through the eval cassette too. This constructor serves the MEANING
26337
+ // call and narration — the two dispatches that decide routing and wording —
26338
+ // so leaving it unwrapped meant a recorded suite still hit a live model for
26339
+ // exactly the calls whose non-determinism it was recorded to remove. A local
26340
+ // baseline reproduced that: the same question blocked on one run and answered
26341
+ // on the next, and zero cassettes were written.
26342
+ return await provider.available() ? applyEvalCassette(provider) : null;
25825
26343
  }
25826
26344
  /** Convert a governed answer's result payload into a bounded synthesis preview. */
25827
26345
  function agentResultToSynthesisPreview(result) {
@@ -28658,7 +29176,45 @@ async function buildAgentSchemaContextFromCatalog(projectRoot, question, prepare
28658
29176
  const RUNTIME_SNAPSHOT_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
28659
29177
  // A resolver compares at most 12 compact cards and never performs tool calls;
28660
29178
  // ten seconds is the full allowance, not the start of another planning loop.
28661
- const AGENT_MEANING_TIMEOUT_MS = 10_000;
29179
+ /**
29180
+ * Ceiling on the one bounded meaning-resolution call.
29181
+ *
29182
+ * 10s assumes a hosted model. A local Ollama model needs ~7s for a ONE-WORD
29183
+ * reply, so a 600-token resolution over a dozen candidates never lands: it
29184
+ * aborts, the router falls back to its evidence-only decision, and
29185
+ * `mayAssumeInterpretation` goes false — which sends every ambiguous question to
29186
+ * the clarification gate (AGT-017). The effect is that a local model cannot
29187
+ * answer anything ambiguous, in a product whose whole positioning is local-first.
29188
+ *
29189
+ * Scaled by the same `DQL_AGENT_DEADLINE_SCALE` as the run budget, so one
29190
+ * setting moves the provider's whole time envelope together rather than leaving
29191
+ * an inner bound to silently cap an outer one.
29192
+ */
29193
+ /**
29194
+ * Predict how long the next provider call will take, for admission control.
29195
+ *
29196
+ * With fewer than three samples the MAX is the only honest predictor: there is
29197
+ * no distribution yet, and admitting a call the deadline then kills wastes the
29198
+ * whole remaining budget.
29199
+ *
29200
+ * With a real sample, p75 rather than the max. One slow response — a cold model
29201
+ * load, a retried connection — otherwise poisons admission control for the rest
29202
+ * of the run: every later call is refused against a worst case that already
29203
+ * passed. A recorded run tripped RUN_DEADLINE_INSUFFICIENT 6.4s into a 45s
29204
+ * budget for exactly that reason. p75 still errs slow, so a genuinely slow
29205
+ * provider is still respected.
29206
+ */
29207
+ export function predictDispatchMs(observed, assumedMs = ASSUMED_PROVIDER_DISPATCH_MS) {
29208
+ if (observed.length === 0)
29209
+ return assumedMs;
29210
+ const sorted = [...observed].sort((left, right) => left - right);
29211
+ if (sorted.length < 3)
29212
+ return sorted[sorted.length - 1];
29213
+ const index = Math.max(0, Math.min(sorted.length - 1, Math.ceil(sorted.length * 0.75) - 1));
29214
+ return sorted[index];
29215
+ }
29216
+ const AGENT_MEANING_TIMEOUT_BASE_MS = 10_000;
29217
+ const AGENT_MEANING_TIMEOUT_MS = AGENT_MEANING_TIMEOUT_BASE_MS * deadlineScale();
28662
29218
  export function boundedAgentMeaningSignal(signal, timeoutMs = AGENT_MEANING_TIMEOUT_MS) {
28663
29219
  const timeout = AbortSignal.timeout(Math.max(1, timeoutMs));
28664
29220
  return signal ? AbortSignal.any([signal, timeout]) : timeout;
@@ -30043,49 +30599,9 @@ function scoreAgentValueProbeColumn(table, column) {
30043
30599
  return score;
30044
30600
  }
30045
30601
  export function isAgentValueProbeColumn(column) {
30046
- const name = column.name.toLowerCase();
30047
- // Tokenize underscore/camel names before applying the hard deny-list. This is
30048
- // intentionally independent of an allowlist: secrets and free-text payloads
30049
- // can never be probed through automatic grounding.
30050
- const normalizedName = column.name
30051
- .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
30052
- .replace(/[_-]+/g, ' ')
30053
- .toLowerCase();
30054
- if (/\b(password|secret|token|credential|hash|salt|notes?|comments?|description|message|body|payload|content)\b/.test(normalizedName))
30055
- return false;
30056
- if (/\bemail\b/.test(normalizedName))
30057
- return false;
30058
- if (!hasAgentSchemaToken(name, [
30059
- 'account',
30060
- 'category',
30061
- 'channel',
30062
- 'city',
30063
- 'code',
30064
- 'country',
30065
- 'customer',
30066
- 'email',
30067
- 'full',
30068
- 'id',
30069
- 'key',
30070
- 'member',
30071
- 'name',
30072
- 'number',
30073
- 'product',
30074
- 'region',
30075
- 'segment',
30076
- 'sku',
30077
- 'state',
30078
- 'status',
30079
- 'subscriber',
30080
- 'type',
30081
- 'user',
30082
- ])) {
30083
- return false;
30084
- }
30085
- const type = column.type?.toLowerCase() ?? '';
30086
- if (!type)
30087
- return true;
30088
- return /\b(char|character|clob|email|string|text|uuid|varchar)\b/.test(type);
30602
+ // Delegates to the canonical predicate in dql-agent. Two copies of a security
30603
+ // rule drift, and the one that drifts is the one nobody is looking at.
30604
+ return isProbeSafeColumn({ name: column.name, ...(column.type ? { type: column.type } : {}) });
30089
30605
  }
30090
30606
  export function buildAgentValueProbeSql(table, column, searchTerms, connection) {
30091
30607
  const relation = quoteAgentRelation(table.relation, connection);