@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
|
@@ -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.
|
|
1
|
+
export const PACKAGE_VERSION = '1.5.0'
|