@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
@@ -0,0 +1,180 @@
1
+ import { minimatch } from 'minimatch'
2
+
3
+ import {
4
+ RuleIdSchema,
5
+ RuleSeveritySchema,
6
+ RulesConfigSchema,
7
+ type RuleId,
8
+ type RuleSeverity,
9
+ type RulesConfig,
10
+ } from '../config/schema.js'
11
+ import type { ReconciliationReportV1 } from '../schemas/knowledge.js'
12
+
13
+ export type RuleMode = 'default' | 'recommended' | 'strict'
14
+
15
+ export type RuleEngineOptions = {
16
+ readonly config?: RulesConfig
17
+ readonly preset?: RuleMode
18
+ readonly severity?: Partial<Record<RuleId, RuleSeverity>>
19
+ readonly ignore?: readonly RuleId[]
20
+ readonly criticalEntities?: readonly string[]
21
+ readonly criticalPaths?: readonly string[]
22
+ readonly warningThresholds?: Partial<Record<RuleId, number>>
23
+ }
24
+
25
+ export type RuleFinding = {
26
+ readonly id: string
27
+ readonly ruleId: RuleId
28
+ readonly code: string
29
+ readonly status: ReconciliationReportV1['diagnostics'][number]['status']
30
+ readonly severity: RuleSeverity
31
+ readonly message: string
32
+ readonly evidence: ReconciliationReportV1['diagnostics'][number]['evidence']
33
+ readonly entityIds?: readonly string[]
34
+ readonly relationIds?: readonly string[]
35
+ readonly remediation?: string
36
+ readonly sourceDiagnosticCode?: string
37
+ }
38
+
39
+ export type RuleEvaluationResult = {
40
+ readonly mode: RuleMode
41
+ readonly findings: readonly RuleFinding[]
42
+ readonly exitCode: 0 | 1
43
+ }
44
+
45
+ const diagnosticRules: Readonly<Record<string, RuleId>> = {
46
+ DOCUMENTATION_QUALITY: 'documentation-quality',
47
+ RELATION_UNDOCUMENTED: 'graph-undocumented-relation',
48
+ DECLARED_RELATION_STALE: 'declared-unobserved-relation',
49
+ UNRESOLVED_ENTITY_REFERENCE: 'unresolved-reference',
50
+ CONFLICTING_DECLARATIONS: 'conflicting-declaration',
51
+ RELATION_NOT_ANALYZED: 'not-analyzed-coverage',
52
+ STALE_DOCUMENTATION: 'stale-documentation',
53
+ FRESHNESS_FAILURE: 'freshness',
54
+ OWNERSHIP_GAP: 'ownership',
55
+ CENTRALITY_RISK: 'centrality-risk',
56
+ CRITICAL_PATH_RISK: 'critical-path-risk',
57
+ }
58
+
59
+ const defaultSeverity = (mode: RuleMode, ruleId: RuleId): RuleSeverity => {
60
+ if (mode === 'default') return 'info'
61
+ if (mode === 'strict') return ruleId === 'not-analyzed-coverage' ? 'warn' : 'error'
62
+ return ruleId === 'not-analyzed-coverage' ? 'info' : 'warn'
63
+ }
64
+
65
+ const resolvedOptions = (options: RuleEngineOptions): {
66
+ readonly mode: RuleMode
67
+ readonly severity: Partial<Record<RuleId, RuleSeverity>>
68
+ readonly ignore: ReadonlySet<RuleId>
69
+ readonly criticalEntities: readonly string[]
70
+ readonly criticalPaths: readonly string[]
71
+ readonly warningThresholds: Partial<Record<RuleId, number>>
72
+ } => {
73
+ const config = RulesConfigSchema.parse(options.config ?? {})
74
+ const mode = options.preset ?? config.mode ?? 'default'
75
+ const severity = { ...config.severity, ...options.severity }
76
+ const ignore = new Set<RuleId>([...(config.ignore ?? []), ...(options.ignore ?? [])])
77
+ return {
78
+ mode,
79
+ severity,
80
+ ignore,
81
+ criticalEntities: options.criticalEntities ?? config.criticalEntities ?? [],
82
+ criticalPaths: options.criticalPaths ?? config.criticalPaths ?? [],
83
+ warningThresholds: { ...config.warningThresholds, ...options.warningThresholds },
84
+ }
85
+ }
86
+
87
+ const severityFor = (
88
+ ruleId: RuleId,
89
+ mode: RuleMode,
90
+ overrides: Partial<Record<RuleId, RuleSeverity>>,
91
+ ): RuleSeverity => overrides[ruleId] ?? defaultSeverity(mode, ruleId)
92
+
93
+ const findingFromDiagnostic = (
94
+ diagnostic: ReconciliationReportV1['diagnostics'][number],
95
+ ruleId: RuleId,
96
+ severity: RuleSeverity,
97
+ ): RuleFinding => ({
98
+ id: `${diagnostic.id}:${ruleId}`,
99
+ ruleId,
100
+ code: ruleId,
101
+ status: diagnostic.status,
102
+ severity,
103
+ message: diagnostic.message,
104
+ evidence: diagnostic.evidence,
105
+ ...(diagnostic.entityIds ? { entityIds: diagnostic.entityIds } : {}),
106
+ ...(diagnostic.relationIds ? { relationIds: diagnostic.relationIds } : {}),
107
+ ...(diagnostic.remediation ? { remediation: diagnostic.remediation } : {}),
108
+ sourceDiagnosticCode: diagnostic.code,
109
+ })
110
+
111
+ const criticalFinding = (
112
+ finding: RuleFinding,
113
+ severity: RuleSeverity,
114
+ target: string,
115
+ ): RuleFinding => ({
116
+ ...finding,
117
+ id: `${finding.id}:critical:${target}`,
118
+ ruleId: 'critical-path-risk',
119
+ code: 'critical-path-risk',
120
+ severity,
121
+ message: `Critical path or entity is affected: ${target}. ${finding.message}`,
122
+ })
123
+
124
+ export const evaluateRules = (
125
+ report: ReconciliationReportV1,
126
+ options: RuleEngineOptions = {},
127
+ ): RuleEvaluationResult => {
128
+ const resolved = resolvedOptions(options)
129
+ const findings: RuleFinding[] = []
130
+
131
+ for (const diagnostic of [...report.diagnostics].sort((a, b) => a.id.localeCompare(b.id))) {
132
+ const ruleId = diagnosticRules[diagnostic.code]
133
+ if (!ruleId || resolved.ignore.has(ruleId)) continue
134
+ const severity = severityFor(ruleId, resolved.mode, resolved.severity)
135
+ if (severity !== 'off') findings.push(findingFromDiagnostic(diagnostic, ruleId, severity))
136
+ }
137
+
138
+ const criticalSeverity = severityFor('critical-path-risk', resolved.mode, resolved.severity)
139
+ const criticalEntitySet = new Set(resolved.criticalEntities)
140
+ for (const finding of [...findings]) {
141
+ const matchingEntity = (finding.entityIds ?? []).find((id) => criticalEntitySet.has(id))
142
+ if (matchingEntity && !resolved.ignore.has('critical-path-risk') && criticalSeverity !== 'off') {
143
+ findings.push(criticalFinding(finding, criticalSeverity, matchingEntity))
144
+ }
145
+ for (const path of resolved.criticalPaths) {
146
+ if (finding.evidence.some((item) => minimatch(item.path, path, { dot: true })) && !resolved.ignore.has('critical-path-risk') && criticalSeverity !== 'off') {
147
+ findings.push(criticalFinding(finding, criticalSeverity, path))
148
+ }
149
+ }
150
+ }
151
+
152
+ const centralityThreshold = resolved.warningThresholds['centrality-risk'] ?? 3
153
+ const centralitySeverity = severityFor('centrality-risk', resolved.mode, resolved.severity)
154
+ if (!resolved.ignore.has('centrality-risk') && centralitySeverity !== 'off') {
155
+ const counts = new Map<string, number>()
156
+ for (const finding of findings.filter((item) => item.ruleId === 'graph-undocumented-relation')) {
157
+ for (const entityId of finding.entityIds ?? []) counts.set(entityId, (counts.get(entityId) ?? 0) + 1)
158
+ }
159
+ for (const [entityId, count] of [...counts.entries()].sort(([a], [b]) => a.localeCompare(b))) {
160
+ if (count < centralityThreshold || !criticalEntitySet.has(entityId)) continue
161
+ findings.push({
162
+ id: `centrality-risk:${entityId}`,
163
+ ruleId: 'centrality-risk',
164
+ code: 'centrality-risk',
165
+ status: 'unresolved',
166
+ severity: centralitySeverity,
167
+ message: `Critical entity has ${count} undocumented relation finding(s); static centrality is a review signal, not a runtime availability claim.`,
168
+ evidence: findings.filter((item) => item.ruleId === 'graph-undocumented-relation' && item.entityIds?.includes(entityId)).flatMap((item) => item.evidence),
169
+ entityIds: [entityId],
170
+ remediation: 'Review ownership, dependency boundaries, and runtime availability before declaring an SPOF.',
171
+ })
172
+ }
173
+ }
174
+
175
+ const sortedFindings = [...findings].sort((a, b) => a.id.localeCompare(b.id))
176
+ return { mode: resolved.mode, findings: sortedFindings, exitCode: sortedFindings.some((finding) => finding.severity === 'error') ? 1 : 0 }
177
+ }
178
+
179
+ export const parseRuleId = (value: string): RuleId => RuleIdSchema.parse(value)
180
+ export const parseRuleSeverity = (value: string): RuleSeverity => RuleSeveritySchema.parse(value)
@@ -0,0 +1,84 @@
1
+ import { lstatSync, readdirSync, realpathSync, statSync } from 'node:fs'
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path'
3
+
4
+ import { minimatch } from 'minimatch'
5
+
6
+ export const DEFAULT_SAFETY_EXCLUDES = ['**/.git/**', '**/node_modules/**', '**/dist/**', '**/build/**', '**/coverage/**', '**/.doc-bridge/**', '**/.env', '**/.env.*', '**/*secret*', '**/*credential*', '**/*.pem', '**/*.key'] as const
7
+
8
+ export type SafeWalkOptions = {
9
+ readonly extensions?: readonly string[]
10
+ readonly exclude?: readonly string[]
11
+ readonly maxFiles?: number
12
+ readonly maxBytes?: number
13
+ readonly maxTimeMs?: number
14
+ readonly maxMemoryMb?: number
15
+ }
16
+
17
+ export type SafeWalkResult = {
18
+ readonly files: readonly string[]
19
+ readonly incomplete: boolean
20
+ readonly reason?: string
21
+ }
22
+
23
+ export const containedPath = (root: string, candidate: string): string | undefined => {
24
+ const projectRoot = realpathSync.native(resolve(root))
25
+ const unresolved = resolve(projectRoot, candidate)
26
+ const unresolvedRelative = relative(projectRoot, unresolved)
27
+ if (isAbsolute(unresolvedRelative) || unresolvedRelative === '..' || unresolvedRelative.startsWith(`..${sep}`)) return undefined
28
+ try {
29
+ const canonical = realpathSync.native(unresolved)
30
+ const canonicalRelative = relative(projectRoot, canonical)
31
+ return isAbsolute(canonicalRelative) || canonicalRelative === '..' || canonicalRelative.startsWith(`..${sep}`) ? undefined : canonical
32
+ } catch {
33
+ return unresolved
34
+ }
35
+ }
36
+
37
+ export const safeWalkFiles = (root: string, options: SafeWalkOptions = {}): SafeWalkResult => {
38
+ const projectRoot = resolve(root)
39
+ const extensions = options.extensions ?? []
40
+ const excludes = options.exclude ?? DEFAULT_SAFETY_EXCLUDES
41
+ const files: string[] = []
42
+ let bytes = 0
43
+ let reason: string | undefined
44
+ const started = Date.now()
45
+ const matchesExclude = (path: string): boolean => excludes.some((pattern) => minimatch(path, pattern, { dot: true }))
46
+ const visit = (directory: string): void => {
47
+ if (reason) return
48
+ if (options.maxTimeMs !== undefined && Date.now() - started >= options.maxTimeMs) { reason = `Repository scan exceeded the ${options.maxTimeMs} ms time limit.`; return }
49
+ if (options.maxMemoryMb !== undefined && process.memoryUsage().heapUsed > options.maxMemoryMb * 1024 * 1024) { reason = `Repository scan exceeded the ${options.maxMemoryMb} MiB memory limit.`; return }
50
+ let entries: string[]
51
+ try { entries = readdirSync(directory) } catch { return }
52
+ for (const name of entries.sort()) {
53
+ const absolute = resolve(directory, name)
54
+ const relativePath = relative(projectRoot, absolute).split(sep).join('/')
55
+ if (matchesExclude(relativePath) || name === '.git') continue
56
+ let stats
57
+ try { stats = lstatSync(absolute) } catch { continue }
58
+ if (stats.isSymbolicLink()) continue
59
+ if (stats.isDirectory()) { visit(absolute); if (reason) return; continue }
60
+ if (!stats.isFile() || (extensions.length > 0 && !extensions.some((extension) => name.endsWith(extension)))) continue
61
+ if (files.length >= (options.maxFiles ?? 10_000)) { reason = `Repository scan exceeded the ${options.maxFiles ?? 10_000} file limit.`; return }
62
+ bytes += statSync(absolute).size
63
+ if (options.maxBytes !== undefined && bytes > options.maxBytes) { reason = `Repository scan exceeded the ${options.maxBytes} byte limit.`; return }
64
+ files.push(absolute)
65
+ }
66
+ }
67
+ visit(projectRoot)
68
+ return { files: files.sort(), incomplete: reason !== undefined, ...(reason ? { reason } : {}) }
69
+ }
70
+
71
+ const SECRET_PATTERNS = [
72
+ /\b(?:sk|pk)[_-](?:live|test)[_-][A-Za-z0-9_-]{12,}\b/g,
73
+ /\b(?:ghp|github_pat|xox[baprs])_[A-Za-z0-9_-]{12,}\b/g,
74
+ /\bAKIA[0-9A-Z]{16}\b/g,
75
+ /(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*["']?[^\s,"']+/gi,
76
+ ]
77
+
78
+ export const redactSecrets = (value: string): string => SECRET_PATTERNS.reduce((result, pattern) => result.replace(pattern, '[REDACTED]'), value)
79
+
80
+ export const redactValue = (value: unknown): unknown => Array.isArray(value)
81
+ ? value.map(redactValue)
82
+ : value && typeof value === 'object'
83
+ ? Object.fromEntries(Object.entries(value as Record<string, unknown>).map(([key, item]) => [key, redactValue(item)]))
84
+ : typeof value === 'string' ? redactSecrets(value) : value
@@ -0,0 +1,315 @@
1
+ import { z } from 'zod'
2
+
3
+ export const KNOWLEDGE_SCHEMA_VERSION = 1 as const
4
+ export const KNOWLEDGE_CONTENT_HASH_ALGO = 'sha256-normalized-v1' as const
5
+
6
+ const hash = z.string().regex(/^[a-f0-9]{64}$/)
7
+ const boundedString = (max: number) => z.string().min(1).max(max)
8
+
9
+ export const ProvenanceSchema = z.enum(['observed', 'declared', 'proposed'])
10
+ export type Provenance = z.infer<typeof ProvenanceSchema>
11
+
12
+ export const FindingStatusSchema = z.enum([
13
+ 'confirmed',
14
+ 'undocumented',
15
+ 'stale-or-unverified',
16
+ 'conflict',
17
+ 'unresolved',
18
+ 'not-analyzed',
19
+ ])
20
+ export type FindingStatus = z.infer<typeof FindingStatusSchema>
21
+
22
+ export const DiagnosticSeveritySchema = z.enum(['off', 'info', 'warn', 'error'])
23
+ export type DiagnosticSeverity = z.infer<typeof DiagnosticSeveritySchema>
24
+
25
+ export const EvidenceSourceSchema = z.enum([
26
+ 'code',
27
+ 'configuration',
28
+ 'documentation',
29
+ 'agent',
30
+ 'derived',
31
+ ])
32
+
33
+ export const EvidenceSchema = z
34
+ .object({
35
+ source: EvidenceSourceSchema,
36
+ path: boundedString(512),
37
+ lineStart: z.number().int().positive().optional(),
38
+ lineEnd: z.number().int().positive().optional(),
39
+ contentHash: hash.optional(),
40
+ context: z.string().max(1_024).optional(),
41
+ })
42
+ .strict()
43
+ .superRefine((value, context) => {
44
+ if (value.lineStart !== undefined && value.lineEnd !== undefined && value.lineEnd < value.lineStart) {
45
+ context.addIssue({
46
+ code: z.ZodIssueCode.custom,
47
+ path: ['lineEnd'],
48
+ message: 'Must be greater than or equal to lineStart',
49
+ })
50
+ }
51
+ })
52
+ export type Evidence = z.infer<typeof EvidenceSchema>
53
+
54
+ export const CoverageStatusSchema = z.enum(['complete', 'partial', 'not-analyzed'])
55
+
56
+ export const CoverageSchema = z
57
+ .object({
58
+ analyzer: boundedString(128),
59
+ scope: boundedString(512),
60
+ status: CoverageStatusSchema,
61
+ reason: z.string().max(1_024).optional(),
62
+ evidence: z.array(EvidenceSchema).max(32).optional(),
63
+ })
64
+ .strict()
65
+ export type Coverage = z.infer<typeof CoverageSchema>
66
+
67
+ export const ProjectIdentitySchema = z
68
+ .object({
69
+ name: boundedString(128),
70
+ root: boundedString(512).optional(),
71
+ })
72
+ .strict()
73
+
74
+ const ArtifactMetadata = {
75
+ schemaVersion: z.literal(KNOWLEDGE_SCHEMA_VERSION),
76
+ contentHash: hash,
77
+ contentHashAlgo: z.literal(KNOWLEDGE_CONTENT_HASH_ALGO),
78
+ project: ProjectIdentitySchema,
79
+ sourceRevision: boundedString(128),
80
+ sourceRevisionKind: z.enum(['git', 'content']),
81
+ configurationHash: hash,
82
+ pipelineVersion: boundedString(64),
83
+ analyzerVersions: z.record(boundedString(128), boundedString(64)),
84
+ }
85
+
86
+ export const EntitySchema = z
87
+ .object({
88
+ id: boundedString(256),
89
+ kind: boundedString(128),
90
+ name: boundedString(256),
91
+ path: boundedString(512).optional(),
92
+ aliases: z.array(boundedString(256)).max(32).optional(),
93
+ provenance: ProvenanceSchema,
94
+ evidence: z.array(EvidenceSchema).max(64),
95
+ metadata: z.record(z.unknown()).optional(),
96
+ })
97
+ .strict()
98
+ export type KnowledgeEntity = z.infer<typeof EntitySchema>
99
+
100
+ export const RelationSchema = z
101
+ .object({
102
+ id: boundedString(256),
103
+ kind: boundedString(128),
104
+ from: boundedString(256),
105
+ to: boundedString(256),
106
+ discriminator: boundedString(256).optional(),
107
+ provenance: ProvenanceSchema,
108
+ evidence: z.array(EvidenceSchema).max(64),
109
+ metadata: z.record(z.unknown()).optional(),
110
+ })
111
+ .strict()
112
+ export type KnowledgeRelation = z.infer<typeof RelationSchema>
113
+
114
+ export const DiscoverySnapshotV1Schema = z
115
+ .object({
116
+ type: z.literal('discovery-snapshot'),
117
+ ...ArtifactMetadata,
118
+ entities: z.array(EntitySchema).max(50_000),
119
+ relations: z.array(RelationSchema).max(100_000),
120
+ coverage: z.array(CoverageSchema).max(1_000),
121
+ })
122
+ .strict()
123
+ .superRefine((value, context) => {
124
+ const entityIds = new Set<string>()
125
+ for (const [index, entity] of value.entities.entries()) {
126
+ if (entityIds.has(entity.id)) {
127
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ['entities', index, 'id'], message: `Duplicate entity id: ${entity.id}` })
128
+ }
129
+ entityIds.add(entity.id)
130
+ }
131
+
132
+ const relationIds = new Set<string>()
133
+ for (const [index, relation] of value.relations.entries()) {
134
+ if (relationIds.has(relation.id)) {
135
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ['relations', index, 'id'], message: `Duplicate relation id: ${relation.id}` })
136
+ }
137
+ relationIds.add(relation.id)
138
+ }
139
+ })
140
+ export type DiscoverySnapshotV1 = z.infer<typeof DiscoverySnapshotV1Schema>
141
+
142
+ export const DiagnosticSchema = z
143
+ .object({
144
+ id: boundedString(256),
145
+ code: boundedString(128),
146
+ status: FindingStatusSchema,
147
+ severity: DiagnosticSeveritySchema,
148
+ message: boundedString(2_048),
149
+ evidence: z.array(EvidenceSchema).max(64),
150
+ entityIds: z.array(boundedString(256)).max(64).optional(),
151
+ relationIds: z.array(boundedString(256)).max(64).optional(),
152
+ remediation: z.string().max(2_048).optional(),
153
+ })
154
+ .strict()
155
+ export type KnowledgeDiagnostic = z.infer<typeof DiagnosticSchema>
156
+
157
+ export const ReconciliationReportV1Schema = z
158
+ .object({
159
+ type: z.literal('reconciliation-report'),
160
+ ...ArtifactMetadata,
161
+ snapshotHash: hash,
162
+ diagnostics: z.array(DiagnosticSchema).max(100_000),
163
+ summary: z
164
+ .object({
165
+ entityCount: z.number().int().nonnegative(),
166
+ relationCount: z.number().int().nonnegative(),
167
+ diagnosticCount: z.number().int().nonnegative(),
168
+ })
169
+ .strict(),
170
+ })
171
+ .strict()
172
+ export type ReconciliationReportV1 = z.infer<typeof ReconciliationReportV1Schema>
173
+
174
+ export const WorkflowStateSchema = z.enum([
175
+ 'created',
176
+ 'discovering',
177
+ 'analyzed',
178
+ 'compared',
179
+ 'awaiting-agent',
180
+ 'proposed',
181
+ 'awaiting-approval',
182
+ 'validating',
183
+ 'delivered',
184
+ 'failed',
185
+ 'cancelled',
186
+ 'stale',
187
+ 'superseded',
188
+ ])
189
+ export type WorkflowState = z.infer<typeof WorkflowStateSchema>
190
+
191
+ export const WorkflowStepSchema = z
192
+ .object({
193
+ name: boundedString(128),
194
+ status: z.enum(['pending', 'running', 'completed', 'failed', 'skipped']),
195
+ inputHash: hash,
196
+ outputHash: hash.optional(),
197
+ artifactRefs: z.array(boundedString(512)).max(32).optional(),
198
+ })
199
+ .strict()
200
+ export type WorkflowStep = z.infer<typeof WorkflowStepSchema>
201
+
202
+ export const WorkflowTransitionSchema = z
203
+ .object({
204
+ from: WorkflowStateSchema.nullable(),
205
+ to: WorkflowStateSchema,
206
+ at: z.string().datetime(),
207
+ reason: z.string().max(1_024).optional(),
208
+ })
209
+ .strict()
210
+ export type WorkflowTransition = z.infer<typeof WorkflowTransitionSchema>
211
+
212
+ export const WorkflowRunV1Schema = z
213
+ .object({
214
+ type: z.literal('workflow-run'),
215
+ ...ArtifactMetadata,
216
+ runId: boundedString(128),
217
+ state: WorkflowStateSchema,
218
+ steps: z.array(WorkflowStepSchema).max(32),
219
+ transitions: z.array(WorkflowTransitionSchema).max(1_000),
220
+ artifactRefs: z.array(boundedString(512)).max(128),
221
+ })
222
+ .strict()
223
+ export type WorkflowRunV1 = z.infer<typeof WorkflowRunV1Schema>
224
+
225
+ export const ProposalOriginSchema = z
226
+ .object({
227
+ kind: z.enum(['registry-agent', 'manual', 'deterministic']),
228
+ id: boundedString(256).optional(),
229
+ version: boundedString(64).optional(),
230
+ provider: boundedString(128).optional(),
231
+ model: boundedString(256).optional(),
232
+ capabilities: z.array(boundedString(128)).max(32).optional(),
233
+ })
234
+ .strict()
235
+ .superRefine((value, context) => {
236
+ if (value.kind === 'registry-agent' && value.id === undefined) {
237
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ['id'], message: 'Registry agent origin requires an id' })
238
+ }
239
+ })
240
+
241
+ export const AgentProposalV1Schema = z
242
+ .object({
243
+ type: z.literal('agent-proposal'),
244
+ ...ArtifactMetadata,
245
+ proposalId: boundedString(128),
246
+ baseSnapshotHash: hash,
247
+ baseReportHash: hash,
248
+ relatedDiagnosticIds: z.array(boundedString(256)).max(64),
249
+ rationale: boundedString(4_000),
250
+ confidence: z.number().min(0).max(1),
251
+ evidence: z.array(EvidenceSchema).max(128),
252
+ intendedChanges: z.array(boundedString(4_000)).max(64),
253
+ origin: ProposalOriginSchema,
254
+ checks: z.array(boundedString(512)).max(32),
255
+ })
256
+ .strict()
257
+ export type AgentProposalV1 = z.infer<typeof AgentProposalV1Schema>
258
+
259
+ export const FixProposalStatusSchema = z.enum(['proposed', 'approved', 'rejected', 'stale', 'applied', 'failed'])
260
+
261
+ export const AffectedFileSchema = z
262
+ .object({
263
+ path: boundedString(512),
264
+ contentHash: hash,
265
+ })
266
+ .strict()
267
+
268
+ export const FixChangeSchema = z
269
+ .object({
270
+ path: boundedString(512),
271
+ before: z.string().max(100_000),
272
+ after: z.string().max(100_000),
273
+ })
274
+ .strict()
275
+ export type FixChange = z.infer<typeof FixChangeSchema>
276
+
277
+ export const FixProposalV1Schema = z
278
+ .object({
279
+ type: z.literal('fix-proposal'),
280
+ ...ArtifactMetadata,
281
+ proposalId: boundedString(128),
282
+ baseRevision: boundedString(128),
283
+ affectedFiles: z.array(AffectedFileSchema).max(256),
284
+ changes: z.array(FixChangeSchema).max(256).optional(),
285
+ preconditions: z.array(boundedString(2_048)).max(64),
286
+ diff: boundedString(100_000),
287
+ postconditions: z.array(boundedString(2_048)).max(64),
288
+ approval: z
289
+ .object({
290
+ proposalHash: hash,
291
+ approvedAt: z.string().datetime(),
292
+ approvedBy: boundedString(256),
293
+ })
294
+ .strict()
295
+ .optional(),
296
+ status: FixProposalStatusSchema,
297
+ })
298
+ .strict()
299
+ .superRefine((value, context) => {
300
+ if ((value.status === 'approved' || value.status === 'applied') && value.approval === undefined) {
301
+ context.addIssue({
302
+ code: z.ZodIssueCode.custom,
303
+ path: ['approval'],
304
+ message: `Fix proposal status ${value.status} requires an approval record`,
305
+ })
306
+ }
307
+ })
308
+ export type FixProposalV1 = z.infer<typeof FixProposalV1Schema>
309
+
310
+ export type KnowledgeArtifactV1 =
311
+ | DiscoverySnapshotV1
312
+ | ReconciliationReportV1
313
+ | WorkflowRunV1
314
+ | AgentProposalV1
315
+ | FixProposalV1
package/src/validate.ts CHANGED
@@ -14,6 +14,18 @@ import {
14
14
  MemoryCandidateV1Schema,
15
15
  type MemoryCandidateV1,
16
16
  } from './schemas/memory-candidate.js'
17
+ import {
18
+ AgentProposalV1Schema,
19
+ DiscoverySnapshotV1Schema,
20
+ FixProposalV1Schema,
21
+ ReconciliationReportV1Schema,
22
+ WorkflowRunV1Schema,
23
+ type AgentProposalV1,
24
+ type DiscoverySnapshotV1,
25
+ type FixProposalV1,
26
+ type ReconciliationReportV1,
27
+ type WorkflowRunV1,
28
+ } from './schemas/knowledge.js'
17
29
 
18
30
  export type ParseIssue = {
19
31
  readonly path: string
@@ -56,6 +68,18 @@ export const parseDocBridgeIndex = (input: unknown): DocBridgeIndexV1 =>
56
68
  export const parseMemoryCandidate = (input: unknown): MemoryCandidateV1 =>
57
69
  MemoryCandidateV1Schema.parse(input)
58
70
 
71
+ export const parseDiscoverySnapshot = (input: unknown): DiscoverySnapshotV1 =>
72
+ DiscoverySnapshotV1Schema.parse(input)
73
+
74
+ export const parseReconciliationReport = (input: unknown): ReconciliationReportV1 =>
75
+ ReconciliationReportV1Schema.parse(input)
76
+
77
+ export const parseWorkflowRun = (input: unknown): WorkflowRunV1 => WorkflowRunV1Schema.parse(input)
78
+
79
+ export const parseAgentProposal = (input: unknown): AgentProposalV1 => AgentProposalV1Schema.parse(input)
80
+
81
+ export const parseFixProposal = (input: unknown): FixProposalV1 => FixProposalV1Schema.parse(input)
82
+
59
83
  export const parseDocBridgeConfig = (input: unknown): DocBridgeConfigV1 => {
60
84
  const result = DocBridgeConfigV1Schema.safeParse(input)
61
85
  if (!result.success) {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '1.4.1'
1
+ export const PACKAGE_VERSION = '1.5.0'