@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.
- package/CHANGELOG.md +471 -0
- package/CONTRIBUTING.md +29 -4
- package/README.md +87 -40
- package/SECURITY.md +7 -0
- package/action.yml +1 -1
- package/bin/ak-docs.js +2 -2
- package/bin/ak-verify.js +13 -7
- package/dist/cli/program.d.ts +3 -1
- package/dist/cli/program.js +15888 -6061
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +91 -9
- package/dist/config/index.js.map +1 -1
- package/dist/index-Beor6Yhi.d.ts +792 -0
- package/dist/index.d.ts +9979 -3257
- package/dist/index.js +15954 -5774
- package/dist/index.js.map +1 -1
- package/docs/MARKETPLACE.md +1 -1
- package/docs/PRD-documentation-efficiency-study.md +406 -0
- package/docs/PRD-knowledge-retrieval-and-enrichment.md +466 -0
- package/docs/RELEASE.md +22 -8
- package/docs/adr/0002-documentation-audit-boundary.md +22 -0
- package/docs/adr/0003-study-protocol-and-historical-evidence.md +40 -0
- package/docs/adr/0004-controlled-study-runner.md +25 -0
- package/docs/adr/0005-documentation-quality-and-criticality.md +20 -0
- package/docs/adr/0006-registry-semantic-grounding.md +20 -0
- package/docs/adr/0007-longitudinal-study-metrics.md +21 -0
- package/docs/adr/0008-study-verification-boundary.md +21 -0
- package/docs/adr/0009-study-provider-cli-adapter.md +25 -0
- package/docs/agent-corpus/INDEX.md +14 -3
- package/docs/agent-corpus/OVERVIEW.md +25 -0
- package/docs/agent-corpus/chat.md +7 -3
- package/docs/agent-corpus/cli.md +18 -2
- package/docs/agent-corpus/conformance.md +14 -2
- package/docs/agent-corpus/doc-bridge.md +48 -1
- package/docs/agent-corpus/doctor.md +10 -2
- package/docs/agent-corpus/gates.md +6 -2
- package/docs/agent-corpus/mcp.md +15 -2
- package/docs/agent-corpus/memory.md +6 -2
- package/docs/agent-corpus/query.md +35 -2
- package/docs/bench/README.md +122 -0
- package/docs/bench/retrieval-baseline-v1.json +28 -0
- package/docs/bench/retrieval-suite-v1.json +1033 -0
- package/docs/chat-and-rag.md +3 -2
- package/docs/for-agents.md +9 -1
- package/docs/getting-started.md +4 -11
- package/docs/guides/gate-ci.md +11 -1
- package/docs/guides/install-and-run.md +9 -65
- package/docs/index.md +22 -1
- package/docs/knowledge-engine-runbook.md +51 -4
- package/docs/landing/assets/context-payload-reduction.svg +21 -0
- package/docs/landing/assets/controlled-ab-comparison.svg +30 -0
- package/docs/landing/index.html +119 -5
- package/docs/loop-workflow.md +117 -0
- package/docs/mcp.md +6 -1
- package/docs/parity/public-claims-v1.json +145 -0
- package/docs/playbook/doc-bridge-pattern.md +1 -1
- package/docs/query.md +90 -2
- package/docs/recipes/index-pipeline.md +1 -1
- package/docs/schemas/agent-handoff-v1.md +15 -0
- package/docs/schemas/doc-bridge-index-v1.md +65 -0
- package/docs/spec/benchmark-v1.md +39 -1
- package/docs/spec/cli.md +30 -10
- package/docs/spec/config-v1.md +192 -8
- package/docs/spec/documentation-audit-v1.md +61 -0
- package/docs/spec/enrichment-overlay-v1.md +241 -0
- package/docs/spec/graph-signals-v1.md +92 -0
- package/docs/spec/incremental-scan-v1.md +102 -0
- package/docs/spec/markdown-analyzer-v1.md +73 -0
- package/docs/spec/mcp-knowledge-tools-v1.md +147 -0
- package/docs/spec/measured-enrichment-v1.md +229 -0
- package/docs/spec/public-parity-v1.md +119 -0
- package/docs/spec/registry-agents.md +6 -0
- package/docs/spec/render-v1.md +122 -0
- package/docs/spec/retrieval-index-v1.md +164 -0
- package/docs/spec/study-metrics-v1.md +58 -0
- package/docs/spec/study-protocol-v1.md +46 -0
- package/docs/spec/study-provider-cli-v1.md +116 -0
- package/docs/spec/study-runner-v1.md +35 -0
- package/docs/spec/study-task-suite-v1.md +41 -0
- package/docs/spec/study-verification-v1.md +40 -0
- package/docs/study/README.md +84 -0
- package/docs/study/ab-adjudicated-cost-analysis-v1.md +29 -0
- package/docs/study/ab-adjudicated-cost-plan-v1.json +33 -0
- package/docs/study/ab-adjudicated-cost-plan-v2-v1.json +33 -0
- package/docs/study/ab-adjudicated-cost-result-v1.json +80 -0
- package/docs/study/ab-baseline-analysis-v1.md +21 -0
- package/docs/study/ab-baseline-plan-v1.json +33 -0
- package/docs/study/ab-baseline-recovery-plan-v1.json +33 -0
- package/docs/study/ab-baseline-result-v1.json +79 -0
- package/docs/study/documentation-audit-round-2026-08-31.json +183 -0
- package/docs/study/historical-evidence-v1.json +252 -0
- package/docs/study/observation-ledger-v1.json +30632 -0
- package/docs/study/phase3-task-coverage-v1.json +34 -0
- package/docs/study/phase4-public-pilot-ledger-v1.json +1344 -0
- package/docs/study/phase4-public-pilot-result-v1.json +52 -0
- package/docs/study/phase4-public-pilot-run-plan-v1.json +26 -0
- package/docs/study/phase4-public-pilot-task-suite-v1.json +71 -0
- package/docs/study/pilot-round-2026-08-31.json +46 -0
- package/docs/study/protocol-v1.json +90 -0
- package/docs/study/publication-gate-v1.md +45 -0
- package/docs/study/quality-scorecard-cycle-plan.md +545 -0
- package/docs/study/quality-scorecard-v1.json +38 -0
- package/docs/study/round-1-adjudicated-smoke-v1.json +30642 -0
- package/docs/study/round-1-instrumentation-plan-v1.md +39 -0
- package/docs/study/round-2-expanded-adjudication-v1.json +91 -0
- package/docs/study/round-2-expanded-validation-v1.md +58 -0
- package/docs/study/round-3-evidence-contract-v1.json +75 -0
- package/docs/study/round-3-evidence-contract-v1.md +57 -0
- package/docs/study/round-4-confirmation-v1.json +75 -0
- package/docs/study/round-4-confirmation-v1.md +55 -0
- package/docs/study/run-plan-v1.json +33 -0
- package/docs/study/semantic-adjudication-cycle-8.md +20 -0
- package/docs/study/task-suite-v1.json +96 -0
- package/docs/study/token-efficiency-plan-v1.md +337 -0
- package/docs/study/token-efficiency-protocol-v2.json +62 -0
- package/docs/study/verification-binding-v1.json +27 -0
- package/docs/validation-cycle-plan.md +33 -0
- package/docs/verification-harness.md +15 -6
- package/ecosystem-claims.json +2 -2
- package/ecosystem-upstream.json +2 -2
- package/ecosystem.json +4 -4
- package/mcpb/manifest.json +9 -1
- package/package.json +89 -72
- package/scripts/check-ecosystem-upstream.mjs +36 -7
- package/scripts/report-visual-check.mjs +20 -3
- package/skills/doc-bridge-handoff/fixtures/synthetic-repo/docs/for-agents/packages/payments.md +7 -0
- package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
- package/src/agents/registry-adapter.ts +192 -24
- package/src/audit/documentation.ts +513 -0
- package/src/bench/baseline.ts +198 -0
- package/src/bench/overlay-delta.ts +139 -0
- package/src/bench/retrieval.ts +319 -0
- package/src/budget/compile.ts +91 -0
- package/src/budget/sections.ts +70 -0
- package/src/cli/demo.ts +2 -2
- package/src/cli/program.ts +699 -79
- package/src/cli/usage.ts +71 -0
- package/src/config/defaults.ts +1 -0
- package/src/config/index.ts +4 -0
- package/src/config/load-config.ts +7 -1
- package/src/config/schema.ts +121 -4
- package/src/conformance/documentation-standard-v1.ts +22 -14
- package/src/discovery/areas.ts +182 -0
- package/src/discovery/documentation.ts +255 -23
- package/src/discovery/identity.ts +24 -0
- package/src/discovery/incremental.ts +314 -0
- package/src/discovery/inputs.ts +110 -0
- package/src/discovery/markdown.ts +481 -0
- package/src/discovery/repository.ts +557 -125
- package/src/doctor/run-doctor.ts +246 -27
- package/src/enrich/approvals.ts +190 -0
- package/src/enrich/cache.ts +93 -0
- package/src/enrich/context-pack.ts +272 -0
- package/src/enrich/overlay.ts +255 -0
- package/src/enrich/review.ts +106 -0
- package/src/enrich/stage.ts +374 -0
- package/src/enrich/stats.ts +100 -0
- package/src/enrich/validate.ts +410 -0
- package/src/federation/llms.ts +74 -24
- package/src/findings/report.ts +103 -0
- package/src/fixes/proposals.ts +4 -3
- package/src/graph/build.ts +356 -0
- package/src/graph/memory.ts +208 -0
- package/src/index-builder/build-handoffs.ts +22 -11
- package/src/index-builder/build-index.ts +132 -3
- package/src/index-builder/human-adapters/fumadocs.ts +1 -1
- package/src/index-builder/llms-txt.ts +48 -8
- package/src/index-builder/project-corpus.ts +111 -0
- package/src/index-builder/watch-index.ts +1 -1
- package/src/index.ts +630 -2
- package/src/lib/bounded-text.ts +15 -10
- package/src/lib/fuzzy-match.ts +235 -0
- package/src/mcp/knowledge.ts +554 -0
- package/src/mcp/server.ts +113 -18
- package/src/metrics/benchmark.ts +21 -0
- package/src/parity/check.ts +309 -0
- package/src/parity/claims.ts +259 -0
- package/src/parity/resolve.ts +160 -0
- package/src/query/handoff.ts +326 -0
- package/src/query/load-index.ts +53 -1
- package/src/query/query.ts +92 -59
- package/src/query/search.ts +289 -92
- package/src/query/text.ts +155 -0
- package/src/reconciliation/reconcile.ts +148 -15
- package/src/render/data.ts +356 -0
- package/src/render/engine.ts +398 -0
- package/src/render/generated.ts +77 -0
- package/src/render/render.ts +209 -0
- package/src/render/template-source.ts +52 -0
- package/src/render/templates.ts +289 -0
- package/src/report/html.ts +23 -17
- package/src/retrieval/bm25.ts +161 -0
- package/src/retrieval/project.ts +495 -0
- package/src/retrieval/rank.ts +383 -0
- package/src/retrieval/weights.ts +39 -0
- package/src/retriever/doc-bridge-retriever.ts +100 -15
- package/src/rules/engine.ts +45 -12
- package/src/safety/repository.ts +1 -1
- package/src/schemas/agent-handoff.ts +56 -0
- package/src/schemas/budget.ts +37 -0
- package/src/schemas/doc-bridge-index.ts +53 -2
- package/src/schemas/enrichment.ts +369 -0
- package/src/schemas/json-schemas.ts +39 -2
- package/src/schemas/knowledge.ts +19 -3
- package/src/schemas/retrieval-index.ts +152 -0
- package/src/shims/graphology.d.ts +91 -0
- package/src/study/adjudication.ts +196 -0
- package/src/study/execution.ts +350 -0
- package/src/study/expectations.ts +219 -0
- package/src/study/metrics.ts +467 -0
- package/src/study/protocol.ts +271 -0
- package/src/study/provider-cli.ts +115 -0
- package/src/study/provider-telemetry.ts +47 -0
- package/src/study/quality-scorecard.ts +164 -0
- package/src/study/runner.ts +461 -0
- package/src/study/task-suite.ts +321 -0
- package/src/study/verification.ts +134 -0
- package/src/validate.ts +8 -5
- package/src/version.ts +1 -1
- package/src/workflow/engine.ts +36 -11
- package/dist/index-C2PCQSrB.d.ts +0 -2251
- 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
|
+
}
|