@agentskit/doc-bridge 1.0.1 → 1.1.1

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 (48) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/CONTRIBUTING.md +7 -0
  3. package/README.md +81 -15
  4. package/SECURITY.md +1 -1
  5. package/action.yml +2 -2
  6. package/dist/cli/program.js +1248 -316
  7. package/dist/cli/program.js.map +1 -1
  8. package/dist/config/index.d.ts +1 -1
  9. package/dist/config/index.js +338 -13
  10. package/dist/config/index.js.map +1 -1
  11. package/dist/{index-CPUJbTbg.d.ts → index-DGI9TBLE.d.ts} +906 -11
  12. package/dist/index.d.ts +65 -11
  13. package/dist/index.js +1084 -171
  14. package/dist/index.js.map +1 -1
  15. package/docs/RELEASE.md +19 -21
  16. package/docs/getting-started.md +27 -2
  17. package/docs/landing/assets/doc-bridge-hero.webp +0 -0
  18. package/docs/landing/assets/doc-bridge-surfaces.webp +0 -0
  19. package/docs/landing/assets/doc-bridge-two-way.webp +0 -0
  20. package/docs/landing/index.html +70 -10
  21. package/docs/playbook/doc-bridge-pattern.md +2 -2
  22. package/docs/recipes/index-pipeline.md +2 -2
  23. package/docs/schemas/memory-candidate-v1.md +10 -1
  24. package/docs/spec/cli.md +1 -0
  25. package/docs/spec/config-v1.md +51 -6
  26. package/docs/spec/documentation-standard-v1.md +131 -0
  27. package/ecosystem-claims.json +187 -0
  28. package/ecosystem-upstream.json +9 -0
  29. package/ecosystem.json +231 -0
  30. package/package.json +14 -2
  31. package/scripts/check-ecosystem-upstream.mjs +50 -0
  32. package/src/cli/program.ts +43 -9
  33. package/src/config/index.ts +7 -1
  34. package/src/config/load-config.ts +4 -14
  35. package/src/config/schema.ts +91 -0
  36. package/src/conformance/documentation-standard-v1.ts +502 -0
  37. package/src/conformance/ecosystem-contract.ts +175 -0
  38. package/src/gates/run-gates.ts +33 -4
  39. package/src/index-builder/human-adapters/core.ts +12 -5
  40. package/src/index-builder/human-adapters/docusaurus.ts +29 -44
  41. package/src/index-builder/human-adapters/index.ts +15 -3
  42. package/src/index-builder/scan-corpus.ts +6 -6
  43. package/src/index.ts +17 -0
  44. package/src/lib/bounded-text.ts +25 -0
  45. package/src/lib/paths.ts +20 -2
  46. package/src/lib/static-js-literal.ts +261 -0
  47. package/src/lib/walk.ts +23 -4
  48. package/src/version.ts +1 -1
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
2
  import { basename, dirname, join, resolve } from 'node:path'
3
- import vm from 'node:vm'
4
3
 
4
+ import { parseStaticJsObject } from '../lib/static-js-literal.js'
5
5
  import { applyConfigDefaults } from './defaults.js'
6
6
  import { DocBridgeConfigV1Schema, type DocBridgeConfigV1 } from './schema.js'
7
7
 
@@ -60,28 +60,18 @@ const parseJsonConfig = (raw: string, path: string): unknown => {
60
60
  }
61
61
 
62
62
  const parseCodeConfig = (raw: string, path: string): unknown => {
63
- const code = raw
63
+ const unsupportedImports = raw
64
64
  .replace(/import\s+type\s+[\s\S]*?;?\n/g, '')
65
65
  .replace(/import\s+\{\s*defineConfig\s*\}\s+from\s+['"]@agentskit\/doc-bridge(?:\/config)?['"];?\n?/g, '')
66
- .replace(/\s+satisfies\s+[A-Za-z0-9_.$<>{}\[\],\s]+(?=\s*(?:;|\)|$))/g, '')
67
- .replace(/export\s+default/, 'module.exports.default =')
68
-
69
- if (/\bimport\b/.test(code)) {
66
+ if (/\bimport\b/.test(unsupportedImports)) {
70
67
  throw new Error(`Unsupported import in ${path}. Static config only supports defineConfig imports.`)
71
68
  }
72
-
73
- const sandbox = {
74
- module: { exports: {} as { default?: unknown } },
75
- exports: {},
76
- defineConfig: (config: unknown) => config,
77
- }
78
69
  try {
79
- vm.runInNewContext(code, sandbox, { timeout: 250, filename: path })
70
+ return parseStaticJsObject(raw)
80
71
  } catch (error) {
81
72
  const message = error instanceof Error ? error.message : String(error)
82
73
  throw new Error(`Failed to load config at ${path}: ${message}`)
83
74
  }
84
- return sandbox.module.exports.default
85
75
  }
86
76
 
87
77
  const parseConfig = (input: unknown): DocBridgeConfigV1 => {
@@ -124,6 +124,7 @@ export const GatesConfigSchema = z
124
124
  'docs-style',
125
125
  'routing-currency',
126
126
  'bootstrap-size',
127
+ 'documentation-standard-v1',
127
128
  ]),
128
129
  )
129
130
  .max(16)
@@ -138,6 +139,7 @@ export const GatesConfigSchema = z
138
139
  'docs-style',
139
140
  'routing-currency',
140
141
  'bootstrap-size',
142
+ 'documentation-standard-v1',
141
143
  ]),
142
144
  )
143
145
  .max(16)
@@ -264,6 +266,92 @@ export const FederationConfigSchema = z
264
266
  })
265
267
  .strict()
266
268
 
269
+ export const DocumentationEvidenceFileSchema = z
270
+ .object({
271
+ path: z.string().min(1).max(512),
272
+ contains: z.array(z.string().min(1).max(512)).min(1).max(32),
273
+ })
274
+ .strict()
275
+
276
+ export const DocumentationLinkEvidenceSchema = z
277
+ .object({
278
+ url: z.string().url().max(512),
279
+ paths: z.array(z.string().min(1).max(512)).min(1).max(32),
280
+ })
281
+ .strict()
282
+
283
+ export const DocumentationQuickstartEvidenceSchema = z
284
+ .object({
285
+ id: z.string().regex(/^[a-z][a-z0-9-]*$/).max(128),
286
+ doc: z.string().min(1).max(512),
287
+ test: z.string().min(1).max(512),
288
+ command: z.string().min(1).max(512),
289
+ testContains: z.array(z.string().min(1).max(256)).min(1).max(16),
290
+ })
291
+ .strict()
292
+
293
+ export const EcosystemContractEvidenceSchema = z
294
+ .object({
295
+ manifest: z.string().min(1).max(512),
296
+ claims: z.string().min(1).max(512),
297
+ productId: z.string().regex(/^[a-z][a-z0-9-]*$/).max(128),
298
+ })
299
+ .strict()
300
+
301
+ export const DocumentationStandardRuleIdSchema = z.enum([
302
+ 'human-docs',
303
+ 'llms-and-raw-source',
304
+ 'agent-handoffs',
305
+ 'contribution',
306
+ 'metadata',
307
+ 'cross-links',
308
+ 'tested-quickstarts',
309
+ 'visual-explanations',
310
+ 'structured-diagrams',
311
+ ])
312
+
313
+ export const DocumentationStandardExceptionSchema = z
314
+ .object({
315
+ ruleId: DocumentationStandardRuleIdSchema,
316
+ reason: z.string().min(10).max(1_024),
317
+ approvedBy: z.string().min(1).max(256),
318
+ trackingUrl: z.string().url().max(512),
319
+ })
320
+ .strict()
321
+
322
+ export const DocumentationStandardV1ConfigSchema = z
323
+ .object({
324
+ rawSources: z.array(z.string().min(1).max(512)).max(64).optional(),
325
+ contributionPaths: z.array(z.string().min(1).max(512)).max(16).optional(),
326
+ metadata: z.array(DocumentationEvidenceFileSchema).max(32).optional(),
327
+ links: z.array(DocumentationLinkEvidenceSchema).max(64).optional(),
328
+ quickstarts: z.array(DocumentationQuickstartEvidenceSchema).max(32).optional(),
329
+ visuals: z.array(z.string().min(1).max(512)).max(64).optional(),
330
+ diagrams: z.array(DocumentationEvidenceFileSchema).max(32).optional(),
331
+ ecosystemContract: EcosystemContractEvidenceSchema.optional(),
332
+ exceptions: z.array(DocumentationStandardExceptionSchema).max(32).optional(),
333
+ })
334
+ .strict()
335
+ .superRefine((value, context) => {
336
+ const seen = new Set<string>()
337
+ for (const [index, exception] of (value.exceptions ?? []).entries()) {
338
+ if (seen.has(exception.ruleId)) {
339
+ context.addIssue({
340
+ code: 'custom',
341
+ path: ['exceptions', index, 'ruleId'],
342
+ message: `Duplicate exception for rule ${exception.ruleId}`,
343
+ })
344
+ }
345
+ seen.add(exception.ruleId)
346
+ }
347
+ })
348
+
349
+ export const ConformanceConfigSchema = z
350
+ .object({
351
+ documentationStandardV1: DocumentationStandardV1ConfigSchema.optional(),
352
+ })
353
+ .strict()
354
+
267
355
  export const DocBridgeConfigV1Schema = z
268
356
  .object({
269
357
  schemaVersion: z.literal(CONFIG_SCHEMA_VERSION),
@@ -286,9 +374,12 @@ export const DocBridgeConfigV1Schema = z
286
374
  surfaces: SurfacesConfigSchema.optional(),
287
375
  intelligence: IntelligenceConfigSchema.optional(),
288
376
  federation: FederationConfigSchema.optional(),
377
+ conformance: ConformanceConfigSchema.optional(),
289
378
  })
290
379
  .strict()
291
380
 
292
381
  export type DocBridgeConfigV1 = z.infer<typeof DocBridgeConfigV1Schema>
293
382
  export type AgentCorpusConfig = z.infer<typeof AgentCorpusConfigSchema>
294
383
  export type HumanCorpusConfig = z.infer<typeof HumanCorpusConfigSchema>
384
+ export type DocumentationStandardV1Config = z.infer<typeof DocumentationStandardV1ConfigSchema>
385
+ export type DocumentationStandardRuleId = z.infer<typeof DocumentationStandardRuleIdSchema>
@@ -0,0 +1,502 @@
1
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path'
3
+
4
+ import type {
5
+ DocBridgeConfigV1,
6
+ DocumentationStandardRuleId,
7
+ DocumentationStandardV1Config,
8
+ } from '../config/schema.js'
9
+ import { parseCanonicalEcosystemContract } from './ecosystem-contract.js'
10
+ import { buildDocBridgeIndex } from '../index-builder/build-index.js'
11
+ import { scanHumanDocRecords } from '../index-builder/human-adapters/index.js'
12
+ import { renderLlmsTxt } from '../index-builder/llms-txt.js'
13
+ import { toPosix } from '../lib/paths.js'
14
+
15
+ export const DOCUMENTATION_STANDARD_V1_ID = 'documentation-standard-v1' as const
16
+ export const DOCUMENTATION_STANDARD_V1_STATUS = 'stable' as const
17
+
18
+ const MAX_TEXT_EVIDENCE_BYTES = 4 * 1_024 * 1_024
19
+
20
+ export type { DocumentationStandardRuleId } from '../config/schema.js'
21
+
22
+ export type DocumentationStandardRuleLevel = 'required' | 'recommended'
23
+ export type DocumentationStandardRuleStatus = 'pass' | 'fail' | 'excepted'
24
+
25
+ export type DocumentationStandardEvidence = {
26
+ readonly path: string
27
+ readonly detail: string
28
+ }
29
+
30
+ export type DocumentationStandardRemediation = {
31
+ readonly command: string
32
+ readonly detail: string
33
+ }
34
+
35
+ export type DocumentationStandardRuleResult = {
36
+ readonly id: DocumentationStandardRuleId
37
+ readonly level: DocumentationStandardRuleLevel
38
+ readonly status: DocumentationStandardRuleStatus
39
+ readonly ok: boolean
40
+ readonly message: string
41
+ readonly evidence: readonly DocumentationStandardEvidence[]
42
+ readonly remediation: DocumentationStandardRemediation
43
+ readonly exception?: {
44
+ readonly reason: string
45
+ readonly approvedBy: string
46
+ readonly trackingUrl: string
47
+ }
48
+ }
49
+
50
+ export type DocumentationConformanceReportV1 = {
51
+ readonly schemaVersion: 1
52
+ readonly profile: {
53
+ readonly id: typeof DOCUMENTATION_STANDARD_V1_ID
54
+ readonly version: 1
55
+ readonly status: typeof DOCUMENTATION_STANDARD_V1_STATUS
56
+ }
57
+ readonly ok: boolean
58
+ readonly recommendedOk: boolean
59
+ readonly summary: {
60
+ readonly required: { readonly passed: number; readonly failed: number; readonly excepted: number }
61
+ readonly recommended: { readonly passed: number; readonly failed: number; readonly excepted: number }
62
+ }
63
+ readonly results: readonly DocumentationStandardRuleResult[]
64
+ }
65
+
66
+ type RuleDraft = Omit<DocumentationStandardRuleResult, 'status' | 'ok' | 'exception'> & {
67
+ readonly passed: boolean
68
+ }
69
+
70
+ const safePath = (root: string, path: string): string | undefined => {
71
+ const rootAbs = realpathSync.native(resolve(root))
72
+ const unresolved = resolve(rootAbs, path)
73
+ const unresolvedRel = relative(rootAbs, unresolved)
74
+ if (isAbsolute(unresolvedRel) || unresolvedRel === '..' || unresolvedRel.startsWith(`..${sep}`)) return undefined
75
+ if (!existsSync(unresolved)) return unresolved
76
+ try {
77
+ const abs = realpathSync.native(unresolved)
78
+ const rel = relative(rootAbs, abs)
79
+ return !isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`) ? abs : undefined
80
+ } catch {
81
+ return undefined
82
+ }
83
+ }
84
+
85
+ const fileEvidence = (
86
+ root: string,
87
+ path: string,
88
+ options?: { readonly readContent?: boolean },
89
+ ): { readonly exists: boolean; readonly content: string; readonly evidence: DocumentationStandardEvidence } => {
90
+ const abs = safePath(root, path)
91
+ if (!abs) {
92
+ return {
93
+ exists: false,
94
+ content: '',
95
+ evidence: { path, detail: 'Path escapes the project root.' },
96
+ }
97
+ }
98
+ if (!existsSync(abs)) {
99
+ return { exists: false, content: '', evidence: { path, detail: 'File does not exist.' } }
100
+ }
101
+ try {
102
+ const stat = statSync(abs)
103
+ if (!stat.isFile()) {
104
+ return { exists: false, content: '', evidence: { path, detail: 'Path is not a regular file.' } }
105
+ }
106
+ if (stat.size === 0) {
107
+ return { exists: false, content: '', evidence: { path, detail: 'File is empty.' } }
108
+ }
109
+ if (options?.readContent === false) {
110
+ return {
111
+ exists: true,
112
+ content: '',
113
+ evidence: { path: toPosix(relative(resolve(root), abs)) || '.', detail: 'File exists and is non-empty.' },
114
+ }
115
+ }
116
+ if (stat.size > MAX_TEXT_EVIDENCE_BYTES) {
117
+ return {
118
+ exists: false,
119
+ content: '',
120
+ evidence: { path, detail: `Text evidence exceeds ${MAX_TEXT_EVIDENCE_BYTES} bytes.` },
121
+ }
122
+ }
123
+ const content = readFileSync(abs, 'utf8')
124
+ return {
125
+ exists: content.trim().length > 0,
126
+ content,
127
+ evidence: {
128
+ path: toPosix(relative(resolve(root), abs)) || '.',
129
+ detail: content.trim().length > 0 ? 'File exists and is non-empty.' : 'File is empty.',
130
+ },
131
+ }
132
+ } catch {
133
+ return { exists: false, content: '', evidence: { path, detail: 'File is not readable text.' } }
134
+ }
135
+ }
136
+
137
+ const resultWithException = (
138
+ draft: RuleDraft,
139
+ options: DocumentationStandardV1Config,
140
+ ): DocumentationStandardRuleResult => {
141
+ if (draft.passed) {
142
+ const { passed: _passed, ...result } = draft
143
+ return { ...result, status: 'pass', ok: true }
144
+ }
145
+
146
+ const exception = options.exceptions?.find((candidate) => candidate.ruleId === draft.id)
147
+ const { passed: _passed, ...result } = draft
148
+ if (!exception) return { ...result, status: 'fail', ok: false }
149
+ return {
150
+ ...result,
151
+ status: 'excepted',
152
+ ok: true,
153
+ exception: {
154
+ reason: exception.reason,
155
+ approvedBy: exception.approvedBy,
156
+ trackingUrl: exception.trackingUrl,
157
+ },
158
+ }
159
+ }
160
+
161
+ const humanDocsRule = (root: string, config: DocBridgeConfigV1): RuleDraft => {
162
+ const docs = scanHumanDocRecords(root, config)
163
+ return {
164
+ id: 'human-docs',
165
+ level: 'required',
166
+ passed: docs.length > 0,
167
+ message: docs.length > 0 ? `Found ${docs.length} human document(s).` : 'No human documentation was discovered.',
168
+ evidence: docs.slice(0, 10).map((doc) => ({
169
+ path: toPosix(relative(resolve(root), doc.path)),
170
+ detail: `Human route: ${doc.url}`,
171
+ })),
172
+ remediation: {
173
+ command: 'edit doc-bridge.config.json',
174
+ detail: 'Configure corpus.human with a supported adapter and a non-agent documentation root.',
175
+ },
176
+ }
177
+ }
178
+
179
+ const llmsRule = (
180
+ root: string,
181
+ config: DocBridgeConfigV1,
182
+ options: DocumentationStandardV1Config,
183
+ ): RuleDraft => {
184
+ const llmsPath = config.index?.llmsTxt?.outFile ?? 'llms.txt'
185
+ const llmsKey = safePath(root, llmsPath) ?? resolve(root, llmsPath)
186
+ const rawSources = new Map<string, string>()
187
+ for (const path of options.rawSources ?? []) {
188
+ const key = safePath(root, path) ?? resolve(root, path)
189
+ if (key !== llmsKey && !rawSources.has(key)) rawSources.set(key, path)
190
+ }
191
+ const paths = [llmsPath, ...rawSources.values()]
192
+ const evidence = paths.map((path) => fileEvidence(root, path))
193
+ const generated = buildDocBridgeIndex({ root, config, write: false }).index
194
+ const expectedLlms = renderLlmsTxt(config, generated.knowledge, generated.project?.name ?? 'project')
195
+ const llmsIsFresh = evidence[0]?.content === expectedLlms
196
+ if (evidence[0]?.exists) {
197
+ evidence[0] = {
198
+ ...evidence[0],
199
+ evidence: {
200
+ ...evidence[0].evidence,
201
+ detail: llmsIsFresh
202
+ ? 'File matches the deterministic ak-docs output.'
203
+ : 'File is stale or was not generated by the current ak-docs inputs.',
204
+ },
205
+ }
206
+ }
207
+ const passed =
208
+ config.index?.llmsTxt?.enabled !== false &&
209
+ paths.length > 1 &&
210
+ llmsIsFresh &&
211
+ evidence.every((item) => item.exists)
212
+ return {
213
+ id: 'llms-and-raw-source',
214
+ level: 'required',
215
+ passed,
216
+ message: passed
217
+ ? `Resolved llms.txt and ${paths.length - 1} raw source(s).`
218
+ : 'llms.txt must be enabled, current, and accompanied by at least one readable raw source.',
219
+ evidence: evidence.map((item) => item.evidence),
220
+ remediation: {
221
+ command: 'ak-docs index',
222
+ detail: 'Generate llms.txt and configure conformance.documentationStandardV1.rawSources.',
223
+ },
224
+ }
225
+ }
226
+
227
+ const normalizedUrl = (value: string): string => value.replace(/\/$/, '')
228
+
229
+ const ecosystemContract = (
230
+ root: string,
231
+ options: DocumentationStandardV1Config,
232
+ ): {
233
+ readonly passed: boolean
234
+ readonly urls: ReadonlySet<string>
235
+ readonly evidence: readonly DocumentationStandardEvidence[]
236
+ } => {
237
+ const declaration = options.ecosystemContract
238
+ if (!declaration) {
239
+ return {
240
+ passed: false,
241
+ urls: new Set(),
242
+ evidence: [{ path: 'doc-bridge.config.json', detail: 'Canonical ecosystem contract evidence is not declared.' }],
243
+ }
244
+ }
245
+
246
+ const manifestFile = fileEvidence(root, declaration.manifest)
247
+ const claimsFile = fileEvidence(root, declaration.claims)
248
+ const evidence: DocumentationStandardEvidence[] = [manifestFile.evidence, claimsFile.evidence]
249
+ if (!manifestFile.exists || !claimsFile.exists) return { passed: false, urls: new Set(), evidence }
250
+
251
+ try {
252
+ const manifest: unknown = JSON.parse(manifestFile.content)
253
+ const claims: unknown = JSON.parse(claimsFile.content)
254
+ const contract = parseCanonicalEcosystemContract(manifest, claims)
255
+ const productIds = contract.manifest.products.map((product) => product.id)
256
+ if (!productIds.includes(declaration.productId)) throw new Error(`Manifest is missing product ${declaration.productId}.`)
257
+
258
+ const urls = new Set<string>()
259
+ for (const product of contract.manifest.products) {
260
+ for (const value of [
261
+ product.surfaces.home,
262
+ product.surfaces.docs,
263
+ product.surfaces.llms,
264
+ product.surfaces.stats,
265
+ ]) {
266
+ if (typeof value === 'string' && /^https:\/\//.test(value)) urls.add(normalizedUrl(value))
267
+ }
268
+ }
269
+ evidence[0] = { path: declaration.manifest, detail: `Validated ${productIds.length} canonical product(s), including ${declaration.productId}.` }
270
+ evidence[1] = { path: declaration.claims, detail: `Validated claim-ledger identity for ${contract.claims.products.length} product(s).` }
271
+ return { passed: urls.size > 0, urls, evidence }
272
+ } catch (error) {
273
+ evidence.push({
274
+ path: `${declaration.manifest}, ${declaration.claims}`,
275
+ detail: error instanceof Error ? error.message : 'Canonical ecosystem contract is invalid.',
276
+ })
277
+ return { passed: false, urls: new Set(), evidence }
278
+ }
279
+ }
280
+
281
+ const handoffsRule = (root: string, config: DocBridgeConfigV1): RuleDraft => {
282
+ const index = buildDocBridgeIndex({ root, config, write: false }).index
283
+ const handoffs = Object.values(index.handoffs ?? {})
284
+ const ready = handoffs.filter(
285
+ (handoff) =>
286
+ handoff.startHere.length > 0 &&
287
+ handoff.editRoots.length > 0 &&
288
+ handoff.checks.length > 0 &&
289
+ (handoff.bridge?.humanDoc === 'linked' || handoff.bridge?.humanDoc === 'external'),
290
+ )
291
+ return {
292
+ id: 'agent-handoffs',
293
+ level: 'required',
294
+ passed: ready.length > 0 && ready.length === handoffs.length,
295
+ message:
296
+ ready.length > 0 && ready.length === handoffs.length
297
+ ? `${ready.length} handoff(s) are action-ready and human-linked.`
298
+ : `${ready.length}/${handoffs.length} handoff(s) are action-ready and human-linked.`,
299
+ evidence: handoffs.map((handoff) => ({
300
+ path: handoff.startHere,
301
+ detail: `${handoff.target.id}: ${handoff.editRoots.length} edit root(s), ${handoff.checks.length} check(s), bridge=${handoff.bridge?.humanDoc ?? 'none'}`,
302
+ })),
303
+ remediation: {
304
+ command: 'ak-docs bootstrap agent-docs && ak-docs index',
305
+ detail: 'Add ownership checks and a resolvable humanDoc for every handoff.',
306
+ },
307
+ }
308
+ }
309
+
310
+ const contributionRule = (root: string, options: DocumentationStandardV1Config): RuleDraft => {
311
+ const paths = options.contributionPaths?.length ? options.contributionPaths : ['CONTRIBUTING.md']
312
+ const evidence = paths.map((path) => fileEvidence(root, path))
313
+ return {
314
+ id: 'contribution',
315
+ level: 'required',
316
+ passed: evidence.some((item) => item.exists),
317
+ message: evidence.some((item) => item.exists) ? 'Contribution guidance is available.' : 'Contribution guidance is missing.',
318
+ evidence: evidence.map((item) => item.evidence),
319
+ remediation: {
320
+ command: 'edit CONTRIBUTING.md',
321
+ detail: 'Document setup, validation commands, and the pull-request workflow.',
322
+ },
323
+ }
324
+ }
325
+
326
+ const markersRule = (
327
+ root: string,
328
+ options: DocumentationStandardV1Config,
329
+ kind: 'metadata' | 'structured-diagrams',
330
+ level: DocumentationStandardRuleLevel,
331
+ ): RuleDraft => {
332
+ const declarations = options[kind === 'metadata' ? 'metadata' : 'diagrams'] ?? []
333
+ const evidence: DocumentationStandardEvidence[] = []
334
+ let passed = declarations.length > 0
335
+ for (const declaration of declarations) {
336
+ const file = fileEvidence(root, declaration.path)
337
+ const missing = declaration.contains.filter((marker) => !file.content.includes(marker))
338
+ if (!file.exists || missing.length > 0) passed = false
339
+ evidence.push({
340
+ path: declaration.path,
341
+ detail: !file.exists
342
+ ? file.evidence.detail
343
+ : missing.length
344
+ ? `Missing marker(s): ${missing.join(', ')}`
345
+ : `Found marker(s): ${declaration.contains.join(', ')}`,
346
+ })
347
+ }
348
+ return {
349
+ id: kind,
350
+ level,
351
+ passed,
352
+ message: passed ? `${kind} evidence is complete.` : `${kind} evidence is incomplete.`,
353
+ evidence,
354
+ remediation: {
355
+ command: 'edit doc-bridge.config.json',
356
+ detail: `Declare ${kind} evidence paths and markers that exist in the repository.`,
357
+ },
358
+ }
359
+ }
360
+
361
+ const linksRule = (root: string, options: DocumentationStandardV1Config): RuleDraft => {
362
+ const links = options.links ?? []
363
+ const contract = ecosystemContract(root, options)
364
+ const evidence: DocumentationStandardEvidence[] = [...contract.evidence]
365
+ let passed = links.length > 0 && contract.passed
366
+ for (const link of links) {
367
+ const sources = link.paths.map((path) => ({ path, file: fileEvidence(root, path) }))
368
+ const matches = sources.filter(({ file }) => file.exists && file.content.includes(link.url))
369
+ const canonical = contract.urls.has(normalizedUrl(link.url))
370
+ if (matches.length === 0 || !canonical) passed = false
371
+ evidence.push({
372
+ path: sources.map((source) => source.path).join(', '),
373
+ detail:
374
+ matches.length === 0
375
+ ? `Missing ${link.url}`
376
+ : canonical
377
+ ? `Found canonical ecosystem URL ${link.url}`
378
+ : `Found ${link.url}, but it is absent from the canonical ecosystem manifest.`,
379
+ })
380
+ }
381
+ return {
382
+ id: 'cross-links',
383
+ level: 'required',
384
+ passed,
385
+ message: passed ? `${links.length} required ecosystem link(s) resolve in source.` : 'One or more required ecosystem links are missing from source.',
386
+ evidence,
387
+ remediation: {
388
+ command: 'edit README.md',
389
+ detail: 'Sync the canonical ecosystem snapshots and add each configured canonical URL to a declared documentation source.',
390
+ },
391
+ }
392
+ }
393
+
394
+ const quickstartsRule = (root: string, options: DocumentationStandardV1Config): RuleDraft => {
395
+ const quickstarts = options.quickstarts ?? []
396
+ const evidence: DocumentationStandardEvidence[] = []
397
+ let passed = quickstarts.length > 0
398
+ for (const quickstart of quickstarts) {
399
+ const doc = fileEvidence(root, quickstart.doc)
400
+ const test = fileEvidence(root, quickstart.test)
401
+ const missingMarkers = quickstart.testContains.filter((marker) => !test.content.includes(marker))
402
+ if (!doc.exists || !test.exists || missingMarkers.length > 0 || !quickstart.command.trim()) passed = false
403
+ evidence.push(
404
+ { path: quickstart.doc, detail: `${quickstart.id}: ${doc.evidence.detail}` },
405
+ {
406
+ path: quickstart.test,
407
+ detail: missingMarkers.length
408
+ ? `${quickstart.id}: missing test marker(s): ${missingMarkers.join(', ')}`
409
+ : `${quickstart.id}: test evidence; CI command: ${quickstart.command}`,
410
+ },
411
+ )
412
+ }
413
+ return {
414
+ id: 'tested-quickstarts',
415
+ level: 'required',
416
+ passed,
417
+ message: passed ? `${quickstarts.length} quickstart(s) have executable test evidence.` : 'Quickstart test evidence is incomplete.',
418
+ evidence,
419
+ remediation: {
420
+ command: 'pnpm test',
421
+ detail: 'Map every quickstart to a documentation path, test file, identifying marker, and CI command.',
422
+ },
423
+ }
424
+ }
425
+
426
+ const visualsRule = (root: string, options: DocumentationStandardV1Config): RuleDraft => {
427
+ const visuals = options.visuals ?? []
428
+ const evidence = visuals.map((path) => fileEvidence(root, path, { readContent: false }))
429
+ return {
430
+ id: 'visual-explanations',
431
+ level: 'recommended',
432
+ passed: visuals.length > 0 && evidence.every((item) => item.exists),
433
+ message: visuals.length > 0 && evidence.every((item) => item.exists) ? `${visuals.length} visual asset(s) found.` : 'Visual explanation evidence is incomplete.',
434
+ evidence: evidence.map((item) => item.evidence),
435
+ remediation: {
436
+ command: 'edit doc-bridge.config.json',
437
+ detail: 'Declare the images or animations that explain the product workflow.',
438
+ },
439
+ }
440
+ }
441
+
442
+ const count = (
443
+ results: readonly DocumentationStandardRuleResult[],
444
+ level: DocumentationStandardRuleLevel,
445
+ status: DocumentationStandardRuleStatus,
446
+ ): number => results.filter((result) => result.level === level && result.status === status).length
447
+
448
+ export const runDocumentationStandardV1 = (
449
+ root: string,
450
+ config: DocBridgeConfigV1,
451
+ ): DocumentationConformanceReportV1 => {
452
+ const options = config.conformance?.documentationStandardV1 ?? {}
453
+ const drafts: RuleDraft[] = [
454
+ humanDocsRule(root, config),
455
+ llmsRule(root, config, options),
456
+ handoffsRule(root, config),
457
+ contributionRule(root, options),
458
+ markersRule(root, options, 'metadata', 'required'),
459
+ linksRule(root, options),
460
+ quickstartsRule(root, options),
461
+ visualsRule(root, options),
462
+ markersRule(root, options, 'structured-diagrams', 'recommended'),
463
+ ]
464
+ const results = drafts.map((draft) => resultWithException(draft, options))
465
+ return {
466
+ schemaVersion: 1,
467
+ profile: { id: DOCUMENTATION_STANDARD_V1_ID, version: 1, status: DOCUMENTATION_STANDARD_V1_STATUS },
468
+ ok: results.filter((result) => result.level === 'required').every((result) => result.ok),
469
+ recommendedOk: results.filter((result) => result.level === 'recommended').every((result) => result.ok),
470
+ summary: {
471
+ required: {
472
+ passed: count(results, 'required', 'pass'),
473
+ failed: count(results, 'required', 'fail'),
474
+ excepted: count(results, 'required', 'excepted'),
475
+ },
476
+ recommended: {
477
+ passed: count(results, 'recommended', 'pass'),
478
+ failed: count(results, 'recommended', 'fail'),
479
+ excepted: count(results, 'recommended', 'excepted'),
480
+ },
481
+ },
482
+ results,
483
+ }
484
+ }
485
+
486
+ export const formatDocumentationStandardText = (
487
+ report: DocumentationConformanceReportV1,
488
+ ): string[] => [
489
+ `Documentation Standard v1 (${report.profile.status})`,
490
+ `Required: ${report.summary.required.passed} passed · ${report.summary.required.failed} failed · ${report.summary.required.excepted} excepted`,
491
+ `Recommended: ${report.summary.recommended.passed} passed · ${report.summary.recommended.failed} failed · ${report.summary.recommended.excepted} excepted`,
492
+ '',
493
+ ...report.results.flatMap((result) => [
494
+ `${result.status === 'pass' ? 'PASS' : result.status === 'excepted' ? 'EXCEPTED' : 'FAIL'} [${result.level}] ${result.id}: ${result.message}`,
495
+ ...result.evidence.map((evidence) => ` evidence: ${evidence.path} — ${evidence.detail}`),
496
+ ...(result.exception
497
+ ? [` exception: ${result.exception.reason} — ${result.exception.approvedBy} (${result.exception.trackingUrl})`]
498
+ : result.ok
499
+ ? []
500
+ : [` fix: ${result.remediation.command} — ${result.remediation.detail}`]),
501
+ ]),
502
+ ]