@agentskit/doc-bridge 1.6.4 → 1.7.44
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 +243 -0
- package/action.yml +1 -1
- package/dist/cli/program.js +793 -137
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +38 -2
- package/dist/config/index.js.map +1 -1
- package/dist/{index-DudNuwI5.d.ts → index-C2PCQSrB.d.ts} +216 -25
- package/dist/index.d.ts +837 -67
- package/dist/index.js +858 -127
- package/dist/index.js.map +1 -1
- package/docs/PRD-enterprise-hardening.md +288 -0
- package/docs/adr/0001-enterprise-verification-contract.md +35 -0
- package/docs/knowledge-engine-runbook.md +18 -2
- package/docs/spec/analyzer-plugin-v1.md +24 -0
- package/docs/spec/benchmark-v1.md +30 -0
- package/docs/spec/config-v1.md +111 -0
- package/docs/validation-cycle-plan.md +236 -0
- package/docs/verification-harness.md +33 -4
- package/mcpb/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/report-visual-check.mjs +45 -10
- package/scripts/verification-harness.mjs +216 -13
- package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
- package/src/agents/registry-adapter.ts +31 -7
- package/src/cli/program.ts +44 -11
- package/src/config/index.ts +2 -0
- package/src/config/schema.ts +56 -0
- package/src/discovery/documentation.ts +46 -5
- package/src/discovery/repository.ts +95 -16
- package/src/index.ts +29 -0
- package/src/metrics/benchmark.ts +176 -0
- package/src/plugins/contract.ts +89 -0
- package/src/reconciliation/reconcile.ts +137 -3
- package/src/report/html.ts +302 -78
- package/src/schemas/knowledge.ts +16 -1
- package/src/version.ts +1 -1
- package/src/workflow/engine.ts +65 -9
|
@@ -15,17 +15,39 @@ import {
|
|
|
15
15
|
} from 'node:fs'
|
|
16
16
|
import { dirname, join, relative, resolve, sep } from 'node:path'
|
|
17
17
|
|
|
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'])
|
|
18
|
+
const VERSION = '1.4.0'
|
|
19
|
+
const STATES = new Set(['CLARIFYING', 'PLANNED', 'VERIFYING', 'AWAITING_HUMAN_APPROVAL', 'AWAITING_AUTHORIZATION', 'COMPLETE', 'BLOCKED', 'FAILED'])
|
|
20
|
+
const PROFILES = new Set(['default', 'strict', 'poc', 'custom', 'enterprise'])
|
|
21
|
+
const SURFACES = ['logic', 'endpoint', 'database', 'cli', 'mcp', 'ui', 'docs']
|
|
22
|
+
const PROFILE_POLICIES = {
|
|
23
|
+
default: { requiresExplicitExemptions: false, requiresAllSurfaces: false, requiresMeasurement: false, requiresTracking: false },
|
|
24
|
+
strict: { requiresExplicitExemptions: false, requiresAllSurfaces: false, requiresMeasurement: false, requiresTracking: false },
|
|
25
|
+
poc: { requiresExplicitExemptions: true, requiresAllSurfaces: false, requiresMeasurement: false, requiresTracking: false },
|
|
26
|
+
custom: { requiresExplicitExemptions: true, requiresAllSurfaces: false, requiresMeasurement: false, requiresTracking: false },
|
|
27
|
+
enterprise: { requiresExplicitExemptions: false, requiresAllSurfaces: true, requiresMeasurement: true, requiresTracking: true },
|
|
28
|
+
}
|
|
21
29
|
const CATEGORIES = new Set(['build', 'test', 'lint', 'logic', 'endpoint', 'database', 'cli', 'mcp', 'ui', 'docs', 'custom'])
|
|
22
30
|
const INTENTS = new Set(['ok', 'certo', 'certa', 'aprovado', 'aprovada', 'approve', 'approved', 'confirmo', 'confirmado', 'confirmed', 'yes'])
|
|
23
31
|
const DEFAULT_CONFIG = '.codex/verification.json'
|
|
32
|
+
const LEGAL_TRANSITIONS = {
|
|
33
|
+
null: ['PLANNED'],
|
|
34
|
+
CLARIFYING: ['PLANNED', 'BLOCKED', 'FAILED'],
|
|
35
|
+
PLANNED: ['CLARIFYING', 'VERIFYING', 'BLOCKED', 'FAILED'],
|
|
36
|
+
VERIFYING: ['AWAITING_HUMAN_APPROVAL', 'AWAITING_AUTHORIZATION', 'COMPLETE', 'BLOCKED', 'FAILED'],
|
|
37
|
+
AWAITING_HUMAN_APPROVAL: ['AWAITING_AUTHORIZATION', 'COMPLETE', 'BLOCKED', 'FAILED'],
|
|
38
|
+
AWAITING_AUTHORIZATION: ['COMPLETE', 'BLOCKED', 'FAILED'],
|
|
39
|
+
BLOCKED: [],
|
|
40
|
+
FAILED: [],
|
|
41
|
+
COMPLETE: [],
|
|
42
|
+
}
|
|
24
43
|
|
|
25
44
|
const fail = (message) => { throw new Error(message) }
|
|
26
45
|
const hash = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex')
|
|
27
46
|
const now = () => new Date().toISOString()
|
|
28
47
|
const readJson = (path) => JSON.parse(readFileSync(path, 'utf8'))
|
|
48
|
+
const assertKnownKeys = (value, allowed, label) => {
|
|
49
|
+
for (const key of Object.keys(value ?? {})) if (!allowed.has(key)) fail(`${label}.${key} is not supported.`)
|
|
50
|
+
}
|
|
29
51
|
const writeAtomic = (path, value) => {
|
|
30
52
|
mkdirSync(dirname(path), { recursive: true })
|
|
31
53
|
const temp = `${path}.tmp-${process.pid}`
|
|
@@ -58,6 +80,7 @@ const inside = (root, path) => {
|
|
|
58
80
|
const surfaceRequired = (value, name) => {
|
|
59
81
|
if (typeof value === 'boolean') return { required: value, reason: value ? undefined : `${name} is not part of this verification target.` }
|
|
60
82
|
if (!value || typeof value !== 'object' || Array.isArray(value)) fail(`surfaces.${name} must be a boolean or { required, reason }.`)
|
|
83
|
+
assertKnownKeys(value, new Set(['required', 'reason']), `surfaces.${name}`)
|
|
61
84
|
if (typeof value.required !== 'boolean') fail(`surfaces.${name}.required must be boolean.`)
|
|
62
85
|
if (!value.required && typeof value.reason !== 'string') fail(`surfaces.${name}.reason is required when the surface is not required.`)
|
|
63
86
|
return { required: value.required, ...(value.reason ? { reason: value.reason } : {}) }
|
|
@@ -65,35 +88,91 @@ const surfaceRequired = (value, name) => {
|
|
|
65
88
|
|
|
66
89
|
const validateConfig = (raw, configPath) => {
|
|
67
90
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) fail(`Invalid verification config: ${configPath}`)
|
|
91
|
+
assertKnownKeys(raw, new Set(['schemaVersion', 'project', 'root', 'profile', 'contract', 'surfaces', 'checks', 'exemptions', 'measurement', 'tracking', 'cleanup', 'overrides']), 'verification config')
|
|
68
92
|
if (raw.schemaVersion !== 1) fail('verification config schemaVersion must be 1.')
|
|
69
93
|
if (typeof raw.project !== 'string' || !raw.project) fail('verification config project is required.')
|
|
70
94
|
if (raw.root !== undefined && typeof raw.root !== 'string') fail('verification config root must be a string.')
|
|
71
|
-
|
|
95
|
+
const profile = raw.profile ?? 'default'
|
|
96
|
+
if (!PROFILES.has(profile)) fail(`verification config profile must be one of: ${[...PROFILES].join(', ')}.`)
|
|
97
|
+
const policy = PROFILE_POLICIES[profile]
|
|
98
|
+
if (raw.overrides !== undefined) {
|
|
99
|
+
if (!raw.overrides || typeof raw.overrides !== 'object' || Array.isArray(raw.overrides)) fail('overrides must be an object.')
|
|
100
|
+
assertKnownKeys(raw.overrides, new Set(['profile']), 'overrides')
|
|
101
|
+
if (raw.overrides.profile !== undefined) fail('overrides.profile is not supported; set profile explicitly.')
|
|
102
|
+
}
|
|
72
103
|
if (!Array.isArray(raw.checks) || raw.checks.length === 0) fail('verification config requires at least one check.')
|
|
73
104
|
const checks = raw.checks.map((check, index) => {
|
|
74
105
|
if (!check || typeof check !== 'object' || Array.isArray(check)) fail(`checks[${index}] must be an object.`)
|
|
106
|
+
assertKnownKeys(check, new Set(['id', 'category', 'command', 'required', 'timeoutMs', 'execution', 'capabilities']), `checks[${index}]`)
|
|
75
107
|
if (typeof check.id !== 'string' || !check.id) fail(`checks[${index}].id is required.`)
|
|
76
108
|
if (typeof check.command !== 'string' || !check.command) fail(`checks[${index}].command is required.`)
|
|
77
109
|
if (!CATEGORIES.has(check.category)) fail(`checks[${index}].category is invalid.`)
|
|
78
110
|
if (check.required !== undefined && typeof check.required !== 'boolean') fail(`checks[${index}].required must be boolean.`)
|
|
79
111
|
if (check.timeoutMs !== undefined && (!Number.isInteger(check.timeoutMs) || check.timeoutMs < 1)) fail(`checks[${index}].timeoutMs must be a positive integer.`)
|
|
80
112
|
if (['endpoint', 'database', 'cli', 'mcp', 'ui'].includes(check.category) && check.execution !== 'real') fail(`checks[${index}] requires execution: "real".`)
|
|
113
|
+
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.`)
|
|
114
|
+
if (check.category === 'ui') {
|
|
115
|
+
for (const capability of ['real-browser', 'screenshot']) if (!check.capabilities?.includes(capability)) fail(`checks[${index}] requires capability "${capability}".`)
|
|
116
|
+
}
|
|
81
117
|
return { required: true, timeoutMs: 120_000, ...check }
|
|
82
118
|
})
|
|
119
|
+
if (new Set(checks.map((check) => check.id)).size !== checks.length) fail('check ids must be unique.')
|
|
120
|
+
if (!raw.contract || typeof raw.contract !== 'object' || Array.isArray(raw.contract)) fail('verification contract is required.')
|
|
121
|
+
assertKnownKeys(raw.contract, new Set(['intent', 'outcomes']), 'contract')
|
|
122
|
+
if (typeof raw.contract.intent !== 'string' || !raw.contract.intent.trim()) fail('contract.intent is required.')
|
|
123
|
+
if (!Array.isArray(raw.contract.outcomes) || raw.contract.outcomes.length === 0) fail('contract.outcomes requires at least one outcome.')
|
|
124
|
+
const checkIds = new Set(checks.map((check) => check.id))
|
|
125
|
+
const outcomes = raw.contract.outcomes.map((outcome, index) => {
|
|
126
|
+
if (!outcome || typeof outcome !== 'object' || Array.isArray(outcome)) fail(`contract.outcomes[${index}] must be an object.`)
|
|
127
|
+
assertKnownKeys(outcome, new Set(['id', 'statement', 'checks']), `contract.outcomes[${index}]`)
|
|
128
|
+
if (typeof outcome.id !== 'string' || !outcome.id.trim()) fail(`contract.outcomes[${index}].id is required.`)
|
|
129
|
+
if (typeof outcome.statement !== 'string' || !outcome.statement.trim()) fail(`contract.outcomes[${index}].statement is required.`)
|
|
130
|
+
if (!Array.isArray(outcome.checks) || outcome.checks.length === 0) fail(`contract.outcomes[${index}].checks requires at least one check id.`)
|
|
131
|
+
if (outcome.checks.some((checkId) => typeof checkId !== 'string' || !checkIds.has(checkId))) fail(`contract.outcomes[${index}] references an unknown check.`)
|
|
132
|
+
if (outcome.checks.some((checkId) => !checks.find((check) => check.id === checkId)?.required)) fail(`contract.outcomes[${index}] references a non-required check.`)
|
|
133
|
+
return { id: outcome.id, statement: outcome.statement, checks: [...new Set(outcome.checks)] }
|
|
134
|
+
})
|
|
135
|
+
if (new Set(outcomes.map((outcome) => outcome.id)).size !== outcomes.length) fail('contract outcome ids must be unique.')
|
|
136
|
+
if (raw.surfaces !== undefined && (!raw.surfaces || typeof raw.surfaces !== 'object' || Array.isArray(raw.surfaces))) fail('surfaces must be an object.')
|
|
137
|
+
if (raw.surfaces) for (const name of Object.keys(raw.surfaces)) if (!SURFACES.includes(name)) fail(`surfaces.${name} is not supported.`)
|
|
83
138
|
const surfaces = {}
|
|
84
|
-
for (const name of
|
|
139
|
+
for (const name of SURFACES) {
|
|
140
|
+
if (policy.requiresAllSurfaces && !Object.hasOwn(raw.surfaces ?? {}, name)) fail(`enterprise profile requires surfaces.${name} to be declared.`)
|
|
141
|
+
surfaces[name] = surfaceRequired(raw.surfaces?.[name] ?? (name === 'logic'), name)
|
|
142
|
+
}
|
|
85
143
|
for (const [name, surface] of Object.entries(surfaces)) {
|
|
86
144
|
const matching = checks.some((check) => check.category === name && check.required)
|
|
87
145
|
if (surface.required && !matching) fail(`Required surface "${name}" has no required check.`)
|
|
88
146
|
}
|
|
89
|
-
if (
|
|
147
|
+
if (policy.requiresExplicitExemptions && (!Array.isArray(raw.exemptions) || raw.exemptions.length === 0)) fail('This profile requires explicit exemptions.')
|
|
90
148
|
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
|
|
149
|
+
const measurement = raw.measurement ?? { required: false }
|
|
150
|
+
if (!measurement || typeof measurement !== 'object' || Array.isArray(measurement)) fail('measurement must be an object.')
|
|
151
|
+
assertKnownKeys(measurement, new Set(['required', 'checkId', 'baseline']), 'measurement')
|
|
152
|
+
if (typeof measurement.required !== 'boolean') fail('measurement.required must be boolean.')
|
|
153
|
+
if (policy.requiresMeasurement && !measurement.required) fail('enterprise profile requires measurement.required to be true.')
|
|
154
|
+
if (measurement.required) {
|
|
155
|
+
if (typeof measurement.checkId !== 'string' || !measurement.checkId.trim()) fail('measurement.checkId is required when measurement is required.')
|
|
156
|
+
const measurementCheck = checks.find((check) => check.id === measurement.checkId)
|
|
157
|
+
if (!measurementCheck) fail(`measurement.checkId references an unknown check: ${measurement.checkId}.`)
|
|
158
|
+
if (!measurementCheck.required) fail('measurement.checkId must reference a required check.')
|
|
159
|
+
if (typeof measurement.baseline !== 'string' || !measurement.baseline.trim()) fail('measurement.baseline is required when measurement is required.')
|
|
160
|
+
}
|
|
161
|
+
const tracking = raw.tracking ?? (policy.requiresTracking
|
|
162
|
+
? { required: true, authorization: 'ask' }
|
|
163
|
+
: { required: false, reason: 'tracking is not configured for this run.' })
|
|
164
|
+
assertKnownKeys(tracking, new Set(['required', 'authorization', 'target', 'reason']), 'tracking')
|
|
92
165
|
if (typeof tracking.required !== 'boolean') fail('tracking.required must be boolean.')
|
|
166
|
+
if (policy.requiresTracking && !tracking.required) fail('enterprise profile requires tracking.required to be true.')
|
|
93
167
|
if (tracking.required && tracking.authorization !== 'ask') fail('tracking.authorization must be "ask".')
|
|
94
168
|
if (tracking.required && typeof tracking.target !== 'string') fail('tracking.target is required when tracking is required.')
|
|
95
169
|
if (!tracking.required && typeof tracking.reason !== 'string') fail('tracking.reason is required when tracking is not required.')
|
|
96
|
-
|
|
170
|
+
if (raw.cleanup !== undefined) {
|
|
171
|
+
if (!raw.cleanup || typeof raw.cleanup !== 'object' || Array.isArray(raw.cleanup)) fail('cleanup must be an object.')
|
|
172
|
+
assertKnownKeys(raw.cleanup, new Set(['roots']), 'cleanup')
|
|
173
|
+
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.')
|
|
174
|
+
}
|
|
175
|
+
return { ...raw, profile, checks, contract: { intent: raw.contract.intent.trim(), outcomes }, surfaces, tracking, measurement, configPath, profilePolicy: policy }
|
|
97
176
|
}
|
|
98
177
|
|
|
99
178
|
const sourceRevision = (root) => {
|
|
@@ -143,6 +222,53 @@ const commandResult = (root, check) => new Promise((resolveResult) => {
|
|
|
143
222
|
})
|
|
144
223
|
})
|
|
145
224
|
|
|
225
|
+
const validateUiEvidence = (root, run) => ({
|
|
226
|
+
...run,
|
|
227
|
+
checks: run.checks.map((check) => {
|
|
228
|
+
if (check.category !== 'ui' || check.status === 'failed') return check
|
|
229
|
+
const evidence = check.verification
|
|
230
|
+
const failures = []
|
|
231
|
+
if (!evidence || evidence.capability !== 'real-browser') failures.push('UI evidence must declare capability "real-browser"')
|
|
232
|
+
if (!Array.isArray(evidence?.artifacts) || evidence.artifacts.length === 0) failures.push('UI evidence must include screenshot artifacts')
|
|
233
|
+
for (const artifact of evidence?.artifacts ?? []) {
|
|
234
|
+
if (artifact?.type !== 'screenshot' || typeof artifact.path !== 'string' || typeof artifact.sha256 !== 'string' || typeof artifact.viewport !== 'string') {
|
|
235
|
+
failures.push('Each UI artifact must include type=screenshot, path, sha256, and viewport')
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
const path = resolve(root, artifact.path)
|
|
239
|
+
if (!inside(root, path) || !existsSync(path)) failures.push(`Screenshot artifact is missing or outside the project: ${artifact.path}`)
|
|
240
|
+
else if (createHash('sha256').update(readFileSync(path)).digest('hex') !== artifact.sha256) failures.push(`Screenshot hash mismatch: ${artifact.path}`)
|
|
241
|
+
}
|
|
242
|
+
const criterionIds = run.contract.outcomes.filter((outcome) => outcome.checks.includes(check.id)).map((outcome) => outcome.id)
|
|
243
|
+
if (!evidence?.criteria || typeof evidence.criteria !== 'object' || Array.isArray(evidence.criteria)) failures.push('UI evidence must include criterion-level results')
|
|
244
|
+
for (const id of criterionIds) if (evidence?.criteria?.[id]?.status !== 'passed') failures.push(`UI criterion did not pass: ${id}`)
|
|
245
|
+
if (!failures.length) return check
|
|
246
|
+
return { ...check, status: 'failed', verificationStatus: 'failed', verification: { ...(evidence ?? {}), status: 'failed', failures } }
|
|
247
|
+
}),
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const validateMeasurementResult = (run, measurement) => {
|
|
251
|
+
if (!measurement.required) return run
|
|
252
|
+
const check = run.checks.find((item) => item.id === measurement.checkId)
|
|
253
|
+
const verification = check?.verification
|
|
254
|
+
const failures = []
|
|
255
|
+
if (check?.status !== 'passed') failures.push(`measurement check ${measurement.checkId} did not pass`)
|
|
256
|
+
if (!verification || typeof verification.metrics !== 'object' || Array.isArray(verification.metrics)) failures.push('measurement evidence must include an object named metrics')
|
|
257
|
+
if (typeof verification?.baselineHash !== 'string' || !verification.baselineHash) failures.push('measurement evidence must include baselineHash')
|
|
258
|
+
if (!Array.isArray(verification?.regressions)) failures.push('measurement evidence must include a regressions array')
|
|
259
|
+
else if (verification.regressions.length) failures.push(`measurement regressions detected: ${verification.regressions.join('; ')}`)
|
|
260
|
+
if (!failures.length) return run
|
|
261
|
+
return {
|
|
262
|
+
...run,
|
|
263
|
+
checks: run.checks.map((item) => item.id === measurement.checkId ? {
|
|
264
|
+
...item,
|
|
265
|
+
status: 'failed',
|
|
266
|
+
verificationStatus: 'failed',
|
|
267
|
+
verification: { status: 'failed', failures },
|
|
268
|
+
} : item),
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
146
272
|
const stateDirFor = (root) => join(root, '.codex', 'verification')
|
|
147
273
|
const runDirFor = (root, runId) => join(stateDirFor(root), 'runs', runId)
|
|
148
274
|
const latestPathFor = (root) => join(stateDirFor(root), 'latest.json')
|
|
@@ -152,7 +278,23 @@ const saveRun = (root, run) => {
|
|
|
152
278
|
writeAtomic(join(dir, 'run.json'), run)
|
|
153
279
|
writeAtomic(latestPathFor(root), { runId: run.runId, state: run.state, path: relative(root, join(dir, 'run.json')), updatedAt: now() })
|
|
154
280
|
}
|
|
155
|
-
const transition = (run, state, reason) =>
|
|
281
|
+
const transition = (run, state, reason) => {
|
|
282
|
+
if (!STATES.has(state)) fail(`Unknown verification state: ${state}.`)
|
|
283
|
+
const allowed = LEGAL_TRANSITIONS[String(run.state)] ?? []
|
|
284
|
+
if (!allowed.includes(state)) fail(`Illegal verification transition ${run.state} -> ${state}.`)
|
|
285
|
+
return { ...run, state, transitions: [...run.transitions, { from: run.state, to: state, at: now(), ...(reason ? { reason } : {}) }] }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const attachEvidence = (run) => {
|
|
289
|
+
const evidenceReferences = run.checks.map((check) => ({
|
|
290
|
+
checkId: check.id,
|
|
291
|
+
status: check.status,
|
|
292
|
+
...(check.verificationStatus ? { verificationStatus: check.verificationStatus } : {}),
|
|
293
|
+
evidenceHash: hash({ id: check.id, status: check.status, stdout: check.stdout, stderr: check.stderr, verification: check.verification }),
|
|
294
|
+
}))
|
|
295
|
+
const metrics = Object.fromEntries(run.checks.flatMap((check) => Object.entries(check.verification?.metrics ?? {})))
|
|
296
|
+
return { ...run, evidenceReferences, metrics, outputHash: hash({ checks: run.checks, outcomes: run.outcomes, evidenceReferences, metrics }) }
|
|
297
|
+
}
|
|
156
298
|
|
|
157
299
|
const runVerification = async (root, config, runId) => {
|
|
158
300
|
const source = sourceRevision(root)
|
|
@@ -173,12 +315,16 @@ const runVerification = async (root, config, runId) => {
|
|
|
173
315
|
runId: id,
|
|
174
316
|
project: config.project,
|
|
175
317
|
profile: config.profile,
|
|
318
|
+
profilePolicy: config.profilePolicy,
|
|
176
319
|
sourceRevision: source,
|
|
320
|
+
contractHash: hash(config.contract),
|
|
177
321
|
inputHash,
|
|
178
322
|
state: 'PLANNED',
|
|
179
323
|
configPath: relative(root, config.configPath),
|
|
180
|
-
|
|
324
|
+
contract: config.contract,
|
|
325
|
+
checks: config.checks.map(({ id: checkId, category, command, required, timeoutMs, capabilities }) => ({ id: checkId, category, command, required, timeoutMs, ...(capabilities ? { capabilities } : {}), status: 'pending' })),
|
|
181
326
|
surfaces: config.surfaces,
|
|
327
|
+
applicability: config.surfaces,
|
|
182
328
|
tracking: config.tracking,
|
|
183
329
|
exemptions: config.exemptions ?? [],
|
|
184
330
|
transitions: [{ from: null, to: 'PLANNED', at: now() }],
|
|
@@ -191,8 +337,29 @@ const runVerification = async (root, config, runId) => {
|
|
|
191
337
|
run = { ...run, checks: run.checks.map((item, itemIndex) => itemIndex === index ? { ...item, ...result } : item) }
|
|
192
338
|
saveRun(root, run)
|
|
193
339
|
}
|
|
340
|
+
run = validateUiEvidence(root, run)
|
|
341
|
+
run = {
|
|
342
|
+
...run,
|
|
343
|
+
outcomes: run.contract.outcomes.map((outcome) => {
|
|
344
|
+
const checks = run.checks.filter((check) => outcome.checks.includes(check.id))
|
|
345
|
+
const status = checks.some((check) => check.status === 'failed') ? 'failed' : checks.some((check) => check.status === 'awaiting-human-approval') ? 'awaiting-human-approval' : 'passed'
|
|
346
|
+
return { ...outcome, status }
|
|
347
|
+
}),
|
|
348
|
+
}
|
|
349
|
+
run = validateMeasurementResult(run, config.measurement)
|
|
350
|
+
run = {
|
|
351
|
+
...run,
|
|
352
|
+
outcomes: run.contract.outcomes.map((outcome) => {
|
|
353
|
+
const checks = run.checks.filter((check) => outcome.checks.includes(check.id))
|
|
354
|
+
const status = checks.some((check) => check.status === 'failed') ? 'failed' : checks.some((check) => check.status === 'awaiting-human-approval') ? 'awaiting-human-approval' : 'passed'
|
|
355
|
+
return { ...outcome, status }
|
|
356
|
+
}),
|
|
357
|
+
}
|
|
358
|
+
run = attachEvidence(run)
|
|
359
|
+
const failedOutcomes = run.outcomes.filter((outcome) => outcome.status === 'failed')
|
|
194
360
|
const failed = run.checks.filter((check) => check.required && !['passed', 'awaiting-human-approval'].includes(check.status))
|
|
195
361
|
if (failed.length) run = transition(run, 'BLOCKED', `Required checks failed: ${failed.map((check) => check.id).join(', ')}`)
|
|
362
|
+
else if (failedOutcomes.length) run = transition(run, 'BLOCKED', `Contract outcomes failed: ${failedOutcomes.map((outcome) => outcome.id).join(', ')}`)
|
|
196
363
|
else if (config.surfaces.ui.required) run = transition(run, 'AWAITING_HUMAN_APPROVAL', 'Visual UI approval is required.')
|
|
197
364
|
else if (config.tracking.required) run = transition(run, 'AWAITING_AUTHORIZATION', `Tracking authorization is required for ${config.tracking.target}.`)
|
|
198
365
|
else run = transition(run, 'COMPLETE', 'All configured verification gates passed.')
|
|
@@ -211,13 +378,43 @@ const updateApproval = (root, run, type, rawIntent, by) => {
|
|
|
211
378
|
if (run.state !== expectedState) fail(`Cannot record ${type} while run is ${run.state}.`)
|
|
212
379
|
const approvedIntent = intent(rawIntent)
|
|
213
380
|
const dir = runDirFor(root, run.runId)
|
|
214
|
-
writeAtomic(join(dir, `${type}.json`), {
|
|
381
|
+
writeAtomic(join(dir, `${type}.json`), {
|
|
382
|
+
type,
|
|
383
|
+
runId: run.runId,
|
|
384
|
+
runInputHash: run.inputHash,
|
|
385
|
+
runSourceRevision: run.sourceRevision,
|
|
386
|
+
runContractHash: run.contractHash,
|
|
387
|
+
runOutputHash: run.outputHash,
|
|
388
|
+
intent: approvedIntent,
|
|
389
|
+
by: by ?? 'human',
|
|
390
|
+
at: now(),
|
|
391
|
+
})
|
|
392
|
+
if (type === 'human-approval') run = { ...run, outcomes: run.outcomes.map((outcome) => outcome.status === 'awaiting-human-approval' ? { ...outcome, status: 'passed' } : outcome) }
|
|
215
393
|
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
394
|
if (type === 'tracking-authorization' && run.state === 'AWAITING_AUTHORIZATION') run = transition(run, 'COMPLETE', `Tracking authorization recorded for ${run.tracking.target}.`)
|
|
217
395
|
saveRun(root, run)
|
|
218
396
|
return run
|
|
219
397
|
}
|
|
220
398
|
|
|
399
|
+
const replaceBaseline = (root, config, sourcePath, rawIntent, by) => {
|
|
400
|
+
if (!config.measurement.required) fail('Baseline replacement requires measurement.required to be true.')
|
|
401
|
+
if (!config.measurement.baseline) fail('Baseline replacement requires measurement.baseline.')
|
|
402
|
+
if (!by) fail('Baseline replacement requires --by.')
|
|
403
|
+
const approvedIntent = intent(rawIntent)
|
|
404
|
+
const target = resolve(root, config.measurement.baseline)
|
|
405
|
+
const source = resolve(root, sourcePath ?? '')
|
|
406
|
+
if (!inside(root, target) || !inside(root, source)) fail('Baseline source and target must be inside the project root.')
|
|
407
|
+
if (!existsSync(source)) fail(`Baseline source not found: ${sourcePath}`)
|
|
408
|
+
if (source === target) fail('Baseline source must differ from the configured baseline target.')
|
|
409
|
+
const value = readJson(source)
|
|
410
|
+
writeAtomic(target, value)
|
|
411
|
+
const auditPath = join(stateDirFor(root), 'baseline-audit.jsonl')
|
|
412
|
+
mkdirSync(dirname(auditPath), { recursive: true })
|
|
413
|
+
const entry = { action: 'replace-baseline', source: relative(root, source), target: relative(root, target), baselineHash: hash(value), intent: approvedIntent, by, at: now() }
|
|
414
|
+
appendFileSync(auditPath, `${JSON.stringify(entry)}\n`, 'utf8')
|
|
415
|
+
return { status: 'baseline-replaced', ...entry }
|
|
416
|
+
}
|
|
417
|
+
|
|
221
418
|
const clean = (root, config, periodic) => {
|
|
222
419
|
const manifestPath = join(stateDirFor(root), 'owned-artifacts.json')
|
|
223
420
|
if (!existsSync(manifestPath)) return { removed: [], skipped: [], periodic, message: 'No task-owned artifacts are registered.' }
|
|
@@ -240,13 +437,19 @@ const main = async (argv) => {
|
|
|
240
437
|
const { flags, values, positional } = parseArgs(argv)
|
|
241
438
|
const command = positional[0] ?? 'help'
|
|
242
439
|
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')
|
|
440
|
+
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
441
|
return 0
|
|
245
442
|
}
|
|
246
443
|
const configPath = resolve(values.get('config') ?? DEFAULT_CONFIG)
|
|
247
444
|
if (!existsSync(configPath)) fail(`Verification contract not found: ${configPath}`)
|
|
248
445
|
const root = projectRoot(configPath, readJson(configPath))
|
|
249
446
|
const config = validateConfig(readJson(configPath), configPath)
|
|
447
|
+
if (command === 'baseline') {
|
|
448
|
+
if (positional[1] !== 'replace') fail('Use: baseline replace <source> <intent> --by <actor>.')
|
|
449
|
+
const result = replaceBaseline(root, config, positional[2], positional[3], values.get('by'))
|
|
450
|
+
output(result, flags.has('json'))
|
|
451
|
+
return 0
|
|
452
|
+
}
|
|
250
453
|
if (command === 'run') {
|
|
251
454
|
const run = await runVerification(root, config, values.get('run-id'))
|
|
252
455
|
output(run, flags.has('json'))
|
|
@@ -277,4 +480,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
277
480
|
catch (error) { process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); process.exitCode = 2 }
|
|
278
481
|
}
|
|
279
482
|
|
|
280
|
-
export { INTENTS, main, validateConfig }
|
|
483
|
+
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/program.ts
CHANGED
|
@@ -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,23 @@ 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?.includeOrphanedDocuments === undefined ? {} : { includeOrphanedDocuments: config.reconciliation.includeOrphanedDocuments }),
|
|
504
|
+
})
|
|
505
|
+
return runWorkflow(workflowOptions(root, config, snapshot.sourceRevision, 'reconcile', { reconcile: () => report }, { pipelineVersion: snapshot.pipelineVersion, analyzerVersions: snapshot.analyzerVersions }))
|
|
494
506
|
}
|
|
495
507
|
|
|
496
508
|
const checkWorkflow = (root: string, config: DocBridgeConfigV1): WorkflowExecutionResult => {
|
|
497
509
|
const reconciled = reconcileWorkflow(root, config)
|
|
498
510
|
const report = parseReconciliationReport(loadWorkflowStepOutput(reconciled.stateDir, 'reconcile'))
|
|
499
|
-
const
|
|
500
|
-
|
|
511
|
+
const versions = { pipelineVersion: report.pipelineVersion, analyzerVersions: report.analyzerVersions }
|
|
512
|
+
runWorkflow(workflowOptions(root, config, report.sourceRevision, 'evaluate', { evaluate: () => evaluateRules(report, { ...(config.rules ? { config: config.rules } : {}) }) }, versions))
|
|
513
|
+
return runWorkflow(workflowOptions(root, config, report.sourceRevision, 'report', { report: ({ input }) => input }, versions))
|
|
501
514
|
}
|
|
502
515
|
|
|
503
516
|
const workflowOutput = (result: WorkflowExecutionResult): Record<string, unknown> => {
|
|
@@ -593,7 +606,7 @@ const runWorkflowCommand = (
|
|
|
593
606
|
const thresholdValue = optionValues(argv, '--report-threshold')[0]
|
|
594
607
|
const thresholdBytes = thresholdValue === undefined ? undefined : Number(thresholdValue)
|
|
595
608
|
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 }) })
|
|
609
|
+
const artifact = renderOfflineReportArtifact({ snapshot, report }, { ...(config.report?.privacy ? { privacy: config.report.privacy } : {}), ...(thresholdBytes === undefined ? {} : { thresholdBytes }) })
|
|
597
610
|
output.htmlPath = writeReportArtifact(htmlPath, artifact)
|
|
598
611
|
output.htmlMode = artifact.mode
|
|
599
612
|
}
|
|
@@ -947,6 +960,26 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
947
960
|
}
|
|
948
961
|
}
|
|
949
962
|
|
|
963
|
+
if (command === 'benchmark') {
|
|
964
|
+
const fixturePath = positional[1]
|
|
965
|
+
const observationPath = positional[2]
|
|
966
|
+
if (!fixturePath || !observationPath) {
|
|
967
|
+
process.stderr.write('Usage: ak-docs benchmark <fixture.json> <observation.json> [--text|--json]\n')
|
|
968
|
+
return 1
|
|
969
|
+
}
|
|
970
|
+
try {
|
|
971
|
+
const fixture = benchmarkFixture(JSON.parse(readFileSync(resolve(fixturePath), 'utf8')) as unknown)
|
|
972
|
+
const observation = JSON.parse(readFileSync(resolve(observationPath), 'utf8')) as Parameters<typeof measureBenchmark>[0]
|
|
973
|
+
const result = measureBenchmark(observation, fixture)
|
|
974
|
+
if (flags.has('--text')) writeLines([formatBenchmarkText(result)])
|
|
975
|
+
else writeJson(result)
|
|
976
|
+
return result.regressions.length ? 1 : 0
|
|
977
|
+
} catch (error) {
|
|
978
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
979
|
+
return 2
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
950
983
|
if (command === 'scan' || command === 'reconcile' || command === 'check' || command === 'map') {
|
|
951
984
|
return runWorkflowCommand(command, flags, configPath, argv)
|
|
952
985
|
}
|
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'
|