@agentskit/doc-bridge 1.6.3 → 1.7.44
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/action.yml +1 -1
- package/dist/cli/program.js +793 -137
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +38 -2
- package/dist/config/index.js.map +1 -1
- package/dist/{index-DudNuwI5.d.ts → index-C2PCQSrB.d.ts} +216 -25
- package/dist/index.d.ts +837 -67
- package/dist/index.js +858 -127
- package/dist/index.js.map +1 -1
- package/docs/PRD-enterprise-hardening.md +288 -0
- package/docs/adr/0001-enterprise-verification-contract.md +35 -0
- package/docs/knowledge-engine-runbook.md +18 -2
- package/docs/spec/analyzer-plugin-v1.md +24 -0
- package/docs/spec/benchmark-v1.md +30 -0
- package/docs/spec/config-v1.md +111 -0
- package/docs/validation-cycle-plan.md +236 -0
- package/docs/verification-harness.md +33 -4
- package/mcpb/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/report-visual-check.mjs +63 -17
- package/scripts/verification-harness.mjs +216 -13
- package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
- package/src/agents/registry-adapter.ts +31 -7
- package/src/cli/program.ts +44 -11
- package/src/config/index.ts +2 -0
- package/src/config/schema.ts +56 -0
- package/src/discovery/documentation.ts +46 -5
- package/src/discovery/repository.ts +95 -16
- package/src/index.ts +29 -0
- package/src/metrics/benchmark.ts +176 -0
- package/src/plugins/contract.ts +89 -0
- package/src/reconciliation/reconcile.ts +137 -3
- package/src/report/html.ts +302 -78
- package/src/schemas/knowledge.ts +16 -1
- package/src/version.ts +1 -1
- package/src/workflow/engine.ts +65 -9
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { CoverageSchema, DiagnosticSchema, EntitySchema, RelationSchema, type Coverage, type KnowledgeDiagnostic, type KnowledgeEntity, type KnowledgeRelation } from '../schemas/knowledge.js'
|
|
4
|
+
|
|
5
|
+
export const ANALYZER_PLUGIN_CONTRACT_VERSION = 1 as const
|
|
6
|
+
|
|
7
|
+
export const AnalyzerPluginManifestSchema = z.object({
|
|
8
|
+
id: z.string().regex(/^[a-z][a-z0-9-]*$/).max(128),
|
|
9
|
+
version: z.string().min(1).max(64),
|
|
10
|
+
languages: z.array(z.string().min(1).max(64)).min(1).max(32),
|
|
11
|
+
frameworks: z.array(z.string().min(1).max(128)).max(32).default([]),
|
|
12
|
+
capabilities: z.array(z.string().min(1).max(128)).min(1).max(32),
|
|
13
|
+
knowledgeSchemaVersion: z.literal(1),
|
|
14
|
+
compatibility: z.object({ pipelineMajor: z.number().int().nonnegative() }).strict(),
|
|
15
|
+
unsupportedConstructs: z.array(z.string().min(1).max(256)).max(128).default([]),
|
|
16
|
+
resourceLimits: z.object({ maxFiles: z.number().int().positive().optional(), maxBytes: z.number().int().positive().optional() }).strict().default({}),
|
|
17
|
+
}).strict()
|
|
18
|
+
|
|
19
|
+
export type AnalyzerPluginManifest = z.infer<typeof AnalyzerPluginManifestSchema>
|
|
20
|
+
|
|
21
|
+
export const AnalyzerPluginOutputSchema = z.object({
|
|
22
|
+
entities: z.array(EntitySchema).max(50_000).default([]),
|
|
23
|
+
relations: z.array(RelationSchema).max(100_000).default([]),
|
|
24
|
+
coverage: z.array(CoverageSchema).max(1_000).default([]),
|
|
25
|
+
diagnostics: z.array(DiagnosticSchema).max(100_000).default([]),
|
|
26
|
+
}).strict()
|
|
27
|
+
|
|
28
|
+
export type AnalyzerPluginOutput = z.infer<typeof AnalyzerPluginOutputSchema>
|
|
29
|
+
|
|
30
|
+
export type AnalyzerPluginInput = {
|
|
31
|
+
readonly language: string
|
|
32
|
+
readonly framework?: string
|
|
33
|
+
readonly files: readonly { readonly path: string; readonly bytes: number }[]
|
|
34
|
+
readonly value?: unknown
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type AnalyzerPlugin = {
|
|
38
|
+
readonly manifest: AnalyzerPluginManifest
|
|
39
|
+
readonly analyze: (input: AnalyzerPluginInput) => Promise<unknown> | unknown
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type AnalyzerRegistry = {
|
|
43
|
+
readonly register: (plugin: AnalyzerPlugin) => void
|
|
44
|
+
readonly list: () => readonly AnalyzerPluginManifest[]
|
|
45
|
+
readonly analyze: (id: string, input: AnalyzerPluginInput) => Promise<AnalyzerPluginOutput>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const pipelineMajor = (version: string): number => Number.parseInt(version.split('.')[0] ?? '', 10)
|
|
49
|
+
|
|
50
|
+
export const createAnalyzerRegistry = (options: { readonly pipelineVersion?: string; readonly maxPlugins?: number } = {}): AnalyzerRegistry => {
|
|
51
|
+
const plugins = new Map<string, AnalyzerPlugin>()
|
|
52
|
+
const pipeline = pipelineMajor(options.pipelineVersion ?? '1.0.0')
|
|
53
|
+
return {
|
|
54
|
+
register(plugin) {
|
|
55
|
+
const manifest = AnalyzerPluginManifestSchema.parse(plugin.manifest)
|
|
56
|
+
if (manifest.compatibility.pipelineMajor !== pipeline) throw new Error(`Analyzer plugin "${manifest.id}" requires pipeline major ${manifest.compatibility.pipelineMajor}; current pipeline is ${pipeline}.`)
|
|
57
|
+
if (plugins.has(manifest.id)) throw new Error(`Analyzer plugin "${manifest.id}" is already registered.`)
|
|
58
|
+
if (options.maxPlugins !== undefined && plugins.size >= options.maxPlugins) throw new Error(`Analyzer plugin limit ${options.maxPlugins} exceeded.`)
|
|
59
|
+
plugins.set(manifest.id, { ...plugin, manifest })
|
|
60
|
+
},
|
|
61
|
+
list() {
|
|
62
|
+
return [...plugins.values()].map((plugin) => plugin.manifest).sort((a, b) => a.id.localeCompare(b.id))
|
|
63
|
+
},
|
|
64
|
+
async analyze(id, input) {
|
|
65
|
+
const plugin = plugins.get(id)
|
|
66
|
+
if (!plugin) throw new Error(`Analyzer plugin "${id}" is not registered.`)
|
|
67
|
+
const { maxFiles, maxBytes } = plugin.manifest.resourceLimits
|
|
68
|
+
const bytes = input.files.reduce((total, file) => total + file.bytes, 0)
|
|
69
|
+
if (maxFiles !== undefined && input.files.length > maxFiles) throw new Error(`Analyzer plugin "${id}" file limit ${maxFiles} exceeded.`)
|
|
70
|
+
if (maxBytes !== undefined && bytes > maxBytes) throw new Error(`Analyzer plugin "${id}" byte limit ${maxBytes} exceeded.`)
|
|
71
|
+
try {
|
|
72
|
+
const output = AnalyzerPluginOutputSchema.parse(await plugin.analyze(input))
|
|
73
|
+
return {
|
|
74
|
+
...output,
|
|
75
|
+
coverage: output.coverage.map((entry) => ({ ...entry, analyzer: plugin.manifest.id, analyzerVersion: plugin.manifest.version })),
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return {
|
|
79
|
+
entities: [],
|
|
80
|
+
relations: [],
|
|
81
|
+
diagnostics: [],
|
|
82
|
+
coverage: [{ analyzer: plugin.manifest.id, analyzerVersion: plugin.manifest.version, scope: 'plugin', status: 'not-analyzed', reason: `Plugin failed safely: ${error instanceof Error ? error.message : String(error)}` }],
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type { Coverage, KnowledgeDiagnostic, KnowledgeEntity, KnowledgeRelation }
|
|
@@ -11,8 +11,12 @@ 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
|
+
/** Emit one bounded finding for each observed Markdown document without declarations. */
|
|
19
|
+
readonly includeOrphanedDocuments?: boolean
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
const ignoredDocumentationRelations = new Set(['covers'])
|
|
@@ -51,6 +55,14 @@ const mergeEvidence = (...relations: readonly KnowledgeRelation[]): Evidence[] =
|
|
|
51
55
|
return [...merged.values()].sort((a, b) => evidenceKey(a).localeCompare(evidenceKey(b)))
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
const boundedEvidence = (evidence: readonly Evidence[]): Evidence[] => [...evidence].slice(0, 64)
|
|
59
|
+
|
|
60
|
+
const diagnosticCounts = (values: readonly string[]): Record<string, number> => {
|
|
61
|
+
const counts = new Map<string, number>()
|
|
62
|
+
for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1)
|
|
63
|
+
return Object.fromEntries([...counts.entries()].sort(([a], [b]) => a.localeCompare(b)))
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
const diagnosticId = (code: string, value: unknown): string =>
|
|
55
67
|
`reconciliation:${code}:${sha256NormalizedV1(value).slice(0, 32)}`
|
|
56
68
|
|
|
@@ -72,6 +84,80 @@ const entityById = (snapshots: readonly DiscoverySnapshotV1[]): ReadonlyMap<stri
|
|
|
72
84
|
const isUnresolved = (id: string, entities: ReadonlyMap<string, KnowledgeEntity>): boolean =>
|
|
73
85
|
id.startsWith('unresolved:') || entities.get(id)?.kind === 'unresolved-reference'
|
|
74
86
|
|
|
87
|
+
const normalizedPath = (path: string): string => path.replaceAll('\\', '/').replace(/^\.\//, '')
|
|
88
|
+
|
|
89
|
+
const semanticEntityId = (
|
|
90
|
+
id: string,
|
|
91
|
+
scope: NonNullable<ReconciliationOptions['scope']>,
|
|
92
|
+
entities: ReadonlyMap<string, KnowledgeEntity>,
|
|
93
|
+
packageByModule: ReadonlyMap<string, string>,
|
|
94
|
+
): string => {
|
|
95
|
+
if (scope === 'file' || id.startsWith('external:') || id.startsWith('unresolved:')) return id
|
|
96
|
+
const entity = entities.get(id)
|
|
97
|
+
if (!entity || entity.kind !== 'module' || !entity.path) return id
|
|
98
|
+
if (scope === 'module') return id
|
|
99
|
+
return packageByModule.get(id) ?? id
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const packageLookup = (scope: NonNullable<ReconciliationOptions['scope']>, entities: ReadonlyMap<string, KnowledgeEntity>): Map<string, string> => {
|
|
103
|
+
if (scope !== 'package') return new Map()
|
|
104
|
+
const packages = [...entities.values()]
|
|
105
|
+
.filter((entity) => entity.kind === 'package' && entity.path)
|
|
106
|
+
.map((entity) => ({ id: entity.id, path: normalizedPath(entity.path as string) }))
|
|
107
|
+
.sort((a, b) => b.path.length - a.path.length || a.id.localeCompare(b.id))
|
|
108
|
+
const result = new Map<string, string>()
|
|
109
|
+
for (const entity of entities.values()) {
|
|
110
|
+
if (entity.kind !== 'module' || !entity.path) continue
|
|
111
|
+
const modulePath = normalizedPath(entity.path)
|
|
112
|
+
const packageEntity = packages.find(({ path }) => path === '.' || modulePath === path || modulePath.startsWith(`${path}/`))
|
|
113
|
+
if (packageEntity) result.set(entity.id, packageEntity.id)
|
|
114
|
+
}
|
|
115
|
+
return result
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const aggregatedRelations = (
|
|
119
|
+
relations: readonly KnowledgeRelation[],
|
|
120
|
+
scope: NonNullable<ReconciliationOptions['scope']>,
|
|
121
|
+
entities: ReadonlyMap<string, KnowledgeEntity>,
|
|
122
|
+
packageByModule: ReadonlyMap<string, string>,
|
|
123
|
+
): KnowledgeRelation[] => {
|
|
124
|
+
if (scope === 'file') return [...relations]
|
|
125
|
+
const groups = new Map<string, KnowledgeRelation[]>()
|
|
126
|
+
for (const relation of relations) {
|
|
127
|
+
const from = semanticEntityId(relation.from, scope, entities, packageByModule)
|
|
128
|
+
const to = semanticEntityId(relation.to, scope, entities, packageByModule)
|
|
129
|
+
const detection = relationDetection(relation)
|
|
130
|
+
const key = `${from}\u0000${to}\u0000${relation.kind}\u0000${detection}`
|
|
131
|
+
const group = groups.get(key) ?? []
|
|
132
|
+
group.push(relation)
|
|
133
|
+
groups.set(key, group)
|
|
134
|
+
}
|
|
135
|
+
return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, group]) => {
|
|
136
|
+
const first = group[0] as KnowledgeRelation
|
|
137
|
+
const parts = key.split('\u0000')
|
|
138
|
+
const from = parts[0] ?? first.from
|
|
139
|
+
const to = parts[1] ?? first.to
|
|
140
|
+
const kind = parts[2] ?? first.kind
|
|
141
|
+
const detection = parts[3] ?? relationDetection(first)
|
|
142
|
+
const mergedEvidence = mergeEvidence(...group)
|
|
143
|
+
return {
|
|
144
|
+
...first,
|
|
145
|
+
id: `relation:aggregated:${scope}:${sha256NormalizedV1(key).slice(0, 32)}`,
|
|
146
|
+
from,
|
|
147
|
+
to,
|
|
148
|
+
...(detection === 'static' && !first.discriminator ? {} : { discriminator: detection }),
|
|
149
|
+
evidence: boundedEvidence(mergedEvidence),
|
|
150
|
+
metadata: {
|
|
151
|
+
...(first.metadata ?? {}),
|
|
152
|
+
aggregationScope: scope,
|
|
153
|
+
aggregatedRelationCount: group.length,
|
|
154
|
+
aggregatedEvidenceCount: mergedEvidence.length,
|
|
155
|
+
...(mergedEvidence.length > 64 ? { evidenceTruncated: true } : {}),
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
75
161
|
const relationMatches = (observed: KnowledgeRelation, declared: KnowledgeRelation, resolveEntity: EntityResolver): boolean => {
|
|
76
162
|
if (relationBase(observed, resolveEntity) !== relationBase(declared, resolveEntity)) return false
|
|
77
163
|
return relationDetection(declared) === relationDetection(observed)
|
|
@@ -93,7 +179,7 @@ const reportDiagnostic = (
|
|
|
93
179
|
status,
|
|
94
180
|
severity,
|
|
95
181
|
message,
|
|
96
|
-
evidence:
|
|
182
|
+
evidence: boundedEvidence(evidence),
|
|
97
183
|
...(entityIds?.length ? { entityIds: [...entityIds] } : {}),
|
|
98
184
|
...(relationIds?.length ? { relationIds: [...relationIds] } : {}),
|
|
99
185
|
...(remediation ? { remediation } : {}),
|
|
@@ -106,11 +192,32 @@ export const reconcileKnowledge = (
|
|
|
106
192
|
): ReconciliationReportV1 => {
|
|
107
193
|
const resolveEntity = entityResolver([observed, declared])
|
|
108
194
|
const entities = entityById([observed, declared])
|
|
109
|
-
const
|
|
110
|
-
const
|
|
195
|
+
const scope = options.scope ?? 'file'
|
|
196
|
+
const packageByModule = packageLookup(scope, entities)
|
|
197
|
+
const allDeclaredRelations = declared.relations.filter((relation) => relation.provenance === 'declared')
|
|
198
|
+
const observedRelations = aggregatedRelations(observed.relations.filter((relation) => relation.provenance === 'observed' && !ignoredDocumentationRelations.has(relation.kind)), scope, entities, packageByModule)
|
|
199
|
+
const declaredRelations = aggregatedRelations(declared.relations.filter((relation) => relation.provenance === 'declared' && !ignoredDocumentationRelations.has(relation.kind)), scope, entities, packageByModule)
|
|
111
200
|
const requiredRelationKinds = options.requiredRelationKinds === undefined ? undefined : new Set(options.requiredRelationKinds)
|
|
112
201
|
const diagnostics: ReconciliationReportV1['diagnostics'][number][] = []
|
|
113
202
|
|
|
203
|
+
if (options.includeOrphanedDocuments) {
|
|
204
|
+
const declaredDocumentIds = new Set(allDeclaredRelations.map((relation) => relation.from).filter((id) => id.startsWith('document:')))
|
|
205
|
+
for (const document of observed.entities.filter((entity) => entity.kind === 'document').sort((a, b) => a.id.localeCompare(b.id))) {
|
|
206
|
+
if (declaredDocumentIds.has(document.id)) continue
|
|
207
|
+
diagnostics.push(reportDiagnostic(
|
|
208
|
+
'DOCUMENTATION_ORPHANED',
|
|
209
|
+
'undocumented',
|
|
210
|
+
'info',
|
|
211
|
+
'Markdown document has no Doc Bridge declaration linking it to observed knowledge.',
|
|
212
|
+
document.evidence,
|
|
213
|
+
document.id,
|
|
214
|
+
[document.id],
|
|
215
|
+
undefined,
|
|
216
|
+
'Add a docbridge declaration or exclude this document from the documentation comparison scope.',
|
|
217
|
+
))
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
114
221
|
for (const entity of declared.entities.filter((item) => isUnresolved(item.id, entities)).sort((a, b) => a.id.localeCompare(b.id))) {
|
|
115
222
|
diagnostics.push(reportDiagnostic(
|
|
116
223
|
'UNRESOLVED_ENTITY_REFERENCE',
|
|
@@ -211,6 +318,29 @@ export const reconcileKnowledge = (
|
|
|
211
318
|
}
|
|
212
319
|
|
|
213
320
|
const sortedDiagnostics = [...diagnostics].sort((a, b) => a.id.localeCompare(b.id))
|
|
321
|
+
const packageEntities = observed.entities.filter((entity) => entity.kind === 'package')
|
|
322
|
+
const packageStatus = { fresh: 0, stale: 0, missing: 0, unverified: 0 }
|
|
323
|
+
const diagnosticStatusByEntity = new Map<string, Set<ReconciliationReportV1['diagnostics'][number]['status']>>()
|
|
324
|
+
const relationEndpoints = new Map([...observedRelations, ...declaredRelations].map((relation) => [relation.id, [relation.from, relation.to]]))
|
|
325
|
+
for (const diagnostic of sortedDiagnostics) for (const id of [...(diagnostic.entityIds ?? []), ...(diagnostic.relationIds ?? []).flatMap((relationId) => relationEndpoints.get(relationId) ?? [])]) {
|
|
326
|
+
const statuses = diagnosticStatusByEntity.get(id) ?? new Set()
|
|
327
|
+
statuses.add(diagnostic.status)
|
|
328
|
+
diagnosticStatusByEntity.set(id, statuses)
|
|
329
|
+
}
|
|
330
|
+
for (const packageEntity of packageEntities) {
|
|
331
|
+
const docs = allDeclaredRelations.filter((relation) => relation.from.startsWith('document:') && relation.to === packageEntity.id)
|
|
332
|
+
const statuses = diagnosticStatusByEntity.get(packageEntity.id) ?? new Set()
|
|
333
|
+
if (!docs.length) packageStatus.missing += 1
|
|
334
|
+
else if (statuses.has('conflict') || statuses.has('stale-or-unverified')) packageStatus.stale += 1
|
|
335
|
+
else if (statuses.has('undocumented') || statuses.has('unresolved') || statuses.has('not-analyzed')) packageStatus.unverified += 1
|
|
336
|
+
else packageStatus.fresh += 1
|
|
337
|
+
}
|
|
338
|
+
const documentation = {
|
|
339
|
+
documentCount: observed.entities.filter((entity) => entity.kind === 'document').length,
|
|
340
|
+
documentedDocumentCount: new Set(allDeclaredRelations.filter((relation) => relation.from.startsWith('document:')).map((relation) => relation.from)).size,
|
|
341
|
+
packageCount: packageEntities.length,
|
|
342
|
+
packageStatus,
|
|
343
|
+
}
|
|
214
344
|
const base = {
|
|
215
345
|
type: 'reconciliation-report' as const,
|
|
216
346
|
schemaVersion: 1 as const,
|
|
@@ -228,7 +358,11 @@ export const reconcileKnowledge = (
|
|
|
228
358
|
entityCount: observed.entities.length,
|
|
229
359
|
relationCount: observedRelations.length,
|
|
230
360
|
diagnosticCount: sortedDiagnostics.length,
|
|
361
|
+
...(options.scope === undefined ? {} : { scope: options.scope }),
|
|
231
362
|
...(options.requiredRelationKinds === undefined ? {} : { requiredRelationKinds: [...new Set(options.requiredRelationKinds)].sort() }),
|
|
363
|
+
diagnosticsByCode: diagnosticCounts(sortedDiagnostics.map((diagnostic) => diagnostic.code)),
|
|
364
|
+
diagnosticsByStatus: diagnosticCounts(sortedDiagnostics.map((diagnostic) => diagnostic.status)),
|
|
365
|
+
documentation,
|
|
232
366
|
},
|
|
233
367
|
}
|
|
234
368
|
return ReconciliationReportV1Schema.parse({ ...base, contentHash: contentHashForArtifactV1(base) })
|