@agentskit/doc-bridge 1.7.44 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/CHANGELOG.md +471 -0
  2. package/CONTRIBUTING.md +29 -4
  3. package/README.md +87 -40
  4. package/SECURITY.md +7 -0
  5. package/action.yml +1 -1
  6. package/bin/ak-docs.js +2 -2
  7. package/bin/ak-verify.js +13 -7
  8. package/dist/cli/program.d.ts +3 -1
  9. package/dist/cli/program.js +15888 -6061
  10. package/dist/cli/program.js.map +1 -1
  11. package/dist/config/index.d.ts +1 -1
  12. package/dist/config/index.js +91 -9
  13. package/dist/config/index.js.map +1 -1
  14. package/dist/index-Beor6Yhi.d.ts +792 -0
  15. package/dist/index.d.ts +9979 -3257
  16. package/dist/index.js +15954 -5774
  17. package/dist/index.js.map +1 -1
  18. package/docs/MARKETPLACE.md +1 -1
  19. package/docs/PRD-documentation-efficiency-study.md +406 -0
  20. package/docs/PRD-knowledge-retrieval-and-enrichment.md +466 -0
  21. package/docs/RELEASE.md +22 -8
  22. package/docs/adr/0002-documentation-audit-boundary.md +22 -0
  23. package/docs/adr/0003-study-protocol-and-historical-evidence.md +40 -0
  24. package/docs/adr/0004-controlled-study-runner.md +25 -0
  25. package/docs/adr/0005-documentation-quality-and-criticality.md +20 -0
  26. package/docs/adr/0006-registry-semantic-grounding.md +20 -0
  27. package/docs/adr/0007-longitudinal-study-metrics.md +21 -0
  28. package/docs/adr/0008-study-verification-boundary.md +21 -0
  29. package/docs/adr/0009-study-provider-cli-adapter.md +25 -0
  30. package/docs/agent-corpus/INDEX.md +14 -3
  31. package/docs/agent-corpus/OVERVIEW.md +25 -0
  32. package/docs/agent-corpus/chat.md +7 -3
  33. package/docs/agent-corpus/cli.md +18 -2
  34. package/docs/agent-corpus/conformance.md +14 -2
  35. package/docs/agent-corpus/doc-bridge.md +48 -1
  36. package/docs/agent-corpus/doctor.md +10 -2
  37. package/docs/agent-corpus/gates.md +6 -2
  38. package/docs/agent-corpus/mcp.md +15 -2
  39. package/docs/agent-corpus/memory.md +6 -2
  40. package/docs/agent-corpus/query.md +35 -2
  41. package/docs/bench/README.md +122 -0
  42. package/docs/bench/retrieval-baseline-v1.json +28 -0
  43. package/docs/bench/retrieval-suite-v1.json +1033 -0
  44. package/docs/chat-and-rag.md +3 -2
  45. package/docs/for-agents.md +9 -1
  46. package/docs/getting-started.md +4 -11
  47. package/docs/guides/gate-ci.md +11 -1
  48. package/docs/guides/install-and-run.md +9 -65
  49. package/docs/index.md +22 -1
  50. package/docs/knowledge-engine-runbook.md +51 -4
  51. package/docs/landing/assets/context-payload-reduction.svg +21 -0
  52. package/docs/landing/assets/controlled-ab-comparison.svg +30 -0
  53. package/docs/landing/index.html +119 -5
  54. package/docs/loop-workflow.md +117 -0
  55. package/docs/mcp.md +6 -1
  56. package/docs/parity/public-claims-v1.json +145 -0
  57. package/docs/playbook/doc-bridge-pattern.md +1 -1
  58. package/docs/query.md +90 -2
  59. package/docs/recipes/index-pipeline.md +1 -1
  60. package/docs/schemas/agent-handoff-v1.md +15 -0
  61. package/docs/schemas/doc-bridge-index-v1.md +65 -0
  62. package/docs/spec/benchmark-v1.md +39 -1
  63. package/docs/spec/cli.md +30 -10
  64. package/docs/spec/config-v1.md +192 -8
  65. package/docs/spec/documentation-audit-v1.md +61 -0
  66. package/docs/spec/enrichment-overlay-v1.md +241 -0
  67. package/docs/spec/graph-signals-v1.md +92 -0
  68. package/docs/spec/incremental-scan-v1.md +102 -0
  69. package/docs/spec/markdown-analyzer-v1.md +73 -0
  70. package/docs/spec/mcp-knowledge-tools-v1.md +147 -0
  71. package/docs/spec/measured-enrichment-v1.md +229 -0
  72. package/docs/spec/public-parity-v1.md +119 -0
  73. package/docs/spec/registry-agents.md +6 -0
  74. package/docs/spec/render-v1.md +122 -0
  75. package/docs/spec/retrieval-index-v1.md +164 -0
  76. package/docs/spec/study-metrics-v1.md +58 -0
  77. package/docs/spec/study-protocol-v1.md +46 -0
  78. package/docs/spec/study-provider-cli-v1.md +116 -0
  79. package/docs/spec/study-runner-v1.md +35 -0
  80. package/docs/spec/study-task-suite-v1.md +41 -0
  81. package/docs/spec/study-verification-v1.md +40 -0
  82. package/docs/study/README.md +84 -0
  83. package/docs/study/ab-adjudicated-cost-analysis-v1.md +29 -0
  84. package/docs/study/ab-adjudicated-cost-plan-v1.json +33 -0
  85. package/docs/study/ab-adjudicated-cost-plan-v2-v1.json +33 -0
  86. package/docs/study/ab-adjudicated-cost-result-v1.json +80 -0
  87. package/docs/study/ab-baseline-analysis-v1.md +21 -0
  88. package/docs/study/ab-baseline-plan-v1.json +33 -0
  89. package/docs/study/ab-baseline-recovery-plan-v1.json +33 -0
  90. package/docs/study/ab-baseline-result-v1.json +79 -0
  91. package/docs/study/documentation-audit-round-2026-08-31.json +183 -0
  92. package/docs/study/historical-evidence-v1.json +252 -0
  93. package/docs/study/observation-ledger-v1.json +30632 -0
  94. package/docs/study/phase3-task-coverage-v1.json +34 -0
  95. package/docs/study/phase4-public-pilot-ledger-v1.json +1344 -0
  96. package/docs/study/phase4-public-pilot-result-v1.json +52 -0
  97. package/docs/study/phase4-public-pilot-run-plan-v1.json +26 -0
  98. package/docs/study/phase4-public-pilot-task-suite-v1.json +71 -0
  99. package/docs/study/pilot-round-2026-08-31.json +46 -0
  100. package/docs/study/protocol-v1.json +90 -0
  101. package/docs/study/publication-gate-v1.md +45 -0
  102. package/docs/study/quality-scorecard-cycle-plan.md +545 -0
  103. package/docs/study/quality-scorecard-v1.json +38 -0
  104. package/docs/study/round-1-adjudicated-smoke-v1.json +30642 -0
  105. package/docs/study/round-1-instrumentation-plan-v1.md +39 -0
  106. package/docs/study/round-2-expanded-adjudication-v1.json +91 -0
  107. package/docs/study/round-2-expanded-validation-v1.md +58 -0
  108. package/docs/study/round-3-evidence-contract-v1.json +75 -0
  109. package/docs/study/round-3-evidence-contract-v1.md +57 -0
  110. package/docs/study/round-4-confirmation-v1.json +75 -0
  111. package/docs/study/round-4-confirmation-v1.md +55 -0
  112. package/docs/study/run-plan-v1.json +33 -0
  113. package/docs/study/semantic-adjudication-cycle-8.md +20 -0
  114. package/docs/study/task-suite-v1.json +96 -0
  115. package/docs/study/token-efficiency-plan-v1.md +337 -0
  116. package/docs/study/token-efficiency-protocol-v2.json +62 -0
  117. package/docs/study/verification-binding-v1.json +27 -0
  118. package/docs/validation-cycle-plan.md +33 -0
  119. package/docs/verification-harness.md +15 -6
  120. package/ecosystem-claims.json +2 -2
  121. package/ecosystem-upstream.json +2 -2
  122. package/ecosystem.json +4 -4
  123. package/mcpb/manifest.json +9 -1
  124. package/package.json +89 -72
  125. package/scripts/check-ecosystem-upstream.mjs +36 -7
  126. package/scripts/report-visual-check.mjs +20 -3
  127. package/skills/doc-bridge-handoff/fixtures/synthetic-repo/docs/for-agents/packages/payments.md +7 -0
  128. package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
  129. package/src/agents/registry-adapter.ts +192 -24
  130. package/src/audit/documentation.ts +513 -0
  131. package/src/bench/baseline.ts +198 -0
  132. package/src/bench/overlay-delta.ts +139 -0
  133. package/src/bench/retrieval.ts +319 -0
  134. package/src/budget/compile.ts +91 -0
  135. package/src/budget/sections.ts +70 -0
  136. package/src/cli/demo.ts +2 -2
  137. package/src/cli/program.ts +699 -79
  138. package/src/cli/usage.ts +71 -0
  139. package/src/config/defaults.ts +1 -0
  140. package/src/config/index.ts +4 -0
  141. package/src/config/load-config.ts +7 -1
  142. package/src/config/schema.ts +121 -4
  143. package/src/conformance/documentation-standard-v1.ts +22 -14
  144. package/src/discovery/areas.ts +182 -0
  145. package/src/discovery/documentation.ts +255 -23
  146. package/src/discovery/identity.ts +24 -0
  147. package/src/discovery/incremental.ts +314 -0
  148. package/src/discovery/inputs.ts +110 -0
  149. package/src/discovery/markdown.ts +481 -0
  150. package/src/discovery/repository.ts +557 -125
  151. package/src/doctor/run-doctor.ts +246 -27
  152. package/src/enrich/approvals.ts +190 -0
  153. package/src/enrich/cache.ts +93 -0
  154. package/src/enrich/context-pack.ts +272 -0
  155. package/src/enrich/overlay.ts +255 -0
  156. package/src/enrich/review.ts +106 -0
  157. package/src/enrich/stage.ts +374 -0
  158. package/src/enrich/stats.ts +100 -0
  159. package/src/enrich/validate.ts +410 -0
  160. package/src/federation/llms.ts +74 -24
  161. package/src/findings/report.ts +103 -0
  162. package/src/fixes/proposals.ts +4 -3
  163. package/src/graph/build.ts +356 -0
  164. package/src/graph/memory.ts +208 -0
  165. package/src/index-builder/build-handoffs.ts +22 -11
  166. package/src/index-builder/build-index.ts +132 -3
  167. package/src/index-builder/human-adapters/fumadocs.ts +1 -1
  168. package/src/index-builder/llms-txt.ts +48 -8
  169. package/src/index-builder/project-corpus.ts +111 -0
  170. package/src/index-builder/watch-index.ts +1 -1
  171. package/src/index.ts +630 -2
  172. package/src/lib/bounded-text.ts +15 -10
  173. package/src/lib/fuzzy-match.ts +235 -0
  174. package/src/mcp/knowledge.ts +554 -0
  175. package/src/mcp/server.ts +113 -18
  176. package/src/metrics/benchmark.ts +21 -0
  177. package/src/parity/check.ts +309 -0
  178. package/src/parity/claims.ts +259 -0
  179. package/src/parity/resolve.ts +160 -0
  180. package/src/query/handoff.ts +326 -0
  181. package/src/query/load-index.ts +53 -1
  182. package/src/query/query.ts +92 -59
  183. package/src/query/search.ts +289 -92
  184. package/src/query/text.ts +155 -0
  185. package/src/reconciliation/reconcile.ts +148 -15
  186. package/src/render/data.ts +356 -0
  187. package/src/render/engine.ts +398 -0
  188. package/src/render/generated.ts +77 -0
  189. package/src/render/render.ts +209 -0
  190. package/src/render/template-source.ts +52 -0
  191. package/src/render/templates.ts +289 -0
  192. package/src/report/html.ts +23 -17
  193. package/src/retrieval/bm25.ts +161 -0
  194. package/src/retrieval/project.ts +495 -0
  195. package/src/retrieval/rank.ts +383 -0
  196. package/src/retrieval/weights.ts +39 -0
  197. package/src/retriever/doc-bridge-retriever.ts +100 -15
  198. package/src/rules/engine.ts +45 -12
  199. package/src/safety/repository.ts +1 -1
  200. package/src/schemas/agent-handoff.ts +56 -0
  201. package/src/schemas/budget.ts +37 -0
  202. package/src/schemas/doc-bridge-index.ts +53 -2
  203. package/src/schemas/enrichment.ts +369 -0
  204. package/src/schemas/json-schemas.ts +39 -2
  205. package/src/schemas/knowledge.ts +19 -3
  206. package/src/schemas/retrieval-index.ts +152 -0
  207. package/src/shims/graphology.d.ts +91 -0
  208. package/src/study/adjudication.ts +196 -0
  209. package/src/study/execution.ts +350 -0
  210. package/src/study/expectations.ts +219 -0
  211. package/src/study/metrics.ts +467 -0
  212. package/src/study/protocol.ts +271 -0
  213. package/src/study/provider-cli.ts +115 -0
  214. package/src/study/provider-telemetry.ts +47 -0
  215. package/src/study/quality-scorecard.ts +164 -0
  216. package/src/study/runner.ts +461 -0
  217. package/src/study/task-suite.ts +321 -0
  218. package/src/study/verification.ts +134 -0
  219. package/src/validate.ts +8 -5
  220. package/src/version.ts +1 -1
  221. package/src/workflow/engine.ts +36 -11
  222. package/dist/index-C2PCQSrB.d.ts +0 -2251
  223. package/scripts/verification-harness.mjs +0 -483
@@ -0,0 +1,314 @@
1
+ import { sha256NormalizedV1 } from '../index-builder/content-hash.js'
2
+ import type { DiscoverySnapshotV1, Coverage, KnowledgeEntity, KnowledgeRelation } from '../schemas/knowledge.js'
3
+
4
+ /**
5
+ * Reusing what has not changed.
6
+ *
7
+ * `EvidenceSchema.contentHash` has always existed and discovery never filled it, so every cache
8
+ * and every overlay could only be keyed on "the whole repository changed" — true between any two
9
+ * commits, and therefore useless. With a hash per file-backed entity, a second scan can tell which
10
+ * files it has already read and skip the expensive part: the TypeScript parse and the Markdown
11
+ * parse, which is where nearly all of discovery's time goes.
12
+ *
13
+ * Reuse is only sound when the answer cannot have changed, and two different things can change it:
14
+ *
15
+ * - An entity's own fields depend on its own bytes alone, so a hash match is enough to reuse it.
16
+ * - A relation depends on what else exists. A module importing `./new.js` resolved to nothing
17
+ * before that file was added and resolves to a module after; a document mentioning `src/new.ts`
18
+ * gains an edge the moment the module appears. So relation reuse also requires that the universe
19
+ * the references resolve against is identical, which is what the fingerprints below capture.
20
+ *
21
+ * The consequence is that a fast run and a cold run produce the same snapshot, byte for byte, or
22
+ * the fast run does not happen. A cache that is only usually right is worse than no cache.
23
+ */
24
+
25
+ export const fileContentHash = (text: string): string => sha256NormalizedV1(text)
26
+
27
+ /** Kinds whose entity is one file, and therefore hashable. */
28
+ export const FILE_BACKED_KINDS = ['module', 'document', 'package'] as const
29
+
30
+ export type PriorFile = {
31
+ readonly entity: KnowledgeEntity
32
+ readonly contentHash: string
33
+ /** Relations this entity is the source of. */
34
+ readonly outgoing: readonly KnowledgeRelation[]
35
+ /** Coverage entries scoped to this file. */
36
+ readonly coverage: readonly Coverage[]
37
+ }
38
+
39
+ /** What a caller may hand back from a previous scan. */
40
+ export type PreviousSnapshot = Pick<DiscoverySnapshotV1, 'entities' | 'relations' | 'coverage'> &
41
+ Partial<Pick<DiscoverySnapshotV1, 'pipelineVersion' | 'analyzerVersions' | 'configurationHash'>>
42
+
43
+ /**
44
+ * Whether a previous snapshot may be reused at all, and why not when it may not.
45
+ *
46
+ * A hash says a file has not changed; it says nothing about whether *this* code would still read
47
+ * it the same way. An analyzer that learned to record a document's headings produces different
48
+ * entities from identical bytes, and a configuration change moves area boundaries and
49
+ * runtime-wiring detection. So the snapshot has to have been produced by this pipeline, these
50
+ * analyzers and this configuration — and a snapshot that does not say which is not trusted, since
51
+ * the alternative is trusting a caller's hand-assembled input with a repository scan.
52
+ */
53
+ export const reuseRefusal = (
54
+ previous: PreviousSnapshot,
55
+ current: Pick<DiscoverySnapshotV1, 'pipelineVersion' | 'analyzerVersions' | 'configurationHash'>,
56
+ ): string | undefined => {
57
+ if (!previous.pipelineVersion || !previous.analyzerVersions || !previous.configurationHash) {
58
+ return 'the previous snapshot does not declare the pipeline, analyzers and configuration it was produced by'
59
+ }
60
+ if (previous.pipelineVersion !== current.pipelineVersion) {
61
+ return `the previous snapshot was produced by pipeline ${previous.pipelineVersion}, not ${current.pipelineVersion}`
62
+ }
63
+ if (sha256NormalizedV1(previous.analyzerVersions) !== sha256NormalizedV1(current.analyzerVersions)) {
64
+ return 'an analyzer version changed, so identical bytes would not produce identical entities'
65
+ }
66
+ if (previous.configurationHash !== current.configurationHash) return 'the configuration changed'
67
+ return undefined
68
+ }
69
+
70
+ export type PriorSnapshot = {
71
+ readonly modules: ReadonlyMap<string, PriorFile>
72
+ readonly documents: ReadonlyMap<string, PriorFile>
73
+ readonly moduleUniverse: string
74
+ readonly resolution: string
75
+ /** Every entity by id, for re-adding an endpoint a reused relation still points at. */
76
+ readonly entities: ReadonlyMap<string, KnowledgeEntity>
77
+ }
78
+
79
+ const hashOf = (entity: KnowledgeEntity): string | undefined => entity.evidence[0]?.contentHash
80
+
81
+ /** Coverage scopes that belong to a single module, by the topic they start with. */
82
+ const MODULE_COVERAGE_PREFIXES = ['dynamic-imports:', 'runtime-wiring:'] as const
83
+ /** Coverage scopes that belong to a single document. */
84
+ const DOCUMENT_COVERAGE_PREFIXES = ['relations:', 'mentions-symbol:'] as const
85
+
86
+ const stringList = (value: unknown): readonly string[] =>
87
+ Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []
88
+
89
+ /**
90
+ * Which names a module declares itself, as opposed to forwarding from somewhere else.
91
+ *
92
+ * The distinction decides where a documented symbol resolves to, and it is only visible in the
93
+ * syntax tree — so a module that skipped its parse has to read it back from what the previous scan
94
+ * recorded. `exports` minus `reexports` is that record.
95
+ */
96
+ export const declaredExportsOf = (entity: KnowledgeEntity): readonly string[] => {
97
+ const exports = stringList(entity.metadata?.exports)
98
+ const reexports = new Set(stringList(entity.metadata?.reexports))
99
+ return exports.filter((name) => !reexports.has(name))
100
+ }
101
+
102
+ export const exportsOf = (entity: KnowledgeEntity): readonly string[] => stringList(entity.metadata?.exports)
103
+
104
+ /**
105
+ * What a module's references resolve against: the module paths, the packages, and the compiler
106
+ * options that decide how a specifier becomes a path. A change to any of them can turn an
107
+ * unresolved import into a relation, so it invalidates relation reuse for every module.
108
+ */
109
+ export const moduleUniverseFingerprint = (input: {
110
+ readonly modulePaths: readonly string[]
111
+ readonly packages: readonly { readonly id: string; readonly path: string; readonly name?: string }[]
112
+ readonly compilerOptions: unknown
113
+ }): string =>
114
+ sha256NormalizedV1({
115
+ modules: [...input.modulePaths].sort(),
116
+ packages: [...input.packages].map(({ id, path, name }) => ({ id, path, ...(name ? { name } : {}) })).sort((a, b) => a.id.localeCompare(b.id)),
117
+ compilerOptions: input.compilerOptions,
118
+ })
119
+
120
+ /**
121
+ * What a document's references resolve against: everything a module resolves against, plus the
122
+ * documents, the areas, the package names and which module declares each exported symbol.
123
+ */
124
+ export const resolutionFingerprint = (input: {
125
+ readonly moduleUniverse: string
126
+ readonly documentPaths: readonly string[]
127
+ readonly areaPaths: readonly string[]
128
+ readonly symbols: ReadonlyMap<string, readonly string[]>
129
+ }): string =>
130
+ sha256NormalizedV1({
131
+ moduleUniverse: input.moduleUniverse,
132
+ documents: [...input.documentPaths].sort(),
133
+ areas: [...input.areaPaths].sort(),
134
+ symbols: [...input.symbols.entries()].map(([name, owners]) => [name, [...owners].sort()]).sort(([a], [b]) => String(a).localeCompare(String(b))),
135
+ })
136
+
137
+ /**
138
+ * Index a previous snapshot for reuse, and recompute the fingerprints it was built under.
139
+ *
140
+ * The fingerprints are derived rather than stored: everything they cover is in the snapshot
141
+ * already, and a stored fingerprint is one more thing that can be stale or forged.
142
+ */
143
+ export const indexPriorSnapshot = (previous: PreviousSnapshot, compilerOptions: unknown): PriorSnapshot => {
144
+ const outgoing = new Map<string, KnowledgeRelation[]>()
145
+ for (const relation of previous.relations) {
146
+ const list = outgoing.get(relation.from)
147
+ if (list) list.push(relation)
148
+ else outgoing.set(relation.from, [relation])
149
+ }
150
+
151
+ const collect = (kind: string, prefixes: readonly string[]): Map<string, PriorFile> => {
152
+ const paths = new Map<string, KnowledgeEntity>()
153
+ for (const entity of previous.entities) {
154
+ if (entity.kind !== kind || !entity.path) continue
155
+ if (hashOf(entity)) paths.set(entity.path, entity)
156
+ }
157
+
158
+ /*
159
+ * Per-file coverage, matched back to its file.
160
+ *
161
+ * The scope is `<topic>:<path>` or `<topic>:<path>:<detail>`, and a detail can itself contain
162
+ * a colon, so the path is recovered by trimming from the right until a file is recognised
163
+ * rather than by splitting on the first one.
164
+ */
165
+ const coverage = new Map<string, Coverage[]>()
166
+ for (const entry of previous.coverage) {
167
+ const prefix = prefixes.find((candidate) => entry.scope.startsWith(candidate))
168
+ if (!prefix) continue
169
+ let remainder = entry.scope.slice(prefix.length)
170
+ while (remainder && !paths.has(remainder)) {
171
+ const cut = remainder.lastIndexOf(':')
172
+ remainder = cut < 0 ? '' : remainder.slice(0, cut)
173
+ }
174
+ if (!remainder) continue
175
+ const list = coverage.get(remainder)
176
+ if (list) list.push(entry)
177
+ else coverage.set(remainder, [entry])
178
+ }
179
+
180
+ const result = new Map<string, PriorFile>()
181
+ for (const [path, entity] of paths) {
182
+ result.set(path, {
183
+ entity,
184
+ contentHash: hashOf(entity) as string,
185
+ outgoing: outgoing.get(entity.id) ?? [],
186
+ coverage: coverage.get(path) ?? [],
187
+ })
188
+ }
189
+ return result
190
+ }
191
+
192
+ const modules = collect('module', MODULE_COVERAGE_PREFIXES)
193
+ const documents = collect('document', DOCUMENT_COVERAGE_PREFIXES)
194
+ const packages = previous.entities
195
+ .filter((entity) => entity.kind === 'package')
196
+ .map((entity) => ({ id: entity.id, path: entity.path ?? '.', ...(entity.name ? { name: entity.name } : {}) }))
197
+
198
+ const symbols = new Map<string, readonly string[]>()
199
+ const declaring = new Map<string, string[]>()
200
+ const forwarding = new Map<string, string[]>()
201
+ for (const [, file] of modules) {
202
+ const declared = new Set(declaredExportsOf(file.entity))
203
+ for (const name of exportsOf(file.entity)) {
204
+ if (name === '*' || name === 'default') continue
205
+ const target = declared.has(name) ? declaring : forwarding
206
+ const list = target.get(name)
207
+ if (list) list.push(file.entity.id)
208
+ else target.set(name, [file.entity.id])
209
+ }
210
+ }
211
+ for (const [name, owners] of declaring) symbols.set(name, owners)
212
+ for (const [name, owners] of forwarding) if (!symbols.has(name)) symbols.set(name, owners)
213
+
214
+ const moduleUniverse = moduleUniverseFingerprint({
215
+ modulePaths: [...modules.keys()],
216
+ packages,
217
+ compilerOptions,
218
+ })
219
+
220
+ return {
221
+ modules,
222
+ documents,
223
+ moduleUniverse,
224
+ resolution: resolutionFingerprint({
225
+ moduleUniverse,
226
+ documentPaths: [...documents.keys()],
227
+ areaPaths: previous.entities.filter((entity) => entity.kind === 'area').map((entity) => entity.path ?? ''),
228
+ symbols,
229
+ }),
230
+ entities: new Map(previous.entities.map((entity) => [entity.id, entity])),
231
+ }
232
+ }
233
+
234
+ export type ReuseLedger = {
235
+ /** Entities taken from the previous snapshot instead of recomputed. */
236
+ reusedEntities: number
237
+ /** Files whose syntax tree or Markdown tree had to be built. */
238
+ parsedFiles: string[]
239
+ /** Files whose parse was skipped entirely. */
240
+ skippedFiles: string[]
241
+ /** Why relation reuse was refused, when it was. */
242
+ invalidated: string[]
243
+ }
244
+
245
+ export const emptyLedger = (): ReuseLedger => ({ reusedEntities: 0, parsedFiles: [], skippedFiles: [], invalidated: [] })
246
+
247
+ /** How many file paths a coverage reason lists before it stops being readable. */
248
+ const LISTED_PATHS = 8
249
+
250
+ /**
251
+ * The reuse, as coverage.
252
+ *
253
+ * A run that is ten times faster than the last one has to be able to say why, or nobody can tell
254
+ * a working cache from a broken scan.
255
+ */
256
+ export const reuseCoverage = (ledger: ReuseLedger): Coverage => {
257
+ const total = ledger.skippedFiles.length + ledger.parsedFiles.length
258
+ if (!ledger.skippedFiles.length && !ledger.invalidated.length) {
259
+ return {
260
+ analyzer: 'repository',
261
+ scope: 'reused-entities',
262
+ status: 'not-applicable',
263
+ reason: `No previous snapshot was reused; parsed ${total} file(s).`,
264
+ }
265
+ }
266
+ const listed = [...ledger.parsedFiles].sort().slice(0, LISTED_PATHS)
267
+ const remainder = ledger.parsedFiles.length - listed.length
268
+ return {
269
+ analyzer: 'repository',
270
+ scope: 'reused-entities',
271
+ status: ledger.invalidated.length ? 'partial' : 'complete',
272
+ reason: [
273
+ `Reused ${ledger.reusedEntities} entit${ledger.reusedEntities === 1 ? 'y' : 'ies'} and skipped ${ledger.skippedFiles.length} of ${total} parse(s).`,
274
+ ledger.parsedFiles.length
275
+ ? `Re-parsed: ${listed.join(', ')}${remainder > 0 ? ` and ${remainder} more` : ''}.`
276
+ : 'Nothing needed re-parsing.',
277
+ ...ledger.invalidated.map((reason) => `Reuse refused: ${reason}.`),
278
+ ].join(' '),
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Replay a reused entity's outgoing relations against the entity set that exists now.
284
+ *
285
+ * A relation whose internal target is gone is dropped rather than carried: the file it pointed at
286
+ * was renamed or deleted, and a graph that keeps the edge is lying about the repository. An
287
+ * external endpoint is re-added instead, because an external entity only exists in the snapshot
288
+ * because something referenced it, and the thing that referenced it is exactly what was reused.
289
+ */
290
+ export const replayableRelations = (
291
+ outgoing: readonly KnowledgeRelation[],
292
+ existing: (id: string) => boolean,
293
+ ): {
294
+ readonly relations: readonly KnowledgeRelation[]
295
+ readonly missingEndpoints: readonly string[]
296
+ readonly dropped: readonly KnowledgeRelation[]
297
+ } => {
298
+ const relations: KnowledgeRelation[] = []
299
+ const missingEndpoints: string[] = []
300
+ const dropped: KnowledgeRelation[] = []
301
+ for (const relation of outgoing) {
302
+ if (existing(relation.to)) {
303
+ relations.push(relation)
304
+ continue
305
+ }
306
+ if (relation.to.startsWith('external:') || relation.to.startsWith('unresolved:')) {
307
+ missingEndpoints.push(relation.to)
308
+ relations.push(relation)
309
+ continue
310
+ }
311
+ dropped.push(relation)
312
+ }
313
+ return { relations, missingEndpoints, dropped }
314
+ }
@@ -0,0 +1,110 @@
1
+ import { extname } from 'node:path'
2
+ import * as ts from 'typescript'
3
+
4
+ import type { DocBridgeConfigV1 } from '../config/schema.js'
5
+ import { DEFAULT_SAFETY_EXCLUDES, type SafeWalkOptions } from '../safety/repository.js'
6
+
7
+ /**
8
+ * What counts as a repository input, in one place.
9
+ *
10
+ * The discovery snapshot and the retrieval projection must walk the same files under the same
11
+ * safety limits, or the index would claim to cover entities the snapshot does not have (or miss
12
+ * ones it does). Sharing these definitions makes that agreement structural instead of a comment.
13
+ */
14
+
15
+ export const SOURCE_EXTENSIONS = ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts'] as const
16
+ export const DOCUMENT_EXTENSIONS = ['.md', '.mdx'] as const
17
+ export const CONFIG_EXTENSIONS = ['.json', '.yaml', '.yml', '.js', '.ts'] as const
18
+ export const DEFAULT_MAX_FILES = 10_000
19
+
20
+ export const safeWalkOptions = (config: DocBridgeConfigV1 | undefined, overrides: { readonly maxFiles?: number; readonly maxBytes?: number } = {}): SafeWalkOptions => {
21
+ const safety = config?.safety
22
+ const maxBytes = overrides.maxBytes ?? safety?.maxBytes
23
+ return {
24
+ exclude: [...DEFAULT_SAFETY_EXCLUDES, ...(safety?.exclude ?? [])],
25
+ maxFiles: overrides.maxFiles ?? safety?.maxFiles ?? DEFAULT_MAX_FILES,
26
+ ...(maxBytes !== undefined ? { maxBytes } : {}),
27
+ ...(safety?.maxTimeMs !== undefined ? { maxTimeMs: safety.maxTimeMs } : {}),
28
+ ...(safety?.maxMemoryMb !== undefined ? { maxMemoryMb: safety.maxMemoryMb } : {}),
29
+ }
30
+ }
31
+
32
+ /** Which audience a documentation file is written for, from its location. */
33
+ export const documentClassification = (path: string): string => {
34
+ if (/(^|\/)docs\/for-agents(?:\/|$)/.test(path)) return 'agent'
35
+ if (/(^|\/)docs-archive(?:\/|$)/.test(path)) return 'archive'
36
+ if (/(^|\/)docs(?:\/|$)/.test(path)) return 'human'
37
+ if (/(^|\/)(README|CONTRIBUTING|SECURITY|CHANGELOG)(?:\.|$)/i.test(path)) return 'project'
38
+ return 'unclassified'
39
+ }
40
+
41
+ export const scriptKind = (path: string): ts.ScriptKind => {
42
+ switch (extname(path)) {
43
+ case '.js': return ts.ScriptKind.JS
44
+ case '.jsx': return ts.ScriptKind.JSX
45
+ case '.mjs': return ts.ScriptKind.JS
46
+ case '.cjs': return ts.ScriptKind.JS
47
+ case '.ts': return ts.ScriptKind.TS
48
+ case '.tsx': return ts.ScriptKind.TSX
49
+ case '.mts': return ts.ScriptKind.TS
50
+ case '.cts': return ts.ScriptKind.TS
51
+ default: return ts.ScriptKind.Unknown
52
+ }
53
+ }
54
+
55
+ export const isExported = (node: ts.Node): boolean => {
56
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined
57
+ return modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false
58
+ }
59
+
60
+ export type ExportedNamesOptions = {
61
+ /**
62
+ * Skip names the module only forwards (`export { x } from './y'`).
63
+ *
64
+ * A barrel file re-exports hundreds of names it does not define. Treating those as definitions
65
+ * makes almost every symbol look like it has two owners, which turns an unambiguous reference
66
+ * into an ambiguous one — so the callers that need "the module that defines this" ask for
67
+ * declarations only.
68
+ */
69
+ readonly declaredOnly?: boolean
70
+ }
71
+
72
+ /** Every name a module exports, sorted. `*` stands for a star re-export. */
73
+ export const exportedNames = (sourceFile: ts.SourceFile, options: ExportedNamesOptions = {}): string[] => {
74
+ const declaredOnly = options.declaredOnly ?? false
75
+ const names = new Set<string>()
76
+ const addDeclarationName = (node: ts.Declaration): void => {
77
+ if (!isExported(node)) return
78
+ const name = ts.getNameOfDeclaration(node)
79
+ if (name && ts.isIdentifier(name)) names.add(name.text)
80
+ }
81
+
82
+ const visit = (node: ts.Node): void => {
83
+ if (ts.isExportDeclaration(node)) {
84
+ if (declaredOnly && node.moduleSpecifier) {
85
+ // Forwarded from elsewhere: the other module is the one that defines it.
86
+ } else if (!node.exportClause) names.add('*')
87
+ else if (ts.isNamedExports(node.exportClause)) {
88
+ for (const element of node.exportClause.elements) names.add(element.name.text)
89
+ }
90
+ } else if (ts.isExportAssignment(node)) {
91
+ names.add('default')
92
+ } else if (
93
+ ts.isClassDeclaration(node) ||
94
+ ts.isFunctionDeclaration(node) ||
95
+ ts.isInterfaceDeclaration(node) ||
96
+ ts.isTypeAliasDeclaration(node) ||
97
+ ts.isEnumDeclaration(node) ||
98
+ ts.isModuleDeclaration(node)
99
+ ) {
100
+ addDeclarationName(node)
101
+ } else if (ts.isVariableStatement(node) && isExported(node)) {
102
+ for (const declaration of node.declarationList.declarations) {
103
+ if (ts.isIdentifier(declaration.name)) names.add(declaration.name.text)
104
+ }
105
+ }
106
+ ts.forEachChild(node, visit)
107
+ }
108
+ visit(sourceFile)
109
+ return [...names].sort()
110
+ }