@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,495 @@
|
|
|
1
|
+
import { basename, extname } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
4
|
+
import { GRAPH_ANALYZER_VERSION, canonicality } from '../graph/build.js'
|
|
5
|
+
import { indexConfigurationHash } from '../index-builder/project-corpus.js'
|
|
6
|
+
import { sha256NormalizedV1 } from '../index-builder/content-hash.js'
|
|
7
|
+
import { extractSearchBody } from '../lib/markdown.js'
|
|
8
|
+
import { SEARCH_LEXICON_VERSION } from '../query/text.js'
|
|
9
|
+
import type { KnowledgeEntry } from '../schemas/doc-bridge-index.js'
|
|
10
|
+
import type { DiscoverySnapshotV1, KnowledgeEntity, KnowledgeRelation, Provenance } from '../schemas/knowledge.js'
|
|
11
|
+
import {
|
|
12
|
+
RetrievalIndexV1Schema,
|
|
13
|
+
type Audience,
|
|
14
|
+
type Confidence,
|
|
15
|
+
type RetrievalEdge,
|
|
16
|
+
type RetrievalEntry,
|
|
17
|
+
type RetrievalFields,
|
|
18
|
+
type RetrievalIndexV1,
|
|
19
|
+
type RetrievalKind,
|
|
20
|
+
} from '../schemas/retrieval-index.js'
|
|
21
|
+
import { BM25_VERSION, buildBm25Index } from './bm25.js'
|
|
22
|
+
import { resolveSearchParams, resolveSearchWeights } from './weights.js'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Project the snapshot into the index retrieval reads.
|
|
26
|
+
*
|
|
27
|
+
* Retrieval used to run a scanner of its own: it walked the repository, parsed every module and
|
|
28
|
+
* document a second time, and produced records that shared nothing with the snapshot but a file
|
|
29
|
+
* path. Two views of one repository, built by two pipelines, could disagree — and did, which is
|
|
30
|
+
* how a search could return a module the snapshot said did not exist.
|
|
31
|
+
*
|
|
32
|
+
* This is a function, not a scan. Every entry is a snapshot entity (or a route the configuration
|
|
33
|
+
* declares); every content hash is the entity's own; every graph number is computed from the
|
|
34
|
+
* snapshot's relations. The one thing read from disk is the body text of a document the snapshot
|
|
35
|
+
* already names, because a 4 KB search excerpt does not belong in a snapshot — and that read is
|
|
36
|
+
* verified against the entity's content hash, so a file that changed since the scan is projected
|
|
37
|
+
* from what the snapshot says about it rather than from what is on disk now.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
export const RETRIEVAL_PROJECTION_VERSION = 1 as const
|
|
41
|
+
|
|
42
|
+
/** Documentation body kept for search. Long enough to answer a question, short enough to ship. */
|
|
43
|
+
export const DOCUMENT_BODY_LIMIT = 4_000
|
|
44
|
+
const MAX_EDGES = 64
|
|
45
|
+
const MAX_ALIASES = 32
|
|
46
|
+
const MAX_TAGS = 32
|
|
47
|
+
const MAX_SYMBOLS = 256
|
|
48
|
+
/** A summary is what a result carries into an agent's context; the cap is a token budget, not a display limit. */
|
|
49
|
+
const MAX_SUMMARY = 400
|
|
50
|
+
|
|
51
|
+
/** The overlay hash when there is no overlay: the hash of an empty accepted set. */
|
|
52
|
+
export const EMPTY_OVERLAY_HASH = sha256NormalizedV1({ accepted: [] })
|
|
53
|
+
|
|
54
|
+
const CONFIDENCE_RANK: Readonly<Record<Confidence, number>> = { observed: 0, declared: 1, fuzzy: 2, proposed: 3 }
|
|
55
|
+
|
|
56
|
+
/** The weaker of two confidences: a chain is as trustworthy as its least trustworthy link. */
|
|
57
|
+
export const weakerConfidence = (a: Confidence, b: Confidence): Confidence =>
|
|
58
|
+
CONFIDENCE_RANK[a] >= CONFIDENCE_RANK[b] ? a : b
|
|
59
|
+
|
|
60
|
+
/** What a relation's confidence is: its provenance, unless it resolved by similarity. */
|
|
61
|
+
export const relationConfidence = (relation: Pick<KnowledgeRelation, 'provenance' | 'metadata'>): Confidence =>
|
|
62
|
+
relation.metadata?.confidence === 'fuzzy' ? 'fuzzy' : relation.provenance
|
|
63
|
+
|
|
64
|
+
/** The routes the configuration declares. Passed in rather than re-read, so the projection stays a function. */
|
|
65
|
+
export type RetrievalRoutes = {
|
|
66
|
+
readonly ownership?: Readonly<
|
|
67
|
+
Record<
|
|
68
|
+
string,
|
|
69
|
+
{
|
|
70
|
+
readonly id: string
|
|
71
|
+
readonly path: string
|
|
72
|
+
readonly group?: string
|
|
73
|
+
readonly layer?: string
|
|
74
|
+
readonly purpose?: string
|
|
75
|
+
readonly agentDoc?: string
|
|
76
|
+
readonly humanDoc?: string
|
|
77
|
+
}
|
|
78
|
+
>
|
|
79
|
+
>
|
|
80
|
+
readonly intents?: Readonly<Record<string, { readonly id: string; readonly title: string; readonly paths: readonly string[] }>>
|
|
81
|
+
readonly changes?: Readonly<
|
|
82
|
+
Record<string, { readonly id: string; readonly title: string; readonly startHere: string; readonly relatedPackages?: readonly string[] }>
|
|
83
|
+
>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A curated sidecar the corpus scan found: it lends its id and summary to the document it is. */
|
|
87
|
+
export type CuratedDocument = {
|
|
88
|
+
readonly id: string
|
|
89
|
+
readonly path: string
|
|
90
|
+
readonly title?: string
|
|
91
|
+
readonly description?: string
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The accepted enrichment overlay, as the projection reads it.
|
|
96
|
+
*
|
|
97
|
+
* Already expired against the snapshot by `projectEnrichmentOverlay`: every map here names an
|
|
98
|
+
* entity whose content hash still matches the entry that describes it. The hash is part of the
|
|
99
|
+
* projection's identity; everything else is additive — an alias joins the entity's own, a
|
|
100
|
+
* summary fills a gap, a relation is one more edge with `provenance: proposed` — and nothing
|
|
101
|
+
* here can remove or rewrite what the snapshot observed.
|
|
102
|
+
*/
|
|
103
|
+
export type RetrievalOverlayInput = {
|
|
104
|
+
readonly hash: string
|
|
105
|
+
/** Per-entry share of the bounded agent weight, 0..1. */
|
|
106
|
+
readonly signals?: ReadonlyMap<string, number>
|
|
107
|
+
readonly aliases?: ReadonlyMap<string, readonly string[]>
|
|
108
|
+
/** Used only when the entity has no summary of its own; ranked in either case. */
|
|
109
|
+
readonly summaries?: ReadonlyMap<string, string>
|
|
110
|
+
/** Entity id → the scope it is canonical for. */
|
|
111
|
+
readonly canonical?: ReadonlyMap<string, string>
|
|
112
|
+
readonly intents?: readonly { readonly id: string; readonly title: string; readonly paths: readonly string[] }[]
|
|
113
|
+
/** Accepted relations, `provenance: proposed`. Added next to the observed ones, never in place of any. */
|
|
114
|
+
readonly relations?: readonly KnowledgeRelation[]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type ProjectRetrievalOptions = {
|
|
118
|
+
readonly snapshot: Pick<DiscoverySnapshotV1, 'contentHash' | 'entities' | 'relations'>
|
|
119
|
+
readonly config: DocBridgeConfigV1 | undefined
|
|
120
|
+
readonly routes?: RetrievalRoutes
|
|
121
|
+
readonly curated?: readonly CuratedDocument[]
|
|
122
|
+
readonly overlay?: RetrievalOverlayInput
|
|
123
|
+
/** Body text of a document entity, by path. Return `undefined` when the file is not readable. */
|
|
124
|
+
readonly readDocument?: (path: string) => string | undefined
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const stringList = (value: unknown): readonly string[] =>
|
|
128
|
+
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []
|
|
129
|
+
|
|
130
|
+
const unique = (values: readonly (string | undefined)[], limit: number): string[] =>
|
|
131
|
+
[...new Set(values.filter((value): value is string => Boolean(value)))].slice(0, limit)
|
|
132
|
+
|
|
133
|
+
const audienceOf = (entity: KnowledgeEntity): Audience | undefined => {
|
|
134
|
+
const classification = entity.metadata?.classification
|
|
135
|
+
if (classification === 'human-and-agent' || classification === 'agent' || classification === 'human') return classification
|
|
136
|
+
if (classification === 'project') return 'human'
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const headingTexts = (entity: KnowledgeEntity): readonly string[] =>
|
|
141
|
+
Array.isArray(entity.metadata?.headings)
|
|
142
|
+
? entity.metadata.headings
|
|
143
|
+
.map((heading) => (heading && typeof heading === 'object' && 'text' in heading ? (heading as { text: unknown }).text : undefined))
|
|
144
|
+
.filter((text): text is string => typeof text === 'string')
|
|
145
|
+
: []
|
|
146
|
+
|
|
147
|
+
const TEST_MODULE_PATTERN = /(?:\.test|\.spec|__tests__)/
|
|
148
|
+
|
|
149
|
+
/** The tags the legacy index carried per projected entry, so its readers see the same record. */
|
|
150
|
+
const tagsFor = (kind: RetrievalKind, entity: KnowledgeEntity | undefined, path: string, extra: readonly (string | undefined)[]): string[] =>
|
|
151
|
+
unique(
|
|
152
|
+
[
|
|
153
|
+
kind,
|
|
154
|
+
...(kind === 'document' ? [String(entity?.metadata?.classification ?? '') || undefined, String(entity?.metadata?.frontmatter && typeof entity.metadata.frontmatter === 'object' && 'type' in entity.metadata.frontmatter ? (entity.metadata.frontmatter as { type?: unknown }).type ?? '' : '') || undefined] : []),
|
|
155
|
+
...(kind === 'module'
|
|
156
|
+
? [extname(path).replace('.', '') || undefined, TEST_MODULE_PATTERN.test(path) ? 'test' : undefined, path.split('/').slice(0, -1).pop()]
|
|
157
|
+
: []),
|
|
158
|
+
...extra,
|
|
159
|
+
],
|
|
160
|
+
MAX_TAGS,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
type EntryDraft = Omit<RetrievalEntry, 'fields' | 'graph' | 'confidence'> & { readonly bodyText: string; readonly headingText: string }
|
|
164
|
+
|
|
165
|
+
const fieldsFor = (draft: EntryDraft): RetrievalFields => ({
|
|
166
|
+
title: draft.title.slice(0, 512),
|
|
167
|
+
headings: draft.headingText.slice(0, 4_096),
|
|
168
|
+
path: draft.path.slice(0, 1_024),
|
|
169
|
+
symbols: (draft.symbols ?? []).join(' ').slice(0, 8_192),
|
|
170
|
+
summary: (draft.summary ?? '').slice(0, 2_048),
|
|
171
|
+
body: draft.bodyText.slice(0, 16_000),
|
|
172
|
+
aliases: [...draft.aliases, ...draft.tags].join(' ').slice(0, 2_048),
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
/** A hash for an entry the repository does not store as one file: the entity itself, canonically. */
|
|
176
|
+
const derivedHash = (value: unknown): string => sha256NormalizedV1(value)
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Build the retrieval index.
|
|
180
|
+
*
|
|
181
|
+
* Entries are emitted in id order and every list inside them is sorted, so the artifact — and
|
|
182
|
+
* its content hash — is a function of its inputs and not of the order the snapshot arrived in.
|
|
183
|
+
*/
|
|
184
|
+
export const projectRetrievalIndex = (options: ProjectRetrievalOptions): RetrievalIndexV1 => {
|
|
185
|
+
const { snapshot, config } = options
|
|
186
|
+
const routes = options.routes ?? {}
|
|
187
|
+
const overlayHash = options.overlay?.hash ?? EMPTY_OVERLAY_HASH
|
|
188
|
+
const overlay = options.overlay
|
|
189
|
+
// Proposed relations sit next to the observed ones: one more edge each, with its own confidence.
|
|
190
|
+
const relations = overlay?.relations?.length ? [...snapshot.relations, ...overlay.relations] : snapshot.relations
|
|
191
|
+
const entities = new Map(snapshot.entities.map((entity) => [entity.id, entity]))
|
|
192
|
+
const byPath = new Map<string, KnowledgeEntity>()
|
|
193
|
+
for (const entity of snapshot.entities) if (entity.path && !byPath.has(entity.path)) byPath.set(entity.path, entity)
|
|
194
|
+
const curatedByPath = new Map((options.curated ?? []).map((document) => [document.path, document]))
|
|
195
|
+
|
|
196
|
+
/*
|
|
197
|
+
* Ownership records attach to the entity at their path — an area, a package, a module or a
|
|
198
|
+
* document — and lend it their id as an alias. A record that matches no entity still has to be
|
|
199
|
+
* findable, because a query for it has an answer (the record), so it becomes a declared entry.
|
|
200
|
+
*/
|
|
201
|
+
const ownershipByEntity = new Map<string, NonNullable<RetrievalRoutes['ownership']>[string]>()
|
|
202
|
+
/** Further records at the same path: the same unit under another name, so aliases, not entries. */
|
|
203
|
+
const ownershipAliases = new Map<string, string[]>()
|
|
204
|
+
const orphanOwnership: NonNullable<RetrievalRoutes['ownership']>[string][] = []
|
|
205
|
+
for (const record of Object.values(routes.ownership ?? {}).sort((a, b) => a.id.localeCompare(b.id))) {
|
|
206
|
+
const entity = byPath.get(record.path.replace(/\/$/, ''))
|
|
207
|
+
if (!entity) orphanOwnership.push(record)
|
|
208
|
+
else if (!ownershipByEntity.has(entity.id)) ownershipByEntity.set(entity.id, record)
|
|
209
|
+
else ownershipAliases.set(entity.id, [...(ownershipAliases.get(entity.id) ?? []), record.id])
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Containment, for area and package ids.
|
|
213
|
+
const parentOf = new Map<string, string>()
|
|
214
|
+
const packageOfArea = new Map<string, string>()
|
|
215
|
+
for (const relation of relations) {
|
|
216
|
+
if (relation.kind !== 'contains') continue
|
|
217
|
+
const parent = entities.get(relation.from)
|
|
218
|
+
const child = entities.get(relation.to)
|
|
219
|
+
if (!parent || !child) continue
|
|
220
|
+
if (parent.kind === 'area' && child.kind === 'module') parentOf.set(child.id, parent.id)
|
|
221
|
+
if (parent.kind === 'package' && child.kind === 'module' && !parentOf.has(child.id)) parentOf.set(child.id, parent.id)
|
|
222
|
+
if (parent.kind === 'package' && child.kind === 'area') packageOfArea.set(child.id, parent.id)
|
|
223
|
+
if (parent.kind === 'area' && child.kind === 'area') parentOf.set(child.id, parent.id)
|
|
224
|
+
}
|
|
225
|
+
const areaOf = (id: string): string | undefined => {
|
|
226
|
+
const parent = parentOf.get(id)
|
|
227
|
+
return parent && entities.get(parent)?.kind === 'area' ? parent : undefined
|
|
228
|
+
}
|
|
229
|
+
const packageOf = (id: string): string | undefined => {
|
|
230
|
+
const entity = entities.get(id)
|
|
231
|
+
if (entity?.kind === 'package') return id
|
|
232
|
+
if (entity?.kind === 'area') {
|
|
233
|
+
let current: string | undefined = id
|
|
234
|
+
while (current && entities.get(current)?.kind === 'area') {
|
|
235
|
+
const owner = packageOfArea.get(current)
|
|
236
|
+
if (owner) return owner
|
|
237
|
+
current = parentOf.get(current)
|
|
238
|
+
}
|
|
239
|
+
return undefined
|
|
240
|
+
}
|
|
241
|
+
const parent = parentOf.get(id)
|
|
242
|
+
return parent ? packageOf(parent) : undefined
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Documentation edges, per endpoint, with the confidence of the relation that made them.
|
|
246
|
+
const inbound = new Map<string, RetrievalEdge[]>()
|
|
247
|
+
const outbound = new Map<string, RetrievalEdge[]>()
|
|
248
|
+
const push = (map: Map<string, RetrievalEdge[]>, key: string, edge: RetrievalEdge): void => {
|
|
249
|
+
const list = map.get(key)
|
|
250
|
+
if (list) list.push(edge)
|
|
251
|
+
else map.set(key, [edge])
|
|
252
|
+
}
|
|
253
|
+
for (const relation of relations) {
|
|
254
|
+
const confidence = relationConfidence(relation)
|
|
255
|
+
if (relation.kind === 'covers' || relation.kind === 'mentions' || relation.kind === 'mentions-symbol' || relation.kind === 'links-to') {
|
|
256
|
+
push(inbound, relation.to, { kind: relation.kind, id: relation.from, confidence })
|
|
257
|
+
push(outbound, relation.from, { kind: relation.kind, id: relation.to, confidence })
|
|
258
|
+
}
|
|
259
|
+
if ((relation.kind === 'imports' || relation.kind === 're-exports') && entities.get(relation.to)?.kind === 'module') {
|
|
260
|
+
push(outbound, relation.from, { kind: relation.kind, id: relation.to, confidence })
|
|
261
|
+
push(inbound, relation.to, { kind: relation.kind, id: relation.from, confidence })
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const sortedEdges = (list: readonly RetrievalEdge[] | undefined): RetrievalEdge[] =>
|
|
265
|
+
[...(list ?? [])]
|
|
266
|
+
.sort((a, b) => a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id) || a.confidence.localeCompare(b.confidence))
|
|
267
|
+
.filter((edge, index, all) => index === 0 || edge.kind !== all[index - 1]?.kind || edge.id !== all[index - 1]?.id)
|
|
268
|
+
.slice(0, MAX_EDGES)
|
|
269
|
+
|
|
270
|
+
const pagerank = canonicality({ entities: snapshot.entities, relations })
|
|
271
|
+
|
|
272
|
+
const drafts: EntryDraft[] = []
|
|
273
|
+
|
|
274
|
+
for (const entity of snapshot.entities) {
|
|
275
|
+
if (entity.kind !== 'document' && entity.kind !== 'module' && entity.kind !== 'area' && entity.kind !== 'package') continue
|
|
276
|
+
if (!entity.path) continue
|
|
277
|
+
const kind = entity.kind as RetrievalKind
|
|
278
|
+
const ownership = ownershipByEntity.get(entity.id)
|
|
279
|
+
const curated = kind === 'document' ? curatedByPath.get(entity.path) : undefined
|
|
280
|
+
const hash = entity.evidence[0]?.contentHash
|
|
281
|
+
const symbols = kind === 'module' ? unique(stringList(entity.metadata?.exports).filter((name) => name !== '*'), MAX_SYMBOLS) : undefined
|
|
282
|
+
|
|
283
|
+
let bodyText = ''
|
|
284
|
+
if (kind === 'document') {
|
|
285
|
+
const raw = options.readDocument?.(entity.path)
|
|
286
|
+
// A body that no longer matches the entity is not this entity's body.
|
|
287
|
+
if (raw !== undefined && (!hash || sha256NormalizedV1(raw.replace(/^/, '')) === hash)) bodyText = extractSearchBody(raw, DOCUMENT_BODY_LIMIT)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/*
|
|
291
|
+
* An ownership record's agent document is the documentation of the unit it owns. The unit
|
|
292
|
+
* inherits that text — title, headings, summary, body — so a query the document answers
|
|
293
|
+
* routes to the unit, which is what an agent asking "who owns X" needs, and what the record
|
|
294
|
+
* carried before the projection existed.
|
|
295
|
+
*/
|
|
296
|
+
const agentDoc = ownership?.agentDoc ? byPath.get(ownership.agentDoc) : undefined
|
|
297
|
+
const agentDocCurated = ownership?.agentDoc ? curatedByPath.get(ownership.agentDoc) : undefined
|
|
298
|
+
if (agentDoc && !bodyText) {
|
|
299
|
+
const raw = options.readDocument?.(ownership?.agentDoc ?? '')
|
|
300
|
+
const docHash = agentDoc.evidence[0]?.contentHash
|
|
301
|
+
if (raw !== undefined && (!docHash || sha256NormalizedV1(raw.replace(/^\uFEFF/, '')) === docHash)) bodyText = extractSearchBody(raw, DOCUMENT_BODY_LIMIT)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const title =
|
|
305
|
+
curated?.title ??
|
|
306
|
+
agentDocCurated?.title ??
|
|
307
|
+
(typeof entity.metadata?.title === 'string' ? entity.metadata.title : undefined) ??
|
|
308
|
+
(typeof agentDoc?.metadata?.title === 'string' ? agentDoc.metadata.title : undefined) ??
|
|
309
|
+
(kind === 'document' || kind === 'module' ? basename(entity.path) : entity.name)
|
|
310
|
+
const summary =
|
|
311
|
+
ownership?.purpose ??
|
|
312
|
+
curated?.description ??
|
|
313
|
+
agentDocCurated?.description ??
|
|
314
|
+
(typeof entity.metadata?.summary === 'string' ? entity.metadata.summary : undefined) ??
|
|
315
|
+
(typeof agentDoc?.metadata?.summary === 'string' ? agentDoc.metadata.summary : undefined) ??
|
|
316
|
+
overlay?.summaries?.get(entity.id)
|
|
317
|
+
const shortName = kind === 'package' ? entity.name.split('/').pop() : undefined
|
|
318
|
+
|
|
319
|
+
drafts.push({
|
|
320
|
+
id: entity.id,
|
|
321
|
+
kind,
|
|
322
|
+
path: entity.path,
|
|
323
|
+
title: title.slice(0, 256),
|
|
324
|
+
...(summary ? { summary: summary.slice(0, MAX_SUMMARY) } : {}),
|
|
325
|
+
...(() => {
|
|
326
|
+
const audience = kind === 'document' ? audienceOf(entity) : agentDoc ? audienceOf(agentDoc) : undefined
|
|
327
|
+
return audience ? { audience } : {}
|
|
328
|
+
})(),
|
|
329
|
+
aliases: unique(
|
|
330
|
+
[ownership?.id, ...(ownershipAliases.get(entity.id) ?? []), curated?.id, shortName, kind === 'package' ? entity.name : undefined, ...(entity.aliases ?? []), ...(overlay?.aliases?.get(entity.id) ?? [])],
|
|
331
|
+
MAX_ALIASES,
|
|
332
|
+
),
|
|
333
|
+
...(symbols?.length ? { symbols } : {}),
|
|
334
|
+
tags: tagsFor(kind, entity, entity.path, [ownership ? 'ownership' : undefined, ownership?.group, ownership?.layer, overlay?.canonical?.has(entity.id) ? 'canonical' : undefined]),
|
|
335
|
+
contentHash: hash ?? derivedHash({ id: entity.id, kind, path: entity.path, name: entity.name, metadata: entity.metadata ?? {} }),
|
|
336
|
+
provenance: entity.provenance,
|
|
337
|
+
...(ownership ? { ownershipId: ownership.id } : {}),
|
|
338
|
+
bodyText,
|
|
339
|
+
headingText: [...headingTexts(entity), ...(agentDoc ? headingTexts(agentDoc) : [])].join(' '),
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
for (const record of orphanOwnership) {
|
|
344
|
+
drafts.push({
|
|
345
|
+
id: `ownership:${record.id}`,
|
|
346
|
+
kind: 'package',
|
|
347
|
+
path: record.path,
|
|
348
|
+
title: record.id,
|
|
349
|
+
...(record.purpose ? { summary: record.purpose } : {}),
|
|
350
|
+
aliases: unique([record.id, record.agentDoc, record.humanDoc], MAX_ALIASES),
|
|
351
|
+
tags: unique(['package', 'ownership', record.group, record.layer], MAX_TAGS),
|
|
352
|
+
contentHash: derivedHash(record),
|
|
353
|
+
provenance: 'declared',
|
|
354
|
+
ownershipId: record.id,
|
|
355
|
+
bodyText: '',
|
|
356
|
+
headingText: '',
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const intent of Object.values(routes.intents ?? {})) {
|
|
361
|
+
drafts.push({
|
|
362
|
+
id: intent.id,
|
|
363
|
+
kind: 'intent',
|
|
364
|
+
path: intent.paths[0] ?? intent.id,
|
|
365
|
+
title: intent.title,
|
|
366
|
+
summary: intent.title,
|
|
367
|
+
aliases: [intent.id],
|
|
368
|
+
tags: ['intent'],
|
|
369
|
+
contentHash: derivedHash(intent),
|
|
370
|
+
provenance: 'declared',
|
|
371
|
+
bodyText: intent.paths.join(' '),
|
|
372
|
+
headingText: '',
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// An accepted intent is a route an agent proposed and a validator let through: declared by it, not observed.
|
|
377
|
+
for (const intent of overlay?.intents ?? []) {
|
|
378
|
+
drafts.push({
|
|
379
|
+
id: intent.id,
|
|
380
|
+
kind: 'intent',
|
|
381
|
+
path: intent.paths[0] ?? intent.id,
|
|
382
|
+
title: intent.title,
|
|
383
|
+
summary: intent.title,
|
|
384
|
+
aliases: [intent.id],
|
|
385
|
+
tags: ['intent', 'proposed'],
|
|
386
|
+
contentHash: derivedHash(intent),
|
|
387
|
+
provenance: 'proposed',
|
|
388
|
+
bodyText: intent.paths.join(' '),
|
|
389
|
+
headingText: '',
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
for (const change of Object.values(routes.changes ?? {})) {
|
|
394
|
+
drafts.push({
|
|
395
|
+
id: change.id,
|
|
396
|
+
kind: 'change',
|
|
397
|
+
path: change.startHere,
|
|
398
|
+
title: change.title,
|
|
399
|
+
summary: change.title,
|
|
400
|
+
aliases: unique([change.id, ...(change.relatedPackages ?? [])], MAX_ALIASES),
|
|
401
|
+
tags: unique(['change', ...(change.relatedPackages ?? [])], MAX_TAGS),
|
|
402
|
+
contentHash: derivedHash(change),
|
|
403
|
+
provenance: 'declared',
|
|
404
|
+
bodyText: '',
|
|
405
|
+
headingText: '',
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const entries: RetrievalEntry[] = drafts
|
|
410
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
411
|
+
.map((draft) => {
|
|
412
|
+
const { bodyText: _body, headingText: _headings, ...rest } = draft
|
|
413
|
+
const into = sortedEdges(inbound.get(draft.id))
|
|
414
|
+
const out = sortedEdges(outbound.get(draft.id))
|
|
415
|
+
const areaId = areaOf(draft.id)
|
|
416
|
+
const packageId = packageOf(draft.id)
|
|
417
|
+
const confidence: Confidence = draft.provenance as Provenance
|
|
418
|
+
const agentSignal = overlay?.signals?.get(draft.id)
|
|
419
|
+
return {
|
|
420
|
+
...rest,
|
|
421
|
+
fields: fieldsFor(draft),
|
|
422
|
+
...(agentSignal ? { agentSignal: Math.min(1, Math.max(0, Math.round(agentSignal * 1_000) / 1_000)) } : {}),
|
|
423
|
+
graph: {
|
|
424
|
+
pagerank: pagerank.get(draft.id) ?? 0,
|
|
425
|
+
inboundLinks: into.filter((edge) => edge.kind === 'links-to' || edge.kind === 'covers' || edge.kind === 'mentions' || edge.kind === 'mentions-symbol').length,
|
|
426
|
+
coveredBy: into.filter((edge) => edge.kind === 'covers').map((edge) => edge.id),
|
|
427
|
+
mentionedBy: into.filter((edge) => edge.kind === 'mentions' || edge.kind === 'mentions-symbol').map((edge) => edge.id),
|
|
428
|
+
...(areaId ? { areaId } : {}),
|
|
429
|
+
...(packageId ? { packageId } : {}),
|
|
430
|
+
inbound: into,
|
|
431
|
+
outbound: out,
|
|
432
|
+
},
|
|
433
|
+
confidence,
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
const weights = resolveSearchWeights(config?.retrieval?.weights)
|
|
438
|
+
const params = resolveSearchParams(config?.retrieval?.params)
|
|
439
|
+
const lexical = buildBm25Index(entries.map((entry) => ({ ref: entry.id, fields: entry.fields })), weights, params)
|
|
440
|
+
|
|
441
|
+
const base = {
|
|
442
|
+
type: 'retrieval-index' as const,
|
|
443
|
+
schemaVersion: 1 as const,
|
|
444
|
+
contentHash: '0'.repeat(64),
|
|
445
|
+
contentHashAlgo: 'sha256-normalized-v1' as const,
|
|
446
|
+
snapshotHash: snapshot.contentHash,
|
|
447
|
+
overlayHash,
|
|
448
|
+
configurationHash: indexConfigurationHash(config),
|
|
449
|
+
lexiconVersion: SEARCH_LEXICON_VERSION,
|
|
450
|
+
graphMetricsVersion: GRAPH_ANALYZER_VERSION,
|
|
451
|
+
weights,
|
|
452
|
+
params,
|
|
453
|
+
lexical: {
|
|
454
|
+
version: BM25_VERSION,
|
|
455
|
+
documentCount: lexical.documents.length,
|
|
456
|
+
fieldNames: Object.keys(weights).sort(),
|
|
457
|
+
averageFieldLength: Object.fromEntries([...lexical.averageFieldLength.entries()].sort(([a], [b]) => a.localeCompare(b))),
|
|
458
|
+
},
|
|
459
|
+
entries,
|
|
460
|
+
}
|
|
461
|
+
/*
|
|
462
|
+
* The hash is over the inputs, not the output: the projection is a function, so three equal
|
|
463
|
+
* input hashes mean an equal artifact, and a reader checking freshness compares three hashes
|
|
464
|
+
* instead of re-projecting.
|
|
465
|
+
*/
|
|
466
|
+
const contentHash = sha256NormalizedV1({
|
|
467
|
+
projectionVersion: RETRIEVAL_PROJECTION_VERSION,
|
|
468
|
+
snapshotHash: base.snapshotHash,
|
|
469
|
+
overlayHash: base.overlayHash,
|
|
470
|
+
configurationHash: base.configurationHash,
|
|
471
|
+
lexiconVersion: base.lexiconVersion,
|
|
472
|
+
graphMetricsVersion: base.graphMetricsVersion,
|
|
473
|
+
weights,
|
|
474
|
+
params,
|
|
475
|
+
})
|
|
476
|
+
return RetrievalIndexV1Schema.parse({ ...base, contentHash })
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* The legacy record for a projected entry.
|
|
481
|
+
*
|
|
482
|
+
* `knowledge[]` is what the harness, llms.txt, the doctor and every older reader consume, so it
|
|
483
|
+
* keeps carrying every document and module — but without the body, which now lives once in the
|
|
484
|
+
* projection. A document projected here and a document ranked there are the same entry.
|
|
485
|
+
*/
|
|
486
|
+
export const toKnowledgeEntry = (entry: RetrievalEntry): KnowledgeEntry => ({
|
|
487
|
+
id: entry.id,
|
|
488
|
+
type: entry.kind,
|
|
489
|
+
title: entry.title,
|
|
490
|
+
path: entry.path,
|
|
491
|
+
...(entry.summary ? { description: entry.summary } : {}),
|
|
492
|
+
...(entry.symbols?.length ? { symbols: entry.symbols } : {}),
|
|
493
|
+
tags: entry.tags,
|
|
494
|
+
contentHash: entry.contentHash,
|
|
495
|
+
})
|