@agentskit/doc-bridge 1.4.1 → 1.5.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 +19 -0
- package/action.yml +1 -1
- package/dist/cli/program.js +2461 -274
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +77 -4
- package/dist/config/index.js.map +1 -1
- package/dist/{index-DAeq_OIi.d.ts → index-Di7PkJuf.d.ts} +209 -8
- package/dist/index.d.ts +1942 -4
- package/dist/index.js +2190 -193
- package/dist/index.js.map +1 -1
- package/docs/PRD-doc-bridge-knowledge-engine.md +338 -0
- package/docs/knowledge-engine-runbook.md +44 -0
- package/ecosystem-claims.json +19 -19
- package/ecosystem-upstream.json +2 -2
- package/ecosystem.json +117 -109
- package/mcpb/manifest.json +25 -1
- package/package.json +2 -2
- package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
- package/src/agents/registry-adapter.ts +97 -0
- package/src/cli/program.ts +285 -3
- package/src/config/defaults.ts +14 -1
- package/src/config/schema.ts +69 -0
- package/src/conformance/ecosystem-contract.ts +6 -3
- package/src/discovery/documentation.ts +320 -0
- package/src/discovery/repository.ts +514 -0
- package/src/fixes/proposals.ts +165 -0
- package/src/index-builder/content-hash.ts +9 -2
- package/src/index-builder/human-adapters/plain-markdown.ts +17 -1
- package/src/index-builder/llms-txt.ts +22 -2
- package/src/index-builder/scan-corpus.ts +17 -1
- package/src/index.ts +99 -2
- package/src/mcp/server.ts +178 -8
- package/src/reconciliation/reconcile.ts +227 -0
- package/src/report/html.ts +74 -0
- package/src/rules/engine.ts +180 -0
- package/src/safety/repository.ts +84 -0
- package/src/schemas/knowledge.ts +315 -0
- package/src/validate.ts +24 -0
- package/src/version.ts +1 -1
- package/src/workflow/engine.ts +238 -0
- package/tsup.config.ts +2 -1
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { contentHashForArtifactV1 } from '../index-builder/content-hash.js'
|
|
2
|
+
import {
|
|
3
|
+
DiscoverySnapshotV1Schema,
|
|
4
|
+
type DiscoverySnapshotV1,
|
|
5
|
+
type Evidence,
|
|
6
|
+
type KnowledgeEntity,
|
|
7
|
+
type KnowledgeRelation,
|
|
8
|
+
} from '../schemas/knowledge.js'
|
|
9
|
+
|
|
10
|
+
export type DocumentationDiagnostic = {
|
|
11
|
+
readonly code: string
|
|
12
|
+
readonly message: string
|
|
13
|
+
readonly path: string
|
|
14
|
+
readonly evidence: Evidence
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type DocumentationDeclarationInput = {
|
|
18
|
+
readonly path: string
|
|
19
|
+
readonly content: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type DocumentationDeclarationOptions = {
|
|
23
|
+
readonly snapshot: Pick<DiscoverySnapshotV1, 'entities'>
|
|
24
|
+
readonly documentId?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type DocumentationDeclarationResult = {
|
|
28
|
+
readonly hasDocbridge: boolean
|
|
29
|
+
readonly entities: readonly KnowledgeEntity[]
|
|
30
|
+
readonly relations: readonly KnowledgeRelation[]
|
|
31
|
+
readonly diagnostics: readonly DocumentationDiagnostic[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type DocumentationAnalysisResult = {
|
|
35
|
+
readonly snapshot: DiscoverySnapshotV1
|
|
36
|
+
readonly diagnostics: readonly DocumentationDiagnostic[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type RelationFields = {
|
|
40
|
+
from?: string
|
|
41
|
+
to?: string
|
|
42
|
+
kind?: string
|
|
43
|
+
detection?: string
|
|
44
|
+
startLine: number
|
|
45
|
+
endLine: number
|
|
46
|
+
fields: Set<string>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const detectionValues = new Set(['static', 'dynamic', 'external'])
|
|
50
|
+
|
|
51
|
+
const evidence = (path: string, lineStart: number, lineEnd = lineStart): Evidence => ({
|
|
52
|
+
source: 'documentation',
|
|
53
|
+
path,
|
|
54
|
+
lineStart,
|
|
55
|
+
lineEnd,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const diagnostic = (
|
|
59
|
+
path: string,
|
|
60
|
+
code: string,
|
|
61
|
+
message: string,
|
|
62
|
+
lineStart: number,
|
|
63
|
+
lineEnd = lineStart,
|
|
64
|
+
): DocumentationDiagnostic => ({ code, message, path: 'docbridge', evidence: evidence(path, lineStart, lineEnd) })
|
|
65
|
+
|
|
66
|
+
const scalar = (value: string): string => {
|
|
67
|
+
const trimmed = value.trim()
|
|
68
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
69
|
+
return trimmed.slice(1, -1)
|
|
70
|
+
}
|
|
71
|
+
return trimmed
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const list = (value: string): string[] | undefined => {
|
|
75
|
+
const trimmed = value.trim()
|
|
76
|
+
if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) return undefined
|
|
77
|
+
const body = trimmed.slice(1, -1).trim()
|
|
78
|
+
return body ? body.split(',').map(scalar).filter(Boolean) : []
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const findFrontmatter = (content: string): { readonly lines: readonly string[]; readonly end: number } | undefined => {
|
|
82
|
+
const lines = content.replace(/^\uFEFF/, '').split(/\r?\n/)
|
|
83
|
+
if (lines[0] !== '---') return undefined
|
|
84
|
+
const end = lines.findIndex((line, index) => index > 0 && line === '---')
|
|
85
|
+
return end < 0 ? undefined : { lines, end }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const addDiagnostic = (
|
|
89
|
+
diagnostics: DocumentationDiagnostic[],
|
|
90
|
+
input: DocumentationDeclarationInput,
|
|
91
|
+
code: string,
|
|
92
|
+
message: string,
|
|
93
|
+
line: number,
|
|
94
|
+
endLine = line,
|
|
95
|
+
): void => {
|
|
96
|
+
diagnostics.push(diagnostic(input.path, code, message, line, endLine))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const resolveEntity = (
|
|
100
|
+
reference: string,
|
|
101
|
+
entities: readonly KnowledgeEntity[],
|
|
102
|
+
input: DocumentationDeclarationInput,
|
|
103
|
+
lineStart: number,
|
|
104
|
+
unresolved: Map<string, KnowledgeEntity>,
|
|
105
|
+
): KnowledgeEntity => {
|
|
106
|
+
const resolved = entities.find((entity) => entity.id === reference || entity.aliases?.includes(reference))
|
|
107
|
+
if (resolved) return resolved
|
|
108
|
+
const id = `unresolved:${reference}`
|
|
109
|
+
const existing = unresolved.get(id)
|
|
110
|
+
if (existing) return existing
|
|
111
|
+
const entity: KnowledgeEntity = {
|
|
112
|
+
id,
|
|
113
|
+
kind: 'unresolved-reference',
|
|
114
|
+
name: reference,
|
|
115
|
+
provenance: 'declared',
|
|
116
|
+
evidence: [evidence(input.path, lineStart)],
|
|
117
|
+
}
|
|
118
|
+
unresolved.set(id, entity)
|
|
119
|
+
return entity
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const relationKey = (from: string, to: string, kind: string): string => `${from}\u0000${to}\u0000${kind}`
|
|
123
|
+
|
|
124
|
+
const parseBlock = (
|
|
125
|
+
input: DocumentationDeclarationInput,
|
|
126
|
+
): { readonly covers: readonly { value: string; line: number }[]; readonly relations: readonly RelationFields[]; readonly diagnostics: readonly DocumentationDiagnostic[]; readonly hasDocbridge: boolean } => {
|
|
127
|
+
const frontmatter = findFrontmatter(input.content)
|
|
128
|
+
if (!frontmatter) {
|
|
129
|
+
if (input.content.replace(/^\uFEFF/, '').startsWith('---')) {
|
|
130
|
+
return {
|
|
131
|
+
covers: [],
|
|
132
|
+
relations: [],
|
|
133
|
+
hasDocbridge: true,
|
|
134
|
+
diagnostics: [diagnostic(input.path, 'DOCBRIDGE_FRONTMATTER_MALFORMED', 'Frontmatter must close with a line containing only ---.', 1)],
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { covers: [], relations: [], diagnostics: [], hasDocbridge: false }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { lines, end } = frontmatter
|
|
141
|
+
const docbridgeLine = lines.findIndex((line, index) => index > 0 && index < end && /^docbridge\s*:/.test(line))
|
|
142
|
+
if (docbridgeLine < 0) return { covers: [], relations: [], diagnostics: [], hasDocbridge: false }
|
|
143
|
+
|
|
144
|
+
const diagnostics: DocumentationDiagnostic[] = []
|
|
145
|
+
const covers: { value: string; line: number }[] = []
|
|
146
|
+
const relations: RelationFields[] = []
|
|
147
|
+
const inline = lines[docbridgeLine]?.slice('docbridge:'.length).trim() ?? ''
|
|
148
|
+
if (inline && inline !== '{}') addDiagnostic(diagnostics, input, 'DOCBRIDGE_BLOCK_MALFORMED', 'docbridge must be a nested frontmatter object.', docbridgeLine + 1)
|
|
149
|
+
|
|
150
|
+
let section: 'covers' | 'relations' | undefined
|
|
151
|
+
let current: RelationFields | undefined
|
|
152
|
+
const finishRelation = (): void => {
|
|
153
|
+
if (current) relations.push(current)
|
|
154
|
+
current = undefined
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (let index = docbridgeLine + 1; index < end; index += 1) {
|
|
158
|
+
const raw = lines[index] ?? ''
|
|
159
|
+
const trimmed = raw.trim()
|
|
160
|
+
const line = index + 1
|
|
161
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
162
|
+
if (/\t/.test(raw)) {
|
|
163
|
+
addDiagnostic(diagnostics, input, 'DOCBRIDGE_INDENTATION_INVALID', 'docbridge indentation must use spaces.', line)
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
if (!raw.startsWith(' ')) {
|
|
167
|
+
finishRelation()
|
|
168
|
+
section = undefined
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
if (/^ {2}[A-Za-z][A-Za-z0-9_-]*\s*:/.test(raw)) {
|
|
172
|
+
finishRelation()
|
|
173
|
+
const match = /^ {2}([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/.exec(raw)
|
|
174
|
+
const key = match?.[1]
|
|
175
|
+
const value = match?.[2] ?? ''
|
|
176
|
+
if (key !== 'covers' && key !== 'relations') {
|
|
177
|
+
addDiagnostic(diagnostics, input, 'DOCBRIDGE_FIELD_UNKNOWN', `Unknown docbridge field: ${key ?? '(missing)'}.`, line)
|
|
178
|
+
section = undefined
|
|
179
|
+
} else if (key === 'covers') {
|
|
180
|
+
section = 'covers'
|
|
181
|
+
if (value) {
|
|
182
|
+
const values = list(value)
|
|
183
|
+
if (!values) addDiagnostic(diagnostics, input, 'DOCBRIDGE_COVERS_INVALID', 'covers must be a list of entity references.', line)
|
|
184
|
+
else values.forEach((item) => covers.push({ value: item, line }))
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
section = 'relations'
|
|
188
|
+
if (value) addDiagnostic(diagnostics, input, 'DOCBRIDGE_RELATIONS_INVALID', 'relations must be a list of relation objects.', line)
|
|
189
|
+
}
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
if (section === 'covers' && /^ {4}-\s*/.test(raw)) {
|
|
193
|
+
const value = scalar(raw.replace(/^ {4}-\s*/, ''))
|
|
194
|
+
if (!value) addDiagnostic(diagnostics, input, 'DOCBRIDGE_REFERENCE_MISSING', 'covers entries must not be empty.', line)
|
|
195
|
+
else covers.push({ value, line })
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
if (section === 'relations' && /^ {4}-\s*/.test(raw)) {
|
|
199
|
+
finishRelation()
|
|
200
|
+
const firstField = /^ {4}-\s*([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/.exec(raw)
|
|
201
|
+
current = { startLine: line, endLine: line, fields: new Set() }
|
|
202
|
+
if (firstField?.[1]) {
|
|
203
|
+
current.fields.add(firstField[1])
|
|
204
|
+
current[firstField[1] as 'from' | 'to' | 'kind' | 'detection'] = scalar(firstField[2] ?? '')
|
|
205
|
+
} else if (raw.replace(/^ {4}-\s*/, '').trim()) {
|
|
206
|
+
addDiagnostic(diagnostics, input, 'DOCBRIDGE_RELATION_INVALID', 'Relation entries must be field mappings.', line)
|
|
207
|
+
}
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
if (section === 'relations' && current && /^ {6}[A-Za-z][A-Za-z0-9_-]*\s*:/.test(raw)) {
|
|
211
|
+
const field = /^ {6}([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/.exec(raw)
|
|
212
|
+
const key = field?.[1]
|
|
213
|
+
current.endLine = line
|
|
214
|
+
if (!key || !['from', 'to', 'kind', 'detection'].includes(key)) {
|
|
215
|
+
addDiagnostic(diagnostics, input, 'DOCBRIDGE_FIELD_UNKNOWN', `Unknown relation field: ${key ?? '(missing)'}.`, line)
|
|
216
|
+
} else if (current.fields.has(key)) {
|
|
217
|
+
addDiagnostic(diagnostics, input, 'DOCBRIDGE_FIELD_DUPLICATE', `Duplicate relation field: ${key}.`, line)
|
|
218
|
+
} else {
|
|
219
|
+
current.fields.add(key)
|
|
220
|
+
current[key as 'from' | 'to' | 'kind' | 'detection'] = scalar(field?.[2] ?? '')
|
|
221
|
+
}
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
addDiagnostic(diagnostics, input, 'DOCBRIDGE_STRUCTURE_INVALID', `Invalid docbridge structure at line ${line}.`, line)
|
|
225
|
+
}
|
|
226
|
+
finishRelation()
|
|
227
|
+
return { covers, relations, diagnostics, hasDocbridge: true }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const parseDocumentationDeclarations = (
|
|
231
|
+
input: DocumentationDeclarationInput,
|
|
232
|
+
options: DocumentationDeclarationOptions,
|
|
233
|
+
): DocumentationDeclarationResult => {
|
|
234
|
+
const parsed = parseBlock(input)
|
|
235
|
+
if (!parsed.hasDocbridge) return { hasDocbridge: false, entities: [], relations: [], diagnostics: [] }
|
|
236
|
+
const diagnostics = [...parsed.diagnostics]
|
|
237
|
+
const unresolved = new Map<string, KnowledgeEntity>()
|
|
238
|
+
const relations: KnowledgeRelation[] = []
|
|
239
|
+
const documentId = options.documentId ?? `document:${input.path}`
|
|
240
|
+
const relationClaims = new Map<string, string>()
|
|
241
|
+
|
|
242
|
+
if (!parsed.covers.length && !parsed.relations.length) {
|
|
243
|
+
addDiagnostic(diagnostics, input, 'DOCBRIDGE_CONTENT_MISSING', 'docbridge must declare covers or relations.', 1)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const [index, cover] of parsed.covers.entries()) {
|
|
247
|
+
const target = resolveEntity(cover.value, options.snapshot.entities, input, cover.line, unresolved)
|
|
248
|
+
relations.push({
|
|
249
|
+
id: `relation:declared:${input.path}:covers:${index}`,
|
|
250
|
+
kind: 'covers',
|
|
251
|
+
from: documentId,
|
|
252
|
+
to: target.id,
|
|
253
|
+
provenance: 'declared',
|
|
254
|
+
evidence: [evidence(input.path, cover.line)],
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
for (const [index, declaration] of parsed.relations.entries()) {
|
|
259
|
+
const basePath = `relations[${index}]`
|
|
260
|
+
const missing = (['from', 'to', 'kind', 'detection'] as const).filter((field) => !declaration[field])
|
|
261
|
+
if (missing.length) {
|
|
262
|
+
addDiagnostic(diagnostics, input, 'DOCBRIDGE_RELATION_FIELD_MISSING', `Relation is missing required field(s): ${missing.join(', ')}.`, declaration.startLine, declaration.endLine)
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
const from = declaration.from as string
|
|
266
|
+
const to = declaration.to as string
|
|
267
|
+
const kind = declaration.kind as string
|
|
268
|
+
const detection = declaration.detection as string
|
|
269
|
+
if (!detectionValues.has(detection)) {
|
|
270
|
+
addDiagnostic(diagnostics, input, 'DOCBRIDGE_DETECTION_INVALID', `Invalid relation detection: ${detection}.`, declaration.startLine, declaration.endLine)
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
const fromEntity = resolveEntity(from, options.snapshot.entities, input, declaration.startLine, unresolved)
|
|
274
|
+
const toEntity = resolveEntity(to, options.snapshot.entities, input, declaration.startLine, unresolved)
|
|
275
|
+
const key = relationKey(fromEntity.id, toEntity.id, kind)
|
|
276
|
+
const previousDetection = relationClaims.get(key)
|
|
277
|
+
if (previousDetection === detection) addDiagnostic(diagnostics, input, 'DOCBRIDGE_DECLARATION_DUPLICATE', 'Duplicate relation declaration.', declaration.startLine, declaration.endLine)
|
|
278
|
+
if (previousDetection && previousDetection !== detection) addDiagnostic(diagnostics, input, 'DOCBRIDGE_DECLARATION_CONFLICT', 'Conflicting relation declarations use different detection values.', declaration.startLine, declaration.endLine)
|
|
279
|
+
relationClaims.set(key, previousDetection ?? detection)
|
|
280
|
+
relations.push({
|
|
281
|
+
id: `relation:declared:${input.path}:${basePath}`,
|
|
282
|
+
kind,
|
|
283
|
+
from: fromEntity.id,
|
|
284
|
+
to: toEntity.id,
|
|
285
|
+
discriminator: detection,
|
|
286
|
+
provenance: 'declared',
|
|
287
|
+
evidence: [evidence(input.path, declaration.startLine, declaration.endLine)],
|
|
288
|
+
metadata: { detection },
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
hasDocbridge: true,
|
|
294
|
+
entities: [...unresolved.values()].sort((a, b) => a.id.localeCompare(b.id)),
|
|
295
|
+
relations,
|
|
296
|
+
diagnostics,
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export const applyDocumentationDeclarations = (
|
|
301
|
+
snapshot: DiscoverySnapshotV1,
|
|
302
|
+
documents: readonly DocumentationDeclarationInput[],
|
|
303
|
+
): DocumentationAnalysisResult => {
|
|
304
|
+
const entities = new Map(snapshot.entities.map((entity) => [entity.id, entity]))
|
|
305
|
+
const relations = new Map(snapshot.relations.map((relation) => [relation.id, relation]))
|
|
306
|
+
const diagnostics: DocumentationDiagnostic[] = []
|
|
307
|
+
|
|
308
|
+
for (const document of documents) {
|
|
309
|
+
const result = parseDocumentationDeclarations(document, { snapshot })
|
|
310
|
+
diagnostics.push(...result.diagnostics)
|
|
311
|
+
for (const entity of result.entities) entities.set(entity.id, entity)
|
|
312
|
+
for (const relation of result.relations) relations.set(relation.id, relation)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const base = { ...snapshot, contentHash: '0'.repeat(64), entities: [...entities.values()], relations: [...relations.values()] }
|
|
316
|
+
return {
|
|
317
|
+
snapshot: DiscoverySnapshotV1Schema.parse({ ...base, contentHash: contentHashForArtifactV1(base) }),
|
|
318
|
+
diagnostics,
|
|
319
|
+
}
|
|
320
|
+
}
|