@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
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { parseDocument, type Node as YamlNode } from 'yaml'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
1
4
|
import { contentHashForArtifactV1 } from '../index-builder/content-hash.js'
|
|
2
5
|
import {
|
|
3
6
|
DiscoverySnapshotV1Schema,
|
|
@@ -24,6 +27,7 @@ export type DocumentationDeclarationOptions = {
|
|
|
24
27
|
readonly documentId?: string
|
|
25
28
|
/** Agent corpus root used for conservative package/app path inference. */
|
|
26
29
|
readonly agentRoot?: string
|
|
30
|
+
readonly entityLookup?: EntityLookup
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
export type DocumentationDeclarationResult = {
|
|
@@ -48,6 +52,8 @@ type RelationFields = {
|
|
|
48
52
|
fields: Set<string>
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
type EntityLookup = ReadonlyMap<string, KnowledgeEntity>
|
|
56
|
+
|
|
51
57
|
const detectionValues = new Set(['static', 'dynamic', 'external'])
|
|
52
58
|
|
|
53
59
|
const evidence = (path: string, lineStart: number, lineEnd = lineStart): Evidence => ({
|
|
@@ -65,6 +71,15 @@ const diagnostic = (
|
|
|
65
71
|
lineEnd = lineStart,
|
|
66
72
|
): DocumentationDiagnostic => ({ code, message, path: 'docbridge', evidence: evidence(path, lineStart, lineEnd) })
|
|
67
73
|
|
|
74
|
+
const entityLookup = (entities: readonly KnowledgeEntity[]): EntityLookup => {
|
|
75
|
+
const lookup = new Map<string, KnowledgeEntity>()
|
|
76
|
+
for (const entity of entities) {
|
|
77
|
+
if (!lookup.has(entity.id)) lookup.set(entity.id, entity)
|
|
78
|
+
for (const alias of entity.aliases ?? []) if (!lookup.has(alias)) lookup.set(alias, entity)
|
|
79
|
+
}
|
|
80
|
+
return lookup
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
const scalar = (value: string): string => {
|
|
69
84
|
const trimmed = value.trim()
|
|
70
85
|
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
@@ -73,6 +88,97 @@ const scalar = (value: string): string => {
|
|
|
73
88
|
return trimmed
|
|
74
89
|
}
|
|
75
90
|
|
|
91
|
+
const isFieldName = (value: string): boolean => {
|
|
92
|
+
if (!/^[A-Za-z]/.test(value)) return false
|
|
93
|
+
for (const character of value.slice(1)) {
|
|
94
|
+
if (!/[A-Za-z0-9_-]/.test(character)) return false
|
|
95
|
+
}
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const parseIndentedField = (raw: string, indentation: number): { readonly key: string; readonly value: string } | undefined => {
|
|
100
|
+
const prefix = ' '.repeat(indentation)
|
|
101
|
+
if (!raw.startsWith(prefix) || raw[indentation] === ' ') return undefined
|
|
102
|
+
const body = raw.slice(indentation)
|
|
103
|
+
const separator = body.indexOf(':')
|
|
104
|
+
if (separator <= 0) return undefined
|
|
105
|
+
const key = body.slice(0, separator).trim()
|
|
106
|
+
return isFieldName(key) ? { key, value: body.slice(separator + 1).trim() } : undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const parseListItem = (raw: string, indentation: number): string | undefined => {
|
|
110
|
+
const prefix = `${' '.repeat(indentation)}-`
|
|
111
|
+
if (!raw.startsWith(prefix)) return undefined
|
|
112
|
+
const rest = raw.slice(prefix.length)
|
|
113
|
+
if (rest && !/\s/.test(rest[0] ?? '')) return undefined
|
|
114
|
+
return rest.trim()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/*
|
|
118
|
+
* The `docbridge` block is real YAML validated by Zod.
|
|
119
|
+
*
|
|
120
|
+
* It used to be read by a hand-written parser for a YAML subset, which meant a quoted list, a
|
|
121
|
+
* multi-line string or an anchor was a syntax error in a file every other tool considered valid.
|
|
122
|
+
* The schema is deliberately structural rather than semantic: it says what shape the block has,
|
|
123
|
+
* while the checks that a relation names all four of its fields and a known detection value stay
|
|
124
|
+
* downstream, where they already produce the aggregated messages this contract promises.
|
|
125
|
+
*/
|
|
126
|
+
const DocbridgeRelationSchema = z
|
|
127
|
+
.object({
|
|
128
|
+
from: z.string().min(1).optional(),
|
|
129
|
+
to: z.string().min(1).optional(),
|
|
130
|
+
kind: z.string().min(1).optional(),
|
|
131
|
+
detection: z.string().min(1).optional(),
|
|
132
|
+
})
|
|
133
|
+
.strict()
|
|
134
|
+
|
|
135
|
+
const DocbridgeBlockSchema = z
|
|
136
|
+
.object({
|
|
137
|
+
covers: z.array(z.string().min(1)).optional(),
|
|
138
|
+
relations: z.array(DocbridgeRelationSchema).optional(),
|
|
139
|
+
})
|
|
140
|
+
.strict()
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Map a schema issue onto the diagnostic code it has always had.
|
|
144
|
+
*
|
|
145
|
+
* The codes are a published contract — a repository may fail its build on one — so adopting a
|
|
146
|
+
* schema must not rename them. Zod says precisely what is wrong and where; this decides which
|
|
147
|
+
* existing code that corresponds to.
|
|
148
|
+
*/
|
|
149
|
+
const issueCode = (issue: z.core.$ZodIssue): string => {
|
|
150
|
+
const [first, second, third] = issue.path
|
|
151
|
+
if (issue.code === 'unrecognized_keys') return 'DOCBRIDGE_FIELD_UNKNOWN'
|
|
152
|
+
if (first === undefined) return 'DOCBRIDGE_BLOCK_MALFORMED'
|
|
153
|
+
if (first === 'covers') return second === undefined ? 'DOCBRIDGE_COVERS_INVALID' : 'DOCBRIDGE_REFERENCE_MISSING'
|
|
154
|
+
if (first === 'relations') {
|
|
155
|
+
if (second === undefined) return 'DOCBRIDGE_RELATIONS_INVALID'
|
|
156
|
+
return third === undefined ? 'DOCBRIDGE_RELATION_INVALID' : 'DOCBRIDGE_FIELD_UNKNOWN'
|
|
157
|
+
}
|
|
158
|
+
return 'DOCBRIDGE_STRUCTURE_INVALID'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const issueMessage = (issue: z.core.$ZodIssue): string => {
|
|
162
|
+
const field = issue.path.length ? `docbridge.${issue.path.join('.')}` : 'docbridge'
|
|
163
|
+
const keys = issue.code === 'unrecognized_keys' ? `: ${issue.keys.join(', ')}` : ''
|
|
164
|
+
return `${field}: ${issue.message}${keys}`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Absolute file line of an offset inside the frontmatter body. */
|
|
168
|
+
const lineResolver = (body: string, frontmatterLine: number) => (offset: number): number => {
|
|
169
|
+
const safe = Math.max(0, Math.min(offset, body.length))
|
|
170
|
+
let line = frontmatterLine + 1
|
|
171
|
+
for (let index = 0; index < safe; index += 1) if (body[index] === '\n') line += 1
|
|
172
|
+
return line
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
type YamlRanged = { readonly range?: readonly [number, number, number] | null }
|
|
176
|
+
|
|
177
|
+
const nodeStart = (node: unknown): number | undefined => {
|
|
178
|
+
const range = (node as YamlRanged | null | undefined)?.range
|
|
179
|
+
return range ? range[0] : undefined
|
|
180
|
+
}
|
|
181
|
+
|
|
76
182
|
const conventionalPackageReference = (path: string, agentRoot: string): string | undefined => {
|
|
77
183
|
const prefix = `${agentRoot.replace(/\/$/, '')}/`
|
|
78
184
|
if (!path.startsWith(prefix)) return undefined
|
|
@@ -113,8 +219,9 @@ const resolveEntity = (
|
|
|
113
219
|
input: DocumentationDeclarationInput,
|
|
114
220
|
lineStart: number,
|
|
115
221
|
unresolved: Map<string, KnowledgeEntity>,
|
|
222
|
+
lookup: EntityLookup,
|
|
116
223
|
): KnowledgeEntity => {
|
|
117
|
-
const direct =
|
|
224
|
+
const direct = lookup.get(reference)
|
|
118
225
|
if (direct) return direct
|
|
119
226
|
const packageReference = reference.replace(/^package:/, '')
|
|
120
227
|
const packageCandidates = entities.filter((entity) => {
|
|
@@ -124,6 +231,23 @@ const resolveEntity = (
|
|
|
124
231
|
})
|
|
125
232
|
const resolved = packageCandidates.length === 1 ? packageCandidates[0] : undefined
|
|
126
233
|
if (resolved) return resolved
|
|
234
|
+
|
|
235
|
+
/*
|
|
236
|
+
* An ownership id resolves to the area that carries it.
|
|
237
|
+
*
|
|
238
|
+
* Agent documents declare coverage by ownership id (`editRoot: src/query` becomes ownership
|
|
239
|
+
* `doc-bridge-query`), and until areas existed there was nothing in the graph with that
|
|
240
|
+
* identity — every such declaration became an unresolved reference, and the audit reported a
|
|
241
|
+
* documented area as a gap.
|
|
242
|
+
*/
|
|
243
|
+
const ownedAreas = entities.filter((entity) => entity.kind === 'area' && entity.metadata?.ownershipId === reference)
|
|
244
|
+
if (ownedAreas.length === 1 && ownedAreas[0]) return ownedAreas[0]
|
|
245
|
+
|
|
246
|
+
// A path the repository knows an area, a module or a document by names that thing.
|
|
247
|
+
const path = reference.replace(/^\.\//, '').replace(/\/$/, '')
|
|
248
|
+
const byPath = entities.filter((entity) => (entity.kind === 'area' || entity.kind === 'module' || entity.kind === 'document') && entity.path === path)
|
|
249
|
+
if (byPath.length === 1 && byPath[0]) return byPath[0]
|
|
250
|
+
|
|
127
251
|
const id = `unresolved:${reference}`
|
|
128
252
|
const existing = unresolved.get(id)
|
|
129
253
|
if (existing) return existing
|
|
@@ -140,6 +264,91 @@ const resolveEntity = (
|
|
|
140
264
|
|
|
141
265
|
const relationKey = (from: string, to: string, kind: string): string => `${from}\u0000${to}\u0000${kind}`
|
|
142
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Read the `docbridge` block as YAML.
|
|
269
|
+
*
|
|
270
|
+
* Returns `undefined` when YAML cannot read the block at all — a tab in the indentation, a
|
|
271
|
+
* duplicate key, a scalar where a mapping was opened. The lenient line scanner below then takes
|
|
272
|
+
* over, because on a mangled block a per-line diagnostic is more use to the author than a single
|
|
273
|
+
* parser error, and because those diagnostics are the published contract.
|
|
274
|
+
*/
|
|
275
|
+
const parseDocbridgeYaml = (
|
|
276
|
+
input: DocumentationDeclarationInput,
|
|
277
|
+
lines: readonly string[],
|
|
278
|
+
end: number,
|
|
279
|
+
):
|
|
280
|
+
| {
|
|
281
|
+
readonly covers: readonly { value: string; line: number }[]
|
|
282
|
+
readonly relations: readonly RelationFields[]
|
|
283
|
+
readonly diagnostics: readonly DocumentationDiagnostic[]
|
|
284
|
+
}
|
|
285
|
+
| undefined => {
|
|
286
|
+
const body = lines.slice(1, end).join('\n')
|
|
287
|
+
const document = parseDocument(body, { prettyErrors: false })
|
|
288
|
+
if (document.errors.length) return undefined
|
|
289
|
+
|
|
290
|
+
let data: unknown
|
|
291
|
+
try {
|
|
292
|
+
data = document.toJS()
|
|
293
|
+
} catch {
|
|
294
|
+
return undefined
|
|
295
|
+
}
|
|
296
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) return undefined
|
|
297
|
+
const raw = (data as Record<string, unknown>).docbridge
|
|
298
|
+
const lineAt = lineResolver(body, 1)
|
|
299
|
+
const blockNode = document.get('docbridge', true) as YamlNode | undefined
|
|
300
|
+
|
|
301
|
+
const diagnostics: DocumentationDiagnostic[] = []
|
|
302
|
+
// `docbridge:` with nothing under it is empty, not malformed: DOCBRIDGE_CONTENT_MISSING says so
|
|
303
|
+
// downstream, and two diagnostics for one mistake is one too many.
|
|
304
|
+
if (raw === null || raw === undefined) return { covers: [], relations: [], diagnostics }
|
|
305
|
+
|
|
306
|
+
const parsed = DocbridgeBlockSchema.safeParse(raw)
|
|
307
|
+
if (!parsed.success) {
|
|
308
|
+
for (const issue of parsed.error.issues) {
|
|
309
|
+
const node = issue.path.length ? (document.getIn(['docbridge', ...issue.path], true) as YamlNode | undefined) : blockNode
|
|
310
|
+
diagnostics.push(diagnostic(input.path, issueCode(issue), issueMessage(issue), lineAt(nodeStart(node) ?? nodeStart(blockNode) ?? 0)))
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/*
|
|
315
|
+
* Keep what is individually valid. One unknown key should not discard the declarations around
|
|
316
|
+
* it: the author gets the precise error and the graph still gets the edges they did declare.
|
|
317
|
+
*/
|
|
318
|
+
const covers: { value: string; line: number }[] = []
|
|
319
|
+
const relations: RelationFields[] = []
|
|
320
|
+
// `raw` is neither null nor undefined here: the early return above settled that.
|
|
321
|
+
const record = typeof raw === 'object' && !Array.isArray(raw) ? (raw as Record<string, unknown>) : undefined
|
|
322
|
+
|
|
323
|
+
if (Array.isArray(record?.covers)) {
|
|
324
|
+
for (const [index, entry] of record.covers.entries()) {
|
|
325
|
+
if (typeof entry !== 'string' || !entry.trim()) continue
|
|
326
|
+
const node = document.getIn(['docbridge', 'covers', index], true) as YamlNode | undefined
|
|
327
|
+
covers.push({ value: entry.trim(), line: lineAt(nodeStart(node) ?? nodeStart(blockNode) ?? 0) })
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (Array.isArray(record?.relations)) {
|
|
332
|
+
for (const [index, entry] of record.relations.entries()) {
|
|
333
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) continue
|
|
334
|
+
const node = document.getIn(['docbridge', 'relations', index], true) as YamlNode | undefined
|
|
335
|
+
const startLine = lineAt(nodeStart(node) ?? nodeStart(blockNode) ?? 0)
|
|
336
|
+
const relation: RelationFields = { startLine, endLine: startLine, fields: new Set() }
|
|
337
|
+
for (const field of ['from', 'to', 'kind', 'detection'] as const) {
|
|
338
|
+
const value = (entry as Record<string, unknown>)[field]
|
|
339
|
+
if (typeof value !== 'string' || !value.trim()) continue
|
|
340
|
+
relation.fields.add(field)
|
|
341
|
+
relation[field] = value.trim()
|
|
342
|
+
const valueNode = document.getIn(['docbridge', 'relations', index, field], true) as YamlNode | undefined
|
|
343
|
+
relation.endLine = Math.max(relation.endLine, lineAt(nodeStart(valueNode) ?? nodeStart(node) ?? 0))
|
|
344
|
+
}
|
|
345
|
+
relations.push(relation)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return { covers, relations, diagnostics }
|
|
350
|
+
}
|
|
351
|
+
|
|
143
352
|
const parseBlock = (
|
|
144
353
|
input: DocumentationDeclarationInput,
|
|
145
354
|
options: Pick<DocumentationDeclarationOptions, 'agentRoot'> = {},
|
|
@@ -166,21 +375,42 @@ const parseBlock = (
|
|
|
166
375
|
let typeLine = -1
|
|
167
376
|
let packageLine = -1
|
|
168
377
|
let humanDocLine = -1
|
|
378
|
+
let editRootLine = -1
|
|
379
|
+
let idLine = -1
|
|
169
380
|
for (let index = 1; index < end; index += 1) {
|
|
170
381
|
const line = lines[index] ?? ''
|
|
171
382
|
if (docbridgeLine < 0 && /^docbridge\s*:/.test(line)) docbridgeLine = index
|
|
172
383
|
if (typeLine < 0 && /^type\s*:/.test(line)) typeLine = index
|
|
173
384
|
if (packageLine < 0 && /^package\s*:/.test(line)) packageLine = index
|
|
174
385
|
if (humanDocLine < 0 && /^humanDoc\s*:/.test(line)) humanDocLine = index
|
|
386
|
+
if (editRootLine < 0 && /^editRoot\s*:/.test(line)) editRootLine = index
|
|
387
|
+
if (idLine < 0 && /^id\s*:/.test(line)) idLine = index
|
|
175
388
|
}
|
|
176
389
|
if (docbridgeLine < 0) {
|
|
177
390
|
const type = typeLine >= 0 ? scalar(lines[typeLine]?.slice('type:'.length) ?? '') : ''
|
|
178
391
|
const packageReference = packageLine >= 0 ? scalar(lines[packageLine]?.slice('package:'.length) ?? '') : conventionalPath ?? ''
|
|
179
392
|
if (type === 'package' && packageReference) return { covers: [{ value: packageReference, line: packageLine >= 0 ? packageLine + 1 : typeLine + 1 }], relations: [], diagnostics: [], hasDocbridge: true }
|
|
180
393
|
if (conventionalPath) return { covers: [{ value: packageReference, line: humanDocLine >= 0 ? humanDocLine + 1 : 1 }], relations: [], diagnostics: [], hasDocbridge: true }
|
|
394
|
+
|
|
395
|
+
/*
|
|
396
|
+
* `id` plus `editRoot` is a coverage declaration.
|
|
397
|
+
*
|
|
398
|
+
* The corpus already uses this pair to own a directory — it is what fills the ownership map
|
|
399
|
+
* and the handoff — but discovery never read it, so the graph had no edge from the sidecar to
|
|
400
|
+
* the unit it owns. With areas in the graph the reference resolves, and a documented area
|
|
401
|
+
* stops being reported as a gap.
|
|
402
|
+
*/
|
|
403
|
+
const editRoot = editRootLine >= 0 ? scalar(lines[editRootLine]?.slice('editRoot:'.length) ?? '') : ''
|
|
404
|
+
const identifier = idLine >= 0 ? scalar(lines[idLine]?.slice('id:'.length) ?? '') : ''
|
|
405
|
+
if (editRoot && identifier) {
|
|
406
|
+
return { covers: [{ value: identifier, line: editRootLine + 1 }], relations: [], diagnostics: [], hasDocbridge: true }
|
|
407
|
+
}
|
|
181
408
|
return { covers: [], relations: [], diagnostics: [], hasDocbridge: false }
|
|
182
409
|
}
|
|
183
410
|
|
|
411
|
+
const fromYaml = parseDocbridgeYaml(input, lines, end)
|
|
412
|
+
if (fromYaml) return { ...fromYaml, hasDocbridge: true }
|
|
413
|
+
|
|
184
414
|
const diagnostics: DocumentationDiagnostic[] = []
|
|
185
415
|
const covers: { value: string; line: number }[] = []
|
|
186
416
|
const relations: RelationFields[] = []
|
|
@@ -208,11 +438,10 @@ const parseBlock = (
|
|
|
208
438
|
section = undefined
|
|
209
439
|
continue
|
|
210
440
|
}
|
|
211
|
-
|
|
441
|
+
const sectionField = parseIndentedField(raw, 2)
|
|
442
|
+
if (sectionField) {
|
|
212
443
|
finishRelation()
|
|
213
|
-
const
|
|
214
|
-
const key = match?.[1]
|
|
215
|
-
const value = match?.[2] ?? ''
|
|
444
|
+
const { key, value } = sectionField
|
|
216
445
|
if (key !== 'covers' && key !== 'relations') {
|
|
217
446
|
addDiagnostic(diagnostics, input, 'DOCBRIDGE_FIELD_UNKNOWN', `Unknown docbridge field: ${key ?? '(missing)'}.`, line)
|
|
218
447
|
section = undefined
|
|
@@ -229,35 +458,36 @@ const parseBlock = (
|
|
|
229
458
|
}
|
|
230
459
|
continue
|
|
231
460
|
}
|
|
232
|
-
|
|
233
|
-
|
|
461
|
+
const listValue = parseListItem(raw, 4)
|
|
462
|
+
if (section === 'covers' && listValue !== undefined) {
|
|
463
|
+
const value = scalar(listValue)
|
|
234
464
|
if (!value) addDiagnostic(diagnostics, input, 'DOCBRIDGE_REFERENCE_MISSING', 'covers entries must not be empty.', line)
|
|
235
465
|
else covers.push({ value, line })
|
|
236
466
|
continue
|
|
237
467
|
}
|
|
238
|
-
if (section === 'relations' &&
|
|
468
|
+
if (section === 'relations' && listValue !== undefined) {
|
|
239
469
|
finishRelation()
|
|
240
|
-
const firstField =
|
|
470
|
+
const firstField = parseIndentedField(` ${listValue}`, 4)
|
|
241
471
|
current = { startLine: line, endLine: line, fields: new Set() }
|
|
242
|
-
if (firstField?.
|
|
243
|
-
current.fields.add(firstField
|
|
244
|
-
current[firstField
|
|
245
|
-
} else if (
|
|
472
|
+
if (firstField?.key) {
|
|
473
|
+
current.fields.add(firstField.key)
|
|
474
|
+
current[firstField.key as 'from' | 'to' | 'kind' | 'detection'] = scalar(firstField.value)
|
|
475
|
+
} else if (listValue) {
|
|
246
476
|
addDiagnostic(diagnostics, input, 'DOCBRIDGE_RELATION_INVALID', 'Relation entries must be field mappings.', line)
|
|
247
477
|
}
|
|
248
478
|
continue
|
|
249
479
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const key = field
|
|
480
|
+
const field = parseIndentedField(raw, 6)
|
|
481
|
+
if (section === 'relations' && current && field) {
|
|
482
|
+
const { key, value } = field
|
|
253
483
|
current.endLine = line
|
|
254
|
-
if (!
|
|
255
|
-
addDiagnostic(diagnostics, input, 'DOCBRIDGE_FIELD_UNKNOWN', `Unknown relation field: ${key
|
|
484
|
+
if (!['from', 'to', 'kind', 'detection'].includes(key)) {
|
|
485
|
+
addDiagnostic(diagnostics, input, 'DOCBRIDGE_FIELD_UNKNOWN', `Unknown relation field: ${key}.`, line)
|
|
256
486
|
} else if (current.fields.has(key)) {
|
|
257
487
|
addDiagnostic(diagnostics, input, 'DOCBRIDGE_FIELD_DUPLICATE', `Duplicate relation field: ${key}.`, line)
|
|
258
488
|
} else {
|
|
259
489
|
current.fields.add(key)
|
|
260
|
-
current[key as 'from' | 'to' | 'kind' | 'detection'] = scalar(
|
|
490
|
+
current[key as 'from' | 'to' | 'kind' | 'detection'] = scalar(value)
|
|
261
491
|
}
|
|
262
492
|
continue
|
|
263
493
|
}
|
|
@@ -278,13 +508,14 @@ export const parseDocumentationDeclarations = (
|
|
|
278
508
|
const relations: KnowledgeRelation[] = []
|
|
279
509
|
const documentId = options.documentId ?? `document:${input.path}`
|
|
280
510
|
const relationClaims = new Map<string, string>()
|
|
511
|
+
const lookup = options.entityLookup ?? entityLookup(options.snapshot.entities)
|
|
281
512
|
|
|
282
513
|
if (!parsed.covers.length && !parsed.relations.length) {
|
|
283
514
|
addDiagnostic(diagnostics, input, 'DOCBRIDGE_CONTENT_MISSING', 'docbridge must declare covers or relations.', 1)
|
|
284
515
|
}
|
|
285
516
|
|
|
286
517
|
for (const [index, cover] of parsed.covers.entries()) {
|
|
287
|
-
const target = resolveEntity(cover.value, options.snapshot.entities, input, cover.line, unresolved)
|
|
518
|
+
const target = resolveEntity(cover.value, options.snapshot.entities, input, cover.line, unresolved, lookup)
|
|
288
519
|
relations.push({
|
|
289
520
|
id: `relation:declared:${input.path}:covers:${index}`,
|
|
290
521
|
kind: 'covers',
|
|
@@ -310,8 +541,8 @@ export const parseDocumentationDeclarations = (
|
|
|
310
541
|
addDiagnostic(diagnostics, input, 'DOCBRIDGE_DETECTION_INVALID', `Invalid relation detection: ${detection}.`, declaration.startLine, declaration.endLine)
|
|
311
542
|
continue
|
|
312
543
|
}
|
|
313
|
-
const fromEntity = resolveEntity(from, options.snapshot.entities, input, declaration.startLine, unresolved)
|
|
314
|
-
const toEntity = resolveEntity(to, options.snapshot.entities, input, declaration.startLine, unresolved)
|
|
544
|
+
const fromEntity = resolveEntity(from, options.snapshot.entities, input, declaration.startLine, unresolved, lookup)
|
|
545
|
+
const toEntity = resolveEntity(to, options.snapshot.entities, input, declaration.startLine, unresolved, lookup)
|
|
315
546
|
const key = relationKey(fromEntity.id, toEntity.id, kind)
|
|
316
547
|
const previousDetection = relationClaims.get(key)
|
|
317
548
|
if (previousDetection === detection) addDiagnostic(diagnostics, input, 'DOCBRIDGE_DECLARATION_DUPLICATE', 'Duplicate relation declaration.', declaration.startLine, declaration.endLine)
|
|
@@ -345,9 +576,10 @@ export const applyDocumentationDeclarations = (
|
|
|
345
576
|
const entities = new Map(snapshot.entities.map((entity) => [entity.id, entity]))
|
|
346
577
|
const relations = new Map(snapshot.relations.map((relation) => [relation.id, relation]))
|
|
347
578
|
const diagnostics: DocumentationDiagnostic[] = []
|
|
579
|
+
const lookup = entityLookup(snapshot.entities)
|
|
348
580
|
|
|
349
581
|
for (const document of documents) {
|
|
350
|
-
const result = parseDocumentationDeclarations(document, { snapshot, ...options })
|
|
582
|
+
const result = parseDocumentationDeclarations(document, { snapshot, ...options, entityLookup: lookup })
|
|
351
583
|
diagnostics.push(...result.diagnostics)
|
|
352
584
|
for (const entity of result.entities) entities.set(entity.id, entity)
|
|
353
585
|
for (const relation of result.relations) relations.set(relation.id, relation)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { sha256NormalizedV1 } from '../index-builder/content-hash.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Entity and relation identity, in one place.
|
|
5
|
+
*
|
|
6
|
+
* Every analyzer and every projection has to agree on what a module or a document is called, or
|
|
7
|
+
* the graph silently grows two nodes for one file and nothing joins. The hash suffix keeps a very
|
|
8
|
+
* long path addressable without exceeding the schema's identifier bound.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const MAX_ID_LENGTH = 256
|
|
12
|
+
const ID_HASH_LENGTH = 32
|
|
13
|
+
|
|
14
|
+
export const entityId = (kind: string, value: string): string => {
|
|
15
|
+
const fullId = `${kind}:${value}`
|
|
16
|
+
if (fullId.length <= MAX_ID_LENGTH) return fullId
|
|
17
|
+
|
|
18
|
+
const suffix = `:${sha256NormalizedV1(fullId).slice(0, ID_HASH_LENGTH)}`
|
|
19
|
+
return `${fullId.slice(0, MAX_ID_LENGTH - suffix.length)}${suffix}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Identity of an observed relation: its endpoints and kind, plus an optional discriminator. */
|
|
23
|
+
export const relationId = (from: string, kind: string, to: string, discriminator?: string): string =>
|
|
24
|
+
entityId('relation', discriminator ? `${from}:${kind}:${to}:${discriminator}` : `${from}:${kind}:${to}`)
|