@agentskit/doc-bridge 1.6.4 → 1.7.45
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 +249 -0
- package/CONTRIBUTING.md +6 -4
- package/action.yml +1 -1
- package/dist/cli/program.js +1139 -294
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +43 -5
- package/dist/config/index.js.map +1 -1
- package/dist/index-BUL0q7s8.d.ts +660 -0
- package/dist/index.d.ts +817 -2134
- package/dist/index.js +1154 -244
- package/dist/index.js.map +1 -1
- package/docs/PRD-enterprise-hardening.md +288 -0
- package/docs/RELEASE.md +22 -8
- package/docs/adr/0001-enterprise-verification-contract.md +35 -0
- package/docs/agent-corpus/INDEX.md +2 -2
- package/docs/agent-corpus/chat.md +2 -2
- package/docs/agent-corpus/cli.md +2 -2
- package/docs/agent-corpus/conformance.md +2 -2
- package/docs/agent-corpus/doc-bridge.md +1 -1
- package/docs/agent-corpus/doctor.md +2 -2
- package/docs/agent-corpus/gates.md +2 -2
- package/docs/agent-corpus/mcp.md +2 -2
- package/docs/agent-corpus/memory.md +2 -2
- package/docs/agent-corpus/query.md +2 -2
- package/docs/knowledge-engine-runbook.md +30 -2
- package/docs/spec/analyzer-plugin-v1.md +24 -0
- package/docs/spec/benchmark-v1.md +36 -0
- package/docs/spec/config-v1.md +156 -0
- package/docs/validation-cycle-plan.md +255 -0
- package/docs/verification-harness.md +37 -4
- package/mcpb/manifest.json +1 -1
- package/package.json +68 -70
- package/scripts/check-ecosystem-upstream.mjs +3 -2
- package/scripts/report-visual-check.mjs +64 -12
- package/scripts/verification-harness.mjs +216 -14
- package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
- package/src/agents/registry-adapter.ts +31 -7
- package/src/cli/demo.ts +2 -2
- package/src/cli/program.ts +59 -16
- package/src/config/index.ts +2 -0
- package/src/config/load-config.ts +7 -1
- package/src/config/schema.ts +60 -2
- package/src/conformance/documentation-standard-v1.ts +14 -8
- package/src/discovery/documentation.ts +90 -23
- package/src/discovery/repository.ts +147 -19
- package/src/doctor/run-doctor.ts +2 -15
- package/src/federation/llms.ts +72 -20
- package/src/fixes/proposals.ts +4 -3
- package/src/index-builder/human-adapters/fumadocs.ts +1 -1
- package/src/index-builder/watch-index.ts +1 -1
- package/src/index.ts +29 -0
- package/src/lib/bounded-text.ts +15 -10
- package/src/metrics/benchmark.ts +176 -0
- package/src/plugins/contract.ts +89 -0
- package/src/reconciliation/reconcile.ts +181 -5
- package/src/report/html.ts +318 -88
- package/src/rules/engine.ts +15 -2
- package/src/safety/repository.ts +1 -1
- package/src/schemas/knowledge.ts +21 -3
- package/src/validate.ts +7 -1
- package/src/version.ts +1 -1
- package/src/workflow/engine.ts +65 -9
- package/dist/index-DudNuwI5.d.ts +0 -2060
|
@@ -11,19 +11,29 @@ import {
|
|
|
11
11
|
type EntityResolver = (reference: string) => string
|
|
12
12
|
|
|
13
13
|
export type ReconciliationOptions = {
|
|
14
|
+
/** Compare declarations at a semantic level while retaining raw discovery evidence. */
|
|
15
|
+
readonly scope?: 'file' | 'module' | 'package'
|
|
14
16
|
/** Omit for backwards-compatible all-relation checking; [] disables missing-declaration findings. */
|
|
15
17
|
readonly requiredRelationKinds?: readonly string[]
|
|
18
|
+
/** Limit missing-declaration findings to relations between internal project entities. */
|
|
19
|
+
readonly requiredRelationTargets?: 'all' | 'internal'
|
|
20
|
+
/** Emit one bounded finding for each observed Markdown document without declarations. */
|
|
21
|
+
readonly includeOrphanedDocuments?: boolean
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
const ignoredDocumentationRelations = new Set(['covers'])
|
|
19
25
|
|
|
26
|
+
const isInternalEntity = (id: string): boolean => !id.startsWith('external:') && !id.startsWith('unresolved:')
|
|
27
|
+
|
|
20
28
|
const metadataDetection = (relation: KnowledgeRelation): string | undefined => {
|
|
21
29
|
const detection = relation.metadata?.detection
|
|
22
30
|
return typeof detection === 'string' ? detection : undefined
|
|
23
31
|
}
|
|
24
32
|
|
|
33
|
+
const normalizeDetection = (value: string): string => value === 'dynamic-literal' ? 'dynamic' : value
|
|
34
|
+
|
|
25
35
|
const relationDetection = (relation: KnowledgeRelation): string =>
|
|
26
|
-
relation.discriminator ?? metadataDetection(relation) ?? 'static'
|
|
36
|
+
normalizeDetection(relation.discriminator ?? metadataDetection(relation) ?? 'static')
|
|
27
37
|
|
|
28
38
|
const entityResolver = (snapshots: readonly DiscoverySnapshotV1[]): EntityResolver => {
|
|
29
39
|
const references = new Map<string, string>()
|
|
@@ -51,6 +61,32 @@ const mergeEvidence = (...relations: readonly KnowledgeRelation[]): Evidence[] =
|
|
|
51
61
|
return [...merged.values()].sort((a, b) => evidenceKey(a).localeCompare(evidenceKey(b)))
|
|
52
62
|
}
|
|
53
63
|
|
|
64
|
+
const boundedEvidence = (evidence: readonly Evidence[]): Evidence[] => [...evidence].slice(0, 64)
|
|
65
|
+
|
|
66
|
+
const diagnosticCounts = (values: readonly string[]): Record<string, number> => {
|
|
67
|
+
const counts = new Map<string, number>()
|
|
68
|
+
for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1)
|
|
69
|
+
return Object.fromEntries([...counts.entries()].sort(([a], [b]) => a.localeCompare(b)))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const documentClass = (entity: KnowledgeEntity): string => {
|
|
73
|
+
const classification = entity.metadata?.classification
|
|
74
|
+
return typeof classification === 'string' && classification.length > 0 ? classification : 'unclassified'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const countDocumentClasses = (
|
|
78
|
+
documents: readonly KnowledgeEntity[],
|
|
79
|
+
selectedIds?: ReadonlySet<string>,
|
|
80
|
+
): Record<string, number> => {
|
|
81
|
+
const counts = new Map<string, number>()
|
|
82
|
+
for (const document of documents) {
|
|
83
|
+
if (selectedIds && !selectedIds.has(document.id)) continue
|
|
84
|
+
const classification = documentClass(document)
|
|
85
|
+
counts.set(classification, (counts.get(classification) ?? 0) + 1)
|
|
86
|
+
}
|
|
87
|
+
return Object.fromEntries([...counts.entries()].sort(([a], [b]) => a.localeCompare(b)))
|
|
88
|
+
}
|
|
89
|
+
|
|
54
90
|
const diagnosticId = (code: string, value: unknown): string =>
|
|
55
91
|
`reconciliation:${code}:${sha256NormalizedV1(value).slice(0, 32)}`
|
|
56
92
|
|
|
@@ -72,6 +108,79 @@ const entityById = (snapshots: readonly DiscoverySnapshotV1[]): ReadonlyMap<stri
|
|
|
72
108
|
const isUnresolved = (id: string, entities: ReadonlyMap<string, KnowledgeEntity>): boolean =>
|
|
73
109
|
id.startsWith('unresolved:') || entities.get(id)?.kind === 'unresolved-reference'
|
|
74
110
|
|
|
111
|
+
const normalizedPath = (path: string): string => path.replaceAll('\\', '/').replace(/^\.\//, '')
|
|
112
|
+
|
|
113
|
+
const semanticEntityId = (
|
|
114
|
+
id: string,
|
|
115
|
+
scope: NonNullable<ReconciliationOptions['scope']>,
|
|
116
|
+
entities: ReadonlyMap<string, KnowledgeEntity>,
|
|
117
|
+
packageByModule: ReadonlyMap<string, string>,
|
|
118
|
+
): string => {
|
|
119
|
+
if (scope === 'file' || id.startsWith('external:') || id.startsWith('unresolved:')) return id
|
|
120
|
+
const entity = entities.get(id)
|
|
121
|
+
if (!entity || entity.kind !== 'module' || !entity.path) return id
|
|
122
|
+
if (scope === 'module') return id
|
|
123
|
+
return packageByModule.get(id) ?? id
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const packageLookup = (scope: NonNullable<ReconciliationOptions['scope']>, entities: ReadonlyMap<string, KnowledgeEntity>): Map<string, string> => {
|
|
127
|
+
if (scope !== 'package') return new Map()
|
|
128
|
+
const packages = [...entities.values()]
|
|
129
|
+
.filter((entity) => entity.kind === 'package' && entity.path)
|
|
130
|
+
.map((entity) => ({ id: entity.id, path: normalizedPath(entity.path as string) }))
|
|
131
|
+
.sort((a, b) => b.path.length - a.path.length || a.id.localeCompare(b.id))
|
|
132
|
+
const result = new Map<string, string>()
|
|
133
|
+
for (const entity of entities.values()) {
|
|
134
|
+
if (entity.kind !== 'module' || !entity.path) continue
|
|
135
|
+
const modulePath = normalizedPath(entity.path)
|
|
136
|
+
const packageEntity = packages.find(({ path }) => path === '.' || modulePath === path || modulePath.startsWith(`${path}/`))
|
|
137
|
+
if (packageEntity) result.set(entity.id, packageEntity.id)
|
|
138
|
+
}
|
|
139
|
+
return result
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const aggregatedRelations = (
|
|
143
|
+
relations: readonly KnowledgeRelation[],
|
|
144
|
+
scope: NonNullable<ReconciliationOptions['scope']>,
|
|
145
|
+
entities: ReadonlyMap<string, KnowledgeEntity>,
|
|
146
|
+
packageByModule: ReadonlyMap<string, string>,
|
|
147
|
+
): KnowledgeRelation[] => {
|
|
148
|
+
if (scope === 'file') return [...relations]
|
|
149
|
+
const groups = new Map<string, KnowledgeRelation[]>()
|
|
150
|
+
for (const relation of relations) {
|
|
151
|
+
const from = semanticEntityId(relation.from, scope, entities, packageByModule)
|
|
152
|
+
const to = semanticEntityId(relation.to, scope, entities, packageByModule)
|
|
153
|
+
const detection = relationDetection(relation)
|
|
154
|
+
const key = `${from}\u0000${to}\u0000${relation.kind}\u0000${detection}`
|
|
155
|
+
const group = groups.get(key) ?? []
|
|
156
|
+
group.push(relation)
|
|
157
|
+
groups.set(key, group)
|
|
158
|
+
}
|
|
159
|
+
return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, group]) => {
|
|
160
|
+
const first = group[0] as KnowledgeRelation
|
|
161
|
+
const parts = key.split('\u0000')
|
|
162
|
+
const from = parts[0] ?? first.from
|
|
163
|
+
const to = parts[1] ?? first.to
|
|
164
|
+
const detection = parts[3] ?? relationDetection(first)
|
|
165
|
+
const mergedEvidence = mergeEvidence(...group)
|
|
166
|
+
return {
|
|
167
|
+
...first,
|
|
168
|
+
id: `relation:aggregated:${scope}:${sha256NormalizedV1(key).slice(0, 32)}`,
|
|
169
|
+
from,
|
|
170
|
+
to,
|
|
171
|
+
...(detection === 'static' && !first.discriminator ? {} : { discriminator: detection }),
|
|
172
|
+
evidence: boundedEvidence(mergedEvidence),
|
|
173
|
+
metadata: {
|
|
174
|
+
...(first.metadata ?? {}),
|
|
175
|
+
aggregationScope: scope,
|
|
176
|
+
aggregatedRelationCount: group.length,
|
|
177
|
+
aggregatedEvidenceCount: mergedEvidence.length,
|
|
178
|
+
...(mergedEvidence.length > 64 ? { evidenceTruncated: true } : {}),
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
75
184
|
const relationMatches = (observed: KnowledgeRelation, declared: KnowledgeRelation, resolveEntity: EntityResolver): boolean => {
|
|
76
185
|
if (relationBase(observed, resolveEntity) !== relationBase(declared, resolveEntity)) return false
|
|
77
186
|
return relationDetection(declared) === relationDetection(observed)
|
|
@@ -93,7 +202,7 @@ const reportDiagnostic = (
|
|
|
93
202
|
status,
|
|
94
203
|
severity,
|
|
95
204
|
message,
|
|
96
|
-
evidence:
|
|
205
|
+
evidence: boundedEvidence(evidence),
|
|
97
206
|
...(entityIds?.length ? { entityIds: [...entityIds] } : {}),
|
|
98
207
|
...(relationIds?.length ? { relationIds: [...relationIds] } : {}),
|
|
99
208
|
...(remediation ? { remediation } : {}),
|
|
@@ -106,11 +215,32 @@ export const reconcileKnowledge = (
|
|
|
106
215
|
): ReconciliationReportV1 => {
|
|
107
216
|
const resolveEntity = entityResolver([observed, declared])
|
|
108
217
|
const entities = entityById([observed, declared])
|
|
109
|
-
const
|
|
110
|
-
const
|
|
218
|
+
const scope = options.scope ?? 'file'
|
|
219
|
+
const packageByModule = packageLookup(scope, entities)
|
|
220
|
+
const allDeclaredRelations = declared.relations.filter((relation) => relation.provenance === 'declared')
|
|
221
|
+
const observedRelations = aggregatedRelations(observed.relations.filter((relation) => relation.provenance === 'observed' && !ignoredDocumentationRelations.has(relation.kind)), scope, entities, packageByModule)
|
|
222
|
+
const declaredRelations = aggregatedRelations(declared.relations.filter((relation) => relation.provenance === 'declared' && !ignoredDocumentationRelations.has(relation.kind)), scope, entities, packageByModule)
|
|
111
223
|
const requiredRelationKinds = options.requiredRelationKinds === undefined ? undefined : new Set(options.requiredRelationKinds)
|
|
112
224
|
const diagnostics: ReconciliationReportV1['diagnostics'][number][] = []
|
|
113
225
|
|
|
226
|
+
if (options.includeOrphanedDocuments) {
|
|
227
|
+
const declaredDocumentIds = new Set(allDeclaredRelations.map((relation) => relation.from).filter((id) => id.startsWith('document:')))
|
|
228
|
+
for (const document of observed.entities.filter((entity) => entity.kind === 'document').sort((a, b) => a.id.localeCompare(b.id))) {
|
|
229
|
+
if (declaredDocumentIds.has(document.id)) continue
|
|
230
|
+
diagnostics.push(reportDiagnostic(
|
|
231
|
+
'DOCUMENTATION_ORPHANED',
|
|
232
|
+
'undocumented',
|
|
233
|
+
'info',
|
|
234
|
+
'Markdown document has no Doc Bridge declaration linking it to observed knowledge.',
|
|
235
|
+
document.evidence,
|
|
236
|
+
document.id,
|
|
237
|
+
[document.id],
|
|
238
|
+
undefined,
|
|
239
|
+
'Add a docbridge declaration or exclude this document from the documentation comparison scope.',
|
|
240
|
+
))
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
114
244
|
for (const entity of declared.entities.filter((item) => isUnresolved(item.id, entities)).sort((a, b) => a.id.localeCompare(b.id))) {
|
|
115
245
|
diagnostics.push(reportDiagnostic(
|
|
116
246
|
'UNRESOLVED_ENTITY_REFERENCE',
|
|
@@ -133,9 +263,19 @@ export const reconcileKnowledge = (
|
|
|
133
263
|
declaredByBase.set(base, group)
|
|
134
264
|
}
|
|
135
265
|
|
|
266
|
+
const observedDetectionsByBase = new Map<string, Set<string>>()
|
|
267
|
+
for (const relation of observedRelations) {
|
|
268
|
+
const base = relationBase(relation, resolveEntity)
|
|
269
|
+
const detections = observedDetectionsByBase.get(base) ?? new Set<string>()
|
|
270
|
+
detections.add(relationDetection(relation))
|
|
271
|
+
observedDetectionsByBase.set(base, detections)
|
|
272
|
+
}
|
|
273
|
+
|
|
136
274
|
for (const group of declaredByBase.values()) {
|
|
137
275
|
const detections = new Set(group.map(relationDetection))
|
|
138
276
|
if (detections.size < 2) continue
|
|
277
|
+
const observedDetections = observedDetectionsByBase.get(relationBase(group[0] as KnowledgeRelation, resolveEntity))
|
|
278
|
+
if (observedDetections && [...detections].every((detection) => observedDetections.has(detection))) continue
|
|
139
279
|
diagnostics.push(reportDiagnostic(
|
|
140
280
|
'CONFLICTING_DECLARATIONS',
|
|
141
281
|
'conflict',
|
|
@@ -163,7 +303,11 @@ export const reconcileKnowledge = (
|
|
|
163
303
|
undefined,
|
|
164
304
|
[relation.id, match.id],
|
|
165
305
|
))
|
|
166
|
-
} else if (
|
|
306
|
+
} else if (
|
|
307
|
+
coverageAvailable(observed, relation) &&
|
|
308
|
+
(requiredRelationKinds === undefined || requiredRelationKinds.has(relation.kind)) &&
|
|
309
|
+
(options.requiredRelationTargets !== 'internal' || (relation.from !== relation.to && isInternalEntity(relation.from) && isInternalEntity(relation.to)))
|
|
310
|
+
) {
|
|
167
311
|
diagnostics.push(reportDiagnostic(
|
|
168
312
|
'RELATION_UNDOCUMENTED',
|
|
169
313
|
'undocumented',
|
|
@@ -211,6 +355,33 @@ export const reconcileKnowledge = (
|
|
|
211
355
|
}
|
|
212
356
|
|
|
213
357
|
const sortedDiagnostics = [...diagnostics].sort((a, b) => a.id.localeCompare(b.id))
|
|
358
|
+
const packageEntities = observed.entities.filter((entity) => entity.kind === 'package')
|
|
359
|
+
const packageStatus = { fresh: 0, stale: 0, missing: 0, unverified: 0 }
|
|
360
|
+
const diagnosticStatusByEntity = new Map<string, Set<ReconciliationReportV1['diagnostics'][number]['status']>>()
|
|
361
|
+
const relationEndpoints = new Map([...observedRelations, ...declaredRelations].map((relation) => [relation.id, [relation.from, relation.to]]))
|
|
362
|
+
for (const diagnostic of sortedDiagnostics) for (const id of [...(diagnostic.entityIds ?? []), ...(diagnostic.relationIds ?? []).flatMap((relationId) => relationEndpoints.get(relationId) ?? [])]) {
|
|
363
|
+
const statuses = diagnosticStatusByEntity.get(id) ?? new Set()
|
|
364
|
+
statuses.add(diagnostic.status)
|
|
365
|
+
diagnosticStatusByEntity.set(id, statuses)
|
|
366
|
+
}
|
|
367
|
+
for (const packageEntity of packageEntities) {
|
|
368
|
+
const docs = allDeclaredRelations.filter((relation) => relation.from.startsWith('document:') && relation.to === packageEntity.id)
|
|
369
|
+
const statuses = diagnosticStatusByEntity.get(packageEntity.id) ?? new Set()
|
|
370
|
+
if (!docs.length) packageStatus.missing += 1
|
|
371
|
+
else if (statuses.has('conflict') || statuses.has('stale-or-unverified')) packageStatus.stale += 1
|
|
372
|
+
else if (statuses.has('undocumented') || statuses.has('unresolved') || statuses.has('not-analyzed')) packageStatus.unverified += 1
|
|
373
|
+
else packageStatus.fresh += 1
|
|
374
|
+
}
|
|
375
|
+
const documents = observed.entities.filter((entity) => entity.kind === 'document')
|
|
376
|
+
const documentedDocumentIds = new Set(allDeclaredRelations.filter((relation) => relation.from.startsWith('document:')).map((relation) => relation.from))
|
|
377
|
+
const documentation = {
|
|
378
|
+
documentCount: documents.length,
|
|
379
|
+
documentedDocumentCount: documentedDocumentIds.size,
|
|
380
|
+
documentClassificationCounts: countDocumentClasses(documents),
|
|
381
|
+
documentedDocumentClassificationCounts: countDocumentClasses(documents, documentedDocumentIds),
|
|
382
|
+
packageCount: packageEntities.length,
|
|
383
|
+
packageStatus,
|
|
384
|
+
}
|
|
214
385
|
const base = {
|
|
215
386
|
type: 'reconciliation-report' as const,
|
|
216
387
|
schemaVersion: 1 as const,
|
|
@@ -228,7 +399,12 @@ export const reconcileKnowledge = (
|
|
|
228
399
|
entityCount: observed.entities.length,
|
|
229
400
|
relationCount: observedRelations.length,
|
|
230
401
|
diagnosticCount: sortedDiagnostics.length,
|
|
402
|
+
...(options.scope === undefined ? {} : { scope: options.scope }),
|
|
231
403
|
...(options.requiredRelationKinds === undefined ? {} : { requiredRelationKinds: [...new Set(options.requiredRelationKinds)].sort() }),
|
|
404
|
+
...(options.requiredRelationTargets === undefined ? {} : { requiredRelationTargets: options.requiredRelationTargets }),
|
|
405
|
+
diagnosticsByCode: diagnosticCounts(sortedDiagnostics.map((diagnostic) => diagnostic.code)),
|
|
406
|
+
diagnosticsByStatus: diagnosticCounts(sortedDiagnostics.map((diagnostic) => diagnostic.status)),
|
|
407
|
+
documentation,
|
|
232
408
|
},
|
|
233
409
|
}
|
|
234
410
|
return ReconciliationReportV1Schema.parse({ ...base, contentHash: contentHashForArtifactV1(base) })
|