@agentskit/doc-bridge 1.7.45 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +465 -0
  2. package/CONTRIBUTING.md +23 -0
  3. package/README.md +87 -40
  4. package/SECURITY.md +7 -0
  5. package/action.yml +1 -1
  6. package/bin/ak-docs.js +2 -2
  7. package/bin/ak-verify.js +13 -7
  8. package/dist/cli/program.d.ts +3 -1
  9. package/dist/cli/program.js +15861 -6223
  10. package/dist/cli/program.js.map +1 -1
  11. package/dist/config/index.d.ts +1 -1
  12. package/dist/config/index.js +86 -6
  13. package/dist/config/index.js.map +1 -1
  14. package/dist/{index-BUL0q7s8.d.ts → index-Beor6Yhi.d.ts} +133 -1
  15. package/dist/index.d.ts +9871 -1062
  16. package/dist/index.js +16126 -6125
  17. package/dist/index.js.map +1 -1
  18. package/docs/MARKETPLACE.md +1 -1
  19. package/docs/PRD-documentation-efficiency-study.md +406 -0
  20. package/docs/PRD-knowledge-retrieval-and-enrichment.md +466 -0
  21. package/docs/adr/0002-documentation-audit-boundary.md +22 -0
  22. package/docs/adr/0003-study-protocol-and-historical-evidence.md +40 -0
  23. package/docs/adr/0004-controlled-study-runner.md +25 -0
  24. package/docs/adr/0005-documentation-quality-and-criticality.md +20 -0
  25. package/docs/adr/0006-registry-semantic-grounding.md +20 -0
  26. package/docs/adr/0007-longitudinal-study-metrics.md +21 -0
  27. package/docs/adr/0008-study-verification-boundary.md +21 -0
  28. package/docs/adr/0009-study-provider-cli-adapter.md +25 -0
  29. package/docs/agent-corpus/INDEX.md +12 -1
  30. package/docs/agent-corpus/OVERVIEW.md +25 -0
  31. package/docs/agent-corpus/chat.md +5 -1
  32. package/docs/agent-corpus/cli.md +16 -0
  33. package/docs/agent-corpus/conformance.md +12 -0
  34. package/docs/agent-corpus/doc-bridge.md +47 -0
  35. package/docs/agent-corpus/doctor.md +8 -0
  36. package/docs/agent-corpus/gates.md +4 -0
  37. package/docs/agent-corpus/mcp.md +13 -0
  38. package/docs/agent-corpus/memory.md +4 -0
  39. package/docs/agent-corpus/query.md +33 -0
  40. package/docs/bench/README.md +122 -0
  41. package/docs/bench/retrieval-baseline-v1.json +28 -0
  42. package/docs/bench/retrieval-suite-v1.json +1033 -0
  43. package/docs/chat-and-rag.md +3 -2
  44. package/docs/for-agents.md +9 -1
  45. package/docs/getting-started.md +4 -11
  46. package/docs/guides/gate-ci.md +11 -1
  47. package/docs/guides/install-and-run.md +9 -65
  48. package/docs/index.md +22 -1
  49. package/docs/knowledge-engine-runbook.md +38 -3
  50. package/docs/landing/assets/context-payload-reduction.svg +21 -0
  51. package/docs/landing/assets/controlled-ab-comparison.svg +30 -0
  52. package/docs/landing/index.html +119 -5
  53. package/docs/loop-workflow.md +117 -0
  54. package/docs/mcp.md +6 -1
  55. package/docs/parity/public-claims-v1.json +145 -0
  56. package/docs/playbook/doc-bridge-pattern.md +1 -1
  57. package/docs/query.md +90 -2
  58. package/docs/recipes/index-pipeline.md +1 -1
  59. package/docs/schemas/agent-handoff-v1.md +15 -0
  60. package/docs/schemas/doc-bridge-index-v1.md +65 -0
  61. package/docs/spec/benchmark-v1.md +33 -1
  62. package/docs/spec/cli.md +30 -10
  63. package/docs/spec/config-v1.md +147 -8
  64. package/docs/spec/documentation-audit-v1.md +61 -0
  65. package/docs/spec/enrichment-overlay-v1.md +241 -0
  66. package/docs/spec/graph-signals-v1.md +92 -0
  67. package/docs/spec/incremental-scan-v1.md +102 -0
  68. package/docs/spec/markdown-analyzer-v1.md +73 -0
  69. package/docs/spec/mcp-knowledge-tools-v1.md +147 -0
  70. package/docs/spec/measured-enrichment-v1.md +229 -0
  71. package/docs/spec/public-parity-v1.md +119 -0
  72. package/docs/spec/registry-agents.md +6 -0
  73. package/docs/spec/render-v1.md +122 -0
  74. package/docs/spec/retrieval-index-v1.md +164 -0
  75. package/docs/spec/study-metrics-v1.md +58 -0
  76. package/docs/spec/study-protocol-v1.md +46 -0
  77. package/docs/spec/study-provider-cli-v1.md +116 -0
  78. package/docs/spec/study-runner-v1.md +35 -0
  79. package/docs/spec/study-task-suite-v1.md +41 -0
  80. package/docs/spec/study-verification-v1.md +40 -0
  81. package/docs/study/README.md +84 -0
  82. package/docs/study/ab-adjudicated-cost-analysis-v1.md +29 -0
  83. package/docs/study/ab-adjudicated-cost-plan-v1.json +33 -0
  84. package/docs/study/ab-adjudicated-cost-plan-v2-v1.json +33 -0
  85. package/docs/study/ab-adjudicated-cost-result-v1.json +80 -0
  86. package/docs/study/ab-baseline-analysis-v1.md +21 -0
  87. package/docs/study/ab-baseline-plan-v1.json +33 -0
  88. package/docs/study/ab-baseline-recovery-plan-v1.json +33 -0
  89. package/docs/study/ab-baseline-result-v1.json +79 -0
  90. package/docs/study/documentation-audit-round-2026-08-31.json +183 -0
  91. package/docs/study/historical-evidence-v1.json +252 -0
  92. package/docs/study/observation-ledger-v1.json +30632 -0
  93. package/docs/study/phase3-task-coverage-v1.json +34 -0
  94. package/docs/study/phase4-public-pilot-ledger-v1.json +1344 -0
  95. package/docs/study/phase4-public-pilot-result-v1.json +52 -0
  96. package/docs/study/phase4-public-pilot-run-plan-v1.json +26 -0
  97. package/docs/study/phase4-public-pilot-task-suite-v1.json +71 -0
  98. package/docs/study/pilot-round-2026-08-31.json +46 -0
  99. package/docs/study/protocol-v1.json +90 -0
  100. package/docs/study/publication-gate-v1.md +45 -0
  101. package/docs/study/quality-scorecard-cycle-plan.md +545 -0
  102. package/docs/study/quality-scorecard-v1.json +38 -0
  103. package/docs/study/round-1-adjudicated-smoke-v1.json +30642 -0
  104. package/docs/study/round-1-instrumentation-plan-v1.md +39 -0
  105. package/docs/study/round-2-expanded-adjudication-v1.json +91 -0
  106. package/docs/study/round-2-expanded-validation-v1.md +58 -0
  107. package/docs/study/round-3-evidence-contract-v1.json +75 -0
  108. package/docs/study/round-3-evidence-contract-v1.md +57 -0
  109. package/docs/study/round-4-confirmation-v1.json +75 -0
  110. package/docs/study/round-4-confirmation-v1.md +55 -0
  111. package/docs/study/run-plan-v1.json +33 -0
  112. package/docs/study/semantic-adjudication-cycle-8.md +20 -0
  113. package/docs/study/task-suite-v1.json +96 -0
  114. package/docs/study/token-efficiency-plan-v1.md +337 -0
  115. package/docs/study/token-efficiency-protocol-v2.json +62 -0
  116. package/docs/study/verification-binding-v1.json +27 -0
  117. package/docs/validation-cycle-plan.md +14 -0
  118. package/docs/verification-harness.md +11 -6
  119. package/ecosystem-claims.json +2 -2
  120. package/ecosystem-upstream.json +2 -2
  121. package/ecosystem.json +4 -4
  122. package/mcpb/manifest.json +9 -1
  123. package/package.json +25 -6
  124. package/scripts/check-ecosystem-upstream.mjs +34 -6
  125. package/skills/doc-bridge-handoff/fixtures/synthetic-repo/docs/for-agents/packages/payments.md +7 -0
  126. package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
  127. package/src/agents/registry-adapter.ts +192 -24
  128. package/src/audit/documentation.ts +513 -0
  129. package/src/bench/baseline.ts +198 -0
  130. package/src/bench/overlay-delta.ts +139 -0
  131. package/src/bench/retrieval.ts +319 -0
  132. package/src/budget/compile.ts +91 -0
  133. package/src/budget/sections.ts +70 -0
  134. package/src/cli/program.ts +684 -74
  135. package/src/cli/usage.ts +71 -0
  136. package/src/config/defaults.ts +1 -0
  137. package/src/config/index.ts +4 -0
  138. package/src/config/schema.ts +117 -2
  139. package/src/conformance/documentation-standard-v1.ts +8 -6
  140. package/src/discovery/areas.ts +182 -0
  141. package/src/discovery/documentation.ts +211 -5
  142. package/src/discovery/identity.ts +24 -0
  143. package/src/discovery/incremental.ts +314 -0
  144. package/src/discovery/inputs.ts +110 -0
  145. package/src/discovery/markdown.ts +481 -0
  146. package/src/discovery/repository.ts +484 -101
  147. package/src/doctor/run-doctor.ts +244 -12
  148. package/src/enrich/approvals.ts +190 -0
  149. package/src/enrich/cache.ts +93 -0
  150. package/src/enrich/context-pack.ts +272 -0
  151. package/src/enrich/overlay.ts +255 -0
  152. package/src/enrich/review.ts +106 -0
  153. package/src/enrich/stage.ts +374 -0
  154. package/src/enrich/stats.ts +100 -0
  155. package/src/enrich/validate.ts +410 -0
  156. package/src/federation/llms.ts +2 -4
  157. package/src/findings/report.ts +103 -0
  158. package/src/graph/build.ts +356 -0
  159. package/src/graph/memory.ts +208 -0
  160. package/src/index-builder/build-handoffs.ts +22 -11
  161. package/src/index-builder/build-index.ts +132 -3
  162. package/src/index-builder/llms-txt.ts +48 -8
  163. package/src/index-builder/project-corpus.ts +111 -0
  164. package/src/index.ts +630 -2
  165. package/src/lib/fuzzy-match.ts +235 -0
  166. package/src/mcp/knowledge.ts +554 -0
  167. package/src/mcp/server.ts +113 -18
  168. package/src/metrics/benchmark.ts +21 -0
  169. package/src/parity/check.ts +309 -0
  170. package/src/parity/claims.ts +259 -0
  171. package/src/parity/resolve.ts +160 -0
  172. package/src/query/handoff.ts +326 -0
  173. package/src/query/load-index.ts +53 -1
  174. package/src/query/query.ts +92 -59
  175. package/src/query/search.ts +289 -92
  176. package/src/query/text.ts +155 -0
  177. package/src/reconciliation/reconcile.ts +101 -10
  178. package/src/render/data.ts +356 -0
  179. package/src/render/engine.ts +398 -0
  180. package/src/render/generated.ts +77 -0
  181. package/src/render/render.ts +209 -0
  182. package/src/render/template-source.ts +52 -0
  183. package/src/render/templates.ts +289 -0
  184. package/src/report/html.ts +2 -2
  185. package/src/retrieval/bm25.ts +161 -0
  186. package/src/retrieval/project.ts +495 -0
  187. package/src/retrieval/rank.ts +383 -0
  188. package/src/retrieval/weights.ts +39 -0
  189. package/src/retriever/doc-bridge-retriever.ts +100 -15
  190. package/src/rules/engine.ts +30 -10
  191. package/src/schemas/agent-handoff.ts +56 -0
  192. package/src/schemas/budget.ts +37 -0
  193. package/src/schemas/doc-bridge-index.ts +53 -2
  194. package/src/schemas/enrichment.ts +369 -0
  195. package/src/schemas/json-schemas.ts +39 -2
  196. package/src/schemas/knowledge.ts +14 -1
  197. package/src/schemas/retrieval-index.ts +152 -0
  198. package/src/shims/graphology.d.ts +91 -0
  199. package/src/study/adjudication.ts +196 -0
  200. package/src/study/execution.ts +350 -0
  201. package/src/study/expectations.ts +219 -0
  202. package/src/study/metrics.ts +467 -0
  203. package/src/study/protocol.ts +271 -0
  204. package/src/study/provider-cli.ts +115 -0
  205. package/src/study/provider-telemetry.ts +47 -0
  206. package/src/study/quality-scorecard.ts +164 -0
  207. package/src/study/runner.ts +461 -0
  208. package/src/study/task-suite.ts +321 -0
  209. package/src/study/verification.ts +134 -0
  210. package/src/validate.ts +1 -4
  211. package/src/version.ts +1 -1
  212. package/src/workflow/engine.ts +36 -11
  213. package/scripts/verification-harness.mjs +0 -482
@@ -17,6 +17,7 @@ import { scanHumanDocRecords } from '../index-builder/human-adapters/index.js'
17
17
  import { retrieveHybridChunks } from '../federation/llms.js'
18
18
  import { runGates, type GateId } from '../gates/run-gates.js'
19
19
  import { evaluateRules, parseRuleId, parseRuleSeverity, type RuleMode } from '../rules/engine.js'
20
+ import { centrality } from '../graph/build.js'
20
21
  import { runChatOnce, startInkChat } from '../intelligence/chat.js'
21
22
  import { PeerMissingError, layer1InstallHint } from '../intelligence/peers.js'
22
23
  import { createDocBridgeRag } from '../intelligence/rag.js'
@@ -32,23 +33,72 @@ import {
32
33
  } from '../doctor/badge.js'
33
34
  import { docBridgePatternMarkdown, docBridgePatternPayload } from '../playbook/doc-bridge-pattern.js'
34
35
  import { formatDemoText, runDemo, withDemoWorkspace, type DemoFixture } from './demo.js'
35
- import { formatDoctorText, runDoctor } from '../doctor/run-doctor.js'
36
+ import { DEFAULT_RETRIEVAL_SUITE, formatDoctorText, runDoctor } from '../doctor/run-doctor.js'
36
37
  import { installMcpConfig, mcpSnippet } from '../mcp/install.js'
37
38
  import { startMcpStdioServer } from '../mcp/server.js'
38
- import { IndexNotFoundError, loadDocBridgeIndex } from '../query/load-index.js'
39
+ import { measureOverlayRetrievalDelta, formatOverlayRetrievalDeltaText } from '../bench/overlay-delta.js'
40
+ import { enrichmentCost } from '../enrich/stats.js'
41
+ import { checkStudyExpectations, formatStudyExpectationsText, parseStudyExpectations } from '../study/expectations.js'
42
+ import { IndexNotFoundError, loadFreshDocBridgeIndex } from '../query/load-index.js'
39
43
  import { runQuery, type QueryKind } from '../query/query.js'
40
44
  import { searchIndex } from '../query/search.js'
41
45
  import type { DocBridgeIndexV1 } from '../schemas/doc-bridge-index.js'
42
- import { parseAgentHandoff, parseDocBridgeConfig, parseReconciliationReport } from '../validate.js'
46
+ import { parseAgentHandoff, parseDocBridgeConfig, parseDocBridgeIndex, parseReconciliationReport } from '../validate.js'
43
47
  import { parseDiscoverySnapshot } from '../validate.js'
48
+ import { findingsFromDiagnostics } from '../findings/report.js'
49
+ import {
50
+ formatRetrievalBenchText,
51
+ parseRetrievalSuite,
52
+ runRetrievalBench,
53
+ type RetrievalBenchResultV1,
54
+ } from '../bench/retrieval.js'
55
+ import {
56
+ compareRetrievalBaseline,
57
+ createRetrievalBaseline,
58
+ formatRetrievalComparisonText,
59
+ parseRetrievalBaseline,
60
+ } from '../bench/baseline.js'
44
61
  import { reconcileKnowledge } from '../reconciliation/reconcile.js'
45
62
  import type { DiscoverySnapshotV1, ReconciliationReportV1 } from '../schemas/knowledge.js'
46
63
  import { sha256NormalizedV1 } from '../index-builder/content-hash.js'
47
64
  import { applyFixProposal, approveFixProposal, createArtifactNormalizationProposal, createMarkdownLinkFixProposal } from '../fixes/proposals.js'
48
65
  import { createRegistryAgentAdapter, loadRegistryAgentRunner, persistRegistryAgentProposal } from '../agents/registry-adapter.js'
66
+ import { fixApprovalId, recordApproval, FIX_APPROVAL_GATE } from '../enrich/approvals.js'
67
+ import { readEnrichmentOverlay, withAcceptedRelations } from '../enrich/overlay.js'
68
+ import { decideEnrichment, listEnrichment } from '../enrich/review.js'
69
+ import { formatEnrichmentText, runEnrichment, type EnrichmentRunResult } from '../enrich/stage.js'
49
70
  import { renderOfflineReportArtifact } from '../report/html.js'
50
71
  import { benchmarkFixture, formatBenchmarkText, measureBenchmark } from '../metrics/benchmark.js'
51
72
  import { PACKAGE_VERSION } from '../version.js'
73
+ import { auditDocumentation, formatDocumentationAuditText } from '../audit/documentation.js'
74
+ import { renderArtifact, writeRenderedPages } from '../render/render.js'
75
+ import { CLI_COMMAND_USAGE } from './usage.js'
76
+ import { checkPublicParity, formatPublicParityText } from '../parity/check.js'
77
+ import { parsePublicClaims } from '../parity/claims.js'
78
+ import { resolveTemplateSource } from '../render/template-source.js'
79
+ import { RENDER_TEMPLATE_NAMES, RENDER_TEMPLATES, isRenderTemplateName } from '../render/templates.js'
80
+ import {
81
+ formatHistoricalEvidenceText,
82
+ formatStudyProtocolText,
83
+ parseHistoricalEvidenceRegistry,
84
+ parseStudyProtocol,
85
+ validateHistoricalEvidenceRegistry,
86
+ } from '../study/protocol.js'
87
+ import {
88
+ formatStudyTaskSuiteText,
89
+ parseStudyTaskSuite,
90
+ selectTaskExecutions,
91
+ } from '../study/task-suite.js'
92
+ import {
93
+ formatControlledStudyRunPlanText,
94
+ parseControlledStudyLedger,
95
+ parseControlledStudyRunPlan,
96
+ } from '../study/runner.js'
97
+ import { formatControlledStudyRunText, parseStudyRepositoryConfig, runControlledStudy } from '../study/execution.js'
98
+ import { independentlyAdjudicateStudyLedger, persistIndependentlyAdjudicatedLedger } from '../study/adjudication.js'
99
+ import { formatStudyProviderCliText, parseStudyProviderCliConfig } from '../study/provider-cli.js'
100
+ import { calculateStudyMetrics, formatStudyMetricsText } from '../study/metrics.js'
101
+ import { formatStudyVerificationText, parseStudyVerificationBinding } from '../study/verification.js'
52
102
 
53
103
  type Command =
54
104
  | 'help'
@@ -62,12 +112,15 @@ type Command =
62
112
  | 'registry'
63
113
  | 'discover'
64
114
  | 'benchmark'
115
+ | 'bench'
116
+ | 'study'
65
117
  | 'scan'
66
118
  | 'reconcile'
67
119
  | 'check'
68
120
  | 'map'
69
121
  | 'fix'
70
122
  | 'suggest'
123
+ | 'enrich'
71
124
  | 'index'
72
125
  | 'gate'
73
126
  | 'rules'
@@ -82,48 +135,11 @@ type Command =
82
135
  | 'rag'
83
136
  | 'list'
84
137
  | 'conformance'
138
+ | 'audit'
139
+ | 'parity'
140
+ | 'render'
85
141
 
86
- const usage = `ak-docs — human↔agent documentation bridge (@agentskit/doc-bridge)
87
-
88
- Core (no API key):
89
- ak-docs init [--demo] [--scaffold-workspaces]
90
- ak-docs demo [--fixture example|monorepo] [--text] [--in-project]
91
- ak-docs doctor [--text] [--badge] [--write-badge]
92
- ak-docs index [--watch]
93
- ak-docs discover [--text|--json]
94
- ak-docs benchmark <fixture.json> <observation.json> [--text|--json]
95
- ak-docs scan | reconcile | check | map [--text|--json] [--html] [--report-threshold <bytes>]
96
- ak-docs fix propose links|normalize <artifact> [--output <file>]
97
- ak-docs fix approve|apply <proposal.json> [--by <name>]
98
- ak-docs suggest [--json|--text] run the configured local Registry agent
99
- ak-docs query [package|ownership|intent|change] <id> [--agent] [--text]
100
- ak-docs search <term> [--agent] [--text]
101
- ak-docs list <packages|intents|changes|knowledge> [--text]
102
- ak-docs ask [question] local consult (no LLM)
103
- ak-docs gate run [gate-id]
104
- ak-docs rules run <report.json> [--preset default|recommended|strict] [--severity rule=level] [--ignore rule]
105
- ak-docs conformance run documentation-standard-v1 [--text|--json]
106
- ak-docs mcp
107
- ak-docs mcp install --cursor | --claude
108
- ak-docs memory ingest|classify|promote [--pr] [--dry-run]
109
- ak-docs bootstrap agent-docs
110
- ak-docs validate-config | validate-handoff <file>
111
-
112
- Intelligence (optional AgentsKit peers):
113
- ak-docs rag ingest|search <query>
114
- ak-docs chat terminal chat (Ink + RAG)
115
- ak-docs ask <question> --chat one-shot grounded answer
116
-
117
- Advanced / ecosystem:
118
- ak-docs retrieve <query>
119
- ak-docs registry topology
120
- ak-docs playbook draft | pattern [--text]
121
-
122
- Global flags:
123
- -h, --help --version
124
- --config <path> (project root = config file directory)
125
- --agent --json --text --chat --demo
126
- `
142
+ const usage = CLI_COMMAND_USAGE
127
143
 
128
144
  const QUERY_KINDS = new Set<QueryKind>(['package', 'ownership', 'intent', 'change', 'search'])
129
145
  const LIST_KINDS = new Set(['packages', 'intents', 'changes', 'knowledge'])
@@ -148,6 +164,12 @@ const parseArgs = (argv: readonly string[]) => {
148
164
  i += 1
149
165
  continue
150
166
  }
167
+ if (arg === '--mode' || arg === '--context-budget') {
168
+ const value = argv[i + 1]
169
+ if (value !== undefined && !value.startsWith('-')) flags.add(`${arg}=${value}`)
170
+ i += 1
171
+ continue
172
+ }
151
173
  if (arg.startsWith('-')) {
152
174
  flags.add(arg)
153
175
  continue
@@ -167,12 +189,15 @@ const parseArgs = (argv: readonly string[]) => {
167
189
  else if (positional[0] === 'registry') command = 'registry'
168
190
  else if (positional[0] === 'discover') command = 'discover'
169
191
  else if (positional[0] === 'benchmark') command = 'benchmark'
192
+ else if (positional[0] === 'bench') command = 'bench'
193
+ else if (positional[0] === 'study') command = 'study'
170
194
  else if (positional[0] === 'scan') command = 'scan'
171
195
  else if (positional[0] === 'reconcile') command = 'reconcile'
172
196
  else if (positional[0] === 'check') command = 'check'
173
197
  else if (positional[0] === 'map') command = 'map'
174
198
  else if (positional[0] === 'fix') command = 'fix'
175
199
  else if (positional[0] === 'suggest') command = 'suggest'
200
+ else if (positional[0] === 'enrich') command = 'enrich'
176
201
  else if (positional[0] === 'index') command = 'index'
177
202
  else if (positional[0] === 'gate') command = 'gate'
178
203
  else if (positional[0] === 'rules') command = 'rules'
@@ -187,6 +212,9 @@ const parseArgs = (argv: readonly string[]) => {
187
212
  else if (positional[0] === 'rag') command = 'rag'
188
213
  else if (positional[0] === 'list') command = 'list'
189
214
  else if (positional[0] === 'conformance') command = 'conformance'
215
+ else if (positional[0] === 'audit') command = 'audit'
216
+ else if (positional[0] === 'parity') command = 'parity'
217
+ else if (positional[0] === 'render') command = 'render'
190
218
 
191
219
  return { command, flags, configPath, positional }
192
220
  }
@@ -219,6 +247,10 @@ const writeJson = (payload: unknown): void => {
219
247
  process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`)
220
248
  }
221
249
 
250
+ const writeAgentJson = (payload: unknown): void => {
251
+ process.stdout.write(`${JSON.stringify(payload)}\n`)
252
+ }
253
+
222
254
  const writeLines = (lines: readonly string[]): void => {
223
255
  process.stdout.write(lines.length ? `${lines.join('\n')}\n` : '')
224
256
  }
@@ -270,10 +302,23 @@ const formatSearchMatch = (match: {
270
302
  readonly path: string
271
303
  readonly summary?: string
272
304
  readonly score?: number
305
+ readonly confidence?: string
306
+ readonly explain?: { readonly matched: Readonly<Record<string, readonly string[]>>; readonly components: Readonly<Record<string, number>>; readonly surfacedBy?: { readonly kind: string; readonly id: string } }
273
307
  }): string => {
274
308
  const summary = match.summary ? match.summary.replace(/\s+/g, ' ').slice(0, 100) : ''
275
309
  const score = typeof match.score === 'number' ? ` score=${match.score}` : ''
276
- return ` [${match.type}] ${match.id}${score}\n ${match.path}${summary ? `\n ${summary}` : ''}`
310
+ const confidence = match.confidence ? ` confidence=${match.confidence}` : ''
311
+ const lines = [` [${match.type}] ${match.id}${score}${confidence}`, ` ${match.path}`, ...(summary ? [` ${summary}`] : [])]
312
+ if (match.explain) {
313
+ // Every component with a contribution, so a wrong ranking is reportable with numbers.
314
+ const parts = Object.entries(match.explain.components).filter(([name, value]) => value !== 0 && name !== 'prior').map(([name, value]) => `${name}=${value}`)
315
+ if (match.explain.components.prior !== undefined && match.explain.components.prior !== 1) parts.push(`prior=×${match.explain.components.prior}`)
316
+ const matched = Object.entries(match.explain.matched).map(([field, terms]) => `${field}: ${terms.join(', ')}`)
317
+ lines.push(` why: ${parts.join(' ') || 'no scoring component'}`)
318
+ if (matched.length) lines.push(` matched: ${matched.join(' | ')}`)
319
+ if (match.explain.surfacedBy) lines.push(` via: ${match.explain.surfacedBy.kind} from ${match.explain.surfacedBy.id}`)
320
+ }
321
+ return lines.join('\n')
277
322
  }
278
323
 
279
324
  const writeTextSearch = (
@@ -367,7 +412,7 @@ const writeAsk = (
367
412
  }
368
413
 
369
414
  const readIndexedDoc = (root: string, config: DocBridgeConfigV1, idOrPath: string): string => {
370
- const index = loadDocBridgeIndex(root, config)
415
+ const index = loadFreshDocBridgeIndex(root, config)
371
416
  const entry = index.knowledge.find((doc) => doc.id === idOrPath || doc.path === idOrPath)
372
417
  if (!entry) throw new Error(`Unknown indexed doc "${idOrPath}". Try: search ${idOrPath}`)
373
418
 
@@ -380,7 +425,7 @@ const readIndexedDoc = (root: string, config: DocBridgeConfigV1, idOrPath: strin
380
425
  }
381
426
 
382
427
  const runAskRepl = async (root: string, config: DocBridgeConfigV1): Promise<number> => {
383
- const index = loadDocBridgeIndex(root, config)
428
+ const index = loadFreshDocBridgeIndex(root, config)
384
429
  const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true })
385
430
  try {
386
431
  for (;;) {
@@ -465,7 +510,7 @@ const workflowOptions = (
465
510
  root: string,
466
511
  config: DocBridgeConfigV1,
467
512
  sourceRevision: string,
468
- stage: 'collect' | 'normalize' | 'reconcile' | 'evaluate' | 'report',
513
+ stage: 'collect' | 'normalize' | 'reconcile' | 'enrich' | 'evaluate' | 'report',
469
514
  handlers: Parameters<typeof runWorkflow>[0]['handlers'],
470
515
  versions?: Pick<Parameters<typeof runWorkflow>[0], 'pipelineVersion' | 'analyzerVersions'>,
471
516
  ): Parameters<typeof runWorkflow>[0] => ({
@@ -491,6 +536,12 @@ const documentationInputs = (root: string, snapshot: DiscoverySnapshotV1) => sna
491
536
  .filter((entity) => entity.kind === 'document' && entity.path)
492
537
  .map((entity) => ({ path: entity.path as string, content: readFileSync(resolve(root, entity.path as string), 'utf8') }))
493
538
 
539
+ /** Ownership records as reconciliation sees them, so a path matching nothing gets reported. */
540
+ const ownershipOptions = (config: DocBridgeConfigV1): { readonly ownership?: readonly { readonly id: string; readonly path: string }[] } => {
541
+ const ownership = Object.entries(config.routing?.options?.ownership ?? {}).map(([id, record]) => ({ id, path: record.path }))
542
+ return ownership.length ? { ownership } : {}
543
+ }
544
+
494
545
  const reconcileWorkflow = (root: string, config: DocBridgeConfigV1): WorkflowExecutionResult => {
495
546
  const scanned = scanWorkflow(root, config)
496
547
  const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(scanned.stateDir, 'normalize'))
@@ -502,15 +553,51 @@ const reconcileWorkflow = (root: string, config: DocBridgeConfigV1): WorkflowExe
502
553
  ...(config.reconciliation?.requiredRelationKinds === undefined ? {} : { requiredRelationKinds: config.reconciliation.requiredRelationKinds }),
503
554
  ...(config.reconciliation?.requiredRelationTargets === undefined ? {} : { requiredRelationTargets: config.reconciliation.requiredRelationTargets }),
504
555
  ...(config.reconciliation?.includeOrphanedDocuments === undefined ? {} : { includeOrphanedDocuments: config.reconciliation.includeOrphanedDocuments }),
556
+ ...ownershipOptions(config),
505
557
  })
506
558
  return runWorkflow(workflowOptions(root, config, snapshot.sourceRevision, 'reconcile', { reconcile: () => report }, { pipelineVersion: snapshot.pipelineVersion, analyzerVersions: snapshot.analyzerVersions }))
507
559
  }
508
560
 
509
- const checkWorkflow = (root: string, config: DocBridgeConfigV1): WorkflowExecutionResult => {
510
- const reconciled = reconcileWorkflow(root, config)
561
+ /**
562
+ * Record the overlay as the enrich step of the current run.
563
+ *
564
+ * The step's input names the overlay it attaches, not only the report it was made from: a person
565
+ * approving a proposal changes the overlay without changing the report, and the engine refuses a
566
+ * step whose input has not moved but whose output has. Keying on the overlay's own hash keeps that
567
+ * rule honest — same overlay, same artifact; a new overlay, a new one.
568
+ */
569
+ const attachEnrichmentStage = (root: string, config: DocBridgeConfigV1, report: ReconciliationReportV1, result: EnrichmentRunResult): WorkflowExecutionResult =>
570
+ runWorkflow({
571
+ ...workflowOptions(root, config, report.sourceRevision, 'enrich', { enrich: () => result.overlay }, { pipelineVersion: report.pipelineVersion, analyzerVersions: report.analyzerVersions }),
572
+ inputs: { enrich: { reportHash: report.contentHash, overlayHash: result.overlay.contentHash } },
573
+ })
574
+
575
+ /**
576
+ * The enrich stage, run only on request. Its failure is reported, never propagated: a `check`
577
+ * that asked for enrichment and did not get it is still a `check`, with the same result.
578
+ */
579
+ const enrichWorkflow = async (root: string, config: DocBridgeConfigV1, reconciled: WorkflowExecutionResult): Promise<{ readonly status: 'ok' | 'failed'; readonly result?: EnrichmentRunResult; readonly error?: string }> => {
580
+ try {
581
+ const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(reconciled.stateDir, 'normalize'))
582
+ const report = parseReconciliationReport(loadWorkflowStepOutput(reconciled.stateDir, 'reconcile'))
583
+ const result = await runEnrichment({ root, config, snapshot, report })
584
+ attachEnrichmentStage(root, config, report, result)
585
+ return { status: 'ok', result }
586
+ } catch (error) {
587
+ return { status: 'failed', error: error instanceof Error ? error.message : String(error) }
588
+ }
589
+ }
590
+
591
+ const checkWorkflow = (root: string, config: DocBridgeConfigV1, reconciled = reconcileWorkflow(root, config)): WorkflowExecutionResult => {
511
592
  const report = parseReconciliationReport(loadWorkflowStepOutput(reconciled.stateDir, 'reconcile'))
512
593
  const versions = { pipelineVersion: report.pipelineVersion, analyzerVersions: report.analyzerVersions }
513
- runWorkflow(workflowOptions(root, config, report.sourceRevision, 'evaluate', { evaluate: () => evaluateRules(report, { ...(config.rules ? { config: config.rules } : {}) }) }, versions))
594
+ // `centrality-risk` needs betweenness over the import graph, which only the snapshot can give.
595
+ const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(reconciled.stateDir, 'normalize'))
596
+ const evaluate = () => evaluateRules(report, {
597
+ ...(config.rules ? { config: config.rules } : {}),
598
+ centrality: centrality(snapshot),
599
+ })
600
+ runWorkflow(workflowOptions(root, config, report.sourceRevision, 'evaluate', { evaluate }, versions))
514
601
  return runWorkflow(workflowOptions(root, config, report.sourceRevision, 'report', { report: ({ input }) => input }, versions))
515
602
  }
516
603
 
@@ -592,14 +679,85 @@ const runWorkflowCommand = (
592
679
  flags: ReadonlySet<string>,
593
680
  configPath: string | undefined,
594
681
  argv: readonly string[],
595
- ): number => {
682
+ ): number | Promise<number> => {
683
+ /*
684
+ * Validated before anything runs, and for every path: `--enrich` returns early, and a run that
685
+ * ends up printing findings must have been allowed to ask for them.
686
+ */
687
+ const format = optionValues(argv, '--format')[0]
688
+ if (format !== undefined && format !== 'json' && format !== 'finding') {
689
+ process.stderr.write('--format must be json or finding.\n')
690
+ return 2
691
+ }
692
+ if (format === 'finding' && command !== 'check') {
693
+ process.stderr.write('--format finding is only available for ak-docs check.\n')
694
+ return 2
695
+ }
696
+
697
+ if (command === 'check' && flags.has('--enrich')) {
698
+ return (async () => {
699
+ try {
700
+ const { config, root } = loadProject(configPath)
701
+ const reconciled = reconcileWorkflow(root, config)
702
+ const enrichment = await enrichWorkflow(root, config, reconciled)
703
+ return finishWorkflowCommand(command, flags, argv, config, root, checkWorkflow(root, config, reconciled), {
704
+ enrichment: enrichment.status === 'ok' && enrichment.result
705
+ ? { status: 'ok', overlayHash: enrichment.result.overlay.contentHash, agentCalls: enrichment.result.agentCalls, cacheHits: enrichment.result.cacheHits, accepted: enrichment.result.overlay.accepted.length, pending: enrichment.result.overlay.pending.length, rejected: enrichment.result.overlay.rejected.length }
706
+ : { status: 'failed', error: enrichment.error },
707
+ })
708
+ } catch (error) {
709
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
710
+ return 2
711
+ }
712
+ })()
713
+ }
596
714
  try {
597
715
  const { config, root } = loadProject(configPath)
598
716
  const result = command === 'scan' ? scanWorkflow(root, config) : command === 'reconcile' ? reconcileWorkflow(root, config) : checkWorkflow(root, config)
599
- const output = workflowOutput(result)
717
+ return finishWorkflowCommand(command, flags, argv, config, root, result, {})
718
+ } catch (error) {
719
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
720
+ return 2
721
+ }
722
+ }
723
+
724
+ const finishWorkflowCommand = (
725
+ command: 'scan' | 'reconcile' | 'check' | 'map',
726
+ flags: ReadonlySet<string>,
727
+ argv: readonly string[],
728
+ config: DocBridgeConfigV1,
729
+ root: string,
730
+ result: WorkflowExecutionResult,
731
+ extra: Record<string, unknown>,
732
+ ): number => {
733
+ try {
734
+ const output = { ...workflowOutput(result), ...extra }
600
735
  if (command === 'map') output.kind = 'architecture-map'
736
+ // Read from argv rather than taken as an argument: every caller already passes argv, and the
737
+ // flag is validated where the command is dispatched.
738
+ if (optionValues(argv, '--format')[0] === 'finding') {
739
+ /*
740
+ * The canonical shape the ecosystem consumes: every reconciliation diagnostic as a `Finding`
741
+ * with a severity from `SEVERITY_ORDER`. The rule verdict still decides the exit code, so a
742
+ * dashboard reading findings and a CI job reading the exit code agree on what failed.
743
+ */
744
+ const diagnostics = Array.isArray(output.diagnostics) ? (output.diagnostics as Parameters<typeof findingsFromDiagnostics>[0]) : []
745
+ writeJson({
746
+ ok: output.ok,
747
+ runId: output.runId,
748
+ state: output.state,
749
+ ...(output.snapshotHash ? { snapshotHash: output.snapshotHash } : {}),
750
+ ...(output.reportHash ? { reportHash: output.reportHash } : {}),
751
+ format: 'finding',
752
+ findings: findingsFromDiagnostics(diagnostics),
753
+ })
754
+ const exitCode = output.rules && typeof output.rules === 'object' && 'exitCode' in output.rules && (output.rules as { exitCode?: unknown }).exitCode === 1 ? 1 : 0
755
+ return result.run.state === 'failed' ? 1 : exitCode
756
+ }
601
757
  if (command === 'map' && flags.has('--html')) {
602
- const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(result.stateDir, 'normalize'))
758
+ const observed = parseDiscoverySnapshot(loadWorkflowStepOutput(result.stateDir, 'normalize'))
759
+ // Accepted proposed relations render as dashed edges; nothing observed is removed or redrawn.
760
+ const snapshot = config.intelligence?.registry?.enabled ? withAcceptedRelations(observed, readEnrichmentOverlay(root)) : observed
603
761
  const report = parseReconciliationReport(loadWorkflowStepOutput(result.stateDir, 'reconcile'))
604
762
  const outputPath = optionValues(argv, '--output')[0] ?? '.doc-bridge/report.html'
605
763
  const htmlPath = resolve(root, outputPath)
@@ -622,6 +780,187 @@ const runWorkflowCommand = (
622
780
  }
623
781
  }
624
782
 
783
+ const buildDocumentationAuditReport = (root: string, config: DocBridgeConfigV1) => {
784
+ const scanned = scanWorkflow(root, config)
785
+ const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(scanned.stateDir, 'normalize'))
786
+ const analysis = applyDocumentationDeclarations(snapshot, documentationInputs(root, snapshot), { agentRoot: config.corpus.agent.root })
787
+ const reconciliation = reconcileKnowledge(snapshot, analysis.snapshot, {
788
+ ...(config.reconciliation?.scope === undefined ? {} : { scope: config.reconciliation.scope }),
789
+ ...(config.reconciliation?.requiredRelationKinds === undefined ? {} : { requiredRelationKinds: config.reconciliation.requiredRelationKinds }),
790
+ ...(config.reconciliation?.requiredRelationTargets === undefined ? {} : { requiredRelationTargets: config.reconciliation.requiredRelationTargets }),
791
+ includeOrphanedDocuments: config.reconciliation?.includeOrphanedDocuments ?? true,
792
+ ...ownershipOptions(config),
793
+ })
794
+ return auditDocumentation({
795
+ root,
796
+ snapshot,
797
+ declared: analysis.snapshot,
798
+ reconciliation,
799
+ declarationDiagnostics: analysis.diagnostics,
800
+ ...(config.audit?.documentation ? { config: config.audit.documentation } : {}),
801
+ })
802
+ }
803
+
804
+ const BENCH_USAGE = [
805
+ 'Usage: ak-docs bench retrieval <suite.json> [--index <file>] [--baseline <file>] [--limit <n>] [--text|--json]',
806
+ ' ak-docs bench retrieval <suite.json> --baseline <file> --update-baseline --by <name> [--reason <text>]',
807
+ ].join('\n')
808
+
809
+ const runBenchCommand = (
810
+ flags: ReadonlySet<string>,
811
+ positional: readonly string[],
812
+ configPath: string | undefined,
813
+ argv: readonly string[],
814
+ ): number => {
815
+ if (positional[1] !== 'retrieval' || !positional[2]) {
816
+ process.stderr.write(`${BENCH_USAGE}\n`)
817
+ return 1
818
+ }
819
+ try {
820
+ const { config, root } = loadProject(configPath)
821
+ const suite = parseRetrievalSuite(JSON.parse(readFileSync(resolve(root, positional[2]), 'utf8')) as unknown)
822
+
823
+ const limitOption = optionValues(argv, '--limit')[0]
824
+ const limit = limitOption === undefined ? undefined : Number(limitOption)
825
+ if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {
826
+ throw new Error('--limit must be a positive integer.')
827
+ }
828
+
829
+ if (flags.has('--overlay')) {
830
+ /*
831
+ * Does the accepted overlay earn its cost? The same suite over the same snapshot, once with
832
+ * the overlay projected and once without. A drop in hit@3 exits non-zero: an overlay that
833
+ * makes retrieval worse is a finding about the agent, not a new baseline.
834
+ *
835
+ * Both indexes are projected here, from the snapshot, so this answer needs no index on disk.
836
+ */
837
+ const overlay = readEnrichmentOverlay(root)
838
+ if (!overlay) throw new Error('No enrichment overlay at .doc-bridge/enrich/overlay.json. Run: ak-docs enrich')
839
+ const stateDir = resolve(root, config.workflow?.stateDir ?? '.doc-bridge/workflow')
840
+ const snapshot = (() => {
841
+ try { return parseDiscoverySnapshot(loadWorkflowStepOutput(stateDir, 'normalize')) }
842
+ catch { return discoverRepository({ root, config }) }
843
+ })()
844
+ const delta = measureOverlayRetrievalDelta({ root, config, snapshot, overlay, suite, ...(limit === undefined ? {} : { limit }) })
845
+ if (wantsTextOutput(flags, config)) writeLines(formatOverlayRetrievalDeltaText(delta))
846
+ else writeJson({ ok: !delta.regression, overlayDelta: delta })
847
+ return delta.regression ? 1 : 0
848
+ }
849
+
850
+ const indexOption = optionValues(argv, '--index')[0]
851
+ const index = indexOption
852
+ ? parseDocBridgeIndex(JSON.parse(readFileSync(resolve(root, indexOption), 'utf8')) as unknown)
853
+ : loadFreshDocBridgeIndex(root, config)
854
+
855
+ const result: RetrievalBenchResultV1 = runRetrievalBench({
856
+ index,
857
+ suite,
858
+ ...(limit === undefined ? {} : { limit }),
859
+ })
860
+
861
+ const baselineOption = optionValues(argv, '--baseline')[0]
862
+ const baselinePath = baselineOption ? resolve(root, baselineOption) : undefined
863
+
864
+ if (flags.has('--update-baseline')) {
865
+ const approvedBy = optionValues(argv, '--by')[0]
866
+ if (!baselinePath) throw new Error('--update-baseline requires --baseline <file>.')
867
+ if (!approvedBy) throw new Error('--update-baseline requires --by <name>: a baseline is an approved figure, not a side effect of a run.')
868
+ const reason = optionValues(argv, '--reason')[0]
869
+ const baseline = createRetrievalBaseline({ result, approvedBy, ...(reason ? { reason } : {}) })
870
+ mkdirSync(dirname(baselinePath), { recursive: true })
871
+ writeFileSync(baselinePath, `${JSON.stringify(baseline, null, 2)}\n`, 'utf8')
872
+ if (wantsTextOutput(flags, config)) {
873
+ writeLines([...formatRetrievalBenchText(result), `Baseline written: ${relative(root, baselinePath)} (approved by ${approvedBy})`])
874
+ } else writeJson({ ok: true, result, baseline, baselinePath })
875
+ return 0
876
+ }
877
+
878
+ if (baselinePath && !existsSync(baselinePath)) {
879
+ throw new Error(
880
+ `No baseline at ${relative(root, baselinePath)}. Record the current figures with: ak-docs bench retrieval ${positional[2]} --baseline ${baselineOption} --update-baseline --by <name>`,
881
+ )
882
+ }
883
+ const comparison = baselinePath
884
+ ? compareRetrievalBaseline(result, parseRetrievalBaseline(JSON.parse(readFileSync(baselinePath, 'utf8')) as unknown))
885
+ : undefined
886
+
887
+ if (wantsTextOutput(flags, config)) {
888
+ writeLines([...formatRetrievalBenchText(result), ...(comparison ? formatRetrievalComparisonText(comparison) : [])])
889
+ } else {
890
+ writeJson({ ok: !comparison?.blocking, result, ...(comparison ? { comparison } : {}) })
891
+ }
892
+ return comparison?.blocking ? 1 : 0
893
+ } catch (error) {
894
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
895
+ return 2
896
+ }
897
+ }
898
+
899
+ const runDocumentationAuditCommand = (
900
+ flags: ReadonlySet<string>,
901
+ positional: readonly string[],
902
+ configPath: string | undefined,
903
+ ): number => {
904
+ if (positional[1] !== 'documentation') {
905
+ process.stderr.write('Usage: ak-docs audit documentation [--text|--json]\n')
906
+ return 1
907
+ }
908
+ try {
909
+ const { config, root } = loadProject(configPath)
910
+ const report = buildDocumentationAuditReport(root, config)
911
+ if (wantsTextOutput(flags, config)) writeLines(formatDocumentationAuditText(report))
912
+ else writeJson({ ok: report.status !== 'blocked', report })
913
+ return report.status === 'blocked' ? 1 : 0
914
+ } catch (error) {
915
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
916
+ return 2
917
+ }
918
+ }
919
+
920
+ const RENDER_USAGE = [
921
+ `Usage: ak-docs render <${RENDER_TEMPLATE_NAMES.join('|')}> [--data <artifact>] [--output <path>] [--print-template] [--json]`,
922
+ ...RENDER_TEMPLATE_NAMES.map((name) => ` ${name.padEnd(16)} ${RENDER_TEMPLATES[name].description} (--data: ${RENDER_TEMPLATES[name].data})`),
923
+ ].join('\n')
924
+
925
+ const runRenderCommand = (
926
+ flags: ReadonlySet<string>,
927
+ positional: readonly string[],
928
+ configPath: string | undefined,
929
+ argv: readonly string[],
930
+ ): number => {
931
+ // parseArgs files option values as positionals; the template name is the first one no option consumed.
932
+ const dataPath = optionValues(argv, '--data')[0]
933
+ const outputPath = optionValues(argv, '--output')[0]
934
+ const consumed = new Set([dataPath, outputPath].filter(Boolean))
935
+ const name = positional.slice(1).find((value) => !consumed.has(value))
936
+ if (!name || !isRenderTemplateName(name)) {
937
+ process.stderr.write(`${name ? `Unknown template "${name}".\n` : ''}${RENDER_USAGE}\n`)
938
+ return 1
939
+ }
940
+ try {
941
+ const { config, root } = loadProject(configPath)
942
+ if (flags.has('--print-template')) {
943
+ process.stdout.write(resolveTemplateSource(name, config, root).source)
944
+ return 0
945
+ }
946
+ const result = renderArtifact({ root, config, template: name, ...(dataPath ? { dataPath } : {}) })
947
+ if (outputPath) {
948
+ const written = writeRenderedPages(result, resolve(root, outputPath), root)
949
+ writeJson({ ok: true, template: name, source: result.origin, written })
950
+ return 0
951
+ }
952
+ if (flags.has('--json')) {
953
+ writeJson({ ok: true, template: name, source: result.origin, pages: result.pages })
954
+ return 0
955
+ }
956
+ process.stdout.write(result.pages.map((page) => page.content).join('\n'))
957
+ return 0
958
+ } catch (error) {
959
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
960
+ return 2
961
+ }
962
+ }
963
+
625
964
  const runRulesCommand = (
626
965
  argv: readonly string[],
627
966
  flags: ReadonlySet<string>,
@@ -672,7 +1011,7 @@ const runRulesCommand = (
672
1011
  }
673
1012
  }
674
1013
 
675
- const runFixCommand = (argv: readonly string[], positional: readonly string[], configPath: string | undefined): number => {
1014
+ const runFixCommand = async (argv: readonly string[], positional: readonly string[], configPath: string | undefined): Promise<number> => {
676
1015
  try {
677
1016
  const { config, root } = loadProject(configPath)
678
1017
  const action = positional[1]
@@ -693,8 +1032,12 @@ const runFixCommand = (argv: readonly string[], positional: readonly string[], c
693
1032
  const file = resolve(root, proposalPath)
694
1033
  const proposal = JSON.parse(readFileSync(file, 'utf8')) as unknown
695
1034
  const result = action === 'approve' ? approveFixProposal(proposal, optionValues(argv, '--by')[0] ?? 'human') : applyFixProposal(root, proposal, { currentRevision: sourceRevision })
1035
+ // An approval is recorded through the ecosystem gate too, bound to the proposal and its exact content hash.
1036
+ const recorded = action === 'approve' && result.approval
1037
+ ? await recordApproval(root, { id: fixApprovalId(result.proposalId, result.approval.proposalHash), name: FIX_APPROVAL_GATE, payload: { proposalId: result.proposalId, proposalHash: result.approval.proposalHash }, decision: 'approved', by: result.approval.approvedBy })
1038
+ : undefined
696
1039
  writeFileSync(file, `${JSON.stringify(result, null, 2)}\n`, 'utf8')
697
- writeJson({ ok: true, proposal: result, proposalPath: file })
1040
+ writeJson({ ok: true, proposal: result, proposalPath: file, ...(recorded ? { approvalId: recorded.approval.id } : {}) })
698
1041
  return 0
699
1042
  } catch (error) {
700
1043
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
@@ -702,17 +1045,128 @@ const runFixCommand = (argv: readonly string[], positional: readonly string[], c
702
1045
  }
703
1046
  }
704
1047
 
1048
+ const runEnrichCommand = async (flags: ReadonlySet<string>, positional: readonly string[], argv: readonly string[], configPath: string | undefined): Promise<number> => {
1049
+ try {
1050
+ const { config, root } = loadProject(configPath)
1051
+ const action = positional[1]
1052
+ if (action === 'list') {
1053
+ const review = listEnrichment(root)
1054
+ if (wantsTextOutput(flags, config)) {
1055
+ writeLines(review
1056
+ ? [`Overlay: ${review.overlayHash}`, `Accepted: ${review.accepted.length} Pending: ${review.pending.length} Rejected: ${review.rejected.length}`, ...review.pending.map((entry) => ` pending ${entry.proposal.proposalId.slice(0, 16)} ${entry.proposal.kind} ${entry.proposal.entity}${entry.note ? ` (${entry.note})` : ''}`)]
1057
+ : ['No enrichment overlay. Run `ak-docs enrich` first.'])
1058
+ } else writeJson({ ok: true, enrichment: review ?? null })
1059
+ return 0
1060
+ }
1061
+ if (action === 'approve' || action === 'reject') {
1062
+ const proposalId = positional[2]
1063
+ const by = optionValues(argv, '--by')[0]
1064
+ if (!proposalId || !by) throw new Error('Usage: ak-docs enrich approve|reject <proposalId> --by <name> [--reason <text>]')
1065
+ const stateDir = resolve(root, config.workflow?.stateDir ?? '.doc-bridge/workflow')
1066
+ const snapshot = (() => { try { return parseDiscoverySnapshot(loadWorkflowStepOutput(stateDir, 'normalize')) } catch { return undefined } })()
1067
+ const reason = optionValues(argv, '--reason')[0]
1068
+ const decided = await decideEnrichment({ root, proposalId, decision: action === 'approve' ? 'approved' : 'rejected', by, ...(reason ? { reason } : {}), ...(snapshot ? { snapshot } : {}) })
1069
+ const decidedId = 'proposal' in decided.entry ? decided.entry.proposal.proposalId : decided.entry.proposalId
1070
+ if (wantsTextOutput(flags, config)) writeLines([`${action === 'approve' ? 'Approved' : 'Rejected'}: ${decidedId}`, `Approval: ${decided.approvalId} (${decided.gateSource})`, `Overlay: ${decided.overlay.contentHash}`])
1071
+ else writeJson({ ok: true, approvalId: decided.approvalId, gate: decided.gateSource, entry: decided.entry, overlayHash: decided.overlay.contentHash })
1072
+ return 0
1073
+ }
1074
+ if (action !== undefined) throw new Error('Usage: ak-docs enrich [list | approve <proposalId> --by <name> | reject <proposalId> --by <name>] [--json|--text]')
1075
+ const reconciled = reconcileWorkflow(root, config)
1076
+ const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(reconciled.stateDir, 'normalize'))
1077
+ const report = parseReconciliationReport(loadWorkflowStepOutput(reconciled.stateDir, 'reconcile'))
1078
+ const result = await runEnrichment({ root, config, snapshot, report })
1079
+ attachEnrichmentStage(root, config, report, result)
1080
+ /*
1081
+ * The retrieval delta is opt-in: it runs the golden suite twice, which is the right cost for
1082
+ * an answer about whether the overlay helped and the wrong cost for every routine run.
1083
+ */
1084
+ const delta = flags.has('--retrieval-delta')
1085
+ ? measureOverlayRetrievalDelta({
1086
+ root,
1087
+ config,
1088
+ snapshot,
1089
+ overlay: result.overlay,
1090
+ suite: parseRetrievalSuite(JSON.parse(readFileSync(resolve(root, config.retrieval?.benchmark?.suite ?? DEFAULT_RETRIEVAL_SUITE), 'utf8')) as unknown),
1091
+ })
1092
+ : undefined
1093
+ if (wantsTextOutput(flags, config)) writeLines([...formatEnrichmentText(result), ...(delta ? formatOverlayRetrievalDeltaText(delta) : [])])
1094
+ else writeJson({
1095
+ ok: !delta?.regression,
1096
+ overlayPath: result.overlayPath,
1097
+ overlayHash: result.overlay.contentHash,
1098
+ baseSnapshotHash: result.overlay.baseSnapshotHash,
1099
+ roles: result.roles,
1100
+ packs: result.packs,
1101
+ agentCalls: result.agentCalls,
1102
+ cacheHits: result.cacheHits,
1103
+ rerun: result.rerun,
1104
+ expired: result.expired,
1105
+ stats: result.overlay.stats,
1106
+ cost: enrichmentCost(result.overlay.stats),
1107
+ stability: result.stability,
1108
+ accepted: result.overlay.accepted.length,
1109
+ pending: result.overlay.pending.length,
1110
+ rejected: result.overlay.rejected.length,
1111
+ ...(delta ? { retrievalDelta: delta } : {}),
1112
+ })
1113
+ return delta?.regression ? 1 : 0
1114
+ } catch (error) {
1115
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
1116
+ return 2
1117
+ }
1118
+ }
1119
+
1120
+ /** Where the claim registry lives unless a caller points somewhere else. */
1121
+ export const DEFAULT_PUBLIC_CLAIMS = 'docs/parity/public-claims-v1.json'
1122
+
1123
+ /**
1124
+ * `ak-docs parity`: what this repository says in public, against what it can prove.
1125
+ *
1126
+ * The gate fails on a blocking finding — a stale or contradictory claim nobody accepted — and
1127
+ * leaves warnings and unresolved claims visible without failing, so a repository can adopt the
1128
+ * registry one claim at a time. The doctor runs only when a claim asks for one of its figures:
1129
+ * measuring it costs an index and a benchmark, and most registries never need it.
1130
+ */
1131
+ const runParityCommand = (flags: ReadonlySet<string>, configPath: string | undefined, argv: readonly string[]): number => {
1132
+ try {
1133
+ const { config, root } = loadProject(configPath)
1134
+ const claimsPath = optionValues(argv, '--claims')[0] ?? DEFAULT_PUBLIC_CLAIMS
1135
+ const registry = parsePublicClaims(JSON.parse(readFileSync(resolve(root, claimsPath), 'utf8')) as unknown)
1136
+ const snapshot = discoverRepository({ root, config })
1137
+ const doctor = registry.claims.some((claim) => claim.evidence.kind === 'doctor-metric') ? runDoctor(root, config) : undefined
1138
+ const report = checkPublicParity({
1139
+ root,
1140
+ config,
1141
+ registry,
1142
+ snapshot,
1143
+ ...(doctor ? { doctor } : {}),
1144
+ project: { name: snapshot.project.name },
1145
+ sourceRevision: snapshot.sourceRevision,
1146
+ sourceRevisionKind: snapshot.sourceRevisionKind,
1147
+ })
1148
+ if (wantsTextOutput(flags, config)) writeLines(formatPublicParityText(report))
1149
+ else writeJson({ ok: report.metrics.blocking === 0, parity: report })
1150
+ return report.metrics.blocking ? 1 : 0
1151
+ } catch (error) {
1152
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
1153
+ return 2
1154
+ }
1155
+ }
1156
+
705
1157
  const runSuggestCommand = async (flags: ReadonlySet<string>, configPath: string | undefined): Promise<number> => {
706
1158
  try {
707
1159
  const { config, root } = loadProject(configPath)
708
1160
  const stateDir = resolve(root, config.workflow?.stateDir ?? '.doc-bridge/workflow')
709
1161
  const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(stateDir, 'normalize'))
710
1162
  const report = parseReconciliationReport(loadWorkflowStepOutput(stateDir, 'reconcile'))
711
- const adapter = createRegistryAgentAdapter(root, config, await loadRegistryAgentRunner(root, config))
712
- const proposal = await adapter.run(snapshot, report)
1163
+ const runner = config.intelligence?.registry?.cli ? undefined : await loadRegistryAgentRunner(root, config)
1164
+ const adapter = createRegistryAgentAdapter(root, config, runner)
1165
+ const documentation = flags.has('--documentation') ? buildDocumentationAuditReport(root, config) : undefined
1166
+ const proposal = await adapter.run(snapshot, report, undefined, documentation)
713
1167
  const proposalPath = persistRegistryAgentProposal(stateDir, proposal)
714
- if (flags.has('--text')) writeLines([`Agent: ${adapter.metadata.id}`, `Proposal: ${proposal.proposalId}`, `Hash: ${proposal.contentHash}`, `Saved: ${proposalPath}`])
715
- else writeJson({ ok: true, proposal, proposalPath })
1168
+ if (flags.has('--text')) writeLines([`Agent: ${adapter.metadata.id}`, ...(documentation ? [`Documentation audit: ${documentation.contentHash}`] : []), `Proposal: ${proposal.proposalId}`, `Hash: ${proposal.contentHash}`, `Saved: ${proposalPath}`])
1169
+ else writeJson({ ok: true, ...(documentation ? { documentationAuditHash: documentation.contentHash } : {}), proposal, proposalPath })
716
1170
  return 0
717
1171
  } catch (error) {
718
1172
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
@@ -901,6 +1355,143 @@ const registryTopology = () => ({
901
1355
  mergePolicy: { autoMerge: false, requiresHuman: true },
902
1356
  })
903
1357
 
1358
+ const runStudyCommand = async (flags: ReadonlySet<string>, positional: readonly string[], argv: readonly string[], configPath: string | undefined): Promise<number> => {
1359
+ const action = positional[1]
1360
+ const inputPath = positional[2]
1361
+ if (!inputPath || !['protocol', 'history', 'tasks', 'select', 'plan', 'providers', 'run', 'adjudicate', 'ledger', 'metrics', 'verification', 'expectations'].includes(action ?? '')) {
1362
+ process.stderr.write('Usage: ak-docs study protocol|history|tasks|select|plan|providers|run|adjudicate|ledger|metrics|verification|expectations <artifact.json> [--protocol <protocol.json>] [--providers <provider-cli.json>] [--adjudicator <provider-cli.json>] [--repositories <repositories.json>] [--ledger <ledger.json>] [--output <ledger.json>] [--run-id <id>] [--limit <n>] [--round <id>] [--dry-run] [--baseline-round <id>] [--current-round <id>] [--expectations <expectations.json>] [--index <index.json>] [--repository <id>] [--text|--json]\n')
1363
+ return 1
1364
+ }
1365
+ try {
1366
+ if (action === 'expectations') {
1367
+ /*
1368
+ * The mechanical half of the study, checked by the same benchmark that gates this
1369
+ * repository's retrieval. The suite carries opaque references; the local expectations file
1370
+ * resolves them to entities and documents, which is why it is never published.
1371
+ */
1372
+ const expectationsPath = optionValues(argv, '--expectations')[0]
1373
+ if (!expectationsPath) throw new Error('Study expectations require --expectations <expectations.json>.')
1374
+ const suite = parseStudyTaskSuite(JSON.parse(readFileSync(resolve(inputPath), 'utf8')) as unknown)
1375
+ const expectations = parseStudyExpectations(JSON.parse(readFileSync(resolve(expectationsPath), 'utf8')) as unknown)
1376
+ const repositoryId = optionValues(argv, '--repository')[0]
1377
+ const indexOption = optionValues(argv, '--index')[0]
1378
+ const { config, root } = loadProject(configPath)
1379
+ const index = indexOption
1380
+ ? parseDocBridgeIndex(JSON.parse(readFileSync(resolve(root, indexOption), 'utf8')) as unknown)
1381
+ : loadFreshDocBridgeIndex(root, config)
1382
+ const check = checkStudyExpectations({ taskSuite: suite, expectations, index, ...(repositoryId ? { repositoryId } : {}) })
1383
+ if (flags.has('--text')) writeLines(formatStudyExpectationsText(check))
1384
+ else writeJson({ ok: check.ok, expectations: check })
1385
+ return check.ok ? 0 : 1
1386
+ }
1387
+ if (action === 'run') {
1388
+ const taskSuitePath = positional[3]
1389
+ const providersPath = optionValues(argv, '--providers')[0]
1390
+ const repositoriesPath = optionValues(argv, '--repositories')[0]
1391
+ const ledgerPath = optionValues(argv, '--ledger')[0]
1392
+ if (!taskSuitePath || !providersPath || !repositoriesPath || !ledgerPath) throw new Error('Study run requires a task suite, --providers, --repositories, and --ledger.')
1393
+ const summary = await runControlledStudy({
1394
+ plan: parseControlledStudyRunPlan(JSON.parse(readFileSync(resolve(inputPath), 'utf8')) as unknown),
1395
+ suite: parseStudyTaskSuite(JSON.parse(readFileSync(resolve(taskSuitePath), 'utf8')) as unknown),
1396
+ providers: parseStudyProviderCliConfig(JSON.parse(readFileSync(resolve(providersPath), 'utf8')) as unknown),
1397
+ repositories: parseStudyRepositoryConfig(JSON.parse(readFileSync(resolve(repositoriesPath), 'utf8')) as unknown),
1398
+ ledgerPath: resolve(ledgerPath),
1399
+ ...(optionValues(argv, '--round')[0] === undefined ? {} : { round: optionValues(argv, '--round')[0] }),
1400
+ ...(flags.has('--dry-run') ? { dryRun: true } : {}),
1401
+ })
1402
+ if (flags.has('--text')) writeLines(formatControlledStudyRunText(summary))
1403
+ else writeJson({ ok: summary.status === 'dry-run' || summary.status === 'completed', summary })
1404
+ return 0
1405
+ }
1406
+ if (action === 'adjudicate') {
1407
+ const taskSuitePath = positional[3]
1408
+ const adjudicatorConfigPath = optionValues(argv, '--adjudicator')[0]
1409
+ const outputPath = optionValues(argv, '--output')[0]
1410
+ if (!taskSuitePath || !adjudicatorConfigPath || !outputPath) throw new Error('Study adjudication requires a task suite, --adjudicator with an adjudicator, and --output.')
1411
+ const config = parseStudyProviderCliConfig(JSON.parse(readFileSync(resolve(adjudicatorConfigPath), 'utf8')) as unknown)
1412
+ if (!config.adjudicator) throw new Error('Study provider config must declare an adjudicator.')
1413
+ const ledger = parseControlledStudyLedger(JSON.parse(readFileSync(resolve(inputPath), 'utf8')) as unknown)
1414
+ const suite = parseStudyTaskSuite(JSON.parse(readFileSync(resolve(taskSuitePath), 'utf8')) as unknown)
1415
+ const limitValue = optionValues(argv, '--limit')[0]
1416
+ const limit = limitValue === undefined ? undefined : Number(limitValue)
1417
+ const offsetValue = optionValues(argv, '--offset')[0]
1418
+ const offset = offsetValue === undefined ? undefined : Number(offsetValue)
1419
+ const result = await independentlyAdjudicateStudyLedger({ ledger, taskSuite: suite, config: config.adjudicator, configurationHash: config.contentHash, cwd: process.cwd(), maxRuntimeMs: suite.maxRuntimeMsPerTask, ...(optionValues(argv, '--run-id')[0] === undefined ? {} : { runId: optionValues(argv, '--run-id')[0] }), ...(offset === undefined ? {} : { offset }), ...(limit === undefined ? {} : { limit }) })
1420
+ persistIndependentlyAdjudicatedLedger(outputPath, result)
1421
+ if (flags.has('--text')) writeLines([`Adjudicated observations: ${result.observations.length}`, `Ledger: ${resolve(outputPath)}`, `Content hash: ${result.contentHash}`])
1422
+ else writeJson({ ok: true, ledger: result })
1423
+ return 0
1424
+ }
1425
+ const input = JSON.parse(readFileSync(resolve(inputPath), 'utf8')) as unknown
1426
+ if (action === 'protocol') {
1427
+ const protocol = parseStudyProtocol(input)
1428
+ if (flags.has('--text')) writeLines(formatStudyProtocolText(protocol))
1429
+ else writeJson({ ok: true, protocol })
1430
+ return 0
1431
+ }
1432
+ if (action === 'tasks') {
1433
+ const suite = parseStudyTaskSuite(input)
1434
+ if (flags.has('--text')) writeLines(formatStudyTaskSuiteText(suite))
1435
+ else writeJson({ ok: true, suite })
1436
+ return 0
1437
+ }
1438
+ if (action === 'select') {
1439
+ const suite = parseStudyTaskSuite(input)
1440
+ const executions = selectTaskExecutions(suite)
1441
+ if (flags.has('--text')) writeLines([`Selected executions: ${executions.length}`, `First execution: ${executions[0]?.taskId ?? 'none'} / ${executions[0]?.variantId ?? 'none'}`])
1442
+ else writeJson({ ok: true, executions })
1443
+ return 0
1444
+ }
1445
+ if (action === 'plan') {
1446
+ const plan = parseControlledStudyRunPlan(input)
1447
+ if (flags.has('--text')) writeLines(formatControlledStudyRunPlanText(plan))
1448
+ else writeJson({ ok: true, plan })
1449
+ return 0
1450
+ }
1451
+ if (action === 'providers') {
1452
+ const providers = parseStudyProviderCliConfig(input)
1453
+ if (flags.has('--text')) writeLines(formatStudyProviderCliText(providers))
1454
+ else writeJson({ ok: true, providers })
1455
+ return 0
1456
+ }
1457
+ if (action === 'ledger') {
1458
+ const ledger = parseControlledStudyLedger(input)
1459
+ if (flags.has('--text')) writeLines([`Ledger: ${ledger.ledgerVersion}`, `Observations: ${ledger.observations.length}`, `Content hash: ${ledger.contentHash}`])
1460
+ else writeJson({ ok: true, ledger })
1461
+ return 0
1462
+ }
1463
+ if (action === 'metrics') {
1464
+ const ledger = parseControlledStudyLedger(input)
1465
+ const baselineRound = optionValues(argv, '--baseline-round')[0]
1466
+ const currentRound = optionValues(argv, '--current-round')[0]
1467
+ const baselineRunId = optionValues(argv, '--baseline-run-id')[0]
1468
+ const currentRunId = optionValues(argv, '--current-run-id')[0]
1469
+ const report = calculateStudyMetrics(ledger.observations, { ...(baselineRound === undefined ? {} : { baselineRound }), ...(currentRound === undefined ? {} : { currentRound }), ...(baselineRunId === undefined ? {} : { baselineRunId }), ...(currentRunId === undefined ? {} : { currentRunId }) })
1470
+ if (flags.has('--text')) writeLines(formatStudyMetricsText(report))
1471
+ else writeJson({ ok: true, report })
1472
+ return report.comparisons.some((comparison) => comparison.status === 'regressed') && !flags.has('--allow-regressions') ? 1 : 0
1473
+ }
1474
+ if (action === 'verification') {
1475
+ const binding = parseStudyVerificationBinding(input)
1476
+ if (flags.has('--text')) writeLines(formatStudyVerificationText(binding))
1477
+ else writeJson({ ok: true, binding })
1478
+ return 0
1479
+ }
1480
+ const registry = parseHistoricalEvidenceRegistry(input)
1481
+ const protocolPath = optionValues(argv, '--protocol')[0]
1482
+ if (protocolPath) {
1483
+ const protocol = parseStudyProtocol(JSON.parse(readFileSync(resolve(protocolPath), 'utf8')) as unknown)
1484
+ validateHistoricalEvidenceRegistry(registry, protocol)
1485
+ }
1486
+ if (flags.has('--text')) writeLines(formatHistoricalEvidenceText(registry))
1487
+ else writeJson({ ok: true, registry })
1488
+ return 0
1489
+ } catch (error) {
1490
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
1491
+ return 2
1492
+ }
1493
+ }
1494
+
904
1495
  export const runCli = (argv: readonly string[]): number | undefined | Promise<number> => {
905
1496
  const { command, flags, configPath, positional } = parseArgs(argv)
906
1497
 
@@ -990,12 +1581,19 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
990
1581
  }
991
1582
  }
992
1583
 
1584
+ if (command === 'study') return runStudyCommand(flags, positional, argv, configPath)
1585
+
993
1586
  if (command === 'scan' || command === 'reconcile' || command === 'check' || command === 'map') {
994
1587
  return runWorkflowCommand(command, flags, configPath, argv)
995
1588
  }
1589
+ if (command === 'audit') return runDocumentationAuditCommand(flags, positional, configPath)
1590
+ if (command === 'parity') return runParityCommand(flags, configPath, argv)
1591
+ if (command === 'render') return runRenderCommand(flags, positional, configPath, argv)
1592
+ if (command === 'bench') return runBenchCommand(flags, positional, configPath, argv)
996
1593
 
997
1594
  if (command === 'fix') return runFixCommand(argv, positional, configPath)
998
1595
  if (command === 'suggest') return runSuggestCommand(flags, configPath)
1596
+ if (command === 'enrich') return runEnrichCommand(flags, positional, argv, configPath)
999
1597
 
1000
1598
  if (command === 'init') {
1001
1599
  const root = process.cwd()
@@ -1066,7 +1664,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1066
1664
  writeJson({ ok: true, count: candidates.length, candidates })
1067
1665
  return 0
1068
1666
  }
1069
- const index = loadDocBridgeIndex(root, config)
1667
+ const index = loadFreshDocBridgeIndex(root, config)
1070
1668
  const classifications = classifyMemoryCandidates(candidates, index)
1071
1669
  if (positional[1] === 'classify') {
1072
1670
  writeJson({ ok: true, count: classifications.length, classifications })
@@ -1124,7 +1722,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1124
1722
  }
1125
1723
  try {
1126
1724
  const { config, root } = loadProject(configPath)
1127
- const index = loadDocBridgeIndex(root, config)
1725
+ const index = loadFreshDocBridgeIndex(root, config)
1128
1726
  const draft = draftMemoryPromotion(classifyMemoryCandidates(ingestMemoryCandidates(root), index))
1129
1727
  writeJson({
1130
1728
  ...draft,
@@ -1329,9 +1927,10 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1329
1927
  }
1330
1928
  try {
1331
1929
  const { config, root } = loadProject(configPath)
1332
- const index = loadDocBridgeIndex(root, config)
1333
- const result = runQuery(index, config, { kind, id, agent: flags.has('--agent') })
1930
+ const index = loadFreshDocBridgeIndex(root, config)
1931
+ const result = runQuery(index, config, { kind, id, agent: flags.has('--agent') }, { root })
1334
1932
  if (wantsTextOutput(flags, config)) writeTextQuery(result)
1933
+ else if (flags.has('--agent')) writeAgentJson(result)
1335
1934
  else writeJson(result)
1336
1935
  return 0
1337
1936
  } catch (error) {
@@ -1348,12 +1947,23 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1348
1947
  }
1349
1948
  try {
1350
1949
  const { config, root } = loadProject(configPath)
1351
- const index = loadDocBridgeIndex(root, config)
1950
+ const index = loadFreshDocBridgeIndex(root, config)
1951
+ const explain = flags.has('--explain')
1352
1952
  if (flags.has('--agent')) {
1353
- const result = runQuery(index, config, { kind: 'search', term, agent: true })
1354
- writeJson(result)
1953
+ const mode = optionValues(argv, '--mode')[0]
1954
+ const budgetValue = optionValues(argv, '--context-budget')[0]
1955
+ const contextBudgetTokens = budgetValue === undefined ? undefined : Number(budgetValue)
1956
+ const result = runQuery(index, config, {
1957
+ kind: 'search',
1958
+ term,
1959
+ agent: true,
1960
+ ...(explain ? { explain: true } : {}),
1961
+ ...(mode === undefined ? {} : { mode: mode as 'discovery' | 'editing' | 'debugging' | 'documentation' }),
1962
+ ...(contextBudgetTokens === undefined ? {} : { contextBudgetTokens }),
1963
+ })
1964
+ writeAgentJson(result)
1355
1965
  } else {
1356
- const matches = searchIndex(index, term)
1966
+ const matches = searchIndex(index, term, 20, explain ? { explain: true } : {})
1357
1967
  if (wantsTextOutput(flags, config)) writeTextSearch(term, matches)
1358
1968
  else writeJson({ term, count: matches.length, matches })
1359
1969
  }
@@ -1377,7 +1987,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1377
1987
  return (async () => {
1378
1988
  try {
1379
1989
  const { config, root } = loadProject(configPath)
1380
- const index = loadDocBridgeIndex(root, config)
1990
+ const index = loadFreshDocBridgeIndex(root, config)
1381
1991
  writeJson({ query, chunks: await retrieveHybridChunks(root, config, index, query) })
1382
1992
  return 0
1383
1993
  } catch (error) {
@@ -1396,7 +2006,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1396
2006
  return (async () => {
1397
2007
  try {
1398
2008
  const { config, root } = loadProject(configPath)
1399
- const index = loadDocBridgeIndex(root, config)
2009
+ const index = loadFreshDocBridgeIndex(root, config)
1400
2010
  const rag = await createDocBridgeRag(root, config, index)
1401
2011
  if (action === 'ingest') {
1402
2012
  const result = await rag.ingest()
@@ -1433,7 +2043,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1433
2043
  )
1434
2044
  return 1
1435
2045
  }
1436
- const index = loadDocBridgeIndex(root, config)
2046
+ const index = loadFreshDocBridgeIndex(root, config)
1437
2047
  await startInkChat(root, config, index)
1438
2048
  return 0
1439
2049
  } catch (error) {
@@ -1465,7 +2075,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1465
2075
  }
1466
2076
  return (async () => {
1467
2077
  try {
1468
- const index = loadDocBridgeIndex(root, config)
2078
+ const index = loadFreshDocBridgeIndex(root, config)
1469
2079
  const result = await runChatOnce(root, config, index, question)
1470
2080
  writeLines([result.content])
1471
2081
  return 0
@@ -1484,7 +2094,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1484
2094
  process.stderr.write('Usage: ak-docs ask <question>, or run ak-docs ask in an interactive terminal.\n')
1485
2095
  return 1
1486
2096
  }
1487
- const index = loadDocBridgeIndex(root, config)
2097
+ const index = loadFreshDocBridgeIndex(root, config)
1488
2098
  writeAsk(question, searchIndex(index, question, 8), index, config)
1489
2099
  return 0
1490
2100
  } catch (error) {
@@ -1501,7 +2111,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1501
2111
  }
1502
2112
  try {
1503
2113
  const { config, root } = loadProject(configPath)
1504
- const index = loadDocBridgeIndex(root, config)
2114
+ const index = loadFreshDocBridgeIndex(root, config)
1505
2115
 
1506
2116
  if (kind === 'packages') {
1507
2117
  const items = index.lookup?.packages ?? []