@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
package/src/mcp/server.ts CHANGED
@@ -8,15 +8,20 @@ import { retrieveDocBridgeChunks } from '../retriever/doc-bridge-retriever.js'
8
8
  import { runGates } from '../gates/run-gates.js'
9
9
  import { ingestMemoryCandidates } from '../memory/ingest.js'
10
10
  import { classifyMemoryCandidates, draftMemoryPromotion } from '../memory/pipeline.js'
11
- import { loadDocBridgeIndex } from '../query/load-index.js'
11
+ import { loadFreshDocBridgeIndex } from '../query/load-index.js'
12
12
  import { runQuery } from '../query/query.js'
13
13
  import { searchIndex } from '../query/search.js'
14
+ import { findingsFromDiagnostics } from '../findings/report.js'
15
+ import { budgetedHandoff, formatKnowledgeLookupText, formatKnowledgeSearchText, knowledgeLookup, knowledgeSearch, MAX_LOOKUP_DEPTH } from './knowledge.js'
16
+ import type { AgentHandoffV1 } from '../schemas/agent-handoff.js'
17
+ import { RetrievalKindSchema } from '../schemas/retrieval-index.js'
14
18
  import type { DocBridgeIndexV1 } from '../schemas/doc-bridge-index.js'
15
19
  import { PACKAGE_VERSION } from '../version.js'
16
20
  import { loadWorkflowManifest, loadWorkflowStepOutput } from '../workflow/engine.js'
17
21
  import { parseDiscoverySnapshot, parseReconciliationReport } from '../validate.js'
18
22
  import { applyFixProposal, approveFixProposal, createArtifactNormalizationProposal, createMarkdownLinkFixProposal } from '../fixes/proposals.js'
19
23
  import { createRegistryAgentAdapter, loadRegistryAgentRunner, persistRegistryAgentProposal } from '../agents/registry-adapter.js'
24
+ import { decideEnrichment, listEnrichment } from '../enrich/review.js'
20
25
  import { sha256NormalizedV1 } from '../index-builder/content-hash.js'
21
26
  import { discoverRepository } from '../discovery/repository.js'
22
27
  import { FixProposalV1Schema, type DiscoverySnapshotV1, type ReconciliationReportV1, type FixProposalV1 } from '../schemas/knowledge.js'
@@ -43,7 +48,7 @@ export const MCP_TOOLS = [
43
48
  annotations: { readOnlyHint: true },
44
49
  inputSchema: {
45
50
  type: 'object',
46
- properties: { id: { type: 'string' }, kind: { type: 'string', enum: ['package', 'ownership'] } },
51
+ properties: { id: { type: 'string' }, kind: { type: 'string', enum: ['package', 'ownership'] }, budgetTokens: { type: 'number' } },
47
52
  required: ['id'],
48
53
  },
49
54
  },
@@ -54,7 +59,7 @@ export const MCP_TOOLS = [
54
59
  annotations: { readOnlyHint: true },
55
60
  inputSchema: {
56
61
  type: 'object',
57
- properties: { term: { type: 'string' }, limit: { type: 'number' } },
62
+ properties: { term: { type: 'string' }, limit: { type: 'number' }, agent: { type: 'boolean' }, mode: { type: 'string', enum: ['discovery', 'editing', 'debugging', 'documentation'] }, contextBudgetTokens: { type: 'number' } },
58
63
  required: ['term'],
59
64
  },
60
65
  },
@@ -126,7 +131,7 @@ export const MCP_TOOLS = [
126
131
  title: 'Read reconciliation diagnostics',
127
132
  description: 'Read bounded diagnostics from the latest canonical reconciliation report.',
128
133
  annotations: { readOnlyHint: true },
129
- inputSchema: { type: 'object', properties: { status: { type: 'string' }, severity: { type: 'string' } } },
134
+ inputSchema: { type: 'object', properties: { status: { type: 'string' }, severity: { type: 'string' }, format: { type: 'string', enum: ['diagnostic', 'finding'] } } },
130
135
  },
131
136
  {
132
137
  name: 'docbridge.relations',
@@ -146,21 +151,81 @@ export const MCP_TOOLS = [
146
151
  name: 'docbridge.proposals',
147
152
  title: 'Read or approve proposals',
148
153
  description: 'Create, inspect, approve and apply deterministic proposals through the shared human-gated workflow.',
149
- inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'propose-links', 'propose-normalize', 'suggest', 'approve', 'apply'] }, proposalHash: { type: 'string' }, artifactPath: { type: 'string' }, approvedBy: { type: 'string' }, proposal: { type: 'object' } } },
154
+ inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'propose-links', 'propose-normalize', 'suggest', 'approve', 'apply', 'enrich-list', 'enrich-approve', 'enrich-reject'] }, proposalHash: { type: 'string' }, artifactPath: { type: 'string' }, approvedBy: { type: 'string' }, proposal: { type: 'object' }, proposalId: { type: 'string' }, reason: { type: 'string' } } },
155
+ },
156
+ {
157
+ name: 'knowledge.search',
158
+ title: 'Search the knowledge graph',
159
+ description: 'Rank every indexed entity for a query, optionally by kind, with an explanation and within a token budget.',
160
+ annotations: { readOnlyHint: true },
161
+ inputSchema: {
162
+ type: 'object',
163
+ properties: {
164
+ query: { type: 'string' },
165
+ kinds: { type: 'array', items: { type: 'string', enum: ['document', 'module', 'area', 'package', 'intent', 'change'] } },
166
+ limit: { type: 'number' },
167
+ explain: { type: 'boolean' },
168
+ budgetTokens: { type: 'number' },
169
+ format: { type: 'string', enum: ['json', 'text'] },
170
+ },
171
+ required: ['query'],
172
+ },
173
+ },
174
+ {
175
+ name: 'knowledge.lookup',
176
+ title: 'Look up one entity',
177
+ description: 'Return an entity with its neighbours by relation kind, the documents about it, its handoff, its open diagnostics and its evidence, within a token budget.',
178
+ annotations: { readOnlyHint: true },
179
+ inputSchema: {
180
+ type: 'object',
181
+ properties: {
182
+ id: { type: 'string' },
183
+ path: { type: 'string' },
184
+ depth: { type: 'number' },
185
+ budgetTokens: { type: 'number' },
186
+ format: { type: 'string', enum: ['json', 'text'] },
187
+ },
188
+ },
150
189
  },
151
190
  ] as const
152
191
 
153
192
  const asRecord = (value: unknown): Record<string, unknown> =>
154
193
  value && typeof value === 'object' ? (value as Record<string, unknown>) : {}
155
194
 
195
+ /** A budget is a positive count of tokens; the upper bound keeps a typo from meaning "unbounded". */
196
+ const BudgetTokensSchema = z.number().int().positive().max(1_000_000)
197
+
156
198
  const HandoffResolveArgsSchema = z.object({
157
199
  id: z.string().min(1),
158
200
  kind: z.enum(['package', 'ownership']).optional(),
201
+ budgetTokens: BudgetTokensSchema.optional(),
159
202
  })
160
203
 
204
+ const KnowledgeSearchArgsSchema = z.object({
205
+ query: z.string().min(1),
206
+ kinds: z.array(RetrievalKindSchema).max(6).optional(),
207
+ limit: z.number().int().positive().max(100).optional(),
208
+ explain: z.boolean().optional(),
209
+ budgetTokens: BudgetTokensSchema.optional(),
210
+ format: z.enum(['json', 'text']).optional(),
211
+ })
212
+
213
+ const KnowledgeLookupArgsSchema = z
214
+ .object({
215
+ id: z.string().min(1).optional(),
216
+ path: z.string().min(1).optional(),
217
+ depth: z.number().int().min(1).max(MAX_LOOKUP_DEPTH).optional(),
218
+ budgetTokens: BudgetTokensSchema.optional(),
219
+ format: z.enum(['json', 'text']).optional(),
220
+ })
221
+ .refine((args) => args.id || args.path, 'knowledge.lookup requires id or path')
222
+
161
223
  const DocSearchArgsSchema = z.object({
162
224
  term: z.string().min(1),
163
225
  limit: z.number().int().positive().max(100).optional(),
226
+ agent: z.boolean().optional(),
227
+ mode: z.enum(['discovery', 'editing', 'debugging', 'documentation']).optional(),
228
+ contextBudgetTokens: z.number().int().positive().max(1_000_000).optional(),
164
229
  })
165
230
 
166
231
  const RetrieverQueryArgsSchema = z.object({
@@ -176,9 +241,9 @@ const DocGetArgsSchema = z
176
241
  .refine((args) => args.id || args.path, 'doc.get requires id or path')
177
242
 
178
243
  const WorkflowRunArgsSchema = z.object({ runId: z.string().min(1).optional() })
179
- const DiagnosticsArgsSchema = z.object({ status: z.string().min(1).optional(), severity: z.string().min(1).optional() })
244
+ const DiagnosticsArgsSchema = z.object({ status: z.string().min(1).optional(), severity: z.string().min(1).optional(), format: z.enum(['diagnostic', 'finding']).optional() })
180
245
  const RelationsArgsSchema = z.object({ kind: z.string().min(1).optional(), limit: z.number().int().positive().max(500).optional() })
181
- const ProposalsArgsSchema = z.object({ action: z.enum(['list', 'propose-links', 'propose-normalize', 'suggest', 'approve', 'apply']).optional(), proposalHash: z.string().min(1).optional(), artifactPath: z.string().min(1).optional(), approvedBy: z.string().min(1).optional(), proposal: z.unknown().optional() })
246
+ const ProposalsArgsSchema = z.object({ action: z.enum(['list', 'propose-links', 'propose-normalize', 'suggest', 'approve', 'apply', 'enrich-list', 'enrich-approve', 'enrich-reject']).optional(), proposalHash: z.string().min(1).optional(), artifactPath: z.string().min(1).optional(), approvedBy: z.string().min(1).optional(), proposal: z.unknown().optional(), proposalId: z.string().min(1).optional(), reason: z.string().max(1_024).optional() })
182
247
 
183
248
  const parseToolArgs = <T>(tool: string, schema: z.ZodType<T>, value: unknown): T => {
184
249
  try {
@@ -279,21 +344,37 @@ export const handleMcpRequest = (ctx: McpContext, request: JsonRpcRequest): unkn
279
344
  const args = asRecord(params.arguments)
280
345
  if (typeof name !== 'string') throw new Error('MCP tools/call requires a tool name.')
281
346
  assertMcpToolEnabled(ctx, name)
282
- const index = () => ctx.loadIndex?.() ?? loadDocBridgeIndex(ctx.root, ctx.config)
347
+ const index = () => ctx.loadIndex?.() ?? loadFreshDocBridgeIndex(ctx.root, ctx.config)
283
348
 
284
349
  if (name === 'handoff.resolve') {
285
350
  const parsed = parseToolArgs('handoff.resolve', HandoffResolveArgsSchema, args)
286
- return textResult(
287
- runQuery(index(), ctx.config, {
288
- kind: parsed.kind === 'package' ? 'package' : 'ownership',
289
- id: parsed.id,
290
- agent: true,
291
- }),
292
- )
351
+ const loaded = index()
352
+ const handoff = runQuery(loaded, ctx.config, {
353
+ kind: parsed.kind === 'package' ? 'package' : 'ownership',
354
+ id: parsed.id,
355
+ agent: true,
356
+ }, { root: ctx.root })
357
+ // The payload is the handoff it always was; a declared budget adds `budget` and may shed `related` and the summary note.
358
+ return textResult(parsed.budgetTokens === undefined ? handoff : budgetedHandoff(loaded, handoff as AgentHandoffV1, parsed.budgetTokens))
359
+ }
360
+
361
+ if (name === 'knowledge.search') {
362
+ const { format, ...parsed } = parseToolArgs('knowledge.search', KnowledgeSearchArgsSchema, args)
363
+ const response = knowledgeSearch(index(), parsed)
364
+ return textResult(format === 'text' ? formatKnowledgeSearchText(response) : response)
365
+ }
366
+
367
+ if (name === 'knowledge.lookup') {
368
+ const { format, ...parsed } = parseToolArgs('knowledge.lookup', KnowledgeLookupArgsSchema, args)
369
+ // Diagnostics come from the latest workflow run when there is one; a repository never checked still gets its lookup.
370
+ const report = () => { try { return workflowReport(ctx) } catch { return undefined } }
371
+ const response = knowledgeLookup(index(), ctx.config, parsed, { root: ctx.root, report })
372
+ return textResult(format === 'text' ? formatKnowledgeLookupText(response) : response)
293
373
  }
294
374
 
295
375
  if (name === 'doc.search') {
296
376
  const parsed = parseToolArgs('doc.search', DocSearchArgsSchema, args)
377
+ if (parsed.agent) return textResult(runQuery(index(), ctx.config, { kind: 'search', term: parsed.term, agent: true, ...(parsed.mode === undefined ? {} : { mode: parsed.mode }), ...(parsed.contextBudgetTokens === undefined ? {} : { contextBudgetTokens: parsed.contextBudgetTokens }) }))
297
378
  return textResult(searchIndex(index(), parsed.term, parsed.limit ?? 20))
298
379
  }
299
380
 
@@ -342,6 +423,8 @@ export const handleMcpRequest = (ctx: McpContext, request: JsonRpcRequest): unkn
342
423
  const diagnostics = workflowReport(ctx).diagnostics.filter((diagnostic) =>
343
424
  (!parsed.status || diagnostic.status === parsed.status) && (!parsed.severity || diagnostic.severity === parsed.severity),
344
425
  )
426
+ // `finding` is the ecosystem shape; the filters above still speak the internal vocabulary.
427
+ if (parsed.format === 'finding') return textResult(redactValue({ reportHash: workflowReport(ctx).contentHash, findings: findingsFromDiagnostics(diagnostics) }))
345
428
  return textResult(redactValue({ reportHash: workflowReport(ctx).contentHash, diagnostics }))
346
429
  }
347
430
 
@@ -359,20 +442,32 @@ export const handleMcpRequest = (ctx: McpContext, request: JsonRpcRequest): unkn
359
442
  if (name === 'docbridge.proposals') {
360
443
  const parsed = parseToolArgs('docbridge.proposals', ProposalsArgsSchema, args)
361
444
  const run = (() => { try { return workflowRun(ctx) } catch { return undefined } })()
445
+ // KR-10: enrichment proposals share this tool; a decision goes through the ecosystem approval gate.
446
+ if (parsed.action === 'enrich-list') return textResult(redactValue({ ...(run ? { runId: run.runId } : {}), enrichment: listEnrichment(ctx.root) ?? null }))
447
+ if (parsed.action === 'enrich-approve' || parsed.action === 'enrich-reject') {
448
+ if (!parsed.proposalId) throw new Error(`docbridge.proposals ${parsed.action} requires proposalId`)
449
+ const proposalId = parsed.proposalId
450
+ return (async () => {
451
+ const snapshot = (() => { try { return workflowSnapshot(ctx) } catch { return undefined } })()
452
+ const decided = await decideEnrichment({ root: ctx.root, proposalId, decision: parsed.action === 'enrich-approve' ? 'approved' : 'rejected', by: parsed.approvedBy ?? 'human', ...(parsed.reason ? { reason: parsed.reason } : {}), ...(snapshot ? { snapshot } : {}) })
453
+ return textResult(redactValue({ ...(run ? { runId: run.runId } : {}), approvalId: decided.approvalId, entry: decided.entry, overlayHash: decided.overlay.contentHash }))
454
+ })()
455
+ }
362
456
  if (!parsed.action || parsed.action === 'list') {
363
457
  let proposal: FixProposalV1 | undefined
364
458
  try { proposal = readSavedProposal(ctx, undefined) } catch { proposal = undefined }
365
459
  return textResult(redactValue({ ...(run ? { runId: run.runId } : {}), proposals: proposal ? [proposal] : [] }))
366
460
  }
367
461
  if (parsed.action === 'suggest') {
368
- return loadRegistryAgentRunner(ctx.root, ctx.config).then(async (runner) => {
462
+ return (async () => {
463
+ const runner = ctx.config.intelligence?.registry?.cli ? undefined : await loadRegistryAgentRunner(ctx.root, ctx.config)
369
464
  const snapshot = workflowSnapshot(ctx)
370
465
  const report = workflowReport(ctx)
371
- const adapter = createRegistryAgentAdapter(ctx.root, ctx.config, runner)
466
+ const adapter = createRegistryAgentAdapter(ctx.root, ctx.config, ctx.config.intelligence?.registry?.cli ? undefined : runner)
372
467
  const proposal = await adapter.run(snapshot, report)
373
468
  const savedPath = persistRegistryAgentProposal(workflowStateDir(ctx), proposal)
374
469
  return textResult(redactValue({ ...(run ? { runId: run.runId } : {}), proposal, proposalPath: savedPath }))
375
- })
470
+ })()
376
471
  }
377
472
  const discovered = discoverRepository({ root: ctx.root, config: ctx.config })
378
473
  const options = { baseRevision: discovered.sourceRevision, configurationHash: sha256NormalizedV1(ctx.config), ...(ctx.config.project?.name ? { projectName: ctx.config.project.name } : {}) }
@@ -166,6 +166,27 @@ export const measureAgentEfficiency = (observation: AgentEfficiencyObservation)
166
166
  contextReduction: observation.corpusBytes > 0 ? 1 - (percentile95(observation.responseBytes) / observation.corpusBytes) : 0,
167
167
  })
168
168
 
169
+ export type AgentTaskEfficiencyObservation = {
170
+ readonly correct: boolean
171
+ readonly latencyMs: number
172
+ readonly responseBytes: number
173
+ readonly estimatedTokens: number
174
+ }
175
+
176
+ export const measureAgentTaskEfficiency = (observations: readonly AgentTaskEfficiencyObservation[]) => {
177
+ const correct = observations.filter((observation) => observation.correct)
178
+ return {
179
+ taskCount: observations.length,
180
+ correctTaskCount: correct.length,
181
+ correctnessRate: ratio(correct.length, observations.length),
182
+ latencyP95Ms: percentile95(observations.map((observation) => observation.latencyMs)),
183
+ responseBytesP95: percentile95(observations.map((observation) => observation.responseBytes)),
184
+ estimatedTokensP95: percentile95(observations.map((observation) => observation.estimatedTokens)),
185
+ tokensToCorrectAnswerP95: correct.length ? percentile95(correct.map((observation) => observation.estimatedTokens)) : null,
186
+ timeToCorrectAnswerP95Ms: correct.length ? percentile95(correct.map((observation) => observation.latencyMs)) : null,
187
+ }
188
+ }
189
+
169
190
  export const formatBenchmarkText = (result: BenchmarkResult): string => [
170
191
  `Precision: entities ${result.quality.entities.precision.toFixed(3)}, relations ${result.quality.relations.precision.toFixed(3)}, findings ${result.quality.findings.precision.toFixed(3)}`,
171
192
  `Recall: entities ${result.quality.entities.recall.toFixed(3)}, relations ${result.quality.relations.recall.toFixed(3)}, findings ${result.quality.findings.recall.toFixed(3)}`,
@@ -0,0 +1,309 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+
4
+ import { z } from 'zod'
5
+
6
+ import { contentHashForArtifactV1, sha256NormalizedV1 } from '../index-builder/content-hash.js'
7
+ import { redactSecrets } from '../safety/repository.js'
8
+ import {
9
+ claimPattern,
10
+ claimSurfaces,
11
+ exceptionFor,
12
+ renderClaim,
13
+ type PublicClaim,
14
+ type PublicClaimsV1,
15
+ } from './claims.js'
16
+ import { resolveClaim, type ResolveContext } from './resolve.js'
17
+
18
+ /**
19
+ * Compare what this repository says in public with what it can prove.
20
+ *
21
+ * The four outcomes are kept apart on purpose, because they need different actions. `stale` means a
22
+ * surface states a value the repository has moved past — someone edits one line. `missing` means a
23
+ * surface that promised to carry the claim does not. `contradiction` means two public surfaces
24
+ * disagree with each other, which is the one an agent cannot resolve on its own and the reason this
25
+ * check exists. `not-analyzed` means the claim could not be resolved at all: reported, never
26
+ * counted as a pass.
27
+ *
28
+ * The report is publication-safe by construction. It carries claim ids, repository-relative surface
29
+ * paths, line numbers, the two values, and a bounded excerpt with secrets redacted — never a
30
+ * document's contents, never an absolute path.
31
+ */
32
+
33
+ export const PUBLIC_PARITY_SCHEMA_VERSION = 1 as const
34
+ export const PARITY_EXCERPT_LIMIT = 160
35
+
36
+ const hash = z.string().regex(/^[a-f0-9]{64}$/)
37
+
38
+ export const PARITY_CODES = ['PARITY_STALE', 'PARITY_MISSING', 'PARITY_CONTRADICTION', 'PARITY_NOT_ANALYZED'] as const
39
+ export type ParityCode = (typeof PARITY_CODES)[number]
40
+
41
+ const ParityFindingSchema = z
42
+ .object({
43
+ id: hash,
44
+ code: z.enum(PARITY_CODES),
45
+ claimId: z.string().min(1).max(64),
46
+ owner: z.string().min(1).max(128),
47
+ severity: z.enum(['error', 'warn', 'info']),
48
+ /** False when an exception accepted it. The reason travels with the finding, not in a comment. */
49
+ blocking: z.boolean(),
50
+ surface: z.string().max(512).optional(),
51
+ line: z.number().int().positive().optional(),
52
+ stated: z.string().max(256).optional(),
53
+ canonical: z.string().max(256).optional(),
54
+ message: z.string().min(1).max(1_024),
55
+ remediation: z.string().min(1).max(1_024),
56
+ excerpt: z.string().max(PARITY_EXCERPT_LIMIT).optional(),
57
+ acceptedReason: z.string().max(512).optional(),
58
+ })
59
+ .strict()
60
+
61
+ export type ParityFinding = z.infer<typeof ParityFindingSchema>
62
+
63
+ export const PublicParityReportV1Schema = z
64
+ .object({
65
+ type: z.literal('public-parity-report'),
66
+ schemaVersion: z.literal(PUBLIC_PARITY_SCHEMA_VERSION),
67
+ contentHash: hash,
68
+ contentHashAlgo: z.literal('sha256-normalized-v1'),
69
+ project: z.object({ name: z.string().min(1).max(128) }).strict(),
70
+ sourceRevision: z.string().min(1).max(128),
71
+ sourceRevisionKind: z.enum(['git', 'content']),
72
+ registryVersion: z.string().min(1).max(64),
73
+ registryHash: hash,
74
+ status: z.enum(['pass', 'needs-review', 'blocked']),
75
+ metrics: z
76
+ .object({
77
+ claims: z.number().int().nonnegative(),
78
+ surfaces: z.number().int().nonnegative(),
79
+ matched: z.number().int().nonnegative(),
80
+ stale: z.number().int().nonnegative(),
81
+ missing: z.number().int().nonnegative(),
82
+ contradictions: z.number().int().nonnegative(),
83
+ notAnalyzed: z.number().int().nonnegative(),
84
+ accepted: z.number().int().nonnegative(),
85
+ blocking: z.number().int().nonnegative(),
86
+ })
87
+ .strict(),
88
+ findings: z.array(ParityFindingSchema).max(10_000),
89
+ /** Surfaces the registry names that do not exist in this checkout. */
90
+ missingSurfaces: z.array(z.string().max(512)).max(64),
91
+ limitations: z.array(z.string().min(1).max(512)).max(32),
92
+ })
93
+ .strict()
94
+
95
+ export type PublicParityReportV1 = z.infer<typeof PublicParityReportV1Schema>
96
+
97
+ export type CheckParityOptions = ResolveContext & {
98
+ readonly registry: PublicClaimsV1
99
+ readonly project: { readonly name: string }
100
+ readonly sourceRevision: string
101
+ readonly sourceRevisionKind: 'git' | 'content'
102
+ /** File contents by surface path. Defaults to reading under `root`. */
103
+ readonly readSurface?: (path: string) => string | undefined
104
+ }
105
+
106
+ type Occurrence = { readonly surface: string; readonly line: number; readonly stated: string; readonly excerpt: string }
107
+
108
+ const findingId = (parts: Record<string, unknown>): string => sha256NormalizedV1(parts)
109
+
110
+ const excerptOf = (line: string): string => {
111
+ const collapsed = redactSecrets(line.trim()).replace(/\s+/g, ' ')
112
+ return collapsed.length <= PARITY_EXCERPT_LIMIT ? collapsed : `${collapsed.slice(0, PARITY_EXCERPT_LIMIT - 1)}…`
113
+ }
114
+
115
+ /**
116
+ * Every occurrence of a claim in one surface, with the line it sits on.
117
+ *
118
+ * A `cli-command` claim has no value to extract: the command either appears or it does not, and
119
+ * the canonical value is the command itself. Everything else is read through the claim's template
120
+ * for that surface, so one fact stated two ways is still one claim.
121
+ */
122
+ const occurrences = (claim: PublicClaim, surface: string, content: string): readonly Occurrence[] => {
123
+ const found: Occurrence[] = []
124
+ const lines = content.split('\n')
125
+ for (const [index, line] of lines.entries()) {
126
+ if (claim.evidence.kind === 'cli-command') {
127
+ if (line.includes(claim.evidence.command)) {
128
+ found.push({ surface, line: index + 1, stated: claim.evidence.command, excerpt: excerptOf(line) })
129
+ }
130
+ continue
131
+ }
132
+ // A fresh pattern per line: a global regex carries lastIndex between calls.
133
+ for (const match of line.matchAll(claimPattern(claim, surface))) {
134
+ const stated = match[1]
135
+ if (stated === undefined) continue
136
+ found.push({ surface, line: index + 1, stated, excerpt: excerptOf(line) })
137
+ }
138
+ }
139
+ return found
140
+ }
141
+
142
+ export const checkPublicParity = (options: CheckParityOptions): PublicParityReportV1 => {
143
+ const { registry } = options
144
+ const read = options.readSurface ?? ((path: string): string | undefined => {
145
+ const absolute = resolve(options.root, path)
146
+ return existsSync(absolute) ? readFileSync(absolute, 'utf8') : undefined
147
+ })
148
+
149
+ const surfaces = claimSurfaces(registry)
150
+ const contents = new Map<string, string | undefined>(surfaces.map((surface) => [surface, read(surface)]))
151
+ const missingSurfaces = surfaces.filter((surface) => contents.get(surface) === undefined)
152
+
153
+ const findings: ParityFinding[] = []
154
+ const limitations: string[] = []
155
+ let matched = 0
156
+
157
+ for (const claim of [...registry.claims].sort((a, b) => a.claimId.localeCompare(b.claimId))) {
158
+ const resolved = resolveClaim(claim, options)
159
+ const declared = [...claim.required, ...(claim.optional ?? [])]
160
+
161
+ if (resolved.status === 'not-analyzed') {
162
+ findings.push({
163
+ id: findingId({ code: 'PARITY_NOT_ANALYZED', claimId: claim.claimId }),
164
+ code: 'PARITY_NOT_ANALYZED',
165
+ claimId: claim.claimId,
166
+ owner: claim.owner,
167
+ severity: 'warn',
168
+ blocking: false,
169
+ message: `${claim.statement} could not be resolved: ${resolved.reason}`,
170
+ remediation: claim.remediation,
171
+ })
172
+ limitations.push(`${claim.claimId}: ${resolved.reason}`)
173
+ continue
174
+ }
175
+
176
+ const stated = new Map<string, readonly Occurrence[]>()
177
+ for (const surface of declared) {
178
+ const content = contents.get(surface)
179
+ if (content === undefined) continue
180
+ stated.set(surface, occurrences(claim, surface, content))
181
+ }
182
+
183
+ /*
184
+ * A contradiction is about the surfaces agreeing with each other, so it is decided over the
185
+ * whole claim rather than per surface: two public pages stating different numbers is a finding
186
+ * even when neither of them matches the repository.
187
+ */
188
+ const distinct = [...new Set([...stated.values()].flat().map((entry) => entry.stated))].sort()
189
+ if (distinct.length > 1) {
190
+ findings.push({
191
+ id: findingId({ code: 'PARITY_CONTRADICTION', claimId: claim.claimId, distinct }),
192
+ code: 'PARITY_CONTRADICTION',
193
+ claimId: claim.claimId,
194
+ owner: claim.owner,
195
+ severity: 'error',
196
+ blocking: true,
197
+ canonical: resolved.value,
198
+ message: `Public surfaces state ${distinct.join(' and ')} for ${claim.statement.toLowerCase()}; the repository says ${resolved.value}.`,
199
+ remediation: claim.remediation,
200
+ })
201
+ }
202
+
203
+ for (const surface of declared) {
204
+ const content = contents.get(surface)
205
+ if (content === undefined) continue
206
+ const here = stated.get(surface) ?? []
207
+ const exception = exceptionFor(registry, claim.claimId, surface)
208
+ const accepted = exception !== undefined
209
+
210
+ if (!here.length) {
211
+ if (!claim.required.includes(surface)) continue
212
+ findings.push({
213
+ id: findingId({ code: 'PARITY_MISSING', claimId: claim.claimId, surface }),
214
+ code: 'PARITY_MISSING',
215
+ claimId: claim.claimId,
216
+ owner: claim.owner,
217
+ severity: accepted ? 'info' : claim.severity,
218
+ blocking: !accepted && claim.severity === 'error',
219
+ surface,
220
+ canonical: resolved.value,
221
+ message: `${surface} does not state ${claim.statement.toLowerCase()}`,
222
+ remediation: `Add "${renderClaim(claim, resolved.value, surface)}" to ${surface}, or record an exception with a reason.`,
223
+ ...(accepted ? { acceptedReason: exception.reason } : {}),
224
+ })
225
+ continue
226
+ }
227
+
228
+ for (const occurrence of here) {
229
+ if (occurrence.stated === resolved.value) {
230
+ matched += 1
231
+ continue
232
+ }
233
+ findings.push({
234
+ id: findingId({ code: 'PARITY_STALE', claimId: claim.claimId, surface, line: occurrence.line }),
235
+ code: 'PARITY_STALE',
236
+ claimId: claim.claimId,
237
+ owner: claim.owner,
238
+ severity: accepted ? 'info' : claim.severity,
239
+ blocking: !accepted && claim.severity === 'error',
240
+ surface,
241
+ line: occurrence.line,
242
+ stated: occurrence.stated,
243
+ canonical: resolved.value,
244
+ message: `${surface}:${occurrence.line} states ${occurrence.stated} where the repository says ${resolved.value}.`,
245
+ remediation: claim.remediation,
246
+ excerpt: occurrence.excerpt,
247
+ ...(accepted ? { acceptedReason: exception.reason } : {}),
248
+ })
249
+ }
250
+ }
251
+ }
252
+
253
+ for (const surface of missingSurfaces) {
254
+ limitations.push(`${surface} is named by the registry but absent from this checkout.`)
255
+ }
256
+
257
+ findings.sort((a, b) => a.claimId.localeCompare(b.claimId) || a.code.localeCompare(b.code) || (a.surface ?? '').localeCompare(b.surface ?? '') || (a.line ?? 0) - (b.line ?? 0))
258
+
259
+ const count = (code: ParityCode): number => findings.filter((finding) => finding.code === code).length
260
+ const blocking = findings.filter((finding) => finding.blocking).length
261
+ const payload = {
262
+ type: 'public-parity-report' as const,
263
+ schemaVersion: PUBLIC_PARITY_SCHEMA_VERSION,
264
+ contentHashAlgo: 'sha256-normalized-v1' as const,
265
+ project: { name: options.project.name },
266
+ sourceRevision: options.sourceRevision,
267
+ sourceRevisionKind: options.sourceRevisionKind,
268
+ registryVersion: registry.registryVersion,
269
+ registryHash: registry.contentHash,
270
+ status: blocking ? ('blocked' as const) : findings.length ? ('needs-review' as const) : ('pass' as const),
271
+ metrics: {
272
+ claims: registry.claims.length,
273
+ surfaces: surfaces.length - missingSurfaces.length,
274
+ matched,
275
+ stale: count('PARITY_STALE'),
276
+ missing: count('PARITY_MISSING'),
277
+ contradictions: count('PARITY_CONTRADICTION'),
278
+ notAnalyzed: count('PARITY_NOT_ANALYZED'),
279
+ accepted: findings.filter((finding) => finding.acceptedReason !== undefined).length,
280
+ blocking,
281
+ },
282
+ findings,
283
+ missingSurfaces,
284
+ limitations: [...limitations].sort(),
285
+ }
286
+ return PublicParityReportV1Schema.parse({ ...payload, contentHash: sha256NormalizedV1(payload) })
287
+ }
288
+
289
+ export const parsePublicParityReport = (input: unknown): PublicParityReportV1 => {
290
+ const report = PublicParityReportV1Schema.parse(input)
291
+ if (contentHashForArtifactV1(report) !== report.contentHash) throw new Error('Invalid public-parity report content hash.')
292
+ return report
293
+ }
294
+
295
+ export const formatPublicParityText = (report: PublicParityReportV1): readonly string[] => [
296
+ `Public parity: ${report.status} (registry ${report.registryVersion}, ${report.metrics.claims} claim(s) over ${report.metrics.surfaces} surface(s))`,
297
+ `Matched: ${report.metrics.matched} Stale: ${report.metrics.stale} Missing: ${report.metrics.missing} Contradictions: ${report.metrics.contradictions} Not analyzed: ${report.metrics.notAnalyzed} Accepted: ${report.metrics.accepted}`,
298
+ ...report.findings.flatMap((finding) => {
299
+ const at = finding.surface ? ` ${finding.surface}${finding.line ? `:${finding.line}` : ''}` : ''
300
+ const mark = finding.blocking ? 'BLOCKING' : finding.acceptedReason ? 'accepted' : finding.severity
301
+ return [
302
+ ` [${mark}] ${finding.code}${at} (${finding.claimId}, owner ${finding.owner}): ${finding.message}`,
303
+ ` → ${finding.remediation}`,
304
+ ...(finding.acceptedReason ? [` accepted: ${finding.acceptedReason}`] : []),
305
+ ]
306
+ }),
307
+ ...report.limitations.map((limitation) => ` limitation: ${limitation}`),
308
+ `Content hash: ${report.contentHash}`,
309
+ ]