@agentskit/doc-bridge 1.7.44 → 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 (223) hide show
  1. package/CHANGELOG.md +471 -0
  2. package/CONTRIBUTING.md +29 -4
  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 +15888 -6061
  10. package/dist/cli/program.js.map +1 -1
  11. package/dist/config/index.d.ts +1 -1
  12. package/dist/config/index.js +91 -9
  13. package/dist/config/index.js.map +1 -1
  14. package/dist/index-Beor6Yhi.d.ts +792 -0
  15. package/dist/index.d.ts +9979 -3257
  16. package/dist/index.js +15954 -5774
  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/RELEASE.md +22 -8
  22. package/docs/adr/0002-documentation-audit-boundary.md +22 -0
  23. package/docs/adr/0003-study-protocol-and-historical-evidence.md +40 -0
  24. package/docs/adr/0004-controlled-study-runner.md +25 -0
  25. package/docs/adr/0005-documentation-quality-and-criticality.md +20 -0
  26. package/docs/adr/0006-registry-semantic-grounding.md +20 -0
  27. package/docs/adr/0007-longitudinal-study-metrics.md +21 -0
  28. package/docs/adr/0008-study-verification-boundary.md +21 -0
  29. package/docs/adr/0009-study-provider-cli-adapter.md +25 -0
  30. package/docs/agent-corpus/INDEX.md +14 -3
  31. package/docs/agent-corpus/OVERVIEW.md +25 -0
  32. package/docs/agent-corpus/chat.md +7 -3
  33. package/docs/agent-corpus/cli.md +18 -2
  34. package/docs/agent-corpus/conformance.md +14 -2
  35. package/docs/agent-corpus/doc-bridge.md +48 -1
  36. package/docs/agent-corpus/doctor.md +10 -2
  37. package/docs/agent-corpus/gates.md +6 -2
  38. package/docs/agent-corpus/mcp.md +15 -2
  39. package/docs/agent-corpus/memory.md +6 -2
  40. package/docs/agent-corpus/query.md +35 -2
  41. package/docs/bench/README.md +122 -0
  42. package/docs/bench/retrieval-baseline-v1.json +28 -0
  43. package/docs/bench/retrieval-suite-v1.json +1033 -0
  44. package/docs/chat-and-rag.md +3 -2
  45. package/docs/for-agents.md +9 -1
  46. package/docs/getting-started.md +4 -11
  47. package/docs/guides/gate-ci.md +11 -1
  48. package/docs/guides/install-and-run.md +9 -65
  49. package/docs/index.md +22 -1
  50. package/docs/knowledge-engine-runbook.md +51 -4
  51. package/docs/landing/assets/context-payload-reduction.svg +21 -0
  52. package/docs/landing/assets/controlled-ab-comparison.svg +30 -0
  53. package/docs/landing/index.html +119 -5
  54. package/docs/loop-workflow.md +117 -0
  55. package/docs/mcp.md +6 -1
  56. package/docs/parity/public-claims-v1.json +145 -0
  57. package/docs/playbook/doc-bridge-pattern.md +1 -1
  58. package/docs/query.md +90 -2
  59. package/docs/recipes/index-pipeline.md +1 -1
  60. package/docs/schemas/agent-handoff-v1.md +15 -0
  61. package/docs/schemas/doc-bridge-index-v1.md +65 -0
  62. package/docs/spec/benchmark-v1.md +39 -1
  63. package/docs/spec/cli.md +30 -10
  64. package/docs/spec/config-v1.md +192 -8
  65. package/docs/spec/documentation-audit-v1.md +61 -0
  66. package/docs/spec/enrichment-overlay-v1.md +241 -0
  67. package/docs/spec/graph-signals-v1.md +92 -0
  68. package/docs/spec/incremental-scan-v1.md +102 -0
  69. package/docs/spec/markdown-analyzer-v1.md +73 -0
  70. package/docs/spec/mcp-knowledge-tools-v1.md +147 -0
  71. package/docs/spec/measured-enrichment-v1.md +229 -0
  72. package/docs/spec/public-parity-v1.md +119 -0
  73. package/docs/spec/registry-agents.md +6 -0
  74. package/docs/spec/render-v1.md +122 -0
  75. package/docs/spec/retrieval-index-v1.md +164 -0
  76. package/docs/spec/study-metrics-v1.md +58 -0
  77. package/docs/spec/study-protocol-v1.md +46 -0
  78. package/docs/spec/study-provider-cli-v1.md +116 -0
  79. package/docs/spec/study-runner-v1.md +35 -0
  80. package/docs/spec/study-task-suite-v1.md +41 -0
  81. package/docs/spec/study-verification-v1.md +40 -0
  82. package/docs/study/README.md +84 -0
  83. package/docs/study/ab-adjudicated-cost-analysis-v1.md +29 -0
  84. package/docs/study/ab-adjudicated-cost-plan-v1.json +33 -0
  85. package/docs/study/ab-adjudicated-cost-plan-v2-v1.json +33 -0
  86. package/docs/study/ab-adjudicated-cost-result-v1.json +80 -0
  87. package/docs/study/ab-baseline-analysis-v1.md +21 -0
  88. package/docs/study/ab-baseline-plan-v1.json +33 -0
  89. package/docs/study/ab-baseline-recovery-plan-v1.json +33 -0
  90. package/docs/study/ab-baseline-result-v1.json +79 -0
  91. package/docs/study/documentation-audit-round-2026-08-31.json +183 -0
  92. package/docs/study/historical-evidence-v1.json +252 -0
  93. package/docs/study/observation-ledger-v1.json +30632 -0
  94. package/docs/study/phase3-task-coverage-v1.json +34 -0
  95. package/docs/study/phase4-public-pilot-ledger-v1.json +1344 -0
  96. package/docs/study/phase4-public-pilot-result-v1.json +52 -0
  97. package/docs/study/phase4-public-pilot-run-plan-v1.json +26 -0
  98. package/docs/study/phase4-public-pilot-task-suite-v1.json +71 -0
  99. package/docs/study/pilot-round-2026-08-31.json +46 -0
  100. package/docs/study/protocol-v1.json +90 -0
  101. package/docs/study/publication-gate-v1.md +45 -0
  102. package/docs/study/quality-scorecard-cycle-plan.md +545 -0
  103. package/docs/study/quality-scorecard-v1.json +38 -0
  104. package/docs/study/round-1-adjudicated-smoke-v1.json +30642 -0
  105. package/docs/study/round-1-instrumentation-plan-v1.md +39 -0
  106. package/docs/study/round-2-expanded-adjudication-v1.json +91 -0
  107. package/docs/study/round-2-expanded-validation-v1.md +58 -0
  108. package/docs/study/round-3-evidence-contract-v1.json +75 -0
  109. package/docs/study/round-3-evidence-contract-v1.md +57 -0
  110. package/docs/study/round-4-confirmation-v1.json +75 -0
  111. package/docs/study/round-4-confirmation-v1.md +55 -0
  112. package/docs/study/run-plan-v1.json +33 -0
  113. package/docs/study/semantic-adjudication-cycle-8.md +20 -0
  114. package/docs/study/task-suite-v1.json +96 -0
  115. package/docs/study/token-efficiency-plan-v1.md +337 -0
  116. package/docs/study/token-efficiency-protocol-v2.json +62 -0
  117. package/docs/study/verification-binding-v1.json +27 -0
  118. package/docs/validation-cycle-plan.md +33 -0
  119. package/docs/verification-harness.md +15 -6
  120. package/ecosystem-claims.json +2 -2
  121. package/ecosystem-upstream.json +2 -2
  122. package/ecosystem.json +4 -4
  123. package/mcpb/manifest.json +9 -1
  124. package/package.json +89 -72
  125. package/scripts/check-ecosystem-upstream.mjs +36 -7
  126. package/scripts/report-visual-check.mjs +20 -3
  127. package/skills/doc-bridge-handoff/fixtures/synthetic-repo/docs/for-agents/packages/payments.md +7 -0
  128. package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
  129. package/src/agents/registry-adapter.ts +192 -24
  130. package/src/audit/documentation.ts +513 -0
  131. package/src/bench/baseline.ts +198 -0
  132. package/src/bench/overlay-delta.ts +139 -0
  133. package/src/bench/retrieval.ts +319 -0
  134. package/src/budget/compile.ts +91 -0
  135. package/src/budget/sections.ts +70 -0
  136. package/src/cli/demo.ts +2 -2
  137. package/src/cli/program.ts +699 -79
  138. package/src/cli/usage.ts +71 -0
  139. package/src/config/defaults.ts +1 -0
  140. package/src/config/index.ts +4 -0
  141. package/src/config/load-config.ts +7 -1
  142. package/src/config/schema.ts +121 -4
  143. package/src/conformance/documentation-standard-v1.ts +22 -14
  144. package/src/discovery/areas.ts +182 -0
  145. package/src/discovery/documentation.ts +255 -23
  146. package/src/discovery/identity.ts +24 -0
  147. package/src/discovery/incremental.ts +314 -0
  148. package/src/discovery/inputs.ts +110 -0
  149. package/src/discovery/markdown.ts +481 -0
  150. package/src/discovery/repository.ts +557 -125
  151. package/src/doctor/run-doctor.ts +246 -27
  152. package/src/enrich/approvals.ts +190 -0
  153. package/src/enrich/cache.ts +93 -0
  154. package/src/enrich/context-pack.ts +272 -0
  155. package/src/enrich/overlay.ts +255 -0
  156. package/src/enrich/review.ts +106 -0
  157. package/src/enrich/stage.ts +374 -0
  158. package/src/enrich/stats.ts +100 -0
  159. package/src/enrich/validate.ts +410 -0
  160. package/src/federation/llms.ts +74 -24
  161. package/src/findings/report.ts +103 -0
  162. package/src/fixes/proposals.ts +4 -3
  163. package/src/graph/build.ts +356 -0
  164. package/src/graph/memory.ts +208 -0
  165. package/src/index-builder/build-handoffs.ts +22 -11
  166. package/src/index-builder/build-index.ts +132 -3
  167. package/src/index-builder/human-adapters/fumadocs.ts +1 -1
  168. package/src/index-builder/llms-txt.ts +48 -8
  169. package/src/index-builder/project-corpus.ts +111 -0
  170. package/src/index-builder/watch-index.ts +1 -1
  171. package/src/index.ts +630 -2
  172. package/src/lib/bounded-text.ts +15 -10
  173. package/src/lib/fuzzy-match.ts +235 -0
  174. package/src/mcp/knowledge.ts +554 -0
  175. package/src/mcp/server.ts +113 -18
  176. package/src/metrics/benchmark.ts +21 -0
  177. package/src/parity/check.ts +309 -0
  178. package/src/parity/claims.ts +259 -0
  179. package/src/parity/resolve.ts +160 -0
  180. package/src/query/handoff.ts +326 -0
  181. package/src/query/load-index.ts +53 -1
  182. package/src/query/query.ts +92 -59
  183. package/src/query/search.ts +289 -92
  184. package/src/query/text.ts +155 -0
  185. package/src/reconciliation/reconcile.ts +148 -15
  186. package/src/render/data.ts +356 -0
  187. package/src/render/engine.ts +398 -0
  188. package/src/render/generated.ts +77 -0
  189. package/src/render/render.ts +209 -0
  190. package/src/render/template-source.ts +52 -0
  191. package/src/render/templates.ts +289 -0
  192. package/src/report/html.ts +23 -17
  193. package/src/retrieval/bm25.ts +161 -0
  194. package/src/retrieval/project.ts +495 -0
  195. package/src/retrieval/rank.ts +383 -0
  196. package/src/retrieval/weights.ts +39 -0
  197. package/src/retriever/doc-bridge-retriever.ts +100 -15
  198. package/src/rules/engine.ts +45 -12
  199. package/src/safety/repository.ts +1 -1
  200. package/src/schemas/agent-handoff.ts +56 -0
  201. package/src/schemas/budget.ts +37 -0
  202. package/src/schemas/doc-bridge-index.ts +53 -2
  203. package/src/schemas/enrichment.ts +369 -0
  204. package/src/schemas/json-schemas.ts +39 -2
  205. package/src/schemas/knowledge.ts +19 -3
  206. package/src/schemas/retrieval-index.ts +152 -0
  207. package/src/shims/graphology.d.ts +91 -0
  208. package/src/study/adjudication.ts +196 -0
  209. package/src/study/execution.ts +350 -0
  210. package/src/study/expectations.ts +219 -0
  211. package/src/study/metrics.ts +467 -0
  212. package/src/study/protocol.ts +271 -0
  213. package/src/study/provider-cli.ts +115 -0
  214. package/src/study/provider-telemetry.ts +47 -0
  215. package/src/study/quality-scorecard.ts +164 -0
  216. package/src/study/runner.ts +461 -0
  217. package/src/study/task-suite.ts +321 -0
  218. package/src/study/verification.ts +134 -0
  219. package/src/validate.ts +8 -5
  220. package/src/version.ts +1 -1
  221. package/src/workflow/engine.ts +36 -11
  222. package/dist/index-C2PCQSrB.d.ts +0 -2251
  223. package/scripts/verification-harness.mjs +0 -483
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
1
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
2
2
  import { dirname, relative, resolve } from 'node:path'
3
3
  import { createInterface } from 'node:readline/promises'
4
4
 
@@ -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'
@@ -25,30 +26,79 @@ import { ingestMemoryCandidates } from '../memory/ingest.js'
25
26
  import { classifyMemoryCandidates, draftMemoryPromotion } from '../memory/pipeline.js'
26
27
  import { promoteMemoryToGithubPr } from '../memory/github-pr.js'
27
28
  import { watchDocBridgeIndex } from '../index-builder/watch-index.js'
28
- import { loadWorkflowManifest, loadWorkflowStepOutput, runWorkflow, type WorkflowExecutionResult } from '../workflow/engine.js'
29
+ import { loadWorkflowStepOutput, runWorkflow, type WorkflowExecutionResult } from '../workflow/engine.js'
29
30
  import {
30
31
  formatDoctorBadgeJson,
31
32
  formatDoctorBadgeMarkdown,
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'))
@@ -500,16 +551,53 @@ const reconcileWorkflow = (root: string, config: DocBridgeConfigV1): WorkflowExe
500
551
  const report = reconcileKnowledge(snapshot, declared, {
501
552
  ...(config.reconciliation?.scope === undefined ? {} : { scope: config.reconciliation.scope }),
502
553
  ...(config.reconciliation?.requiredRelationKinds === undefined ? {} : { requiredRelationKinds: config.reconciliation.requiredRelationKinds }),
554
+ ...(config.reconciliation?.requiredRelationTargets === undefined ? {} : { requiredRelationTargets: config.reconciliation.requiredRelationTargets }),
503
555
  ...(config.reconciliation?.includeOrphanedDocuments === undefined ? {} : { includeOrphanedDocuments: config.reconciliation.includeOrphanedDocuments }),
556
+ ...ownershipOptions(config),
504
557
  })
505
558
  return runWorkflow(workflowOptions(root, config, snapshot.sourceRevision, 'reconcile', { reconcile: () => report }, { pipelineVersion: snapshot.pipelineVersion, analyzerVersions: snapshot.analyzerVersions }))
506
559
  }
507
560
 
508
- const checkWorkflow = (root: string, config: DocBridgeConfigV1): WorkflowExecutionResult => {
509
- 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 => {
510
592
  const report = parseReconciliationReport(loadWorkflowStepOutput(reconciled.stateDir, 'reconcile'))
511
593
  const versions = { pipelineVersion: report.pipelineVersion, analyzerVersions: report.analyzerVersions }
512
- 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))
513
601
  return runWorkflow(workflowOptions(root, config, report.sourceRevision, 'report', { report: ({ input }) => input }, versions))
514
602
  }
515
603
 
@@ -591,14 +679,85 @@ const runWorkflowCommand = (
591
679
  flags: ReadonlySet<string>,
592
680
  configPath: string | undefined,
593
681
  argv: readonly string[],
594
- ): 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
+ }
595
714
  try {
596
715
  const { config, root } = loadProject(configPath)
597
716
  const result = command === 'scan' ? scanWorkflow(root, config) : command === 'reconcile' ? reconcileWorkflow(root, config) : checkWorkflow(root, config)
598
- 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 }
599
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
+ }
600
757
  if (command === 'map' && flags.has('--html')) {
601
- 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
602
761
  const report = parseReconciliationReport(loadWorkflowStepOutput(result.stateDir, 'reconcile'))
603
762
  const outputPath = optionValues(argv, '--output')[0] ?? '.doc-bridge/report.html'
604
763
  const htmlPath = resolve(root, outputPath)
@@ -621,6 +780,187 @@ const runWorkflowCommand = (
621
780
  }
622
781
  }
623
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
+
624
964
  const runRulesCommand = (
625
965
  argv: readonly string[],
626
966
  flags: ReadonlySet<string>,
@@ -671,7 +1011,7 @@ const runRulesCommand = (
671
1011
  }
672
1012
  }
673
1013
 
674
- 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> => {
675
1015
  try {
676
1016
  const { config, root } = loadProject(configPath)
677
1017
  const action = positional[1]
@@ -692,8 +1032,12 @@ const runFixCommand = (argv: readonly string[], positional: readonly string[], c
692
1032
  const file = resolve(root, proposalPath)
693
1033
  const proposal = JSON.parse(readFileSync(file, 'utf8')) as unknown
694
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
695
1039
  writeFileSync(file, `${JSON.stringify(result, null, 2)}\n`, 'utf8')
696
- writeJson({ ok: true, proposal: result, proposalPath: file })
1040
+ writeJson({ ok: true, proposal: result, proposalPath: file, ...(recorded ? { approvalId: recorded.approval.id } : {}) })
697
1041
  return 0
698
1042
  } catch (error) {
699
1043
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
@@ -701,17 +1045,128 @@ const runFixCommand = (argv: readonly string[], positional: readonly string[], c
701
1045
  }
702
1046
  }
703
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
+
704
1157
  const runSuggestCommand = async (flags: ReadonlySet<string>, configPath: string | undefined): Promise<number> => {
705
1158
  try {
706
1159
  const { config, root } = loadProject(configPath)
707
1160
  const stateDir = resolve(root, config.workflow?.stateDir ?? '.doc-bridge/workflow')
708
1161
  const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(stateDir, 'normalize'))
709
1162
  const report = parseReconciliationReport(loadWorkflowStepOutput(stateDir, 'reconcile'))
710
- const adapter = createRegistryAgentAdapter(root, config, await loadRegistryAgentRunner(root, config))
711
- 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)
712
1167
  const proposalPath = persistRegistryAgentProposal(stateDir, proposal)
713
- if (flags.has('--text')) writeLines([`Agent: ${adapter.metadata.id}`, `Proposal: ${proposal.proposalId}`, `Hash: ${proposal.contentHash}`, `Saved: ${proposalPath}`])
714
- 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 })
715
1170
  return 0
716
1171
  } catch (error) {
717
1172
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
@@ -720,10 +1175,19 @@ const runSuggestCommand = async (flags: ReadonlySet<string>, configPath: string
720
1175
  }
721
1176
 
722
1177
  const writeIfMissing = (path: string, contents: string): boolean => {
723
- if (existsSync(path)) return false
724
1178
  mkdirSync(dirname(path), { recursive: true })
725
- writeFileSync(path, contents, 'utf8')
726
- return true
1179
+ try {
1180
+ const fd = openSync(path, 'wx')
1181
+ try {
1182
+ writeFileSync(fd, contents, 'utf8')
1183
+ return true
1184
+ } finally {
1185
+ closeSync(fd)
1186
+ }
1187
+ } catch (error) {
1188
+ if ((error as NodeJS.ErrnoException).code === 'EEXIST') return false
1189
+ throw error
1190
+ }
727
1191
  }
728
1192
 
729
1193
  const demoOwnership = {
@@ -891,6 +1355,143 @@ const registryTopology = () => ({
891
1355
  mergePolicy: { autoMerge: false, requiresHuman: true },
892
1356
  })
893
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
+
894
1495
  export const runCli = (argv: readonly string[]): number | undefined | Promise<number> => {
895
1496
  const { command, flags, configPath, positional } = parseArgs(argv)
896
1497
 
@@ -980,12 +1581,19 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
980
1581
  }
981
1582
  }
982
1583
 
1584
+ if (command === 'study') return runStudyCommand(flags, positional, argv, configPath)
1585
+
983
1586
  if (command === 'scan' || command === 'reconcile' || command === 'check' || command === 'map') {
984
1587
  return runWorkflowCommand(command, flags, configPath, argv)
985
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)
986
1593
 
987
1594
  if (command === 'fix') return runFixCommand(argv, positional, configPath)
988
1595
  if (command === 'suggest') return runSuggestCommand(flags, configPath)
1596
+ if (command === 'enrich') return runEnrichCommand(flags, positional, argv, configPath)
989
1597
 
990
1598
  if (command === 'init') {
991
1599
  const root = process.cwd()
@@ -1056,7 +1664,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1056
1664
  writeJson({ ok: true, count: candidates.length, candidates })
1057
1665
  return 0
1058
1666
  }
1059
- const index = loadDocBridgeIndex(root, config)
1667
+ const index = loadFreshDocBridgeIndex(root, config)
1060
1668
  const classifications = classifyMemoryCandidates(candidates, index)
1061
1669
  if (positional[1] === 'classify') {
1062
1670
  writeJson({ ok: true, count: classifications.length, classifications })
@@ -1114,7 +1722,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1114
1722
  }
1115
1723
  try {
1116
1724
  const { config, root } = loadProject(configPath)
1117
- const index = loadDocBridgeIndex(root, config)
1725
+ const index = loadFreshDocBridgeIndex(root, config)
1118
1726
  const draft = draftMemoryPromotion(classifyMemoryCandidates(ingestMemoryCandidates(root), index))
1119
1727
  writeJson({
1120
1728
  ...draft,
@@ -1319,9 +1927,10 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1319
1927
  }
1320
1928
  try {
1321
1929
  const { config, root } = loadProject(configPath)
1322
- const index = loadDocBridgeIndex(root, config)
1323
- 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 })
1324
1932
  if (wantsTextOutput(flags, config)) writeTextQuery(result)
1933
+ else if (flags.has('--agent')) writeAgentJson(result)
1325
1934
  else writeJson(result)
1326
1935
  return 0
1327
1936
  } catch (error) {
@@ -1338,12 +1947,23 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1338
1947
  }
1339
1948
  try {
1340
1949
  const { config, root } = loadProject(configPath)
1341
- const index = loadDocBridgeIndex(root, config)
1950
+ const index = loadFreshDocBridgeIndex(root, config)
1951
+ const explain = flags.has('--explain')
1342
1952
  if (flags.has('--agent')) {
1343
- const result = runQuery(index, config, { kind: 'search', term, agent: true })
1344
- 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)
1345
1965
  } else {
1346
- const matches = searchIndex(index, term)
1966
+ const matches = searchIndex(index, term, 20, explain ? { explain: true } : {})
1347
1967
  if (wantsTextOutput(flags, config)) writeTextSearch(term, matches)
1348
1968
  else writeJson({ term, count: matches.length, matches })
1349
1969
  }
@@ -1367,7 +1987,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1367
1987
  return (async () => {
1368
1988
  try {
1369
1989
  const { config, root } = loadProject(configPath)
1370
- const index = loadDocBridgeIndex(root, config)
1990
+ const index = loadFreshDocBridgeIndex(root, config)
1371
1991
  writeJson({ query, chunks: await retrieveHybridChunks(root, config, index, query) })
1372
1992
  return 0
1373
1993
  } catch (error) {
@@ -1386,7 +2006,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1386
2006
  return (async () => {
1387
2007
  try {
1388
2008
  const { config, root } = loadProject(configPath)
1389
- const index = loadDocBridgeIndex(root, config)
2009
+ const index = loadFreshDocBridgeIndex(root, config)
1390
2010
  const rag = await createDocBridgeRag(root, config, index)
1391
2011
  if (action === 'ingest') {
1392
2012
  const result = await rag.ingest()
@@ -1423,7 +2043,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1423
2043
  )
1424
2044
  return 1
1425
2045
  }
1426
- const index = loadDocBridgeIndex(root, config)
2046
+ const index = loadFreshDocBridgeIndex(root, config)
1427
2047
  await startInkChat(root, config, index)
1428
2048
  return 0
1429
2049
  } catch (error) {
@@ -1455,7 +2075,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1455
2075
  }
1456
2076
  return (async () => {
1457
2077
  try {
1458
- const index = loadDocBridgeIndex(root, config)
2078
+ const index = loadFreshDocBridgeIndex(root, config)
1459
2079
  const result = await runChatOnce(root, config, index, question)
1460
2080
  writeLines([result.content])
1461
2081
  return 0
@@ -1474,7 +2094,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1474
2094
  process.stderr.write('Usage: ak-docs ask <question>, or run ak-docs ask in an interactive terminal.\n')
1475
2095
  return 1
1476
2096
  }
1477
- const index = loadDocBridgeIndex(root, config)
2097
+ const index = loadFreshDocBridgeIndex(root, config)
1478
2098
  writeAsk(question, searchIndex(index, question, 8), index, config)
1479
2099
  return 0
1480
2100
  } catch (error) {
@@ -1491,7 +2111,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
1491
2111
  }
1492
2112
  try {
1493
2113
  const { config, root } = loadProject(configPath)
1494
- const index = loadDocBridgeIndex(root, config)
2114
+ const index = loadFreshDocBridgeIndex(root, config)
1495
2115
 
1496
2116
  if (kind === 'packages') {
1497
2117
  const items = index.lookup?.packages ?? []