@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/action.yml +1 -1
  3. package/dist/cli/program.js +2461 -274
  4. package/dist/cli/program.js.map +1 -1
  5. package/dist/config/index.d.ts +1 -1
  6. package/dist/config/index.js +77 -4
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/{index-DAeq_OIi.d.ts → index-Di7PkJuf.d.ts} +209 -8
  9. package/dist/index.d.ts +1942 -4
  10. package/dist/index.js +2190 -193
  11. package/dist/index.js.map +1 -1
  12. package/docs/PRD-doc-bridge-knowledge-engine.md +338 -0
  13. package/docs/knowledge-engine-runbook.md +44 -0
  14. package/ecosystem-claims.json +19 -19
  15. package/ecosystem-upstream.json +2 -2
  16. package/ecosystem.json +117 -109
  17. package/mcpb/manifest.json +25 -1
  18. package/package.json +2 -2
  19. package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
  20. package/src/agents/registry-adapter.ts +97 -0
  21. package/src/cli/program.ts +285 -3
  22. package/src/config/defaults.ts +14 -1
  23. package/src/config/schema.ts +69 -0
  24. package/src/conformance/ecosystem-contract.ts +6 -3
  25. package/src/discovery/documentation.ts +320 -0
  26. package/src/discovery/repository.ts +514 -0
  27. package/src/fixes/proposals.ts +165 -0
  28. package/src/index-builder/content-hash.ts +9 -2
  29. package/src/index-builder/human-adapters/plain-markdown.ts +17 -1
  30. package/src/index-builder/llms-txt.ts +22 -2
  31. package/src/index-builder/scan-corpus.ts +17 -1
  32. package/src/index.ts +99 -2
  33. package/src/mcp/server.ts +178 -8
  34. package/src/reconciliation/reconcile.ts +227 -0
  35. package/src/report/html.ts +74 -0
  36. package/src/rules/engine.ts +180 -0
  37. package/src/safety/repository.ts +84 -0
  38. package/src/schemas/knowledge.ts +315 -0
  39. package/src/validate.ts +24 -0
  40. package/src/version.ts +1 -1
  41. package/src/workflow/engine.ts +238 -0
  42. package/tsup.config.ts +2 -1
@@ -2,18 +2,21 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
2
  import { dirname, resolve } from 'node:path'
3
3
  import { createInterface } from 'node:readline/promises'
4
4
 
5
- import { loadConfig, projectRootFromConfigPath } from '../config/load-config.js'
6
- import type { DocBridgeConfigV1 } from '../config/schema.js'
5
+ import { ConfigNotFoundError, loadConfig, projectRootFromConfigPath } from '../config/load-config.js'
6
+ import type { DocBridgeConfigV1, RuleId, RuleSeverity } from '../config/schema.js'
7
7
  import {
8
8
  DOCUMENTATION_STANDARD_V1_ID,
9
9
  formatDocumentationStandardText,
10
10
  runDocumentationStandardV1,
11
11
  } from '../conformance/documentation-standard-v1.js'
12
12
  import { buildDocBridgeIndex } from '../index-builder/build-index.js'
13
+ import { applyDocumentationDeclarations } from '../discovery/documentation.js'
14
+ import { discoverRepository } from '../discovery/repository.js'
13
15
  import { discoverPnpmPackages } from '../index-builder/plugins/pnpm-monorepo.js'
14
16
  import { scanHumanDocRecords } from '../index-builder/human-adapters/index.js'
15
17
  import { retrieveHybridChunks } from '../federation/llms.js'
16
18
  import { runGates, type GateId } from '../gates/run-gates.js'
19
+ import { evaluateRules, parseRuleId, parseRuleSeverity, type RuleMode } from '../rules/engine.js'
17
20
  import { runChatOnce, startInkChat } from '../intelligence/chat.js'
18
21
  import { PeerMissingError, layer1InstallHint } from '../intelligence/peers.js'
19
22
  import { createDocBridgeRag } from '../intelligence/rag.js'
@@ -22,6 +25,7 @@ import { ingestMemoryCandidates } from '../memory/ingest.js'
22
25
  import { classifyMemoryCandidates, draftMemoryPromotion } from '../memory/pipeline.js'
23
26
  import { promoteMemoryToGithubPr } from '../memory/github-pr.js'
24
27
  import { watchDocBridgeIndex } from '../index-builder/watch-index.js'
28
+ import { loadWorkflowManifest, loadWorkflowStepOutput, runWorkflow, type WorkflowExecutionResult } from '../workflow/engine.js'
25
29
  import {
26
30
  formatDoctorBadgeJson,
27
31
  formatDoctorBadgeMarkdown,
@@ -35,7 +39,14 @@ import { IndexNotFoundError, loadDocBridgeIndex } from '../query/load-index.js'
35
39
  import { runQuery, type QueryKind } from '../query/query.js'
36
40
  import { searchIndex } from '../query/search.js'
37
41
  import type { DocBridgeIndexV1 } from '../schemas/doc-bridge-index.js'
38
- import { parseAgentHandoff, parseDocBridgeConfig } from '../validate.js'
42
+ import { parseAgentHandoff, parseDocBridgeConfig, parseReconciliationReport } from '../validate.js'
43
+ import { parseDiscoverySnapshot } from '../validate.js'
44
+ import { reconcileKnowledge } from '../reconciliation/reconcile.js'
45
+ import type { DiscoverySnapshotV1, ReconciliationReportV1 } from '../schemas/knowledge.js'
46
+ import { sha256NormalizedV1 } from '../index-builder/content-hash.js'
47
+ import { applyFixProposal, approveFixProposal, createArtifactNormalizationProposal, createMarkdownLinkFixProposal } from '../fixes/proposals.js'
48
+ import { createRegistryAgentAdapter, loadRegistryAgentRunner, persistRegistryAgentProposal } from '../agents/registry-adapter.js'
49
+ import { renderOfflineReport } from '../report/html.js'
39
50
  import { PACKAGE_VERSION } from '../version.js'
40
51
 
41
52
  type Command =
@@ -48,8 +59,16 @@ type Command =
48
59
  | 'memory'
49
60
  | 'playbook'
50
61
  | 'registry'
62
+ | 'discover'
63
+ | 'scan'
64
+ | 'reconcile'
65
+ | 'check'
66
+ | 'map'
67
+ | 'fix'
68
+ | 'suggest'
51
69
  | 'index'
52
70
  | 'gate'
71
+ | 'rules'
53
72
  | 'mcp'
54
73
  | 'doctor'
55
74
  | 'demo'
@@ -69,11 +88,17 @@ Core (no API key):
69
88
  ak-docs demo [--fixture example|monorepo] [--text] [--in-project]
70
89
  ak-docs doctor [--text] [--badge] [--write-badge]
71
90
  ak-docs index [--watch]
91
+ ak-docs discover [--text|--json]
92
+ ak-docs scan | reconcile | check | map [--text|--json] [--html]
93
+ ak-docs fix propose links|normalize <artifact> [--output <file>]
94
+ ak-docs fix approve|apply <proposal.json> [--by <name>]
95
+ ak-docs suggest [--json|--text] run the configured local Registry agent
72
96
  ak-docs query [package|ownership|intent|change] <id> [--agent] [--text]
73
97
  ak-docs search <term> [--agent] [--text]
74
98
  ak-docs list <packages|intents|changes|knowledge> [--text]
75
99
  ak-docs ask [question] local consult (no LLM)
76
100
  ak-docs gate run [gate-id]
101
+ ak-docs rules run <report.json> [--preset default|recommended|strict] [--severity rule=level] [--ignore rule]
77
102
  ak-docs conformance run documentation-standard-v1 [--text|--json]
78
103
  ak-docs mcp
79
104
  ak-docs mcp install --cursor | --claude
@@ -137,8 +162,16 @@ const parseArgs = (argv: readonly string[]) => {
137
162
  else if (positional[0] === 'memory') command = 'memory'
138
163
  else if (positional[0] === 'playbook') command = 'playbook'
139
164
  else if (positional[0] === 'registry') command = 'registry'
165
+ else if (positional[0] === 'discover') command = 'discover'
166
+ else if (positional[0] === 'scan') command = 'scan'
167
+ else if (positional[0] === 'reconcile') command = 'reconcile'
168
+ else if (positional[0] === 'check') command = 'check'
169
+ else if (positional[0] === 'map') command = 'map'
170
+ else if (positional[0] === 'fix') command = 'fix'
171
+ else if (positional[0] === 'suggest') command = 'suggest'
140
172
  else if (positional[0] === 'index') command = 'index'
141
173
  else if (positional[0] === 'gate') command = 'gate'
174
+ else if (positional[0] === 'rules') command = 'rules'
142
175
  else if (positional[0] === 'mcp') command = 'mcp'
143
176
  else if (positional[0] === 'doctor') command = 'doctor'
144
177
  else if (positional[0] === 'demo') command = 'demo'
@@ -154,6 +187,30 @@ const parseArgs = (argv: readonly string[]) => {
154
187
  return { command, flags, configPath, positional }
155
188
  }
156
189
 
190
+ const optionValues = (argv: readonly string[], name: string): string[] => {
191
+ const values: string[] = []
192
+ for (let index = 0; index < argv.length; index += 1) {
193
+ const arg = argv[index]
194
+ if (arg?.startsWith(`${name}=`)) values.push(arg.slice(name.length + 1))
195
+ else if (arg === name && argv[index + 1] && !argv[index + 1]?.startsWith('-')) {
196
+ values.push(argv[index + 1] as string)
197
+ index += 1
198
+ }
199
+ }
200
+ return values
201
+ }
202
+
203
+ const parseRuleAssignments = <T>(values: readonly string[], parseValue: (value: string) => T): Record<string, T> => {
204
+ const result: Record<string, T> = {}
205
+ for (const value of values) {
206
+ const separator = value.indexOf('=')
207
+ if (separator <= 0 || separator === value.length - 1) throw new Error(`Invalid rule assignment "${value}". Use rule=value.`)
208
+ const key = value.slice(0, separator)
209
+ result[key] = parseValue(value.slice(separator + 1))
210
+ }
211
+ return result
212
+ }
213
+
157
214
  const writeJson = (payload: unknown): void => {
158
215
  process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`)
159
216
  }
@@ -400,6 +457,198 @@ const diagnosticNextCommands = (
400
457
  ]
401
458
  }
402
459
 
460
+ const workflowOptions = (
461
+ root: string,
462
+ config: DocBridgeConfigV1,
463
+ sourceRevision: string,
464
+ stage: 'collect' | 'normalize' | 'reconcile' | 'evaluate' | 'report',
465
+ handlers: Parameters<typeof runWorkflow>[0]['handlers'],
466
+ ): Parameters<typeof runWorkflow>[0] => ({
467
+ root,
468
+ ...(config.workflow?.stateDir ? { stateDir: config.workflow.stateDir } : {}),
469
+ sourceRevision,
470
+ configurationHash: sha256NormalizedV1(config),
471
+ stage,
472
+ handlers,
473
+ })
474
+
475
+ const scanWorkflow = (root: string, config: DocBridgeConfigV1): WorkflowExecutionResult => {
476
+ const discovered = discoverRepository({ root, config })
477
+ runWorkflow(workflowOptions(root, config, discovered.sourceRevision, 'collect', { collect: () => discovered }))
478
+ return runWorkflow(workflowOptions(root, config, discovered.sourceRevision, 'normalize', { normalize: ({ input }) => input }))
479
+ }
480
+
481
+ const documentationInputs = (root: string, snapshot: DiscoverySnapshotV1) => snapshot.entities
482
+ .filter((entity) => entity.kind === 'document' && entity.path)
483
+ .map((entity) => ({ path: entity.path as string, content: readFileSync(resolve(root, entity.path as string), 'utf8') }))
484
+
485
+ const reconcileWorkflow = (root: string, config: DocBridgeConfigV1): WorkflowExecutionResult => {
486
+ const scanned = scanWorkflow(root, config)
487
+ const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(scanned.stateDir, 'normalize'))
488
+ const declared = applyDocumentationDeclarations(snapshot, documentationInputs(root, snapshot)).snapshot
489
+ const report = reconcileKnowledge(snapshot, declared)
490
+ return runWorkflow(workflowOptions(root, config, snapshot.sourceRevision, 'reconcile', { reconcile: () => report }))
491
+ }
492
+
493
+ const checkWorkflow = (root: string, config: DocBridgeConfigV1): WorkflowExecutionResult => {
494
+ const reconciled = reconcileWorkflow(root, config)
495
+ const report = parseReconciliationReport(loadWorkflowStepOutput(reconciled.stateDir, 'reconcile'))
496
+ const evaluated = runWorkflow(workflowOptions(root, config, report.sourceRevision, 'evaluate', { evaluate: () => evaluateRules(report, { ...(config.rules ? { config: config.rules } : {}) }) }))
497
+ return runWorkflow(workflowOptions(root, config, report.sourceRevision, 'report', { report: ({ input }) => input }))
498
+ }
499
+
500
+ const workflowOutput = (result: WorkflowExecutionResult): Record<string, unknown> => {
501
+ const snapshot = (() => { try { return parseDiscoverySnapshot(loadWorkflowStepOutput(result.stateDir, 'normalize')) } catch { return undefined } })()
502
+ const report = (() => { try { return parseReconciliationReport(loadWorkflowStepOutput(result.stateDir, 'reconcile')) } catch { return undefined } })()
503
+ const rules = (() => {
504
+ try {
505
+ const value = loadWorkflowStepOutput(result.stateDir, 'evaluate')
506
+ return value && typeof value === 'object' ? value : undefined
507
+ } catch { return undefined }
508
+ })()
509
+ return {
510
+ ok: result.run.state !== 'failed' && result.run.state !== 'stale',
511
+ runId: result.run.runId,
512
+ state: result.run.state,
513
+ stateDir: result.stateDir,
514
+ artifactRefs: result.run.artifactRefs,
515
+ steps: result.run.steps,
516
+ reusedStages: result.reusedStages,
517
+ ...(snapshot ? { snapshotHash: snapshot.contentHash, sourceRevision: snapshot.sourceRevision, configurationHash: snapshot.configurationHash, coverage: snapshot.coverage } : {}),
518
+ ...(report ? { reportHash: report.contentHash, diagnostics: report.diagnostics } : {}),
519
+ ...(rules ? { rules } : {}),
520
+ }
521
+ }
522
+
523
+ const runWorkflowCommand = (
524
+ command: 'scan' | 'reconcile' | 'check' | 'map',
525
+ flags: ReadonlySet<string>,
526
+ configPath: string | undefined,
527
+ argv: readonly string[],
528
+ ): number => {
529
+ try {
530
+ const { config, root } = loadProject(configPath)
531
+ const result = command === 'scan' ? scanWorkflow(root, config) : command === 'reconcile' ? reconcileWorkflow(root, config) : checkWorkflow(root, config)
532
+ const output = workflowOutput(result)
533
+ if (command === 'map') output.kind = 'architecture-map'
534
+ if (command === 'map' && flags.has('--html')) {
535
+ const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(result.stateDir, 'normalize'))
536
+ const report = parseReconciliationReport(loadWorkflowStepOutput(result.stateDir, 'reconcile'))
537
+ const outputPath = optionValues(argv, '--output')[0] ?? '.doc-bridge/report.html'
538
+ const htmlPath = resolve(root, outputPath)
539
+ mkdirSync(dirname(htmlPath), { recursive: true })
540
+ writeFileSync(htmlPath, renderOfflineReport({ snapshot, report }), 'utf8')
541
+ output.htmlPath = htmlPath
542
+ }
543
+ if (wantsTextOutput(flags, config)) {
544
+ writeLines([`Run: ${String(output.runId)}`, `State: ${String(output.state)}`, ...(output.snapshotHash ? [`Snapshot: ${String(output.snapshotHash)}`] : []), ...(output.reportHash ? [`Report: ${String(output.reportHash)}`] : []), `Artifacts: ${String((output.artifactRefs as unknown[]).length)}`])
545
+ } else writeJson(output)
546
+ const ruleExitCode = output.rules && typeof output.rules === 'object' && 'exitCode' in output.rules && (output.rules as { exitCode?: unknown }).exitCode === 1 ? 1 : 0
547
+ return result.run.state === 'failed' ? 1 : ruleExitCode
548
+ } catch (error) {
549
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
550
+ return 2
551
+ }
552
+ }
553
+
554
+ const runRulesCommand = (
555
+ argv: readonly string[],
556
+ flags: ReadonlySet<string>,
557
+ positional: readonly string[],
558
+ configPath: string | undefined,
559
+ ): number => {
560
+ const action = positional[1]
561
+ const reportPath = positional[2]
562
+ if (action !== 'run' || !reportPath) {
563
+ process.stderr.write('Usage: ak-docs rules run <report.json> [--preset default|recommended|strict] [--severity rule=level] [--ignore rule]\n')
564
+ return 2
565
+ }
566
+
567
+ try {
568
+ const { config } = loadProject(configPath)
569
+ const report = parseReconciliationReport(JSON.parse(readFileSync(resolve(reportPath), 'utf8')) as unknown)
570
+ const presetValue = optionValues(argv, '--preset')[0]
571
+ const preset = presetValue === undefined
572
+ ? undefined
573
+ : (['default', 'recommended', 'strict'].includes(presetValue) ? presetValue as RuleMode : (() => { throw new Error(`Invalid rules preset "${presetValue}".`) })())
574
+ const severity: Partial<Record<RuleId, RuleSeverity>> = {}
575
+ for (const [rule, level] of Object.entries(parseRuleAssignments(optionValues(argv, '--severity'), parseRuleSeverity))) {
576
+ severity[parseRuleId(rule)] = level
577
+ }
578
+ const ignore = optionValues(argv, '--ignore').map(parseRuleId)
579
+ const result = evaluateRules(report, {
580
+ ...(config.rules ? { config: config.rules } : {}),
581
+ ...(preset ? { preset } : {}),
582
+ ...(Object.keys(severity).length ? { severity } : {}),
583
+ ...(ignore.length ? { ignore } : {}),
584
+ ...(optionValues(argv, '--critical-entity').length ? { criticalEntities: optionValues(argv, '--critical-entity') } : {}),
585
+ ...(optionValues(argv, '--critical-path').length ? { criticalPaths: optionValues(argv, '--critical-path') } : {}),
586
+ })
587
+ if (wantsTextOutput(flags, config)) {
588
+ writeLines([
589
+ `Rules: ${result.mode}`,
590
+ `Findings: ${result.findings.length}`,
591
+ ...(result.findings.length ? result.findings.map((finding) => ` [${finding.severity}] ${finding.code}: ${finding.message}`) : [' (none)']),
592
+ `Exit code: ${result.exitCode}`,
593
+ ])
594
+ } else {
595
+ writeJson({ ok: result.exitCode === 0, reportHash: report.contentHash, ...result })
596
+ }
597
+ return result.exitCode
598
+ } catch (error) {
599
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
600
+ return 2
601
+ }
602
+ }
603
+
604
+ const runFixCommand = (argv: readonly string[], positional: readonly string[], configPath: string | undefined): number => {
605
+ try {
606
+ const { config, root } = loadProject(configPath)
607
+ const action = positional[1]
608
+ const proposalPath = positional[2]
609
+ const sourceRevision = discoverRepository({ root, config }).sourceRevision
610
+ const fixOptions = { baseRevision: sourceRevision, configurationHash: sha256NormalizedV1(config), ...(config.project?.name ? { projectName: config.project.name } : {}) }
611
+ if (action === 'propose') {
612
+ const proposal = positional[2] === 'links'
613
+ ? createMarkdownLinkFixProposal(root, fixOptions)
614
+ : positional[2] === 'normalize' && positional[3] ? createArtifactNormalizationProposal(root, positional[3], fixOptions) : undefined
615
+ if (!proposal) { writeJson({ ok: true, proposal: null }); return 0 }
616
+ const outputPath = optionValues(argv, '--output')[0]
617
+ if (outputPath) { mkdirSync(dirname(resolve(root, outputPath)), { recursive: true }); writeFileSync(resolve(root, outputPath), `${JSON.stringify(proposal, null, 2)}\n`, 'utf8') }
618
+ writeJson({ ok: true, proposal, ...(outputPath ? { proposalPath: resolve(root, outputPath) } : {}) })
619
+ return 0
620
+ }
621
+ if (!proposalPath || !['approve', 'apply'].includes(action ?? '')) throw new Error('Usage: ak-docs fix propose links|normalize <artifact> [--output <file>] | fix approve|apply <proposal.json> [--by <name>]')
622
+ const file = resolve(root, proposalPath)
623
+ const proposal = JSON.parse(readFileSync(file, 'utf8')) as unknown
624
+ const result = action === 'approve' ? approveFixProposal(proposal, optionValues(argv, '--by')[0] ?? 'human') : applyFixProposal(root, proposal, { currentRevision: sourceRevision })
625
+ writeFileSync(file, `${JSON.stringify(result, null, 2)}\n`, 'utf8')
626
+ writeJson({ ok: true, proposal: result, proposalPath: file })
627
+ return 0
628
+ } catch (error) {
629
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
630
+ return 2
631
+ }
632
+ }
633
+
634
+ const runSuggestCommand = async (flags: ReadonlySet<string>, configPath: string | undefined): Promise<number> => {
635
+ try {
636
+ const { config, root } = loadProject(configPath)
637
+ const stateDir = resolve(root, config.workflow?.stateDir ?? '.doc-bridge/workflow')
638
+ const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(stateDir, 'normalize'))
639
+ const report = parseReconciliationReport(loadWorkflowStepOutput(stateDir, 'reconcile'))
640
+ const adapter = createRegistryAgentAdapter(root, config, await loadRegistryAgentRunner(root, config))
641
+ const proposal = await adapter.run(snapshot, report)
642
+ const proposalPath = persistRegistryAgentProposal(stateDir, proposal)
643
+ if (flags.has('--text')) writeLines([`Agent: ${adapter.metadata.id}`, `Proposal: ${proposal.proposalId}`, `Hash: ${proposal.contentHash}`, `Saved: ${proposalPath}`])
644
+ else writeJson({ ok: true, proposal, proposalPath })
645
+ return 0
646
+ } catch (error) {
647
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
648
+ return 2
649
+ }
650
+ }
651
+
403
652
  const writeIfMissing = (path: string, contents: string): boolean => {
404
653
  if (existsSync(path)) return false
405
654
  mkdirSync(dirname(path), { recursive: true })
@@ -617,6 +866,37 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
617
866
  }
618
867
  }
619
868
 
869
+ if (command === 'discover') {
870
+ try {
871
+ let config: DocBridgeConfigV1 | undefined
872
+ let root = process.cwd()
873
+ try {
874
+ const loaded = loadProject(configPath)
875
+ config = loaded.config
876
+ root = loaded.root
877
+ } catch (error) {
878
+ if (!(error instanceof ConfigNotFoundError) || configPath) throw error
879
+ }
880
+ const snapshot = discoverRepository({ root, ...(config ? { config } : {}) })
881
+ if (wantsTextOutput(flags, config ?? { surfaces: { cli: { defaultFormat: 'json' } } } as DocBridgeConfigV1)) {
882
+ writeLines([`Project: ${snapshot.project.name}`, `Entities: ${snapshot.entities.length}`, `Relations: ${snapshot.relations.length}`, `Source revision: ${snapshot.sourceRevision}`, ...(config ? [] : ['No configuration found; discovery used safe defaults.'])])
883
+ } else {
884
+ writeJson({ ok: true, snapshot, ...(config ? {} : { proposedConfig: { schemaVersion: 1, corpus: { agent: { root: 'docs/for-agents' } } } }) })
885
+ }
886
+ return 0
887
+ } catch (error) {
888
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
889
+ return 2
890
+ }
891
+ }
892
+
893
+ if (command === 'scan' || command === 'reconcile' || command === 'check' || command === 'map') {
894
+ return runWorkflowCommand(command, flags, configPath, argv)
895
+ }
896
+
897
+ if (command === 'fix') return runFixCommand(argv, positional, configPath)
898
+ if (command === 'suggest') return runSuggestCommand(flags, configPath)
899
+
620
900
  if (command === 'init') {
621
901
  const root = process.cwd()
622
902
  const withDemo = flags.has('--demo') || !flags.has('--no-demo')
@@ -816,6 +1096,8 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
816
1096
  }
817
1097
  }
818
1098
 
1099
+ if (command === 'rules') return runRulesCommand(argv, flags, positional, configPath)
1100
+
819
1101
  if (command === 'conformance') {
820
1102
  const action = positional[1]
821
1103
  const profile = positional[2]
@@ -36,6 +36,14 @@ export const applyConfigDefaults = (config: DocBridgeConfigV1): DocBridgeConfigV
36
36
  preset: 'minimal',
37
37
  ...config.gates,
38
38
  },
39
+ rules: {
40
+ mode: 'default',
41
+ ...config.rules,
42
+ },
43
+ safety: {
44
+ redactSecrets: true,
45
+ ...config.safety,
46
+ },
39
47
  surfaces: {
40
48
  cli: {
41
49
  bin: 'ak-docs',
@@ -44,7 +52,12 @@ export const applyConfigDefaults = (config: DocBridgeConfigV1): DocBridgeConfigV
44
52
  },
45
53
  mcp: {
46
54
  enabled: true,
47
- tools: ['handoff.resolve', 'doc.search', 'doc.get', 'gate.status'],
55
+ tools: [
56
+ 'handoff.resolve', 'doc.search', 'doc.get', 'gate.status', 'retriever.query',
57
+ 'memory.classify', 'memory.promoteDraft', 'registry.topology',
58
+ 'docbridge.snapshot', 'docbridge.report', 'docbridge.diagnostics',
59
+ 'docbridge.relations', 'docbridge.run', 'docbridge.proposals',
60
+ ],
48
61
  transport: 'stdio',
49
62
  ...config.surfaces?.mcp,
50
63
  },
@@ -44,6 +44,8 @@ export const IndexConfigSchema = z
44
44
  enabled: z.boolean().optional(),
45
45
  outFile: z.string().min(1).max(512).optional(),
46
46
  preamble: z.string().max(4_000).optional(),
47
+ urlPrefix: z.string().url().optional(),
48
+ pathPrefix: z.string().min(1).max(512).optional(),
47
49
  })
48
50
  .strict()
49
51
  .optional(),
@@ -150,6 +152,50 @@ export const GatesConfigSchema = z
150
152
  })
151
153
  .strict()
152
154
 
155
+ export const RuleIdSchema = z.enum([
156
+ 'documentation-quality',
157
+ 'graph-undocumented-relation',
158
+ 'declared-unobserved-relation',
159
+ 'unresolved-reference',
160
+ 'conflicting-declaration',
161
+ 'not-analyzed-coverage',
162
+ 'stale-documentation',
163
+ 'centrality-risk',
164
+ 'critical-path-risk',
165
+ 'freshness',
166
+ 'ownership',
167
+ ])
168
+
169
+ export const RuleSeveritySchema = z.enum(['off', 'info', 'warn', 'error'])
170
+
171
+ export const RulesConfigSchema = z
172
+ .object({
173
+ mode: z.enum(['default', 'recommended', 'strict']).optional(),
174
+ severity: z.record(RuleIdSchema, RuleSeveritySchema).optional(),
175
+ ignore: z.array(RuleIdSchema).max(128).optional(),
176
+ criticalEntities: z.array(z.string().min(1).max(256)).max(128).optional(),
177
+ criticalPaths: z.array(z.string().min(1).max(512)).max(128).optional(),
178
+ warningThresholds: z.record(RuleIdSchema, z.number().int().min(1).max(100_000)).optional(),
179
+ })
180
+ .strict()
181
+
182
+ export const WorkflowConfigSchema = z
183
+ .object({
184
+ stateDir: z.string().min(1).max(512).optional(),
185
+ })
186
+ .strict()
187
+
188
+ export const RepositorySafetyConfigSchema = z
189
+ .object({
190
+ exclude: z.array(z.string().min(1).max(512)).max(128).optional(),
191
+ maxFiles: z.number().int().positive().max(1_000_000).optional(),
192
+ maxBytes: z.number().int().positive().max(10_000_000_000).optional(),
193
+ maxTimeMs: z.number().int().positive().max(86_400_000).optional(),
194
+ maxMemoryMb: z.number().int().positive().max(1_048_576).optional(),
195
+ redactSecrets: z.boolean().optional(),
196
+ })
197
+ .strict()
198
+
153
199
  export const SurfacesConfigSchema = z
154
200
  .object({
155
201
  cli: z
@@ -174,6 +220,12 @@ export const SurfacesConfigSchema = z
174
220
  'memory.classify',
175
221
  'memory.promoteDraft',
176
222
  'registry.topology',
223
+ 'docbridge.snapshot',
224
+ 'docbridge.report',
225
+ 'docbridge.diagnostics',
226
+ 'docbridge.relations',
227
+ 'docbridge.run',
228
+ 'docbridge.proposals',
177
229
  ]),
178
230
  )
179
231
  .max(16)
@@ -245,6 +297,15 @@ export const IntelligenceConfigSchema = z
245
297
  .optional(),
246
298
  runtime: z.enum(['agentskit', 'custom']).optional(),
247
299
  runtimeModule: z.string().min(1).max(512).optional(),
300
+ registry: z
301
+ .object({
302
+ enabled: z.boolean().optional(),
303
+ agentId: z.string().min(1).max(256).optional(),
304
+ agentRoot: z.string().min(1).max(512).optional(),
305
+ runnerModule: z.string().min(1).max(512).optional(),
306
+ })
307
+ .strict()
308
+ .optional(),
248
309
  })
249
310
  .strict()
250
311
 
@@ -373,6 +434,9 @@ export const DocBridgeConfigV1Schema = z
373
434
  index: IndexConfigSchema.optional(),
374
435
  routing: RoutingConfigSchema.optional(),
375
436
  gates: GatesConfigSchema.optional(),
437
+ rules: RulesConfigSchema.optional(),
438
+ workflow: WorkflowConfigSchema.optional(),
439
+ safety: RepositorySafetyConfigSchema.optional(),
376
440
  surfaces: SurfacesConfigSchema.optional(),
377
441
  intelligence: IntelligenceConfigSchema.optional(),
378
442
  federation: FederationConfigSchema.optional(),
@@ -385,3 +449,8 @@ export type AgentCorpusConfig = z.infer<typeof AgentCorpusConfigSchema>
385
449
  export type HumanCorpusConfig = z.infer<typeof HumanCorpusConfigSchema>
386
450
  export type DocumentationStandardV1Config = z.infer<typeof DocumentationStandardV1ConfigSchema>
387
451
  export type DocumentationStandardRuleId = z.infer<typeof DocumentationStandardRuleIdSchema>
452
+ export type RuleId = z.infer<typeof RuleIdSchema>
453
+ export type RuleSeverity = z.infer<typeof RuleSeveritySchema>
454
+ export type RulesConfig = z.infer<typeof RulesConfigSchema>
455
+ export type WorkflowConfig = z.infer<typeof WorkflowConfigSchema>
456
+ export type RepositorySafetyConfig = z.infer<typeof RepositorySafetyConfigSchema>
@@ -22,7 +22,7 @@ const ProductSchema = z.object({
22
22
  role: NonEmptyStringSchema,
23
23
  promise: NonEmptyStringSchema,
24
24
  maturity: z.enum(['planning', 'alpha', 'beta', 'stable', 'deprecated']),
25
- repo: RepoSchema,
25
+ repo: RepoSchema.nullable(),
26
26
  accent: z.string().regex(/^#[0-9A-Fa-f]{6}$/),
27
27
  surfaces: SurfaceSchema,
28
28
  navigation: z.object({
@@ -38,7 +38,7 @@ const LegacyPropertySchema = z.object({
38
38
  barLabel: NonEmptyStringSchema,
39
39
  domain: NonEmptyStringSchema,
40
40
  url: HttpsUrlSchema,
41
- repo: RepoSchema,
41
+ repo: RepoSchema.nullable(),
42
42
  tagline: NonEmptyStringSchema,
43
43
  kind: NonEmptyStringSchema,
44
44
  accent: z.string().regex(/^#[0-9A-Fa-f]{6}$/),
@@ -82,6 +82,7 @@ const ClaimProductSchema = z.object({
82
82
  source: z.discriminatedUnion('type', [
83
83
  z.object({ type: z.literal('endpoint'), url: HttpsUrlSchema }).passthrough(),
84
84
  z.object({ type: z.literal('repository'), repo: RepoSchema }).passthrough(),
85
+ z.object({ type: z.literal('declaration'), summary: NonEmptyStringSchema }).passthrough(),
85
86
  ]),
86
87
  verification: z.enum(['verified', 'declared']),
87
88
  claims: z.array(ClaimSchema),
@@ -171,8 +172,10 @@ export const parseCanonicalEcosystemContract = (
171
172
  if (!claimProduct) throw new Error(`Claims are missing product ${productId}.`)
172
173
  if (claimProduct.source.type === 'endpoint') {
173
174
  if (claimProduct.source.url !== product.surfaces.stats) throw new Error(`Claims source for ${productId} must match stats.`)
174
- } else if (claimProduct.source.repo !== product.repo) {
175
+ } else if (claimProduct.source.type === 'repository' && claimProduct.source.repo !== product.repo) {
175
176
  throw new Error(`Claims source for ${productId} must match repo.`)
177
+ } else if (claimProduct.source.type === 'declaration' && product.repo !== null) {
178
+ throw new Error(`Declaration source for ${productId} requires a null repo.`)
176
179
  }
177
180
  if (claimProduct.verification === 'declared' && claimProduct.claims.length > 0) {
178
181
  throw new Error(`Declared product ${productId} cannot publish claims.`)