@agentskit/doc-bridge 1.7.45 → 1.10.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 +488 -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 +15871 -6218
  10. package/dist/cli/program.js.map +1 -1
  11. package/dist/config/index.d.ts +1 -1
  12. package/dist/config/index.js +95 -6
  13. package/dist/config/index.js.map +1 -1
  14. package/dist/{index-BUL0q7s8.d.ts → index-7wYGbllW.d.ts} +134 -1
  15. package/dist/index.d.ts +9880 -1062
  16. package/dist/index.js +16143 -6127
  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 +157 -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 +34 -15
  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 +126 -2
  139. package/src/conformance/documentation-standard-v1.ts +8 -6
  140. package/src/discovery/areas.ts +197 -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 +485 -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
@@ -2,8 +2,14 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
3
 
4
4
  import type { DocBridgeConfigV1 } from '../config/schema.js'
5
+ import { applyDocumentationDeclarations } from '../discovery/documentation.js'
6
+ import { discoverRepository } from '../discovery/repository.js'
7
+ import { readBoundedText, type TextReadBudget } from '../lib/bounded-text.js'
5
8
  import { toPosix } from '../lib/paths.js'
6
- import type { DocBridgeIndexV1 } from '../schemas/doc-bridge-index.js'
9
+ import type { DocBridgeIndexV1, KnowledgeEntry } from '../schemas/doc-bridge-index.js'
10
+ import { RETRIEVAL_MAX_ENTRIES } from '../schemas/retrieval-index.js'
11
+ import type { DiscoverySnapshotV1 } from '../schemas/knowledge.js'
12
+ import type { RetrievalIndexV1 } from '../schemas/retrieval-index.js'
7
13
  import { buildLookup, collectPackages } from './build-handoffs.js'
8
14
  import { renderCapabilitiesJson } from './capabilities.js'
9
15
  import { sha256NormalizedV1 } from './content-hash.js'
@@ -11,12 +17,33 @@ import { renderLlmsTxt } from './llms-txt.js'
11
17
  import { scanHumanDocs } from './human-adapters/index.js'
12
18
  import { discoverNxProjects } from './plugins/nx.js'
13
19
  import { discoverPnpmPackages } from './plugins/pnpm-monorepo.js'
20
+ import { repositoryInputs } from './project-corpus.js'
14
21
  import { scanAgentCorpus } from './scan-corpus.js'
22
+ import { SEARCH_LEXICON_VERSION } from '../query/text.js'
23
+ import { projectRetrievalIndex, toKnowledgeEntry } from '../retrieval/project.js'
24
+ import { resolveSearchParams, resolveSearchWeights } from '../retrieval/weights.js'
25
+ import { projectEnrichmentOverlay, readEnrichmentOverlay } from '../enrich/overlay.js'
26
+ import type { EnrichmentOverlayV1 } from '../schemas/enrichment.js'
15
27
 
16
28
  export type BuildIndexOptions = {
17
29
  readonly root?: string
18
30
  readonly config: DocBridgeConfigV1
19
31
  readonly write?: boolean
32
+ /**
33
+ * A snapshot to project instead of scanning. The build scans when none is given; a caller that
34
+ * already holds the snapshot — a workflow stage, a test — passes it so the index and the
35
+ * artifacts it sits next to describe the same observation.
36
+ */
37
+ readonly snapshot?: DiscoverySnapshotV1
38
+ /**
39
+ * Which enrichment overlay the projection reads.
40
+ *
41
+ * Omitted, the builder reads the one on disk while the Registry is enabled — the normal path.
42
+ * `'ignore'` builds the deterministic baseline even then, and an overlay object projects that
43
+ * one instead. Both exist so the retrieval delta can measure the same snapshot twice, with the
44
+ * overlay and without it, rather than comparing two different repositories.
45
+ */
46
+ readonly overlay?: EnrichmentOverlayV1 | 'ignore'
20
47
  }
21
48
 
22
49
  export type BuildIndexResult = {
@@ -37,6 +64,58 @@ const projectName = (root: string, config: DocBridgeConfigV1): string => {
37
64
  return toPosix(root.split('/').pop() ?? 'project')
38
65
  }
39
66
 
67
+ /**
68
+ * Observe, declare, project.
69
+ *
70
+ * The snapshot is the observation; documentation declarations (`covers`, frontmatter relations)
71
+ * are applied on top of it so a document's declared coverage ranks and routes like an observed
72
+ * edge; the projection is computed from the result. Document bodies are read once, bounded, and
73
+ * handed to both steps by path.
74
+ */
75
+ const projectFromSnapshot = (
76
+ root: string,
77
+ config: DocBridgeConfigV1,
78
+ given: DiscoverySnapshotV1 | undefined,
79
+ lookup: ReturnType<typeof buildLookup>['lookup'],
80
+ curated: readonly KnowledgeEntry[],
81
+ requested: BuildIndexOptions['overlay'],
82
+ ): { readonly projection: RetrievalIndexV1 } => {
83
+ const observed = given ?? discoverRepository({ root, config })
84
+ const budget: TextReadBudget = { used: 0 }
85
+ const contents = new Map<string, string>()
86
+ for (const entity of observed.entities) {
87
+ if (entity.kind !== 'document' || !entity.path) continue
88
+ try {
89
+ contents.set(entity.path, readBoundedText(join(root, entity.path), budget))
90
+ } catch {
91
+ // Unreadable now: the entity still projects from what the snapshot recorded about it.
92
+ }
93
+ }
94
+ const declared = applyDocumentationDeclarations(
95
+ observed,
96
+ [...contents.entries()].map(([path, content]) => ({ path, content })),
97
+ { agentRoot: config.corpus.agent.root },
98
+ ).snapshot
99
+ /*
100
+ * The accepted enrichment overlay is consulted only while the Registry is enabled: switching it
101
+ * off restores the deterministic baseline exactly. The read never writes, and an entry whose
102
+ * target moved since it was accepted is expired here rather than ranked.
103
+ */
104
+ const accepted = requested === 'ignore'
105
+ ? undefined
106
+ : (requested ?? (config.intelligence?.registry?.enabled ? readEnrichmentOverlay(root) : undefined))
107
+ const overlay = accepted ? projectEnrichmentOverlay(accepted, declared) : undefined
108
+ const projection = projectRetrievalIndex({
109
+ snapshot: declared,
110
+ config,
111
+ routes: lookup,
112
+ curated: curated.map((entry) => ({ id: entry.id, path: entry.path, title: entry.title, ...(entry.description ? { description: entry.description } : {}) })),
113
+ ...(overlay ? { overlay } : {}),
114
+ readDocument: (path) => contents.get(path),
115
+ })
116
+ return { projection }
117
+ }
118
+
40
119
  const existingGeneratedAt = (indexPath: string, contentHash: string): string | undefined => {
41
120
  try {
42
121
  const index = JSON.parse(readFileSync(indexPath, 'utf8')) as {
@@ -59,7 +138,12 @@ export const buildDocBridgeIndex = (opts: BuildIndexOptions): BuildIndexResult =
59
138
  const indexPath = join(root, outFile)
60
139
 
61
140
  const corpus = scanAgentCorpus(root, config)
62
- const knowledge = corpus.map(({ absPath: _a, relPath: _r, frontmatter: _f, ...entry }) => entry)
141
+ const curated = corpus.map(({ absPath: _a, relPath: _r, frontmatter: _f, ...entry }) => entry)
142
+ const retrieval = {
143
+ lexiconVersion: SEARCH_LEXICON_VERSION,
144
+ weights: resolveSearchWeights(config.retrieval?.weights),
145
+ params: resolveSearchParams(config.retrieval?.params),
146
+ }
63
147
 
64
148
  const shouldDiscover =
65
149
  config.routing?.plugin === 'pnpm-monorepo' ||
@@ -78,11 +162,39 @@ export const buildDocBridgeIndex = (opts: BuildIndexOptions): BuildIndexResult =
78
162
 
79
163
  const { lookup, handoffs } = buildLookup(config, packages, corpus, outFile, humanDocs, root)
80
164
 
165
+ /*
166
+ * Retrieval reads this index, so whatever is missing here is invisible to an agent however well
167
+ * it is ranked. The projection puts every entity the snapshot observed in it — documents,
168
+ * modules, areas, packages — next to the routes the configuration declares. It is a function of
169
+ * the snapshot: the index has no scanner of its own, so a record retrieval can find is an entity
170
+ * discovery observed, with the same id and the same content hash.
171
+ */
172
+ const projected = config.retrieval?.corpus?.enabled === false ? undefined : projectFromSnapshot(root, config, opts.snapshot, lookup, curated, opts.overlay)
173
+ const projection = projected?.projection
174
+ const inputs = projected ? repositoryInputs(root, config) : undefined
175
+ const curatedPaths = new Set(curated.map((entry) => entry.path))
176
+ /*
177
+ * `knowledge[]` keeps every reader that predates the projection working: the curated sidecars
178
+ * first, in reading order, then every projected document and module. Body text lives once, in
179
+ * the projection, so a projected index carries no bodies here.
180
+ */
181
+ const knowledge: KnowledgeEntry[] = projection
182
+ ? [
183
+ ...curated.map(({ body: _body, ...entry }) => entry),
184
+ ...projection.entries
185
+ .filter((entry) => (entry.kind === 'document' || entry.kind === 'module') && !curatedPaths.has(entry.path))
186
+ .map(toKnowledgeEntry),
187
+ ]
188
+ : curated
189
+
81
190
  const hashPayload = {
82
191
  schemaVersion: 1,
83
192
  knowledge,
84
193
  handoffs,
85
194
  lookup,
195
+ retrieval,
196
+ ...(inputs ? { inputs } : {}),
197
+ ...(projection ? { projection: projection.contentHash } : {}),
86
198
  }
87
199
 
88
200
  const contentHash = sha256NormalizedV1(hashPayload)
@@ -95,6 +207,23 @@ export const buildDocBridgeIndex = (opts: BuildIndexOptions): BuildIndexResult =
95
207
  knowledge,
96
208
  handoffs,
97
209
  lookup,
210
+ ...(inputs ? { inputs } : {}),
211
+ retrieval,
212
+ ...(projection ? { projection } : {}),
213
+ }
214
+
215
+ /*
216
+ * An artifact its own parser refuses is not an artifact. The bound on `knowledge[]` used to be
217
+ * lower than the bound on the projection it mirrors, so a large repository got an index that
218
+ * `index` reported writing and every reader — `doctor`, `search`, the MCP server — then rejected,
219
+ * with a schema dump naming an array instead of a corpus. The bounds are one constant now; this
220
+ * says so at the point of production, where the numbers and the remedy are both known.
221
+ */
222
+ if (knowledge.length > RETRIEVAL_MAX_ENTRIES) {
223
+ throw new Error(
224
+ `The index would carry ${knowledge.length} knowledge entries, above the ${RETRIEVAL_MAX_ENTRIES} an index may hold. ` +
225
+ 'Narrow the corpus with `corpus.*.include` or `corpus.*.exclude`, or split the repository into more than one index.',
226
+ )
98
227
  }
99
228
 
100
229
  if (write) {
@@ -111,7 +240,7 @@ export const buildDocBridgeIndex = (opts: BuildIndexOptions): BuildIndexResult =
111
240
  if (write) {
112
241
  writeFileSync(
113
242
  llmsTxtPath,
114
- renderLlmsTxt(config, knowledge, index.project?.name ?? 'project'),
243
+ renderLlmsTxt(config, knowledge, index.project?.name ?? 'project', { root }),
115
244
  'utf8',
116
245
  )
117
246
  }
@@ -1,5 +1,7 @@
1
1
  import type { DocBridgeConfigV1 } from '../config/schema.js'
2
+ import { renderNamedTemplate } from '../render/template-source.js'
2
3
  import type { KnowledgeEntry } from '../schemas/doc-bridge-index.js'
4
+ import { isProjectedEntry } from './project-corpus.js'
3
5
 
4
6
  const routePath = (path: string, pathPrefix: string | undefined): string => {
5
7
  const normalizedPath = path.replaceAll('\\', '/').replace(/\.(?:md|mdx)$/, '')
@@ -20,20 +22,58 @@ export const knowledgeUrl = (
20
22
  return new URL(relativePath, base).toString()
21
23
  }
22
24
 
23
- export const renderLlmsTxt = (
25
+ export type LlmsTxtEntry = {
26
+ readonly title: string
27
+ readonly url: string
28
+ readonly description?: string
29
+ }
30
+
31
+ export type LlmsTxtVariables = {
32
+ readonly project: string
33
+ readonly preamble: string
34
+ readonly entries: readonly LlmsTxtEntry[]
35
+ }
36
+
37
+ /** What the `llms.txt` template sees. Exported so a project overriding the template knows the variables. */
38
+ export const llmsTxtVariables = (
24
39
  config: DocBridgeConfigV1,
25
40
  knowledge: readonly KnowledgeEntry[],
26
41
  projectName: string,
27
- ): string => {
42
+ ): LlmsTxtVariables => {
28
43
  const preamble =
29
44
  config.index?.llmsTxt?.preamble ??
30
45
  `# ${projectName}\n\n> Agent-readable documentation index generated by ak-docs (@agentskit/doc-bridge).\n`
31
46
 
32
- const lines = knowledge.slice(0, 500).map((entry) => {
33
- const desc = entry.description ? `: ${entry.description}` : ''
34
- const url = knowledgeUrl(entry.path, config.index?.llmsTxt)
35
- return `- [${entry.title}](${url})${desc}`
36
- })
47
+ /*
48
+ * llms.txt is a reading order, not an inventory. The index also carries every document and
49
+ * module the repository projects into it for retrieval; listing those here would bury the
50
+ * curated entry points an agent is meant to start from. The filter lives in the renderer so
51
+ * every caller — the builder and the conformance profile that re-renders to check freshness —
52
+ * agrees on what the file contains.
53
+ */
54
+ const entries = knowledge
55
+ .filter((entry) => !isProjectedEntry(entry))
56
+ .slice(0, 500)
57
+ .map((entry) => ({
58
+ title: entry.title,
59
+ url: knowledgeUrl(entry.path, config.index?.llmsTxt),
60
+ ...(entry.description ? { description: entry.description } : {}),
61
+ }))
37
62
 
38
- return `${preamble.trim()}\n\n## Knowledge\n\n${lines.join('\n')}\n`
63
+ return { project: projectName, preamble: preamble.trim(), entries }
39
64
  }
65
+
66
+ /**
67
+ * Render `llms.txt` from the bundled template, or the project's under `render.templates`.
68
+ *
69
+ * `root` resolves a project template path; the builder and the conformance profile both pass
70
+ * theirs, so the file the builder writes is the file the profile expects. The bundled template
71
+ * reproduces the previous concatenation byte for byte, which the llms and federation tests hold
72
+ * it to.
73
+ */
74
+ export const renderLlmsTxt = (
75
+ config: DocBridgeConfigV1,
76
+ knowledge: readonly KnowledgeEntry[],
77
+ projectName: string,
78
+ options: { readonly root?: string } = {},
79
+ ): string => renderNamedTemplate('llms.txt', llmsTxtVariables(config, knowledge, projectName), config, options.root)
@@ -0,0 +1,111 @@
1
+ import { basename, extname, relative, resolve, sep } from 'node:path'
2
+ import { readFileSync } from 'node:fs'
3
+
4
+ import type { DocBridgeConfigV1 } from '../config/schema.js'
5
+ import { DOCUMENT_EXTENSIONS, SOURCE_EXTENSIONS, safeWalkOptions } from '../discovery/inputs.js'
6
+ import { toPosix } from '../lib/paths.js'
7
+ import { safeWalkFiles } from '../safety/repository.js'
8
+ import type { KnowledgeEntry } from '../schemas/doc-bridge-index.js'
9
+ import { sha256NormalizedV1 } from './content-hash.js'
10
+
11
+ /**
12
+ * The freshness fingerprint of the index.
13
+ *
14
+ * This file used to be retrieval's own scanner: it walked the repository, parsed every module and
15
+ * document a second time, and projected them into index records that shared nothing with the
16
+ * snapshot but a path. That projection is now a function of the snapshot (`src/retrieval/project.ts`),
17
+ * and what remains here is the one thing a query needs on every call: a cheap way to tell whether
18
+ * the index on disk still describes the files on disk. One walk, one hash per file, no parsing.
19
+ */
20
+
21
+ /**
22
+ * Bumped when what the index derives from its inputs changes shape, so an index built by older
23
+ * code is stale by version even when every file matches.
24
+ */
25
+ export const CORPUS_PROJECTION_VERSION = 2 as const
26
+
27
+ /**
28
+ * Entry types the projection produces into `knowledge[]`. Curated corpus entries are always
29
+ * `agent-doc`, so this is what distinguishes a record retrieval discovered from one a human wrote
30
+ * a sidecar for — which surfaces such as llms.txt need, since a reading order is curated, not
31
+ * enumerated.
32
+ */
33
+ export const PROJECTED_ENTRY_TYPES = ['document', 'module'] as const
34
+
35
+ export const isProjectedEntry = (entry: Pick<KnowledgeEntry, 'type'>): boolean =>
36
+ (PROJECTED_ENTRY_TYPES as readonly string[]).includes(entry.type)
37
+
38
+ /**
39
+ * Configuration files the index is derived from. Narrow on purpose: any `.json` would make an
40
+ * unrelated data file mark the index stale, and a generated artifact could then invalidate the
41
+ * artifact generated from it.
42
+ */
43
+ const CONFIG_INPUT_PATTERN =
44
+ /(?:^|\/)(?:package\.json|pnpm-workspace\.ya?ml|tsconfig(?:\.[\w.-]+)?\.json|jsconfig\.json|meta\.json|doc-bridge\.config\.(?:json|ya?ml|js|ts|mjs|cjs))$/
45
+
46
+ const INPUT_EXTENSIONS = [...new Set([...SOURCE_EXTENSIONS, ...DOCUMENT_EXTENSIONS, '.json', '.yaml', '.yml'])]
47
+
48
+ /**
49
+ * Configuration sections the index is derived from.
50
+ *
51
+ * The same files under a different configuration project a different index, so the configuration
52
+ * belongs in the fingerprint — but only the part of it that can change the artifact. Hashing the
53
+ * whole configuration would report a stale index when an unrelated section changed (a gate
54
+ * preset, a report option), which is a false alarm that teaches people to ignore the check.
55
+ */
56
+ const INDEX_CONFIGURATION_KEYS = ['project', 'corpus', 'index', 'routing', 'safety', 'retrieval', 'analysis'] as const
57
+
58
+ export const indexConfigurationHash = (config: DocBridgeConfigV1 | undefined): string =>
59
+ sha256NormalizedV1(
60
+ Object.fromEntries(
61
+ INDEX_CONFIGURATION_KEYS.filter((key) => config?.[key] !== undefined).map((key) => [key, config?.[key]]),
62
+ ),
63
+ )
64
+
65
+ export type RepositoryInputsV1 = {
66
+ /** Hash of every input path and its content. Equal hashes mean an equal projection. */
67
+ readonly hash: string
68
+ readonly fileCount: number
69
+ readonly projectionVersion: number
70
+ /** True when a safety limit stopped the walk, so the file set is not the whole repository. */
71
+ readonly incomplete?: boolean
72
+ }
73
+
74
+ const isInput = (path: string, name: string): boolean => {
75
+ const extension = extname(name)
76
+ if ((DOCUMENT_EXTENSIONS as readonly string[]).includes(extension)) return true
77
+ if ((SOURCE_EXTENSIONS as readonly string[]).includes(extension)) return true
78
+ return CONFIG_INPUT_PATTERN.test(path)
79
+ }
80
+
81
+ /**
82
+ * Walk the repository once and hash every input the index derives from: sources, documents and
83
+ * the configuration files that decide how they resolve.
84
+ */
85
+ export const repositoryInputs = (root: string, config: DocBridgeConfigV1 | undefined): RepositoryInputsV1 => {
86
+ const projectRoot = resolve(root)
87
+ const walk = safeWalkFiles(projectRoot, { extensions: INPUT_EXTENSIONS, ...safeWalkOptions(config) })
88
+ const fingerprints: [string, string][] = []
89
+
90
+ for (const absPath of walk.files) {
91
+ const path = toPosix(relative(projectRoot, absPath).split(sep).join('/'))
92
+ if (!isInput(path, basename(absPath))) continue
93
+ try {
94
+ fingerprints.push([path, sha256NormalizedV1(readFileSync(absPath, 'utf8'))])
95
+ } catch {
96
+ // An unreadable input cannot be projected, and must not silently change the hash either.
97
+ fingerprints.push([path, 'unreadable'])
98
+ }
99
+ }
100
+
101
+ return {
102
+ hash: sha256NormalizedV1({
103
+ projectionVersion: CORPUS_PROJECTION_VERSION,
104
+ configurationHash: indexConfigurationHash(config),
105
+ files: fingerprints,
106
+ }),
107
+ fileCount: fingerprints.length,
108
+ projectionVersion: CORPUS_PROJECTION_VERSION,
109
+ ...(walk.incomplete ? { incomplete: true } : {}),
110
+ }
111
+ }