@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.
- package/CHANGELOG.md +19 -0
- package/action.yml +1 -1
- package/dist/cli/program.js +2461 -274
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +77 -4
- package/dist/config/index.js.map +1 -1
- package/dist/{index-DAeq_OIi.d.ts → index-Di7PkJuf.d.ts} +209 -8
- package/dist/index.d.ts +1942 -4
- package/dist/index.js +2190 -193
- package/dist/index.js.map +1 -1
- package/docs/PRD-doc-bridge-knowledge-engine.md +338 -0
- package/docs/knowledge-engine-runbook.md +44 -0
- package/ecosystem-claims.json +19 -19
- package/ecosystem-upstream.json +2 -2
- package/ecosystem.json +117 -109
- package/mcpb/manifest.json +25 -1
- package/package.json +2 -2
- package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
- package/src/agents/registry-adapter.ts +97 -0
- package/src/cli/program.ts +285 -3
- package/src/config/defaults.ts +14 -1
- package/src/config/schema.ts +69 -0
- package/src/conformance/ecosystem-contract.ts +6 -3
- package/src/discovery/documentation.ts +320 -0
- package/src/discovery/repository.ts +514 -0
- package/src/fixes/proposals.ts +165 -0
- package/src/index-builder/content-hash.ts +9 -2
- package/src/index-builder/human-adapters/plain-markdown.ts +17 -1
- package/src/index-builder/llms-txt.ts +22 -2
- package/src/index-builder/scan-corpus.ts +17 -1
- package/src/index.ts +99 -2
- package/src/mcp/server.ts +178 -8
- package/src/reconciliation/reconcile.ts +227 -0
- package/src/report/html.ts +74 -0
- package/src/rules/engine.ts +180 -0
- package/src/safety/repository.ts +84 -0
- package/src/schemas/knowledge.ts +315 -0
- package/src/validate.ts +24 -0
- package/src/version.ts +1 -1
- package/src/workflow/engine.ts +238 -0
- package/tsup.config.ts +2 -1
package/src/cli/program.ts
CHANGED
|
@@ -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]
|
package/src/config/defaults.ts
CHANGED
|
@@ -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: [
|
|
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
|
},
|
package/src/config/schema.ts
CHANGED
|
@@ -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.`)
|