@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.
Files changed (64) hide show
  1. package/CHANGELOG.md +249 -0
  2. package/CONTRIBUTING.md +6 -4
  3. package/action.yml +1 -1
  4. package/dist/cli/program.js +1139 -294
  5. package/dist/cli/program.js.map +1 -1
  6. package/dist/config/index.d.ts +1 -1
  7. package/dist/config/index.js +43 -5
  8. package/dist/config/index.js.map +1 -1
  9. package/dist/index-BUL0q7s8.d.ts +660 -0
  10. package/dist/index.d.ts +817 -2134
  11. package/dist/index.js +1154 -244
  12. package/dist/index.js.map +1 -1
  13. package/docs/PRD-enterprise-hardening.md +288 -0
  14. package/docs/RELEASE.md +22 -8
  15. package/docs/adr/0001-enterprise-verification-contract.md +35 -0
  16. package/docs/agent-corpus/INDEX.md +2 -2
  17. package/docs/agent-corpus/chat.md +2 -2
  18. package/docs/agent-corpus/cli.md +2 -2
  19. package/docs/agent-corpus/conformance.md +2 -2
  20. package/docs/agent-corpus/doc-bridge.md +1 -1
  21. package/docs/agent-corpus/doctor.md +2 -2
  22. package/docs/agent-corpus/gates.md +2 -2
  23. package/docs/agent-corpus/mcp.md +2 -2
  24. package/docs/agent-corpus/memory.md +2 -2
  25. package/docs/agent-corpus/query.md +2 -2
  26. package/docs/knowledge-engine-runbook.md +30 -2
  27. package/docs/spec/analyzer-plugin-v1.md +24 -0
  28. package/docs/spec/benchmark-v1.md +36 -0
  29. package/docs/spec/config-v1.md +156 -0
  30. package/docs/validation-cycle-plan.md +255 -0
  31. package/docs/verification-harness.md +37 -4
  32. package/mcpb/manifest.json +1 -1
  33. package/package.json +68 -70
  34. package/scripts/check-ecosystem-upstream.mjs +3 -2
  35. package/scripts/report-visual-check.mjs +64 -12
  36. package/scripts/verification-harness.mjs +216 -14
  37. package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
  38. package/src/agents/registry-adapter.ts +31 -7
  39. package/src/cli/demo.ts +2 -2
  40. package/src/cli/program.ts +59 -16
  41. package/src/config/index.ts +2 -0
  42. package/src/config/load-config.ts +7 -1
  43. package/src/config/schema.ts +60 -2
  44. package/src/conformance/documentation-standard-v1.ts +14 -8
  45. package/src/discovery/documentation.ts +90 -23
  46. package/src/discovery/repository.ts +147 -19
  47. package/src/doctor/run-doctor.ts +2 -15
  48. package/src/federation/llms.ts +72 -20
  49. package/src/fixes/proposals.ts +4 -3
  50. package/src/index-builder/human-adapters/fumadocs.ts +1 -1
  51. package/src/index-builder/watch-index.ts +1 -1
  52. package/src/index.ts +29 -0
  53. package/src/lib/bounded-text.ts +15 -10
  54. package/src/metrics/benchmark.ts +176 -0
  55. package/src/plugins/contract.ts +89 -0
  56. package/src/reconciliation/reconcile.ts +181 -5
  57. package/src/report/html.ts +318 -88
  58. package/src/rules/engine.ts +15 -2
  59. package/src/safety/repository.ts +1 -1
  60. package/src/schemas/knowledge.ts +21 -3
  61. package/src/validate.ts +7 -1
  62. package/src/version.ts +1 -1
  63. package/src/workflow/engine.ts +65 -9
  64. 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: [...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 observedRelations = observed.relations.filter((relation) => relation.provenance === 'observed' && !ignoredDocumentationRelations.has(relation.kind))
110
- const declaredRelations = declared.relations.filter((relation) => relation.provenance === 'declared' && !ignoredDocumentationRelations.has(relation.kind))
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 (coverageAvailable(observed, relation) && (requiredRelationKinds === undefined || requiredRelationKinds.has(relation.kind))) {
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) })