@agentskit/doc-bridge 1.6.4 → 1.7.45
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 +249 -0
- package/CONTRIBUTING.md +6 -4
- package/action.yml +1 -1
- package/dist/cli/program.js +1139 -294
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +43 -5
- package/dist/config/index.js.map +1 -1
- package/dist/index-BUL0q7s8.d.ts +660 -0
- package/dist/index.d.ts +817 -2134
- package/dist/index.js +1154 -244
- package/dist/index.js.map +1 -1
- package/docs/PRD-enterprise-hardening.md +288 -0
- package/docs/RELEASE.md +22 -8
- package/docs/adr/0001-enterprise-verification-contract.md +35 -0
- package/docs/agent-corpus/INDEX.md +2 -2
- package/docs/agent-corpus/chat.md +2 -2
- package/docs/agent-corpus/cli.md +2 -2
- package/docs/agent-corpus/conformance.md +2 -2
- package/docs/agent-corpus/doc-bridge.md +1 -1
- package/docs/agent-corpus/doctor.md +2 -2
- package/docs/agent-corpus/gates.md +2 -2
- package/docs/agent-corpus/mcp.md +2 -2
- package/docs/agent-corpus/memory.md +2 -2
- package/docs/agent-corpus/query.md +2 -2
- package/docs/knowledge-engine-runbook.md +30 -2
- package/docs/spec/analyzer-plugin-v1.md +24 -0
- package/docs/spec/benchmark-v1.md +36 -0
- package/docs/spec/config-v1.md +156 -0
- package/docs/validation-cycle-plan.md +255 -0
- package/docs/verification-harness.md +37 -4
- package/mcpb/manifest.json +1 -1
- package/package.json +68 -70
- package/scripts/check-ecosystem-upstream.mjs +3 -2
- package/scripts/report-visual-check.mjs +64 -12
- package/scripts/verification-harness.mjs +216 -14
- package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
- package/src/agents/registry-adapter.ts +31 -7
- package/src/cli/demo.ts +2 -2
- package/src/cli/program.ts +59 -16
- package/src/config/index.ts +2 -0
- package/src/config/load-config.ts +7 -1
- package/src/config/schema.ts +60 -2
- package/src/conformance/documentation-standard-v1.ts +14 -8
- package/src/discovery/documentation.ts +90 -23
- package/src/discovery/repository.ts +147 -19
- package/src/doctor/run-doctor.ts +2 -15
- package/src/federation/llms.ts +72 -20
- package/src/fixes/proposals.ts +4 -3
- package/src/index-builder/human-adapters/fumadocs.ts +1 -1
- package/src/index-builder/watch-index.ts +1 -1
- package/src/index.ts +29 -0
- package/src/lib/bounded-text.ts +15 -10
- package/src/metrics/benchmark.ts +176 -0
- package/src/plugins/contract.ts +89 -0
- package/src/reconciliation/reconcile.ts +181 -5
- package/src/report/html.ts +318 -88
- package/src/rules/engine.ts +15 -2
- package/src/safety/repository.ts +1 -1
- package/src/schemas/knowledge.ts +21 -3
- package/src/validate.ts +7 -1
- package/src/version.ts +1 -1
- package/src/workflow/engine.ts +65 -9
- package/dist/index-DudNuwI5.d.ts +0 -2060
|
@@ -10,22 +10,43 @@ import {
|
|
|
10
10
|
readdirSync,
|
|
11
11
|
renameSync,
|
|
12
12
|
rmSync,
|
|
13
|
-
statSync,
|
|
14
13
|
writeFileSync,
|
|
15
14
|
} from 'node:fs'
|
|
16
15
|
import { dirname, join, relative, resolve, sep } from 'node:path'
|
|
17
16
|
|
|
18
|
-
const VERSION = '1.
|
|
19
|
-
const STATES = new Set(['PLANNED', 'VERIFYING', 'AWAITING_HUMAN_APPROVAL', 'AWAITING_AUTHORIZATION', 'COMPLETE', 'BLOCKED', 'FAILED'])
|
|
20
|
-
const PROFILES = new Set(['strict', 'poc', 'custom'])
|
|
17
|
+
const VERSION = '1.4.0'
|
|
18
|
+
const STATES = new Set(['CLARIFYING', 'PLANNED', 'VERIFYING', 'AWAITING_HUMAN_APPROVAL', 'AWAITING_AUTHORIZATION', 'COMPLETE', 'BLOCKED', 'FAILED'])
|
|
19
|
+
const PROFILES = new Set(['default', 'strict', 'poc', 'custom', 'enterprise'])
|
|
20
|
+
const SURFACES = ['logic', 'endpoint', 'database', 'cli', 'mcp', 'ui', 'docs']
|
|
21
|
+
const PROFILE_POLICIES = {
|
|
22
|
+
default: { requiresExplicitExemptions: false, requiresAllSurfaces: false, requiresMeasurement: false, requiresTracking: false },
|
|
23
|
+
strict: { requiresExplicitExemptions: false, requiresAllSurfaces: false, requiresMeasurement: false, requiresTracking: false },
|
|
24
|
+
poc: { requiresExplicitExemptions: true, requiresAllSurfaces: false, requiresMeasurement: false, requiresTracking: false },
|
|
25
|
+
custom: { requiresExplicitExemptions: true, requiresAllSurfaces: false, requiresMeasurement: false, requiresTracking: false },
|
|
26
|
+
enterprise: { requiresExplicitExemptions: false, requiresAllSurfaces: true, requiresMeasurement: true, requiresTracking: true },
|
|
27
|
+
}
|
|
21
28
|
const CATEGORIES = new Set(['build', 'test', 'lint', 'logic', 'endpoint', 'database', 'cli', 'mcp', 'ui', 'docs', 'custom'])
|
|
22
29
|
const INTENTS = new Set(['ok', 'certo', 'certa', 'aprovado', 'aprovada', 'approve', 'approved', 'confirmo', 'confirmado', 'confirmed', 'yes'])
|
|
23
30
|
const DEFAULT_CONFIG = '.codex/verification.json'
|
|
31
|
+
const LEGAL_TRANSITIONS = {
|
|
32
|
+
null: ['PLANNED'],
|
|
33
|
+
CLARIFYING: ['PLANNED', 'BLOCKED', 'FAILED'],
|
|
34
|
+
PLANNED: ['CLARIFYING', 'VERIFYING', 'BLOCKED', 'FAILED'],
|
|
35
|
+
VERIFYING: ['AWAITING_HUMAN_APPROVAL', 'AWAITING_AUTHORIZATION', 'COMPLETE', 'BLOCKED', 'FAILED'],
|
|
36
|
+
AWAITING_HUMAN_APPROVAL: ['AWAITING_AUTHORIZATION', 'COMPLETE', 'BLOCKED', 'FAILED'],
|
|
37
|
+
AWAITING_AUTHORIZATION: ['COMPLETE', 'BLOCKED', 'FAILED'],
|
|
38
|
+
BLOCKED: [],
|
|
39
|
+
FAILED: [],
|
|
40
|
+
COMPLETE: [],
|
|
41
|
+
}
|
|
24
42
|
|
|
25
43
|
const fail = (message) => { throw new Error(message) }
|
|
26
44
|
const hash = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex')
|
|
27
45
|
const now = () => new Date().toISOString()
|
|
28
46
|
const readJson = (path) => JSON.parse(readFileSync(path, 'utf8'))
|
|
47
|
+
const assertKnownKeys = (value, allowed, label) => {
|
|
48
|
+
for (const key of Object.keys(value ?? {})) if (!allowed.has(key)) fail(`${label}.${key} is not supported.`)
|
|
49
|
+
}
|
|
29
50
|
const writeAtomic = (path, value) => {
|
|
30
51
|
mkdirSync(dirname(path), { recursive: true })
|
|
31
52
|
const temp = `${path}.tmp-${process.pid}`
|
|
@@ -58,6 +79,7 @@ const inside = (root, path) => {
|
|
|
58
79
|
const surfaceRequired = (value, name) => {
|
|
59
80
|
if (typeof value === 'boolean') return { required: value, reason: value ? undefined : `${name} is not part of this verification target.` }
|
|
60
81
|
if (!value || typeof value !== 'object' || Array.isArray(value)) fail(`surfaces.${name} must be a boolean or { required, reason }.`)
|
|
82
|
+
assertKnownKeys(value, new Set(['required', 'reason']), `surfaces.${name}`)
|
|
61
83
|
if (typeof value.required !== 'boolean') fail(`surfaces.${name}.required must be boolean.`)
|
|
62
84
|
if (!value.required && typeof value.reason !== 'string') fail(`surfaces.${name}.reason is required when the surface is not required.`)
|
|
63
85
|
return { required: value.required, ...(value.reason ? { reason: value.reason } : {}) }
|
|
@@ -65,35 +87,91 @@ const surfaceRequired = (value, name) => {
|
|
|
65
87
|
|
|
66
88
|
const validateConfig = (raw, configPath) => {
|
|
67
89
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) fail(`Invalid verification config: ${configPath}`)
|
|
90
|
+
assertKnownKeys(raw, new Set(['schemaVersion', 'project', 'root', 'profile', 'contract', 'surfaces', 'checks', 'exemptions', 'measurement', 'tracking', 'cleanup', 'overrides']), 'verification config')
|
|
68
91
|
if (raw.schemaVersion !== 1) fail('verification config schemaVersion must be 1.')
|
|
69
92
|
if (typeof raw.project !== 'string' || !raw.project) fail('verification config project is required.')
|
|
70
93
|
if (raw.root !== undefined && typeof raw.root !== 'string') fail('verification config root must be a string.')
|
|
71
|
-
|
|
94
|
+
const profile = raw.profile ?? 'default'
|
|
95
|
+
if (!PROFILES.has(profile)) fail(`verification config profile must be one of: ${[...PROFILES].join(', ')}.`)
|
|
96
|
+
const policy = PROFILE_POLICIES[profile]
|
|
97
|
+
if (raw.overrides !== undefined) {
|
|
98
|
+
if (!raw.overrides || typeof raw.overrides !== 'object' || Array.isArray(raw.overrides)) fail('overrides must be an object.')
|
|
99
|
+
assertKnownKeys(raw.overrides, new Set(['profile']), 'overrides')
|
|
100
|
+
if (raw.overrides.profile !== undefined) fail('overrides.profile is not supported; set profile explicitly.')
|
|
101
|
+
}
|
|
72
102
|
if (!Array.isArray(raw.checks) || raw.checks.length === 0) fail('verification config requires at least one check.')
|
|
73
103
|
const checks = raw.checks.map((check, index) => {
|
|
74
104
|
if (!check || typeof check !== 'object' || Array.isArray(check)) fail(`checks[${index}] must be an object.`)
|
|
105
|
+
assertKnownKeys(check, new Set(['id', 'category', 'command', 'required', 'timeoutMs', 'execution', 'capabilities']), `checks[${index}]`)
|
|
75
106
|
if (typeof check.id !== 'string' || !check.id) fail(`checks[${index}].id is required.`)
|
|
76
107
|
if (typeof check.command !== 'string' || !check.command) fail(`checks[${index}].command is required.`)
|
|
77
108
|
if (!CATEGORIES.has(check.category)) fail(`checks[${index}].category is invalid.`)
|
|
78
109
|
if (check.required !== undefined && typeof check.required !== 'boolean') fail(`checks[${index}].required must be boolean.`)
|
|
79
110
|
if (check.timeoutMs !== undefined && (!Number.isInteger(check.timeoutMs) || check.timeoutMs < 1)) fail(`checks[${index}].timeoutMs must be a positive integer.`)
|
|
80
111
|
if (['endpoint', 'database', 'cli', 'mcp', 'ui'].includes(check.category) && check.execution !== 'real') fail(`checks[${index}] requires execution: "real".`)
|
|
112
|
+
if (check.capabilities !== undefined && (!Array.isArray(check.capabilities) || check.capabilities.some((item) => typeof item !== 'string' || !item.trim()))) fail(`checks[${index}].capabilities must contain non-empty strings.`)
|
|
113
|
+
if (check.category === 'ui') {
|
|
114
|
+
for (const capability of ['real-browser', 'screenshot']) if (!check.capabilities?.includes(capability)) fail(`checks[${index}] requires capability "${capability}".`)
|
|
115
|
+
}
|
|
81
116
|
return { required: true, timeoutMs: 120_000, ...check }
|
|
82
117
|
})
|
|
118
|
+
if (new Set(checks.map((check) => check.id)).size !== checks.length) fail('check ids must be unique.')
|
|
119
|
+
if (!raw.contract || typeof raw.contract !== 'object' || Array.isArray(raw.contract)) fail('verification contract is required.')
|
|
120
|
+
assertKnownKeys(raw.contract, new Set(['intent', 'outcomes']), 'contract')
|
|
121
|
+
if (typeof raw.contract.intent !== 'string' || !raw.contract.intent.trim()) fail('contract.intent is required.')
|
|
122
|
+
if (!Array.isArray(raw.contract.outcomes) || raw.contract.outcomes.length === 0) fail('contract.outcomes requires at least one outcome.')
|
|
123
|
+
const checkIds = new Set(checks.map((check) => check.id))
|
|
124
|
+
const outcomes = raw.contract.outcomes.map((outcome, index) => {
|
|
125
|
+
if (!outcome || typeof outcome !== 'object' || Array.isArray(outcome)) fail(`contract.outcomes[${index}] must be an object.`)
|
|
126
|
+
assertKnownKeys(outcome, new Set(['id', 'statement', 'checks']), `contract.outcomes[${index}]`)
|
|
127
|
+
if (typeof outcome.id !== 'string' || !outcome.id.trim()) fail(`contract.outcomes[${index}].id is required.`)
|
|
128
|
+
if (typeof outcome.statement !== 'string' || !outcome.statement.trim()) fail(`contract.outcomes[${index}].statement is required.`)
|
|
129
|
+
if (!Array.isArray(outcome.checks) || outcome.checks.length === 0) fail(`contract.outcomes[${index}].checks requires at least one check id.`)
|
|
130
|
+
if (outcome.checks.some((checkId) => typeof checkId !== 'string' || !checkIds.has(checkId))) fail(`contract.outcomes[${index}] references an unknown check.`)
|
|
131
|
+
if (outcome.checks.some((checkId) => !checks.find((check) => check.id === checkId)?.required)) fail(`contract.outcomes[${index}] references a non-required check.`)
|
|
132
|
+
return { id: outcome.id, statement: outcome.statement, checks: [...new Set(outcome.checks)] }
|
|
133
|
+
})
|
|
134
|
+
if (new Set(outcomes.map((outcome) => outcome.id)).size !== outcomes.length) fail('contract outcome ids must be unique.')
|
|
135
|
+
if (raw.surfaces !== undefined && (!raw.surfaces || typeof raw.surfaces !== 'object' || Array.isArray(raw.surfaces))) fail('surfaces must be an object.')
|
|
136
|
+
if (raw.surfaces) for (const name of Object.keys(raw.surfaces)) if (!SURFACES.includes(name)) fail(`surfaces.${name} is not supported.`)
|
|
83
137
|
const surfaces = {}
|
|
84
|
-
for (const name of
|
|
138
|
+
for (const name of SURFACES) {
|
|
139
|
+
if (policy.requiresAllSurfaces && !Object.hasOwn(raw.surfaces ?? {}, name)) fail(`enterprise profile requires surfaces.${name} to be declared.`)
|
|
140
|
+
surfaces[name] = surfaceRequired(raw.surfaces?.[name] ?? (name === 'logic'), name)
|
|
141
|
+
}
|
|
85
142
|
for (const [name, surface] of Object.entries(surfaces)) {
|
|
86
143
|
const matching = checks.some((check) => check.category === name && check.required)
|
|
87
144
|
if (surface.required && !matching) fail(`Required surface "${name}" has no required check.`)
|
|
88
145
|
}
|
|
89
|
-
if (
|
|
146
|
+
if (policy.requiresExplicitExemptions && (!Array.isArray(raw.exemptions) || raw.exemptions.length === 0)) fail('This profile requires explicit exemptions.')
|
|
90
147
|
if (raw.exemptions && (!Array.isArray(raw.exemptions) || raw.exemptions.some((item) => typeof item !== 'string' || !item.trim()))) fail('exemptions must be non-empty strings.')
|
|
91
|
-
const
|
|
148
|
+
const measurement = raw.measurement ?? { required: false }
|
|
149
|
+
if (!measurement || typeof measurement !== 'object' || Array.isArray(measurement)) fail('measurement must be an object.')
|
|
150
|
+
assertKnownKeys(measurement, new Set(['required', 'checkId', 'baseline']), 'measurement')
|
|
151
|
+
if (typeof measurement.required !== 'boolean') fail('measurement.required must be boolean.')
|
|
152
|
+
if (policy.requiresMeasurement && !measurement.required) fail('enterprise profile requires measurement.required to be true.')
|
|
153
|
+
if (measurement.required) {
|
|
154
|
+
if (typeof measurement.checkId !== 'string' || !measurement.checkId.trim()) fail('measurement.checkId is required when measurement is required.')
|
|
155
|
+
const measurementCheck = checks.find((check) => check.id === measurement.checkId)
|
|
156
|
+
if (!measurementCheck) fail(`measurement.checkId references an unknown check: ${measurement.checkId}.`)
|
|
157
|
+
if (!measurementCheck.required) fail('measurement.checkId must reference a required check.')
|
|
158
|
+
if (typeof measurement.baseline !== 'string' || !measurement.baseline.trim()) fail('measurement.baseline is required when measurement is required.')
|
|
159
|
+
}
|
|
160
|
+
const tracking = raw.tracking ?? (policy.requiresTracking
|
|
161
|
+
? { required: true, authorization: 'ask' }
|
|
162
|
+
: { required: false, reason: 'tracking is not configured for this run.' })
|
|
163
|
+
assertKnownKeys(tracking, new Set(['required', 'authorization', 'target', 'reason']), 'tracking')
|
|
92
164
|
if (typeof tracking.required !== 'boolean') fail('tracking.required must be boolean.')
|
|
165
|
+
if (policy.requiresTracking && !tracking.required) fail('enterprise profile requires tracking.required to be true.')
|
|
93
166
|
if (tracking.required && tracking.authorization !== 'ask') fail('tracking.authorization must be "ask".')
|
|
94
167
|
if (tracking.required && typeof tracking.target !== 'string') fail('tracking.target is required when tracking is required.')
|
|
95
168
|
if (!tracking.required && typeof tracking.reason !== 'string') fail('tracking.reason is required when tracking is not required.')
|
|
96
|
-
|
|
169
|
+
if (raw.cleanup !== undefined) {
|
|
170
|
+
if (!raw.cleanup || typeof raw.cleanup !== 'object' || Array.isArray(raw.cleanup)) fail('cleanup must be an object.')
|
|
171
|
+
assertKnownKeys(raw.cleanup, new Set(['roots']), 'cleanup')
|
|
172
|
+
if (raw.cleanup.roots !== undefined && (!Array.isArray(raw.cleanup.roots) || raw.cleanup.roots.some((root) => typeof root !== 'string' || !root.trim()))) fail('cleanup.roots must contain non-empty strings.')
|
|
173
|
+
}
|
|
174
|
+
return { ...raw, profile, checks, contract: { intent: raw.contract.intent.trim(), outcomes }, surfaces, tracking, measurement, configPath, profilePolicy: policy }
|
|
97
175
|
}
|
|
98
176
|
|
|
99
177
|
const sourceRevision = (root) => {
|
|
@@ -143,6 +221,53 @@ const commandResult = (root, check) => new Promise((resolveResult) => {
|
|
|
143
221
|
})
|
|
144
222
|
})
|
|
145
223
|
|
|
224
|
+
const validateUiEvidence = (root, run) => ({
|
|
225
|
+
...run,
|
|
226
|
+
checks: run.checks.map((check) => {
|
|
227
|
+
if (check.category !== 'ui' || check.status === 'failed') return check
|
|
228
|
+
const evidence = check.verification
|
|
229
|
+
const failures = []
|
|
230
|
+
if (!evidence || evidence.capability !== 'real-browser') failures.push('UI evidence must declare capability "real-browser"')
|
|
231
|
+
if (!Array.isArray(evidence?.artifacts) || evidence.artifacts.length === 0) failures.push('UI evidence must include screenshot artifacts')
|
|
232
|
+
for (const artifact of evidence?.artifacts ?? []) {
|
|
233
|
+
if (artifact?.type !== 'screenshot' || typeof artifact.path !== 'string' || typeof artifact.sha256 !== 'string' || typeof artifact.viewport !== 'string') {
|
|
234
|
+
failures.push('Each UI artifact must include type=screenshot, path, sha256, and viewport')
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
const path = resolve(root, artifact.path)
|
|
238
|
+
if (!inside(root, path) || !existsSync(path)) failures.push(`Screenshot artifact is missing or outside the project: ${artifact.path}`)
|
|
239
|
+
else if (createHash('sha256').update(readFileSync(path)).digest('hex') !== artifact.sha256) failures.push(`Screenshot hash mismatch: ${artifact.path}`)
|
|
240
|
+
}
|
|
241
|
+
const criterionIds = run.contract.outcomes.filter((outcome) => outcome.checks.includes(check.id)).map((outcome) => outcome.id)
|
|
242
|
+
if (!evidence?.criteria || typeof evidence.criteria !== 'object' || Array.isArray(evidence.criteria)) failures.push('UI evidence must include criterion-level results')
|
|
243
|
+
for (const id of criterionIds) if (evidence?.criteria?.[id]?.status !== 'passed') failures.push(`UI criterion did not pass: ${id}`)
|
|
244
|
+
if (!failures.length) return check
|
|
245
|
+
return { ...check, status: 'failed', verificationStatus: 'failed', verification: { ...(evidence ?? {}), status: 'failed', failures } }
|
|
246
|
+
}),
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
const validateMeasurementResult = (run, measurement) => {
|
|
250
|
+
if (!measurement.required) return run
|
|
251
|
+
const check = run.checks.find((item) => item.id === measurement.checkId)
|
|
252
|
+
const verification = check?.verification
|
|
253
|
+
const failures = []
|
|
254
|
+
if (check?.status !== 'passed') failures.push(`measurement check ${measurement.checkId} did not pass`)
|
|
255
|
+
if (!verification || typeof verification.metrics !== 'object' || Array.isArray(verification.metrics)) failures.push('measurement evidence must include an object named metrics')
|
|
256
|
+
if (typeof verification?.baselineHash !== 'string' || !verification.baselineHash) failures.push('measurement evidence must include baselineHash')
|
|
257
|
+
if (!Array.isArray(verification?.regressions)) failures.push('measurement evidence must include a regressions array')
|
|
258
|
+
else if (verification.regressions.length) failures.push(`measurement regressions detected: ${verification.regressions.join('; ')}`)
|
|
259
|
+
if (!failures.length) return run
|
|
260
|
+
return {
|
|
261
|
+
...run,
|
|
262
|
+
checks: run.checks.map((item) => item.id === measurement.checkId ? {
|
|
263
|
+
...item,
|
|
264
|
+
status: 'failed',
|
|
265
|
+
verificationStatus: 'failed',
|
|
266
|
+
verification: { status: 'failed', failures },
|
|
267
|
+
} : item),
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
146
271
|
const stateDirFor = (root) => join(root, '.codex', 'verification')
|
|
147
272
|
const runDirFor = (root, runId) => join(stateDirFor(root), 'runs', runId)
|
|
148
273
|
const latestPathFor = (root) => join(stateDirFor(root), 'latest.json')
|
|
@@ -152,7 +277,23 @@ const saveRun = (root, run) => {
|
|
|
152
277
|
writeAtomic(join(dir, 'run.json'), run)
|
|
153
278
|
writeAtomic(latestPathFor(root), { runId: run.runId, state: run.state, path: relative(root, join(dir, 'run.json')), updatedAt: now() })
|
|
154
279
|
}
|
|
155
|
-
const transition = (run, state, reason) =>
|
|
280
|
+
const transition = (run, state, reason) => {
|
|
281
|
+
if (!STATES.has(state)) fail(`Unknown verification state: ${state}.`)
|
|
282
|
+
const allowed = LEGAL_TRANSITIONS[String(run.state)] ?? []
|
|
283
|
+
if (!allowed.includes(state)) fail(`Illegal verification transition ${run.state} -> ${state}.`)
|
|
284
|
+
return { ...run, state, transitions: [...run.transitions, { from: run.state, to: state, at: now(), ...(reason ? { reason } : {}) }] }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const attachEvidence = (run) => {
|
|
288
|
+
const evidenceReferences = run.checks.map((check) => ({
|
|
289
|
+
checkId: check.id,
|
|
290
|
+
status: check.status,
|
|
291
|
+
...(check.verificationStatus ? { verificationStatus: check.verificationStatus } : {}),
|
|
292
|
+
evidenceHash: hash({ id: check.id, status: check.status, stdout: check.stdout, stderr: check.stderr, verification: check.verification }),
|
|
293
|
+
}))
|
|
294
|
+
const metrics = Object.fromEntries(run.checks.flatMap((check) => Object.entries(check.verification?.metrics ?? {})))
|
|
295
|
+
return { ...run, evidenceReferences, metrics, outputHash: hash({ checks: run.checks, outcomes: run.outcomes, evidenceReferences, metrics }) }
|
|
296
|
+
}
|
|
156
297
|
|
|
157
298
|
const runVerification = async (root, config, runId) => {
|
|
158
299
|
const source = sourceRevision(root)
|
|
@@ -173,12 +314,16 @@ const runVerification = async (root, config, runId) => {
|
|
|
173
314
|
runId: id,
|
|
174
315
|
project: config.project,
|
|
175
316
|
profile: config.profile,
|
|
317
|
+
profilePolicy: config.profilePolicy,
|
|
176
318
|
sourceRevision: source,
|
|
319
|
+
contractHash: hash(config.contract),
|
|
177
320
|
inputHash,
|
|
178
321
|
state: 'PLANNED',
|
|
179
322
|
configPath: relative(root, config.configPath),
|
|
180
|
-
|
|
323
|
+
contract: config.contract,
|
|
324
|
+
checks: config.checks.map(({ id: checkId, category, command, required, timeoutMs, capabilities }) => ({ id: checkId, category, command, required, timeoutMs, ...(capabilities ? { capabilities } : {}), status: 'pending' })),
|
|
181
325
|
surfaces: config.surfaces,
|
|
326
|
+
applicability: config.surfaces,
|
|
182
327
|
tracking: config.tracking,
|
|
183
328
|
exemptions: config.exemptions ?? [],
|
|
184
329
|
transitions: [{ from: null, to: 'PLANNED', at: now() }],
|
|
@@ -191,8 +336,29 @@ const runVerification = async (root, config, runId) => {
|
|
|
191
336
|
run = { ...run, checks: run.checks.map((item, itemIndex) => itemIndex === index ? { ...item, ...result } : item) }
|
|
192
337
|
saveRun(root, run)
|
|
193
338
|
}
|
|
339
|
+
run = validateUiEvidence(root, run)
|
|
340
|
+
run = {
|
|
341
|
+
...run,
|
|
342
|
+
outcomes: run.contract.outcomes.map((outcome) => {
|
|
343
|
+
const checks = run.checks.filter((check) => outcome.checks.includes(check.id))
|
|
344
|
+
const status = checks.some((check) => check.status === 'failed') ? 'failed' : checks.some((check) => check.status === 'awaiting-human-approval') ? 'awaiting-human-approval' : 'passed'
|
|
345
|
+
return { ...outcome, status }
|
|
346
|
+
}),
|
|
347
|
+
}
|
|
348
|
+
run = validateMeasurementResult(run, config.measurement)
|
|
349
|
+
run = {
|
|
350
|
+
...run,
|
|
351
|
+
outcomes: run.contract.outcomes.map((outcome) => {
|
|
352
|
+
const checks = run.checks.filter((check) => outcome.checks.includes(check.id))
|
|
353
|
+
const status = checks.some((check) => check.status === 'failed') ? 'failed' : checks.some((check) => check.status === 'awaiting-human-approval') ? 'awaiting-human-approval' : 'passed'
|
|
354
|
+
return { ...outcome, status }
|
|
355
|
+
}),
|
|
356
|
+
}
|
|
357
|
+
run = attachEvidence(run)
|
|
358
|
+
const failedOutcomes = run.outcomes.filter((outcome) => outcome.status === 'failed')
|
|
194
359
|
const failed = run.checks.filter((check) => check.required && !['passed', 'awaiting-human-approval'].includes(check.status))
|
|
195
360
|
if (failed.length) run = transition(run, 'BLOCKED', `Required checks failed: ${failed.map((check) => check.id).join(', ')}`)
|
|
361
|
+
else if (failedOutcomes.length) run = transition(run, 'BLOCKED', `Contract outcomes failed: ${failedOutcomes.map((outcome) => outcome.id).join(', ')}`)
|
|
196
362
|
else if (config.surfaces.ui.required) run = transition(run, 'AWAITING_HUMAN_APPROVAL', 'Visual UI approval is required.')
|
|
197
363
|
else if (config.tracking.required) run = transition(run, 'AWAITING_AUTHORIZATION', `Tracking authorization is required for ${config.tracking.target}.`)
|
|
198
364
|
else run = transition(run, 'COMPLETE', 'All configured verification gates passed.')
|
|
@@ -211,13 +377,43 @@ const updateApproval = (root, run, type, rawIntent, by) => {
|
|
|
211
377
|
if (run.state !== expectedState) fail(`Cannot record ${type} while run is ${run.state}.`)
|
|
212
378
|
const approvedIntent = intent(rawIntent)
|
|
213
379
|
const dir = runDirFor(root, run.runId)
|
|
214
|
-
writeAtomic(join(dir, `${type}.json`), {
|
|
380
|
+
writeAtomic(join(dir, `${type}.json`), {
|
|
381
|
+
type,
|
|
382
|
+
runId: run.runId,
|
|
383
|
+
runInputHash: run.inputHash,
|
|
384
|
+
runSourceRevision: run.sourceRevision,
|
|
385
|
+
runContractHash: run.contractHash,
|
|
386
|
+
runOutputHash: run.outputHash,
|
|
387
|
+
intent: approvedIntent,
|
|
388
|
+
by: by ?? 'human',
|
|
389
|
+
at: now(),
|
|
390
|
+
})
|
|
391
|
+
if (type === 'human-approval') run = { ...run, outcomes: run.outcomes.map((outcome) => outcome.status === 'awaiting-human-approval' ? { ...outcome, status: 'passed' } : outcome) }
|
|
215
392
|
if (type === 'human-approval' && run.state === 'AWAITING_HUMAN_APPROVAL') run = run.tracking.required ? transition(run, 'AWAITING_AUTHORIZATION', `Human approval recorded for ${run.runId}.`) : transition(run, 'COMPLETE', 'Human approval recorded and all gates passed.')
|
|
216
393
|
if (type === 'tracking-authorization' && run.state === 'AWAITING_AUTHORIZATION') run = transition(run, 'COMPLETE', `Tracking authorization recorded for ${run.tracking.target}.`)
|
|
217
394
|
saveRun(root, run)
|
|
218
395
|
return run
|
|
219
396
|
}
|
|
220
397
|
|
|
398
|
+
const replaceBaseline = (root, config, sourcePath, rawIntent, by) => {
|
|
399
|
+
if (!config.measurement.required) fail('Baseline replacement requires measurement.required to be true.')
|
|
400
|
+
if (!config.measurement.baseline) fail('Baseline replacement requires measurement.baseline.')
|
|
401
|
+
if (!by) fail('Baseline replacement requires --by.')
|
|
402
|
+
const approvedIntent = intent(rawIntent)
|
|
403
|
+
const target = resolve(root, config.measurement.baseline)
|
|
404
|
+
const source = resolve(root, sourcePath ?? '')
|
|
405
|
+
if (!inside(root, target) || !inside(root, source)) fail('Baseline source and target must be inside the project root.')
|
|
406
|
+
if (!existsSync(source)) fail(`Baseline source not found: ${sourcePath}`)
|
|
407
|
+
if (source === target) fail('Baseline source must differ from the configured baseline target.')
|
|
408
|
+
const value = readJson(source)
|
|
409
|
+
writeAtomic(target, value)
|
|
410
|
+
const auditPath = join(stateDirFor(root), 'baseline-audit.jsonl')
|
|
411
|
+
mkdirSync(dirname(auditPath), { recursive: true })
|
|
412
|
+
const entry = { action: 'replace-baseline', source: relative(root, source), target: relative(root, target), baselineHash: hash(value), intent: approvedIntent, by, at: now() }
|
|
413
|
+
appendFileSync(auditPath, `${JSON.stringify(entry)}\n`, 'utf8')
|
|
414
|
+
return { status: 'baseline-replaced', ...entry }
|
|
415
|
+
}
|
|
416
|
+
|
|
221
417
|
const clean = (root, config, periodic) => {
|
|
222
418
|
const manifestPath = join(stateDirFor(root), 'owned-artifacts.json')
|
|
223
419
|
if (!existsSync(manifestPath)) return { removed: [], skipped: [], periodic, message: 'No task-owned artifacts are registered.' }
|
|
@@ -240,13 +436,19 @@ const main = async (argv) => {
|
|
|
240
436
|
const { flags, values, positional } = parseArgs(argv)
|
|
241
437
|
const command = positional[0] ?? 'help'
|
|
242
438
|
if (flags.has('help') || command === 'help') {
|
|
243
|
-
process.stdout.write('ak-verify run|status|approve <run-id> <intent>|authorize <run-id> <intent>|clean [--periodic] [--config <path>] [--json]\n')
|
|
439
|
+
process.stdout.write('ak-verify run|status|approve <run-id> <intent>|authorize <run-id> <intent>|baseline replace <source> <intent> [--by <actor>]|clean [--periodic] [--config <path>] [--json]\n')
|
|
244
440
|
return 0
|
|
245
441
|
}
|
|
246
442
|
const configPath = resolve(values.get('config') ?? DEFAULT_CONFIG)
|
|
247
443
|
if (!existsSync(configPath)) fail(`Verification contract not found: ${configPath}`)
|
|
248
444
|
const root = projectRoot(configPath, readJson(configPath))
|
|
249
445
|
const config = validateConfig(readJson(configPath), configPath)
|
|
446
|
+
if (command === 'baseline') {
|
|
447
|
+
if (positional[1] !== 'replace') fail('Use: baseline replace <source> <intent> --by <actor>.')
|
|
448
|
+
const result = replaceBaseline(root, config, positional[2], positional[3], values.get('by'))
|
|
449
|
+
output(result, flags.has('json'))
|
|
450
|
+
return 0
|
|
451
|
+
}
|
|
250
452
|
if (command === 'run') {
|
|
251
453
|
const run = await runVerification(root, config, values.get('run-id'))
|
|
252
454
|
output(run, flags.has('json'))
|
|
@@ -277,4 +479,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
277
479
|
catch (error) { process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); process.exitCode = 2 }
|
|
278
480
|
}
|
|
279
481
|
|
|
280
|
-
export { INTENTS, main, validateConfig }
|
|
482
|
+
export { INTENTS, LEGAL_TRANSITIONS, PROFILES, STATES, main, transition, validateConfig }
|
|
@@ -5,7 +5,7 @@ import { z } from 'zod'
|
|
|
5
5
|
|
|
6
6
|
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
7
7
|
import { AgentProposalV1Schema, type AgentProposalV1, type DiscoverySnapshotV1, type ReconciliationReportV1 } from '../schemas/knowledge.js'
|
|
8
|
-
import { contentHashForArtifactV1 } from '../index-builder/content-hash.js'
|
|
8
|
+
import { contentHashForArtifactV1, sha256NormalizedV1 } from '../index-builder/content-hash.js'
|
|
9
9
|
import { containedPath, redactValue } from '../safety/repository.js'
|
|
10
10
|
|
|
11
11
|
export const DEFAULT_REGISTRY_AGENT_ID = 'ecosystem-doc-bridge-corpus-scanner'
|
|
@@ -27,6 +27,7 @@ export type RegistryAgentContext = {
|
|
|
27
27
|
readonly capabilities: readonly ['snapshot.read', 'evidence.read', 'proposal.write']
|
|
28
28
|
readonly network: false
|
|
29
29
|
readonly shell: false
|
|
30
|
+
readonly deterministic: boolean
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export type RegistryAgentRunner = (context: RegistryAgentContext) => Promise<unknown> | unknown
|
|
@@ -73,15 +74,38 @@ export const loadRegistryAgentMetadata = (root: string, config: DocBridgeConfigV
|
|
|
73
74
|
export const createRegistryAgentAdapter = (root: string, config: DocBridgeConfigV1, runner: RegistryAgentRunner): RegistryAgentAdapter => {
|
|
74
75
|
if (!registryConfig(config)?.enabled) throw new Error('Registry agents are disabled. Set intelligence.registry.enabled: true to run an assisted workflow.')
|
|
75
76
|
const metadata = loadRegistryAgentMetadata(resolve(root), config)
|
|
77
|
+
const settings = registryConfig(config) ?? {}
|
|
78
|
+
const timeoutMs = settings.timeoutMs ?? 120_000
|
|
79
|
+
const maxResponseBytes = settings.maxResponseBytes ?? 256_000
|
|
80
|
+
const maxTokens = settings.maxTokens ?? Math.ceil(maxResponseBytes / 4)
|
|
81
|
+
const maxConcurrency = settings.maxConcurrency ?? 1
|
|
82
|
+
let active = 0
|
|
83
|
+
const deterministicCache = new Map<string, AgentProposalV1>()
|
|
76
84
|
return {
|
|
77
85
|
metadata,
|
|
78
86
|
run: async (snapshot, report, evidence = report.diagnostics.flatMap((diagnostic) => diagnostic.evidence).slice(0, 64)) => {
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
if (active >= maxConcurrency) throw new Error(`Registry agent concurrency limit ${maxConcurrency} exceeded.`)
|
|
88
|
+
const cacheKey = sha256NormalizedV1({ snapshotHash: snapshot.contentHash, reportHash: report.contentHash, agentId: metadata.id, agentVersion: metadata.version, evidence })
|
|
89
|
+
if (settings.deterministic && deterministicCache.has(cacheKey)) return deterministicCache.get(cacheKey) as AgentProposalV1
|
|
90
|
+
active += 1
|
|
91
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
92
|
+
try {
|
|
93
|
+
const context = deepFreeze({ snapshot: redactValue(snapshot), report: redactValue(report), evidence: redactValue(evidence), capabilities: ['snapshot.read', 'evidence.read', 'proposal.write'] as const, network: false as const, shell: false as const, deterministic: settings.deterministic ?? true }) as RegistryAgentContext
|
|
94
|
+
const timeout = new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error(`Registry agent timed out after ${timeoutMs}ms.`)), timeoutMs) })
|
|
95
|
+
const raw = await Promise.race([Promise.resolve(runner(context)), timeout])
|
|
96
|
+
const responseBytes = Buffer.byteLength(JSON.stringify(raw))
|
|
97
|
+
if (responseBytes > maxResponseBytes) throw new Error(`Registry agent response limit ${maxResponseBytes} bytes exceeded.`)
|
|
98
|
+
if (Math.ceil(responseBytes / 4) > maxTokens) throw new Error(`Registry agent token budget ${maxTokens} exceeded.`)
|
|
99
|
+
const proposal = AgentProposalV1Schema.parse(raw)
|
|
100
|
+
if (proposal.contentHash !== contentHashForArtifactV1(proposal)) throw new Error('Registry agent proposal contentHash does not match its canonical contents.')
|
|
101
|
+
if (proposal.baseSnapshotHash !== snapshot.contentHash || proposal.baseReportHash !== report.contentHash) throw new Error('Registry agent proposal is not based on the supplied snapshot/report hashes.')
|
|
102
|
+
if (proposal.origin.kind !== 'registry-agent' || proposal.origin.id !== metadata.id) throw new Error(`Registry agent proposal origin must be ${metadata.id}.`)
|
|
103
|
+
if (settings.deterministic) deterministicCache.set(cacheKey, proposal)
|
|
104
|
+
return proposal
|
|
105
|
+
} finally {
|
|
106
|
+
if (timer) clearTimeout(timer)
|
|
107
|
+
active -= 1
|
|
108
|
+
}
|
|
85
109
|
},
|
|
86
110
|
}
|
|
87
111
|
}
|
package/src/cli/demo.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cpSync, existsSync,
|
|
1
|
+
import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
3
|
import { dirname, join, resolve } from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
@@ -140,4 +140,4 @@ export const withDemoWorkspace = (
|
|
|
140
140
|
} finally {
|
|
141
141
|
rmSync(dir, { recursive: true, force: true })
|
|
142
142
|
}
|
|
143
|
-
}
|
|
143
|
+
}
|
package/src/cli/program.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
1
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import { dirname, relative, resolve } from 'node:path'
|
|
3
3
|
import { createInterface } from 'node:readline/promises'
|
|
4
4
|
|
|
@@ -25,7 +25,7 @@ import { ingestMemoryCandidates } from '../memory/ingest.js'
|
|
|
25
25
|
import { classifyMemoryCandidates, draftMemoryPromotion } from '../memory/pipeline.js'
|
|
26
26
|
import { promoteMemoryToGithubPr } from '../memory/github-pr.js'
|
|
27
27
|
import { watchDocBridgeIndex } from '../index-builder/watch-index.js'
|
|
28
|
-
import {
|
|
28
|
+
import { loadWorkflowStepOutput, runWorkflow, type WorkflowExecutionResult } from '../workflow/engine.js'
|
|
29
29
|
import {
|
|
30
30
|
formatDoctorBadgeJson,
|
|
31
31
|
formatDoctorBadgeMarkdown,
|
|
@@ -47,6 +47,7 @@ import { sha256NormalizedV1 } from '../index-builder/content-hash.js'
|
|
|
47
47
|
import { applyFixProposal, approveFixProposal, createArtifactNormalizationProposal, createMarkdownLinkFixProposal } from '../fixes/proposals.js'
|
|
48
48
|
import { createRegistryAgentAdapter, loadRegistryAgentRunner, persistRegistryAgentProposal } from '../agents/registry-adapter.js'
|
|
49
49
|
import { renderOfflineReportArtifact } from '../report/html.js'
|
|
50
|
+
import { benchmarkFixture, formatBenchmarkText, measureBenchmark } from '../metrics/benchmark.js'
|
|
50
51
|
import { PACKAGE_VERSION } from '../version.js'
|
|
51
52
|
|
|
52
53
|
type Command =
|
|
@@ -60,6 +61,7 @@ type Command =
|
|
|
60
61
|
| 'playbook'
|
|
61
62
|
| 'registry'
|
|
62
63
|
| 'discover'
|
|
64
|
+
| 'benchmark'
|
|
63
65
|
| 'scan'
|
|
64
66
|
| 'reconcile'
|
|
65
67
|
| 'check'
|
|
@@ -89,6 +91,7 @@ Core (no API key):
|
|
|
89
91
|
ak-docs doctor [--text] [--badge] [--write-badge]
|
|
90
92
|
ak-docs index [--watch]
|
|
91
93
|
ak-docs discover [--text|--json]
|
|
94
|
+
ak-docs benchmark <fixture.json> <observation.json> [--text|--json]
|
|
92
95
|
ak-docs scan | reconcile | check | map [--text|--json] [--html] [--report-threshold <bytes>]
|
|
93
96
|
ak-docs fix propose links|normalize <artifact> [--output <file>]
|
|
94
97
|
ak-docs fix approve|apply <proposal.json> [--by <name>]
|
|
@@ -163,6 +166,7 @@ const parseArgs = (argv: readonly string[]) => {
|
|
|
163
166
|
else if (positional[0] === 'playbook') command = 'playbook'
|
|
164
167
|
else if (positional[0] === 'registry') command = 'registry'
|
|
165
168
|
else if (positional[0] === 'discover') command = 'discover'
|
|
169
|
+
else if (positional[0] === 'benchmark') command = 'benchmark'
|
|
166
170
|
else if (positional[0] === 'scan') command = 'scan'
|
|
167
171
|
else if (positional[0] === 'reconcile') command = 'reconcile'
|
|
168
172
|
else if (positional[0] === 'check') command = 'check'
|
|
@@ -463,20 +467,24 @@ const workflowOptions = (
|
|
|
463
467
|
sourceRevision: string,
|
|
464
468
|
stage: 'collect' | 'normalize' | 'reconcile' | 'evaluate' | 'report',
|
|
465
469
|
handlers: Parameters<typeof runWorkflow>[0]['handlers'],
|
|
470
|
+
versions?: Pick<Parameters<typeof runWorkflow>[0], 'pipelineVersion' | 'analyzerVersions'>,
|
|
466
471
|
): Parameters<typeof runWorkflow>[0] => ({
|
|
467
472
|
root,
|
|
468
473
|
...(config.workflow?.stateDir ? { stateDir: config.workflow.stateDir } : {}),
|
|
469
474
|
sourceRevision,
|
|
470
475
|
configurationHash: sha256NormalizedV1(config),
|
|
471
|
-
toolVersion:
|
|
476
|
+
toolVersion: PACKAGE_VERSION,
|
|
477
|
+
...(versions?.pipelineVersion ? { pipelineVersion: versions.pipelineVersion } : {}),
|
|
478
|
+
...(versions?.analyzerVersions ? { analyzerVersions: versions.analyzerVersions } : {}),
|
|
472
479
|
stage,
|
|
473
480
|
handlers,
|
|
474
481
|
})
|
|
475
482
|
|
|
476
483
|
const scanWorkflow = (root: string, config: DocBridgeConfigV1): WorkflowExecutionResult => {
|
|
477
484
|
const discovered = discoverRepository({ root, config })
|
|
478
|
-
|
|
479
|
-
|
|
485
|
+
const versions = { pipelineVersion: discovered.pipelineVersion, analyzerVersions: discovered.analyzerVersions }
|
|
486
|
+
runWorkflow(workflowOptions(root, config, discovered.sourceRevision, 'collect', { collect: () => discovered }, versions))
|
|
487
|
+
return runWorkflow(workflowOptions(root, config, discovered.sourceRevision, 'normalize', { normalize: ({ input }) => input }, versions))
|
|
480
488
|
}
|
|
481
489
|
|
|
482
490
|
const documentationInputs = (root: string, snapshot: DiscoverySnapshotV1) => snapshot.entities
|
|
@@ -486,18 +494,24 @@ const documentationInputs = (root: string, snapshot: DiscoverySnapshotV1) => sna
|
|
|
486
494
|
const reconcileWorkflow = (root: string, config: DocBridgeConfigV1): WorkflowExecutionResult => {
|
|
487
495
|
const scanned = scanWorkflow(root, config)
|
|
488
496
|
const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(scanned.stateDir, 'normalize'))
|
|
489
|
-
const declared = applyDocumentationDeclarations(snapshot, documentationInputs(root, snapshot)
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
497
|
+
const declared = applyDocumentationDeclarations(snapshot, documentationInputs(root, snapshot), {
|
|
498
|
+
agentRoot: config.corpus.agent.root,
|
|
499
|
+
}).snapshot
|
|
500
|
+
const report = reconcileKnowledge(snapshot, declared, {
|
|
501
|
+
...(config.reconciliation?.scope === undefined ? {} : { scope: config.reconciliation.scope }),
|
|
502
|
+
...(config.reconciliation?.requiredRelationKinds === undefined ? {} : { requiredRelationKinds: config.reconciliation.requiredRelationKinds }),
|
|
503
|
+
...(config.reconciliation?.requiredRelationTargets === undefined ? {} : { requiredRelationTargets: config.reconciliation.requiredRelationTargets }),
|
|
504
|
+
...(config.reconciliation?.includeOrphanedDocuments === undefined ? {} : { includeOrphanedDocuments: config.reconciliation.includeOrphanedDocuments }),
|
|
505
|
+
})
|
|
506
|
+
return runWorkflow(workflowOptions(root, config, snapshot.sourceRevision, 'reconcile', { reconcile: () => report }, { pipelineVersion: snapshot.pipelineVersion, analyzerVersions: snapshot.analyzerVersions }))
|
|
494
507
|
}
|
|
495
508
|
|
|
496
509
|
const checkWorkflow = (root: string, config: DocBridgeConfigV1): WorkflowExecutionResult => {
|
|
497
510
|
const reconciled = reconcileWorkflow(root, config)
|
|
498
511
|
const report = parseReconciliationReport(loadWorkflowStepOutput(reconciled.stateDir, 'reconcile'))
|
|
499
|
-
const
|
|
500
|
-
|
|
512
|
+
const versions = { pipelineVersion: report.pipelineVersion, analyzerVersions: report.analyzerVersions }
|
|
513
|
+
runWorkflow(workflowOptions(root, config, report.sourceRevision, 'evaluate', { evaluate: () => evaluateRules(report, { ...(config.rules ? { config: config.rules } : {}) }) }, versions))
|
|
514
|
+
return runWorkflow(workflowOptions(root, config, report.sourceRevision, 'report', { report: ({ input }) => input }, versions))
|
|
501
515
|
}
|
|
502
516
|
|
|
503
517
|
const workflowOutput = (result: WorkflowExecutionResult): Record<string, unknown> => {
|
|
@@ -593,7 +607,7 @@ const runWorkflowCommand = (
|
|
|
593
607
|
const thresholdValue = optionValues(argv, '--report-threshold')[0]
|
|
594
608
|
const thresholdBytes = thresholdValue === undefined ? undefined : Number(thresholdValue)
|
|
595
609
|
if (thresholdBytes !== undefined && (!Number.isSafeInteger(thresholdBytes) || thresholdBytes < 1)) throw new Error('--report-threshold must be a positive integer.')
|
|
596
|
-
const artifact = renderOfflineReportArtifact({ snapshot, report }, { ...(thresholdBytes === undefined ? {} : { thresholdBytes }) })
|
|
610
|
+
const artifact = renderOfflineReportArtifact({ snapshot, report }, { ...(config.report?.privacy ? { privacy: config.report.privacy } : {}), ...(thresholdBytes === undefined ? {} : { thresholdBytes }) })
|
|
597
611
|
output.htmlPath = writeReportArtifact(htmlPath, artifact)
|
|
598
612
|
output.htmlMode = artifact.mode
|
|
599
613
|
}
|
|
@@ -707,10 +721,19 @@ const runSuggestCommand = async (flags: ReadonlySet<string>, configPath: string
|
|
|
707
721
|
}
|
|
708
722
|
|
|
709
723
|
const writeIfMissing = (path: string, contents: string): boolean => {
|
|
710
|
-
if (existsSync(path)) return false
|
|
711
724
|
mkdirSync(dirname(path), { recursive: true })
|
|
712
|
-
|
|
713
|
-
|
|
725
|
+
try {
|
|
726
|
+
const fd = openSync(path, 'wx')
|
|
727
|
+
try {
|
|
728
|
+
writeFileSync(fd, contents, 'utf8')
|
|
729
|
+
return true
|
|
730
|
+
} finally {
|
|
731
|
+
closeSync(fd)
|
|
732
|
+
}
|
|
733
|
+
} catch (error) {
|
|
734
|
+
if ((error as NodeJS.ErrnoException).code === 'EEXIST') return false
|
|
735
|
+
throw error
|
|
736
|
+
}
|
|
714
737
|
}
|
|
715
738
|
|
|
716
739
|
const demoOwnership = {
|
|
@@ -947,6 +970,26 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
947
970
|
}
|
|
948
971
|
}
|
|
949
972
|
|
|
973
|
+
if (command === 'benchmark') {
|
|
974
|
+
const fixturePath = positional[1]
|
|
975
|
+
const observationPath = positional[2]
|
|
976
|
+
if (!fixturePath || !observationPath) {
|
|
977
|
+
process.stderr.write('Usage: ak-docs benchmark <fixture.json> <observation.json> [--text|--json]\n')
|
|
978
|
+
return 1
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
const fixture = benchmarkFixture(JSON.parse(readFileSync(resolve(fixturePath), 'utf8')) as unknown)
|
|
982
|
+
const observation = JSON.parse(readFileSync(resolve(observationPath), 'utf8')) as Parameters<typeof measureBenchmark>[0]
|
|
983
|
+
const result = measureBenchmark(observation, fixture)
|
|
984
|
+
if (flags.has('--text')) writeLines([formatBenchmarkText(result)])
|
|
985
|
+
else writeJson(result)
|
|
986
|
+
return result.regressions.length ? 1 : 0
|
|
987
|
+
} catch (error) {
|
|
988
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
989
|
+
return 2
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
950
993
|
if (command === 'scan' || command === 'reconcile' || command === 'check' || command === 'map') {
|
|
951
994
|
return runWorkflowCommand(command, flags, configPath, argv)
|
|
952
995
|
}
|
package/src/config/index.ts
CHANGED
|
@@ -15,9 +15,11 @@ export {
|
|
|
15
15
|
DocumentationStandardV1ConfigSchema,
|
|
16
16
|
EcosystemContractEvidenceSchema,
|
|
17
17
|
ConformanceConfigSchema,
|
|
18
|
+
ReportConfigSchema,
|
|
18
19
|
type DocBridgeConfigV1,
|
|
19
20
|
type AgentCorpusConfig,
|
|
20
21
|
type DocumentationStandardRuleId,
|
|
21
22
|
type DocumentationStandardV1Config,
|
|
22
23
|
type ReconciliationConfig,
|
|
24
|
+
type ReportConfig,
|
|
23
25
|
} from './schema.js'
|
|
@@ -79,7 +79,13 @@ const parseConfig = (input: unknown): DocBridgeConfigV1 => {
|
|
|
79
79
|
if (result.success) return result.data
|
|
80
80
|
throw new Error(
|
|
81
81
|
`Invalid doc-bridge config:\n${result.error.issues.map((issue) =>
|
|
82
|
-
` - ${issue.path.join('.') || '(root)'}: ${
|
|
82
|
+
` - ${issue.path.join('.') || '(root)'}: ${
|
|
83
|
+
issue.code === 'invalid_type' && issue.message.endsWith('received undefined')
|
|
84
|
+
? 'Required'
|
|
85
|
+
: issue.code === 'invalid_value' && 'values' in issue
|
|
86
|
+
? 'Invalid enum value'
|
|
87
|
+
: issue.message
|
|
88
|
+
}`,
|
|
83
89
|
).join('\n')}`,
|
|
84
90
|
)
|
|
85
91
|
}
|