@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
@@ -1,10 +1,16 @@
1
1
  import { z } from 'zod'
2
2
 
3
+ import { BudgetReportSchema } from './budget.js'
4
+
3
5
  export const HANDOFF_SCHEMA_VERSION = 1 as const
6
+ export const AgentQueryModeSchema = z.enum(['discovery', 'editing', 'debugging', 'documentation'])
7
+ export type AgentQueryMode = z.infer<typeof AgentQueryModeSchema>
4
8
 
5
9
  export const HandoffTargetTypeSchema = z.enum([
6
10
  'package',
11
+ 'area',
7
12
  'module',
13
+ 'document',
8
14
  'app',
9
15
  'screen',
10
16
  'flow',
@@ -38,6 +44,20 @@ export const HandoffBridgeSchema = z
38
44
 
39
45
  export type HandoffBridge = z.infer<typeof HandoffBridgeSchema>
40
46
 
47
+ /** An area or package this target's code depends on, or that depends on it, with what proves it. */
48
+ export const HandoffRelatedSchema = z
49
+ .object({
50
+ id: z.string().min(1).max(256),
51
+ path: z.string().min(1).max(512),
52
+ direction: z.enum(['imports', 'imported-by']),
53
+ /** How many import edges cross the boundary. */
54
+ strength: z.number().int().positive(),
55
+ evidence: z.array(z.string().min(1).max(512)).max(8),
56
+ })
57
+ .strict()
58
+
59
+ export type HandoffRelated = z.infer<typeof HandoffRelatedSchema>
60
+
41
61
  /** v1 — canonical AgentHandoff. Legacy payloads may omit schemaVersion. */
42
62
  export const AgentHandoffV1Schema = z
43
63
  .object({
@@ -53,6 +73,31 @@ export const AgentHandoffV1Schema = z
53
73
  bridge: HandoffBridgeSchema.optional(),
54
74
  playbookPatterns: z.array(z.string().url()).max(16).optional(),
55
75
  notes: z.array(z.string().min(1).max(1024)).max(16),
76
+ /*
77
+ * Optional additions, so a handoff built before they existed is still a valid handoff and a
78
+ * reader that predates them sees the same fields it always did.
79
+ */
80
+ related: z.array(HandoffRelatedSchema).max(16).optional(),
81
+ /** Which relations produced each field, by field name. */
82
+ explain: z.record(z.string().min(1).max(64), z.array(z.string().min(1).max(512)).max(16)).optional(),
83
+ evidence: z
84
+ .array(
85
+ z
86
+ .object({
87
+ source: z.enum(['code', 'configuration', 'documentation', 'agent', 'derived']),
88
+ path: z.string().min(1).max(512),
89
+ lineStart: z.number().int().positive().optional(),
90
+ lineEnd: z.number().int().positive().optional(),
91
+ contentHash: z.string().regex(/^[a-f0-9]{64}$/).optional(),
92
+ context: z.string().max(1_024).optional(),
93
+ })
94
+ .strict(),
95
+ )
96
+ .max(32)
97
+ .optional(),
98
+ metadata: z.record(z.string().min(1).max(64), z.unknown()).optional(),
99
+ /** Present only when the caller declared `budgetTokens`: what the payload cost and what it shed to fit. */
100
+ budget: BudgetReportSchema.optional(),
56
101
  })
57
102
  .strict()
58
103
 
@@ -84,6 +129,17 @@ export const AgentSearchV1Schema = z
84
129
  bestMatch: AgentSearchMatchSchema.nullable(),
85
130
  matches: z.array(AgentSearchMatchSchema).max(32),
86
131
  nextCommands: z.array(z.string().min(1).max(512)).max(16),
132
+ telemetry: z
133
+ .object({
134
+ contextBytes: z.number().int().nonnegative(),
135
+ estimatedTokens: z.number().int().nonnegative(),
136
+ tokenMethod: z.literal('estimate'),
137
+ contextBudgetTokens: z.number().int().positive(),
138
+ mode: AgentQueryModeSchema,
139
+ truncated: z.boolean(),
140
+ })
141
+ .strict()
142
+ .optional(),
87
143
  })
88
144
  .strict()
89
145
 
@@ -0,0 +1,37 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * The sections a budgeted payload may shed, in the order it sheds them.
5
+ *
6
+ * Excerpts go first: they are the longest text and the easiest to fetch again. Then the related
7
+ * areas, then the neighbourhood, then the summaries. What is never on this list — the entity,
8
+ * the evidence paths and hashes, the handoff fields an agent acts on, the open diagnostics —
9
+ * is never dropped: a payload that no longer fits reports `fits: false` rather than losing them.
10
+ */
11
+ export const BUDGET_SECTION_ORDER = ['evidenceExcerpts', 'related', 'neighbours', 'summaries'] as const
12
+
13
+ export const BudgetSectionSchema = z.enum(BUDGET_SECTION_ORDER)
14
+ export type BudgetSection = z.infer<typeof BudgetSectionSchema>
15
+
16
+ export const BudgetReportSchema = z
17
+ .object({
18
+ budgetTokens: z.number().int().positive(),
19
+ tokens: z
20
+ .object({
21
+ total: z.number().int().nonnegative(),
22
+ budget: z.number().int().positive(),
23
+ /** What the payload costs with every droppable section removed. */
24
+ core: z.number().int().nonnegative(),
25
+ sections: z.partialRecord(BudgetSectionSchema, z.number().int().nonnegative()),
26
+ })
27
+ .strict(),
28
+ fits: z.boolean(),
29
+ /** The declared order, so a reader can check that what was dropped is a prefix of it. */
30
+ order: z.array(BudgetSectionSchema).max(8),
31
+ kept: z.array(BudgetSectionSchema).max(8),
32
+ dropped: z.array(BudgetSectionSchema).max(8),
33
+ tokenMethod: z.literal('approximate'),
34
+ })
35
+ .strict()
36
+
37
+ export type BudgetReport = z.infer<typeof BudgetReportSchema>
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod'
2
2
 
3
3
  import { AgentHandoffLegacySchema } from './agent-handoff.js'
4
+ import { RETRIEVAL_MAX_ENTRIES, RetrievalIndexV1Schema } from './retrieval-index.js'
4
5
 
5
6
  export const INDEX_SCHEMA_VERSION = 1 as const
6
7
 
@@ -26,6 +27,10 @@ export const KnowledgeEntrySchema = z
26
27
  body: z.string().max(8_000).optional(),
27
28
  links: z.array(z.string().min(1).max(512)).max(64).optional(),
28
29
  tags: z.array(z.string().min(1).max(64)).max(32).optional(),
30
+ /** Names a module exports. The field an agent's symbol query matches against. */
31
+ symbols: z.array(z.string().min(1).max(128)).max(256).optional(),
32
+ /** Hash of the file this entry was projected from, so a stale entry is detectable per file. */
33
+ contentHash: z.string().regex(/^[a-f0-9]{64}$/).optional(),
29
34
  })
30
35
  .strict()
31
36
 
@@ -45,6 +50,8 @@ export const OwnershipRecordSchema = z
45
50
  layer: z.string().min(1).max(32).optional(),
46
51
  purpose: z.string().max(1024).optional(),
47
52
  checks: z.array(z.string().min(1).max(256)).max(32),
53
+ /** Where `checks` came from, decided where the decision is made rather than guessed later. */
54
+ checksSource: z.enum(['ownership', 'frontmatter', 'package-scripts', 'default']).optional(),
48
55
  agentDoc: z.string().min(1).max(512).optional(),
49
56
  humanDoc: z.string().min(1).max(512).optional(),
50
57
  readme: z.string().min(1).max(512).optional(),
@@ -83,6 +90,35 @@ export const IndexLookupSchema = z
83
90
  })
84
91
  .strict()
85
92
 
93
+ /**
94
+ * Fingerprint of the repository files the index was built from. Freshness can then be checked by
95
+ * re-hashing the inputs instead of rebuilding the whole index on every query.
96
+ */
97
+ export const RepositoryInputsSchema = z
98
+ .object({
99
+ hash: z.string().regex(/^[a-f0-9]{64}$/),
100
+ fileCount: z.number().int().nonnegative().max(1_000_000),
101
+ projectionVersion: z.number().int().nonnegative().max(1_000),
102
+ /** A safety limit stopped the walk, so the projection does not cover the whole repository. */
103
+ incomplete: z.boolean().optional(),
104
+ })
105
+ .strict()
106
+
107
+ /**
108
+ * How this index was prepared for ranking. Recorded so a ranking is reproducible: a different
109
+ * stopword list or a different field weight produces a different artifact, visibly.
110
+ */
111
+ export const RetrievalMetadataSchema = z
112
+ .object({
113
+ lexiconVersion: z.number().int().nonnegative().max(1_000),
114
+ weights: z.record(z.string().min(1).max(64), z.number().min(0).max(1_000)),
115
+ params: z
116
+ .object({ k1: z.number().min(0).max(100), b: z.number().min(0).max(1) })
117
+ .strict()
118
+ .optional(),
119
+ })
120
+ .strict()
121
+
86
122
  export const DocBridgeIndexV1Schema = z
87
123
  .object({
88
124
  schemaVersion: z.literal(INDEX_SCHEMA_VERSION),
@@ -97,12 +133,27 @@ export const DocBridgeIndexV1Schema = z
97
133
  .strict()
98
134
  .optional(),
99
135
  properties: z.array(EcosystemPropertySchema).max(16).optional(),
100
- knowledge: z.array(KnowledgeEntrySchema).max(10_000),
136
+ /*
137
+ * `knowledge[]` and `projection.entries` describe the same entries, so they carry the same
138
+ * bound. They did not: a monorepo projecting eleven thousand entries produced an index the
139
+ * builder wrote and `parseDocBridgeIndex` refused, which made `doctor`, `search` and the MCP
140
+ * server fail on a repository `index` had reported building successfully.
141
+ */
142
+ knowledge: z.array(KnowledgeEntrySchema).max(RETRIEVAL_MAX_ENTRIES),
101
143
  capabilities: z.array(CapabilityRefSchema).max(5_000).optional(),
102
144
  handoffs: z.record(z.string().min(1).max(256), AgentHandoffLegacySchema).optional(),
103
145
  lookup: IndexLookupSchema.optional(),
146
+ inputs: RepositoryInputsSchema.optional(),
147
+ retrieval: RetrievalMetadataSchema.optional(),
148
+ /**
149
+ * The retrieval projection of the snapshot: what ranking reads. `knowledge[]` stays for every
150
+ * reader that predates it, and the two describe the same entries.
151
+ */
152
+ projection: RetrievalIndexV1Schema.optional(),
104
153
  })
105
154
  .strict()
106
155
 
107
156
  export type DocBridgeIndexV1 = z.infer<typeof DocBridgeIndexV1Schema>
108
- export type KnowledgeEntry = z.infer<typeof KnowledgeEntrySchema>
157
+ export type KnowledgeEntry = z.infer<typeof KnowledgeEntrySchema>
158
+ export type RepositoryInputs = z.infer<typeof RepositoryInputsSchema>
159
+ export type RetrievalMetadata = z.infer<typeof RetrievalMetadataSchema>
@@ -0,0 +1,369 @@
1
+ import { z } from 'zod'
2
+
3
+ import { sha256NormalizedV1 } from '../index-builder/content-hash.js'
4
+ import { EvidenceSchema, ProjectIdentitySchema, type Evidence } from './knowledge.js'
5
+
6
+ /**
7
+ * Typed enrichment proposals and the overlay that stores what became of them.
8
+ *
9
+ * `AgentProposalV1` could say "review this finding"; it could not say that a document is
10
+ * canonical for an area, that an alias should resolve to an entity, or that a relation exists
11
+ * with a given confidence. Each of those is a different claim with a different validator and a
12
+ * different acceptance policy, so each is its own kind here, and the union is closed: a kind
13
+ * this file does not know is rejected as `invalid-kind` rather than carried along.
14
+ *
15
+ * Nothing in an overlay is authority. An accepted entry is layered over the observed snapshot
16
+ * at projection time, bound to the content hash of the entity it describes, and expires the
17
+ * moment that hash moves. No entry can delete or alter an observed fact.
18
+ */
19
+
20
+ export const ENRICHMENT_SCHEMA_VERSION = 1 as const
21
+
22
+ const hash = z.string().regex(/^[a-f0-9]{64}$/)
23
+ const boundedString = (max: number) => z.string().min(1).max(max)
24
+ const entityRef = boundedString(256)
25
+ /** A BCP 47-shaped language tag: `en`, `pt-BR`. Presence is what the validators check; this keeps it a tag. */
26
+ const languageTag = z.string().regex(/^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/)
27
+
28
+ export const ENRICHMENT_KINDS = [
29
+ 'classify-document',
30
+ 'summarize',
31
+ 'add-alias',
32
+ 'add-intent',
33
+ 'mark-canonical',
34
+ 'propose-relation',
35
+ 'flag-contradiction',
36
+ 'flag-redundancy',
37
+ 'flag-gap',
38
+ 'rank-hint',
39
+ 'suggest-area',
40
+ ] as const
41
+ export type EnrichmentKind = (typeof ENRICHMENT_KINDS)[number]
42
+
43
+ /**
44
+ * What acceptance means per kind.
45
+ *
46
+ * `policy`: low-risk, reversible, and expiring with its target — accepted by the validators.
47
+ * `human`: structural — a relation, a canonical marker, a conflict — held for a person.
48
+ * `finding`: accepted by policy, but only ever as a finding to review, never as a fact the
49
+ * projection ranks on.
50
+ */
51
+ export type EnrichmentPolicy = 'policy' | 'human' | 'finding'
52
+
53
+ export const ENRICHMENT_POLICY: Readonly<Record<EnrichmentKind, EnrichmentPolicy>> = {
54
+ 'classify-document': 'policy',
55
+ summarize: 'policy',
56
+ 'add-alias': 'policy',
57
+ 'add-intent': 'policy',
58
+ 'mark-canonical': 'human',
59
+ 'propose-relation': 'human',
60
+ 'flag-contradiction': 'human',
61
+ 'flag-redundancy': 'human',
62
+ 'flag-gap': 'finding',
63
+ 'rank-hint': 'policy',
64
+ 'suggest-area': 'human',
65
+ }
66
+
67
+ export const isEnrichmentKind = (value: unknown): value is EnrichmentKind =>
68
+ typeof value === 'string' && (ENRICHMENT_KINDS as readonly string[]).includes(value)
69
+
70
+ /** Relation kinds an agent may propose. Structural kinds the analyzers observe are not for an agent to assert. */
71
+ export const PROPOSABLE_RELATION_KINDS = ['covers', 'mentions', 'links-to', 'depends-on', 'related-to', 'documents'] as const
72
+
73
+ export const EnrichmentOriginSchema = z
74
+ .object({
75
+ agentId: boundedString(256),
76
+ agentVersion: boundedString(64),
77
+ promptVersion: boundedString(64),
78
+ model: boundedString(256).optional(),
79
+ provider: boundedString(128).optional(),
80
+ })
81
+ .strict()
82
+ export type EnrichmentOrigin = z.infer<typeof EnrichmentOriginSchema>
83
+
84
+ export const DocumentTypeSchema = z.enum(['guide', 'reference', 'runbook', 'adr', 'spec', 'index', 'changelog', 'policy', 'other'])
85
+ export const DocumentAudienceSchema = z.enum(['agent', 'human', 'human-and-agent'])
86
+ export const DocumentLifecycleSchema = z.enum(['draft', 'active', 'deprecated', 'archived'])
87
+ export const DocumentCriticalitySchema = z.enum(['tier-0', 'tier-1', 'tier-2'])
88
+
89
+ export const SUMMARY_MAX = 400
90
+ export const ALIAS_MAX = 64
91
+ export const INTENT_MAX = 120
92
+
93
+ const payloads = {
94
+ 'classify-document': z
95
+ .object({ type: DocumentTypeSchema, audience: DocumentAudienceSchema, lifecycle: DocumentLifecycleSchema, criticality: DocumentCriticalitySchema })
96
+ .strict(),
97
+ summarize: z.object({ summary: boundedString(SUMMARY_MAX), language: languageTag }).strict(),
98
+ 'add-alias': z.object({ alias: boundedString(ALIAS_MAX) }).strict(),
99
+ 'add-intent': z.object({ phrase: boundedString(INTENT_MAX), language: languageTag }).strict(),
100
+ 'mark-canonical': z.object({ scope: entityRef }).strict(),
101
+ 'propose-relation': z
102
+ .object({ from: entityRef, to: entityRef, kind: z.enum(PROPOSABLE_RELATION_KINDS), detection: boundedString(512) })
103
+ .strict(),
104
+ 'flag-contradiction': z.object({ against: entityRef, claim: boundedString(1_000), observed: boundedString(1_000) }).strict(),
105
+ 'flag-redundancy': z.object({ with: entityRef }).strict(),
106
+ 'flag-gap': z.object({ area: entityRef, missing: boundedString(1_000) }).strict(),
107
+ 'rank-hint': z.object({ relevance: z.enum(['strong', 'weak']) }).strict(),
108
+ 'suggest-area': z.object({ directories: z.array(boundedString(512)).min(1).max(32), name: boundedString(128) }).strict(),
109
+ } as const
110
+
111
+ export type EnrichmentPayloads = { readonly [K in EnrichmentKind]: z.infer<(typeof payloads)[K]> }
112
+
113
+ const envelope = {
114
+ type: z.literal('enrichment-proposal'),
115
+ schemaVersion: z.literal(ENRICHMENT_SCHEMA_VERSION),
116
+ /** Content-derived: see `enrichmentProposalId`. A re-run over the same inputs produces the same id. */
117
+ proposalId: hash,
118
+ /** The entity the claim is about. It must exist in the snapshot the proposal was made against. */
119
+ entity: entityRef,
120
+ /** The entity's content hash when the proposal was made. The entry expires when this moves. */
121
+ targetContentHash: hash,
122
+ confidence: z.number().min(0).max(1),
123
+ reason: boundedString(1_000),
124
+ /** At least one item, and every item must be present in the supplied snapshot or report. */
125
+ evidence: z.array(EvidenceSchema).min(1).max(32),
126
+ relatedDiagnosticIds: z.array(boundedString(256)).max(32).optional(),
127
+ origin: EnrichmentOriginSchema,
128
+ baseSnapshotHash: hash,
129
+ }
130
+
131
+ const proposalOf = <K extends EnrichmentKind>(kind: K) => z.object({ ...envelope, kind: z.literal(kind), payload: payloads[kind] }).strict()
132
+
133
+ export const EnrichmentProposalV1Schema = z.discriminatedUnion('kind', [
134
+ proposalOf('classify-document'),
135
+ proposalOf('summarize'),
136
+ proposalOf('add-alias'),
137
+ proposalOf('add-intent'),
138
+ proposalOf('mark-canonical'),
139
+ proposalOf('propose-relation'),
140
+ proposalOf('flag-contradiction'),
141
+ proposalOf('flag-redundancy'),
142
+ proposalOf('flag-gap'),
143
+ proposalOf('rank-hint'),
144
+ proposalOf('suggest-area'),
145
+ ])
146
+ export type EnrichmentProposalV1 = z.infer<typeof EnrichmentProposalV1Schema>
147
+ export type EnrichmentProposalOf<K extends EnrichmentKind> = Extract<EnrichmentProposalV1, { kind: K }>
148
+
149
+ /**
150
+ * The part of a payload that makes two proposals of one kind about one entity different things.
151
+ *
152
+ * A summary, a classification, a canonical marker and a rank hint are slots: one per entity per
153
+ * agent and prompt, so a re-run replaces rather than accumulates. An alias, an intent, a relation
154
+ * or a flag can legitimately exist several times for one entity, so the field that tells them
155
+ * apart is part of the identity.
156
+ */
157
+ export const enrichmentProposalKey = (kind: EnrichmentKind, payload: unknown): unknown => {
158
+ const record = (payload && typeof payload === 'object' ? payload : {}) as Record<string, unknown>
159
+ switch (kind) {
160
+ case 'add-alias':
161
+ return { alias: record.alias }
162
+ case 'add-intent':
163
+ return { phrase: record.phrase, language: record.language }
164
+ case 'propose-relation':
165
+ return { from: record.from, to: record.to, kind: record.kind }
166
+ case 'flag-contradiction':
167
+ return { against: record.against }
168
+ case 'flag-redundancy':
169
+ return { with: record.with }
170
+ case 'flag-gap':
171
+ return { area: record.area, missing: record.missing }
172
+ case 'suggest-area':
173
+ return { directories: record.directories }
174
+ default:
175
+ return null
176
+ }
177
+ }
178
+
179
+ export type EnrichmentIdentity = {
180
+ readonly kind: EnrichmentKind
181
+ readonly entity: string
182
+ readonly targetContentHash: string
183
+ readonly origin: Pick<EnrichmentOrigin, 'agentId' | 'promptVersion'>
184
+ readonly payload?: unknown
185
+ }
186
+
187
+ /** The proposal id: kind, entity, target hash, agent identity, prompt version — and the payload key. */
188
+ export const enrichmentProposalId = (identity: EnrichmentIdentity): string =>
189
+ sha256NormalizedV1({
190
+ kind: identity.kind,
191
+ entity: identity.entity,
192
+ targetContentHash: identity.targetContentHash,
193
+ agentId: identity.origin.agentId,
194
+ promptVersion: identity.origin.promptVersion,
195
+ key: enrichmentProposalKey(identity.kind, identity.payload),
196
+ })
197
+
198
+ /** Why a proposal was not accepted. A closed list, so the histogram in `stats` is comparable across runs. */
199
+ export const ENRICHMENT_REJECTION_REASONS = [
200
+ 'invalid-kind',
201
+ 'schema',
202
+ 'no-evidence',
203
+ 'unknown-entity',
204
+ 'stale-target',
205
+ 'base-snapshot-mismatch',
206
+ 'unknown-diagnostic',
207
+ 'evidence-outside-artifacts',
208
+ 'proposal-id-mismatch',
209
+ 'entity-outside-pack',
210
+ 'entity-kind',
211
+ 'redaction',
212
+ 'summary-unchanged',
213
+ 'alias-collision',
214
+ 'unknown-scope',
215
+ 'canonical-conflict',
216
+ 'unknown-endpoint',
217
+ 'relation-already-observed',
218
+ 'evidence-outside-endpoints',
219
+ 'evidence-missing-for-endpoint',
220
+ 'already-duplicate',
221
+ 'already-covered',
222
+ 'unknown-directory',
223
+ 'area-overlap',
224
+ 'self-approval',
225
+ 'self-adjudication',
226
+ 'adjudicated',
227
+ 'human-rejected',
228
+ 'expired',
229
+ ] as const
230
+ export type EnrichmentRejectionReason = (typeof ENRICHMENT_REJECTION_REASONS)[number]
231
+
232
+ export const AcceptedEnrichmentSchema = z
233
+ .object({
234
+ proposal: EnrichmentProposalV1Schema,
235
+ acceptedAt: z.string().datetime(),
236
+ /** `policy` for accept-by-policy kinds; otherwise the person who approved it. Never an agent. */
237
+ acceptedBy: boundedString(256),
238
+ approvalId: hash.optional(),
239
+ })
240
+ .strict()
241
+ .superRefine((value, context) => {
242
+ const policy = ENRICHMENT_POLICY[value.proposal.kind]
243
+ if (value.acceptedBy === 'policy' && policy === 'human') {
244
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ['acceptedBy'], message: `${value.proposal.kind} requires human approval; acceptedBy cannot be "policy"` })
245
+ }
246
+ if (value.acceptedBy === value.proposal.origin.agentId) {
247
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ['acceptedBy'], message: 'a proposal cannot be accepted by its own author' })
248
+ }
249
+ })
250
+ export type AcceptedEnrichment = z.infer<typeof AcceptedEnrichmentSchema>
251
+
252
+ export const PendingEnrichmentSchema = z
253
+ .object({
254
+ proposal: EnrichmentProposalV1Schema,
255
+ /** The approval record this entry waits on: the hash of the proposal id and the target hash. */
256
+ approvalId: hash,
257
+ /** Why it is more than merely pending: a canonical conflict, a disputed contradiction. */
258
+ note: z.enum(['canonical-conflict', 'disputed-contradiction']).optional(),
259
+ })
260
+ .strict()
261
+ export type PendingEnrichment = z.infer<typeof PendingEnrichmentSchema>
262
+
263
+ export const RejectedEnrichmentSchema = z
264
+ .object({
265
+ proposalId: boundedString(128),
266
+ kind: boundedString(128),
267
+ entity: boundedString(256).optional(),
268
+ reason: z.enum(ENRICHMENT_REJECTION_REASONS),
269
+ detail: z.string().max(1_024).optional(),
270
+ origin: EnrichmentOriginSchema.optional(),
271
+ })
272
+ .strict()
273
+ export type RejectedEnrichment = z.infer<typeof RejectedEnrichmentSchema>
274
+
275
+ const count = z.number().int().nonnegative()
276
+
277
+ /**
278
+ * A rejection that means the agent named something that does not exist.
279
+ *
280
+ * It is counted on its own rather than folded into the rejection total, because it is the one
281
+ * number that says whether an agent is inventing structure. A curator that classifies a document
282
+ * badly is wrong about a judgement; one that proposes a relation to a module the repository does
283
+ * not contain is making things up, and that number must trend to zero or the agent is unusable.
284
+ */
285
+ export const INVENTED_RELATION_REASONS = ['unknown-endpoint', 'unknown-entity', 'unknown-scope', 'unknown-directory', 'unknown-diagnostic'] as const
286
+
287
+ export const EnrichmentStatsSchema = z
288
+ .object({
289
+ byKind: z.record(z.string().max(128), z.object({ proposed: count, accepted: count, pending: count, rejected: count }).strict()),
290
+ rejectionReasons: z.record(z.string().max(128), count),
291
+ /** Rejections that named a non-existent entity, endpoint, scope, directory or diagnostic. */
292
+ inventedReferences: count,
293
+ agentRuns: count,
294
+ cacheHits: count,
295
+ /** Cache hits over cache lookups, rounded to six places. 1 means the run asked no agent anything. */
296
+ cacheHitRate: z.number().min(0).max(1),
297
+ packs: count,
298
+ inputBytes: count,
299
+ outputBytes: count,
300
+ /** Wall time of the run. Outside the overlay's content hash, like every other cost figure. */
301
+ wallTimeMs: count,
302
+ expired: count,
303
+ })
304
+ .strict()
305
+ export type EnrichmentStats = z.infer<typeof EnrichmentStatsSchema>
306
+
307
+ export const EnrichmentOverlayV1Schema = z
308
+ .object({
309
+ type: z.literal('enrichment-overlay'),
310
+ schemaVersion: z.literal(ENRICHMENT_SCHEMA_VERSION),
311
+ contentHash: hash,
312
+ contentHashAlgo: z.literal('sha256-normalized-v1'),
313
+ project: ProjectIdentitySchema,
314
+ sourceRevision: boundedString(128),
315
+ sourceRevisionKind: z.enum(['git', 'content']),
316
+ configurationHash: hash,
317
+ pipelineVersion: boundedString(64),
318
+ analyzerVersions: z.record(boundedString(128), boundedString(64)),
319
+ baseSnapshotHash: hash,
320
+ accepted: z.array(AcceptedEnrichmentSchema).max(10_000),
321
+ pending: z.array(PendingEnrichmentSchema).max(10_000),
322
+ rejected: z.array(RejectedEnrichmentSchema).max(10_000),
323
+ stats: EnrichmentStatsSchema,
324
+ })
325
+ .strict()
326
+ export type EnrichmentOverlayV1 = z.infer<typeof EnrichmentOverlayV1Schema>
327
+
328
+ /**
329
+ * The overlay's own hash, over what it decided and not over when or at what cost.
330
+ *
331
+ * `acceptedAt` is a fact about a person's action, and `stats` describe the run that produced the
332
+ * file — how many agent calls, how many cache hits. Neither is a fact about the repository, and
333
+ * two runs over one unchanged repository must produce one overlay hash, so both are outside it.
334
+ * What is inside: every accepted, pending and rejected entry and the hashes they bind to.
335
+ */
336
+ export const enrichmentOverlayContentHash = (overlay: Omit<EnrichmentOverlayV1, 'contentHash'> & { readonly contentHash?: string }): string => {
337
+ const { contentHash: _contentHash, stats: _stats, ...rest } = overlay
338
+ return sha256NormalizedV1({ ...rest, accepted: rest.accepted.map(({ acceptedAt: _acceptedAt, ...entry }) => entry) })
339
+ }
340
+
341
+ /**
342
+ * An adjudication: a third agent's verdict on proposals two others could not settle — a canonical
343
+ * conflict, a disputed contradiction. It judges; it never approves. The agent that made a
344
+ * proposal cannot be the agent that judges it, which the validator enforces by identity.
345
+ */
346
+ export const EnrichmentAdjudicationV1Schema = z
347
+ .object({
348
+ type: z.literal('enrichment-adjudication'),
349
+ schemaVersion: z.literal(ENRICHMENT_SCHEMA_VERSION),
350
+ adjudicationId: hash,
351
+ /** The proposals judged, by id. Every one must be pending. */
352
+ judges: z.array(hash).min(1).max(16),
353
+ /** Which of the judged proposals survive; the rest are rejected as `adjudicated`. */
354
+ keep: z.array(hash).max(16),
355
+ reason: boundedString(1_000),
356
+ origin: EnrichmentOriginSchema,
357
+ })
358
+ .strict()
359
+ .superRefine((value, context) => {
360
+ if (value.keep.some((id) => !value.judges.includes(id))) {
361
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ['keep'], message: 'keep may only name judged proposals' })
362
+ }
363
+ })
364
+ export type EnrichmentAdjudicationV1 = z.infer<typeof EnrichmentAdjudicationV1Schema>
365
+
366
+ export const enrichmentAdjudicationId = (input: Pick<EnrichmentAdjudicationV1, 'judges' | 'keep' | 'origin'>): string =>
367
+ sha256NormalizedV1({ judges: [...input.judges].sort(), keep: [...input.keep].sort(), agentId: input.origin.agentId, promptVersion: input.origin.promptVersion })
368
+
369
+ export type { Evidence as EnrichmentEvidence }
@@ -1,3 +1,5 @@
1
+ import { RETRIEVAL_MAX_ENTRIES } from './retrieval-index.js'
2
+
1
3
  type JsonSchema = {
2
4
  readonly [key: string]: unknown
3
5
  }
@@ -35,7 +37,7 @@ export const AgentHandoffV1JsonSchema = {
35
37
  required: ['type', 'id'],
36
38
  properties: {
37
39
  type: {
38
- enum: ['package', 'module', 'app', 'screen', 'flow', 'component', 'intent', 'change', 'search'],
40
+ enum: ['package', 'area', 'module', 'document', 'app', 'screen', 'flow', 'component', 'intent', 'change', 'search'],
39
41
  },
40
42
  id: { type: 'string', minLength: 1, maxLength: 256 },
41
43
  path: { type: 'string', minLength: 1, maxLength: 512 },
@@ -50,6 +52,41 @@ export const AgentHandoffV1JsonSchema = {
50
52
  humanDoc: { anyOf: [{ type: 'string', minLength: 1, maxLength: 512 }, { type: 'null' }] },
51
53
  playbookPatterns: { type: 'array', items: { type: 'string', format: 'uri' }, maxItems: 16 },
52
54
  notes: stringArray(16),
55
+ related: {
56
+ type: 'array',
57
+ maxItems: 16,
58
+ items: {
59
+ type: 'object',
60
+ additionalProperties: false,
61
+ required: ['id', 'path', 'direction', 'strength', 'evidence'],
62
+ properties: {
63
+ id: { type: 'string', minLength: 1, maxLength: 256 },
64
+ path: { type: 'string', minLength: 1, maxLength: 512 },
65
+ direction: { enum: ['imports', 'imported-by'] },
66
+ strength: { type: 'integer', minimum: 1 },
67
+ evidence: stringArray(8),
68
+ },
69
+ },
70
+ },
71
+ explain: { type: 'object', additionalProperties: stringArray(16) },
72
+ evidence: {
73
+ type: 'array',
74
+ maxItems: 32,
75
+ items: {
76
+ type: 'object',
77
+ additionalProperties: false,
78
+ required: ['source', 'path'],
79
+ properties: {
80
+ source: { enum: ['code', 'configuration', 'documentation', 'agent', 'derived'] },
81
+ path: { type: 'string', minLength: 1, maxLength: 512 },
82
+ lineStart: { type: 'integer', minimum: 1 },
83
+ lineEnd: { type: 'integer', minimum: 1 },
84
+ contentHash: { type: 'string', pattern: '^[a-f0-9]{64}$' },
85
+ context: { type: 'string', maxLength: 1024 },
86
+ },
87
+ },
88
+ },
89
+ metadata: { type: 'object' },
53
90
  },
54
91
  } as const satisfies JsonSchema
55
92
 
@@ -111,7 +148,7 @@ export const DocBridgeIndexV1JsonSchema = {
111
148
  },
112
149
  knowledge: {
113
150
  type: 'array',
114
- maxItems: 10000,
151
+ maxItems: RETRIEVAL_MAX_ENTRIES,
115
152
  items: {
116
153
  type: 'object',
117
154
  additionalProperties: false,