@agentskit/doc-bridge 1.7.45 → 1.10.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 +488 -0
- package/CONTRIBUTING.md +23 -0
- package/README.md +87 -40
- package/SECURITY.md +7 -0
- package/action.yml +1 -1
- package/bin/ak-docs.js +2 -2
- package/bin/ak-verify.js +13 -7
- package/dist/cli/program.d.ts +3 -1
- package/dist/cli/program.js +15871 -6218
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +95 -6
- package/dist/config/index.js.map +1 -1
- package/dist/{index-BUL0q7s8.d.ts → index-7wYGbllW.d.ts} +134 -1
- package/dist/index.d.ts +9880 -1062
- package/dist/index.js +16143 -6127
- package/dist/index.js.map +1 -1
- package/docs/MARKETPLACE.md +1 -1
- package/docs/PRD-documentation-efficiency-study.md +406 -0
- package/docs/PRD-knowledge-retrieval-and-enrichment.md +466 -0
- package/docs/adr/0002-documentation-audit-boundary.md +22 -0
- package/docs/adr/0003-study-protocol-and-historical-evidence.md +40 -0
- package/docs/adr/0004-controlled-study-runner.md +25 -0
- package/docs/adr/0005-documentation-quality-and-criticality.md +20 -0
- package/docs/adr/0006-registry-semantic-grounding.md +20 -0
- package/docs/adr/0007-longitudinal-study-metrics.md +21 -0
- package/docs/adr/0008-study-verification-boundary.md +21 -0
- package/docs/adr/0009-study-provider-cli-adapter.md +25 -0
- package/docs/agent-corpus/INDEX.md +12 -1
- package/docs/agent-corpus/OVERVIEW.md +25 -0
- package/docs/agent-corpus/chat.md +5 -1
- package/docs/agent-corpus/cli.md +16 -0
- package/docs/agent-corpus/conformance.md +12 -0
- package/docs/agent-corpus/doc-bridge.md +47 -0
- package/docs/agent-corpus/doctor.md +8 -0
- package/docs/agent-corpus/gates.md +4 -0
- package/docs/agent-corpus/mcp.md +13 -0
- package/docs/agent-corpus/memory.md +4 -0
- package/docs/agent-corpus/query.md +33 -0
- package/docs/bench/README.md +122 -0
- package/docs/bench/retrieval-baseline-v1.json +28 -0
- package/docs/bench/retrieval-suite-v1.json +1033 -0
- package/docs/chat-and-rag.md +3 -2
- package/docs/for-agents.md +9 -1
- package/docs/getting-started.md +4 -11
- package/docs/guides/gate-ci.md +11 -1
- package/docs/guides/install-and-run.md +9 -65
- package/docs/index.md +22 -1
- package/docs/knowledge-engine-runbook.md +38 -3
- package/docs/landing/assets/context-payload-reduction.svg +21 -0
- package/docs/landing/assets/controlled-ab-comparison.svg +30 -0
- package/docs/landing/index.html +119 -5
- package/docs/loop-workflow.md +117 -0
- package/docs/mcp.md +6 -1
- package/docs/parity/public-claims-v1.json +145 -0
- package/docs/playbook/doc-bridge-pattern.md +1 -1
- package/docs/query.md +90 -2
- package/docs/recipes/index-pipeline.md +1 -1
- package/docs/schemas/agent-handoff-v1.md +15 -0
- package/docs/schemas/doc-bridge-index-v1.md +65 -0
- package/docs/spec/benchmark-v1.md +33 -1
- package/docs/spec/cli.md +30 -10
- package/docs/spec/config-v1.md +157 -8
- package/docs/spec/documentation-audit-v1.md +61 -0
- package/docs/spec/enrichment-overlay-v1.md +241 -0
- package/docs/spec/graph-signals-v1.md +92 -0
- package/docs/spec/incremental-scan-v1.md +102 -0
- package/docs/spec/markdown-analyzer-v1.md +73 -0
- package/docs/spec/mcp-knowledge-tools-v1.md +147 -0
- package/docs/spec/measured-enrichment-v1.md +229 -0
- package/docs/spec/public-parity-v1.md +119 -0
- package/docs/spec/registry-agents.md +6 -0
- package/docs/spec/render-v1.md +122 -0
- package/docs/spec/retrieval-index-v1.md +164 -0
- package/docs/spec/study-metrics-v1.md +58 -0
- package/docs/spec/study-protocol-v1.md +46 -0
- package/docs/spec/study-provider-cli-v1.md +116 -0
- package/docs/spec/study-runner-v1.md +35 -0
- package/docs/spec/study-task-suite-v1.md +41 -0
- package/docs/spec/study-verification-v1.md +40 -0
- package/docs/study/README.md +84 -0
- package/docs/study/ab-adjudicated-cost-analysis-v1.md +29 -0
- package/docs/study/ab-adjudicated-cost-plan-v1.json +33 -0
- package/docs/study/ab-adjudicated-cost-plan-v2-v1.json +33 -0
- package/docs/study/ab-adjudicated-cost-result-v1.json +80 -0
- package/docs/study/ab-baseline-analysis-v1.md +21 -0
- package/docs/study/ab-baseline-plan-v1.json +33 -0
- package/docs/study/ab-baseline-recovery-plan-v1.json +33 -0
- package/docs/study/ab-baseline-result-v1.json +79 -0
- package/docs/study/documentation-audit-round-2026-08-31.json +183 -0
- package/docs/study/historical-evidence-v1.json +252 -0
- package/docs/study/observation-ledger-v1.json +30632 -0
- package/docs/study/phase3-task-coverage-v1.json +34 -0
- package/docs/study/phase4-public-pilot-ledger-v1.json +1344 -0
- package/docs/study/phase4-public-pilot-result-v1.json +52 -0
- package/docs/study/phase4-public-pilot-run-plan-v1.json +26 -0
- package/docs/study/phase4-public-pilot-task-suite-v1.json +71 -0
- package/docs/study/pilot-round-2026-08-31.json +46 -0
- package/docs/study/protocol-v1.json +90 -0
- package/docs/study/publication-gate-v1.md +45 -0
- package/docs/study/quality-scorecard-cycle-plan.md +545 -0
- package/docs/study/quality-scorecard-v1.json +38 -0
- package/docs/study/round-1-adjudicated-smoke-v1.json +30642 -0
- package/docs/study/round-1-instrumentation-plan-v1.md +39 -0
- package/docs/study/round-2-expanded-adjudication-v1.json +91 -0
- package/docs/study/round-2-expanded-validation-v1.md +58 -0
- package/docs/study/round-3-evidence-contract-v1.json +75 -0
- package/docs/study/round-3-evidence-contract-v1.md +57 -0
- package/docs/study/round-4-confirmation-v1.json +75 -0
- package/docs/study/round-4-confirmation-v1.md +55 -0
- package/docs/study/run-plan-v1.json +33 -0
- package/docs/study/semantic-adjudication-cycle-8.md +20 -0
- package/docs/study/task-suite-v1.json +96 -0
- package/docs/study/token-efficiency-plan-v1.md +337 -0
- package/docs/study/token-efficiency-protocol-v2.json +62 -0
- package/docs/study/verification-binding-v1.json +27 -0
- package/docs/validation-cycle-plan.md +14 -0
- package/docs/verification-harness.md +11 -6
- package/ecosystem-claims.json +2 -2
- package/ecosystem-upstream.json +2 -2
- package/ecosystem.json +4 -4
- package/mcpb/manifest.json +9 -1
- package/package.json +34 -15
- package/scripts/check-ecosystem-upstream.mjs +34 -6
- package/skills/doc-bridge-handoff/fixtures/synthetic-repo/docs/for-agents/packages/payments.md +7 -0
- package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
- package/src/agents/registry-adapter.ts +192 -24
- package/src/audit/documentation.ts +513 -0
- package/src/bench/baseline.ts +198 -0
- package/src/bench/overlay-delta.ts +139 -0
- package/src/bench/retrieval.ts +319 -0
- package/src/budget/compile.ts +91 -0
- package/src/budget/sections.ts +70 -0
- package/src/cli/program.ts +684 -74
- package/src/cli/usage.ts +71 -0
- package/src/config/defaults.ts +1 -0
- package/src/config/index.ts +4 -0
- package/src/config/schema.ts +126 -2
- package/src/conformance/documentation-standard-v1.ts +8 -6
- package/src/discovery/areas.ts +197 -0
- package/src/discovery/documentation.ts +211 -5
- package/src/discovery/identity.ts +24 -0
- package/src/discovery/incremental.ts +314 -0
- package/src/discovery/inputs.ts +110 -0
- package/src/discovery/markdown.ts +481 -0
- package/src/discovery/repository.ts +485 -101
- package/src/doctor/run-doctor.ts +244 -12
- package/src/enrich/approvals.ts +190 -0
- package/src/enrich/cache.ts +93 -0
- package/src/enrich/context-pack.ts +272 -0
- package/src/enrich/overlay.ts +255 -0
- package/src/enrich/review.ts +106 -0
- package/src/enrich/stage.ts +374 -0
- package/src/enrich/stats.ts +100 -0
- package/src/enrich/validate.ts +410 -0
- package/src/federation/llms.ts +2 -4
- package/src/findings/report.ts +103 -0
- package/src/graph/build.ts +356 -0
- package/src/graph/memory.ts +208 -0
- package/src/index-builder/build-handoffs.ts +22 -11
- package/src/index-builder/build-index.ts +132 -3
- package/src/index-builder/llms-txt.ts +48 -8
- package/src/index-builder/project-corpus.ts +111 -0
- package/src/index.ts +630 -2
- package/src/lib/fuzzy-match.ts +235 -0
- package/src/mcp/knowledge.ts +554 -0
- package/src/mcp/server.ts +113 -18
- package/src/metrics/benchmark.ts +21 -0
- package/src/parity/check.ts +309 -0
- package/src/parity/claims.ts +259 -0
- package/src/parity/resolve.ts +160 -0
- package/src/query/handoff.ts +326 -0
- package/src/query/load-index.ts +53 -1
- package/src/query/query.ts +92 -59
- package/src/query/search.ts +289 -92
- package/src/query/text.ts +155 -0
- package/src/reconciliation/reconcile.ts +101 -10
- package/src/render/data.ts +356 -0
- package/src/render/engine.ts +398 -0
- package/src/render/generated.ts +77 -0
- package/src/render/render.ts +209 -0
- package/src/render/template-source.ts +52 -0
- package/src/render/templates.ts +289 -0
- package/src/report/html.ts +2 -2
- package/src/retrieval/bm25.ts +161 -0
- package/src/retrieval/project.ts +495 -0
- package/src/retrieval/rank.ts +383 -0
- package/src/retrieval/weights.ts +39 -0
- package/src/retriever/doc-bridge-retriever.ts +100 -15
- package/src/rules/engine.ts +30 -10
- package/src/schemas/agent-handoff.ts +56 -0
- package/src/schemas/budget.ts +37 -0
- package/src/schemas/doc-bridge-index.ts +53 -2
- package/src/schemas/enrichment.ts +369 -0
- package/src/schemas/json-schemas.ts +39 -2
- package/src/schemas/knowledge.ts +14 -1
- package/src/schemas/retrieval-index.ts +152 -0
- package/src/shims/graphology.d.ts +91 -0
- package/src/study/adjudication.ts +196 -0
- package/src/study/execution.ts +350 -0
- package/src/study/expectations.ts +219 -0
- package/src/study/metrics.ts +467 -0
- package/src/study/protocol.ts +271 -0
- package/src/study/provider-cli.ts +115 -0
- package/src/study/provider-telemetry.ts +47 -0
- package/src/study/quality-scorecard.ts +164 -0
- package/src/study/runner.ts +461 -0
- package/src/study/task-suite.ts +321 -0
- package/src/study/verification.ts +134 -0
- package/src/validate.ts +1 -4
- package/src/version.ts +1 -1
- package/src/workflow/engine.ts +36 -11
- package/scripts/verification-harness.mjs +0 -482
|
@@ -1,482 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { execFileSync, spawn } from 'node:child_process'
|
|
4
|
-
import { createHash } from 'node:crypto'
|
|
5
|
-
import {
|
|
6
|
-
appendFileSync,
|
|
7
|
-
existsSync,
|
|
8
|
-
mkdirSync,
|
|
9
|
-
readFileSync,
|
|
10
|
-
readdirSync,
|
|
11
|
-
renameSync,
|
|
12
|
-
rmSync,
|
|
13
|
-
writeFileSync,
|
|
14
|
-
} from 'node:fs'
|
|
15
|
-
import { dirname, join, relative, resolve, sep } from 'node:path'
|
|
16
|
-
|
|
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
|
-
}
|
|
28
|
-
const CATEGORIES = new Set(['build', 'test', 'lint', 'logic', 'endpoint', 'database', 'cli', 'mcp', 'ui', 'docs', 'custom'])
|
|
29
|
-
const INTENTS = new Set(['ok', 'certo', 'certa', 'aprovado', 'aprovada', 'approve', 'approved', 'confirmo', 'confirmado', 'confirmed', 'yes'])
|
|
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
|
-
}
|
|
42
|
-
|
|
43
|
-
const fail = (message) => { throw new Error(message) }
|
|
44
|
-
const hash = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex')
|
|
45
|
-
const now = () => new Date().toISOString()
|
|
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
|
-
}
|
|
50
|
-
const writeAtomic = (path, value) => {
|
|
51
|
-
mkdirSync(dirname(path), { recursive: true })
|
|
52
|
-
const temp = `${path}.tmp-${process.pid}`
|
|
53
|
-
writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
|
|
54
|
-
renameSync(temp, path)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const parseArgs = (argv) => {
|
|
58
|
-
const flags = new Set()
|
|
59
|
-
const values = new Map()
|
|
60
|
-
const positional = []
|
|
61
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
62
|
-
const arg = argv[i]
|
|
63
|
-
if (arg === '--config' || arg === '--run-id' || arg === '--by') {
|
|
64
|
-
const value = argv[++i]
|
|
65
|
-
if (!value) fail(`${arg} requires a value.`)
|
|
66
|
-
values.set(arg.slice(2), value)
|
|
67
|
-
} else if (arg?.startsWith('--')) flags.add(arg.slice(2))
|
|
68
|
-
else if (arg) positional.push(arg)
|
|
69
|
-
}
|
|
70
|
-
return { flags, values, positional }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const projectRoot = (configPath, raw) => resolve(dirname(resolve(configPath)), raw.root ?? '.')
|
|
74
|
-
const inside = (root, path) => {
|
|
75
|
-
const rel = relative(root, path)
|
|
76
|
-
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !rel.startsWith(sep))
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const surfaceRequired = (value, name) => {
|
|
80
|
-
if (typeof value === 'boolean') return { required: value, reason: value ? undefined : `${name} is not part of this verification target.` }
|
|
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}`)
|
|
83
|
-
if (typeof value.required !== 'boolean') fail(`surfaces.${name}.required must be boolean.`)
|
|
84
|
-
if (!value.required && typeof value.reason !== 'string') fail(`surfaces.${name}.reason is required when the surface is not required.`)
|
|
85
|
-
return { required: value.required, ...(value.reason ? { reason: value.reason } : {}) }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const validateConfig = (raw, configPath) => {
|
|
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')
|
|
91
|
-
if (raw.schemaVersion !== 1) fail('verification config schemaVersion must be 1.')
|
|
92
|
-
if (typeof raw.project !== 'string' || !raw.project) fail('verification config project is required.')
|
|
93
|
-
if (raw.root !== undefined && typeof raw.root !== 'string') fail('verification config root must be a string.')
|
|
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
|
-
}
|
|
102
|
-
if (!Array.isArray(raw.checks) || raw.checks.length === 0) fail('verification config requires at least one check.')
|
|
103
|
-
const checks = raw.checks.map((check, index) => {
|
|
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}]`)
|
|
106
|
-
if (typeof check.id !== 'string' || !check.id) fail(`checks[${index}].id is required.`)
|
|
107
|
-
if (typeof check.command !== 'string' || !check.command) fail(`checks[${index}].command is required.`)
|
|
108
|
-
if (!CATEGORIES.has(check.category)) fail(`checks[${index}].category is invalid.`)
|
|
109
|
-
if (check.required !== undefined && typeof check.required !== 'boolean') fail(`checks[${index}].required must be boolean.`)
|
|
110
|
-
if (check.timeoutMs !== undefined && (!Number.isInteger(check.timeoutMs) || check.timeoutMs < 1)) fail(`checks[${index}].timeoutMs must be a positive integer.`)
|
|
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
|
-
}
|
|
116
|
-
return { required: true, timeoutMs: 120_000, ...check }
|
|
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.`)
|
|
137
|
-
const surfaces = {}
|
|
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
|
-
}
|
|
142
|
-
for (const [name, surface] of Object.entries(surfaces)) {
|
|
143
|
-
const matching = checks.some((check) => check.category === name && check.required)
|
|
144
|
-
if (surface.required && !matching) fail(`Required surface "${name}" has no required check.`)
|
|
145
|
-
}
|
|
146
|
-
if (policy.requiresExplicitExemptions && (!Array.isArray(raw.exemptions) || raw.exemptions.length === 0)) fail('This profile requires explicit exemptions.')
|
|
147
|
-
if (raw.exemptions && (!Array.isArray(raw.exemptions) || raw.exemptions.some((item) => typeof item !== 'string' || !item.trim()))) fail('exemptions must be non-empty strings.')
|
|
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')
|
|
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.')
|
|
166
|
-
if (tracking.required && tracking.authorization !== 'ask') fail('tracking.authorization must be "ask".')
|
|
167
|
-
if (tracking.required && typeof tracking.target !== 'string') fail('tracking.target is required when tracking is required.')
|
|
168
|
-
if (!tracking.required && typeof tracking.reason !== 'string') fail('tracking.reason is required when tracking is not required.')
|
|
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 }
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const sourceRevision = (root) => {
|
|
178
|
-
try {
|
|
179
|
-
const gitOptions = { cwd: root, encoding: 'utf8', maxBuffer: 20 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] }
|
|
180
|
-
const head = execFileSync('git', ['rev-parse', 'HEAD'], gitOptions).trim()
|
|
181
|
-
const diff = execFileSync('git', ['diff', '--no-ext-diff', '--binary', 'HEAD'], gitOptions)
|
|
182
|
-
const status = execFileSync('git', ['status', '--porcelain=v1', '--untracked-files=all'], gitOptions)
|
|
183
|
-
const untracked = execFileSync('git', ['ls-files', '--others', '--exclude-standard', '-z'], gitOptions)
|
|
184
|
-
.split('\0').filter(Boolean).map((path) => ({ path, contentHash: hash(readFileSync(resolve(root, path), 'utf8')) }))
|
|
185
|
-
return hash({ head, diff, status, untracked })
|
|
186
|
-
} catch {
|
|
187
|
-
return hash({ files: readdirSync(root).sort() })
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const machineStatusFrom = (stdout) => {
|
|
192
|
-
const lines = String(stdout ?? '').trim().split('\n').reverse()
|
|
193
|
-
for (const line of lines) {
|
|
194
|
-
try {
|
|
195
|
-
const value = JSON.parse(line)
|
|
196
|
-
if (value && ['failed', 'passed', 'pending-human-review'].includes(value.status)) return value
|
|
197
|
-
} catch {}
|
|
198
|
-
}
|
|
199
|
-
return undefined
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const commandResult = (root, check) => new Promise((resolveResult) => {
|
|
203
|
-
const started = Date.now()
|
|
204
|
-
const child = spawn(check.command, { cwd: root, shell: true, env: { ...process.env, CI: process.env.CI ?? '1' } })
|
|
205
|
-
let stdout = ''
|
|
206
|
-
let stderr = ''
|
|
207
|
-
const append = (current, chunk) => `${current}${chunk.toString()}`.slice(-12_000)
|
|
208
|
-
child.stdout.on('data', (chunk) => { stdout = append(stdout, chunk) })
|
|
209
|
-
child.stderr.on('data', (chunk) => { stderr = append(stderr, chunk) })
|
|
210
|
-
const timer = setTimeout(() => child.kill('SIGTERM'), check.timeoutMs)
|
|
211
|
-
child.on('error', (error) => resolveResult({ status: 'failed', exitCode: null, durationMs: Date.now() - started, stdout, stderr: `${stderr}${error.message}`.slice(-12_000) }))
|
|
212
|
-
child.on('close', (exitCode, signal) => {
|
|
213
|
-
clearTimeout(timer)
|
|
214
|
-
const machineResult = machineStatusFrom(stdout)
|
|
215
|
-
const status = exitCode !== 0 || machineResult?.status === 'failed'
|
|
216
|
-
? 'failed'
|
|
217
|
-
: machineResult?.status === 'pending-human-review'
|
|
218
|
-
? 'awaiting-human-approval'
|
|
219
|
-
: 'passed'
|
|
220
|
-
resolveResult({ status, exitCode, signal, durationMs: Date.now() - started, stdout, stderr, ...(machineResult ? { verificationStatus: machineResult.status, verification: machineResult } : {}) })
|
|
221
|
-
})
|
|
222
|
-
})
|
|
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
|
-
|
|
271
|
-
const stateDirFor = (root) => join(root, '.codex', 'verification')
|
|
272
|
-
const runDirFor = (root, runId) => join(stateDirFor(root), 'runs', runId)
|
|
273
|
-
const latestPathFor = (root) => join(stateDirFor(root), 'latest.json')
|
|
274
|
-
const loadLatest = (root) => existsSync(latestPathFor(root)) ? readJson(latestPathFor(root)) : undefined
|
|
275
|
-
const saveRun = (root, run) => {
|
|
276
|
-
const dir = runDirFor(root, run.runId)
|
|
277
|
-
writeAtomic(join(dir, 'run.json'), run)
|
|
278
|
-
writeAtomic(latestPathFor(root), { runId: run.runId, state: run.state, path: relative(root, join(dir, 'run.json')), updatedAt: now() })
|
|
279
|
-
}
|
|
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
|
-
}
|
|
297
|
-
|
|
298
|
-
const runVerification = async (root, config, runId) => {
|
|
299
|
-
const source = sourceRevision(root)
|
|
300
|
-
const inputHash = hash({ source, config: hash(config), version: VERSION })
|
|
301
|
-
const previous = loadLatest(root)
|
|
302
|
-
if (previous?.runId) {
|
|
303
|
-
const previousRunPath = join(root, previous.path)
|
|
304
|
-
if (existsSync(previousRunPath)) {
|
|
305
|
-
const previousRun = readJson(previousRunPath)
|
|
306
|
-
if (previousRun.inputHash === inputHash && ['AWAITING_HUMAN_APPROVAL', 'AWAITING_AUTHORIZATION', 'COMPLETE'].includes(previousRun.state)) return previousRun
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
const id = runId ?? `${Date.now()}-${process.pid}`
|
|
310
|
-
let run = {
|
|
311
|
-
schemaVersion: 1,
|
|
312
|
-
type: 'verification-run',
|
|
313
|
-
harnessVersion: VERSION,
|
|
314
|
-
runId: id,
|
|
315
|
-
project: config.project,
|
|
316
|
-
profile: config.profile,
|
|
317
|
-
profilePolicy: config.profilePolicy,
|
|
318
|
-
sourceRevision: source,
|
|
319
|
-
contractHash: hash(config.contract),
|
|
320
|
-
inputHash,
|
|
321
|
-
state: 'PLANNED',
|
|
322
|
-
configPath: relative(root, config.configPath),
|
|
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' })),
|
|
325
|
-
surfaces: config.surfaces,
|
|
326
|
-
applicability: config.surfaces,
|
|
327
|
-
tracking: config.tracking,
|
|
328
|
-
exemptions: config.exemptions ?? [],
|
|
329
|
-
transitions: [{ from: null, to: 'PLANNED', at: now() }],
|
|
330
|
-
}
|
|
331
|
-
saveRun(root, run)
|
|
332
|
-
run = transition(run, 'VERIFYING')
|
|
333
|
-
saveRun(root, run)
|
|
334
|
-
for (const [index, check] of config.checks.entries()) {
|
|
335
|
-
const result = await commandResult(root, check)
|
|
336
|
-
run = { ...run, checks: run.checks.map((item, itemIndex) => itemIndex === index ? { ...item, ...result } : item) }
|
|
337
|
-
saveRun(root, run)
|
|
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')
|
|
359
|
-
const failed = run.checks.filter((check) => check.required && !['passed', 'awaiting-human-approval'].includes(check.status))
|
|
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(', ')}`)
|
|
362
|
-
else if (config.surfaces.ui.required) run = transition(run, 'AWAITING_HUMAN_APPROVAL', 'Visual UI approval is required.')
|
|
363
|
-
else if (config.tracking.required) run = transition(run, 'AWAITING_AUTHORIZATION', `Tracking authorization is required for ${config.tracking.target}.`)
|
|
364
|
-
else run = transition(run, 'COMPLETE', 'All configured verification gates passed.')
|
|
365
|
-
saveRun(root, run)
|
|
366
|
-
return run
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const intent = (value) => {
|
|
370
|
-
const normalized = String(value ?? '').trim().toLowerCase()
|
|
371
|
-
if (!INTENTS.has(normalized)) fail(`Invalid human intent "${value}". Use an explicit approval word: ok, aprovado, approved, confirmo.`)
|
|
372
|
-
return normalized
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const updateApproval = (root, run, type, rawIntent, by) => {
|
|
376
|
-
const expectedState = type === 'human-approval' ? 'AWAITING_HUMAN_APPROVAL' : 'AWAITING_AUTHORIZATION'
|
|
377
|
-
if (run.state !== expectedState) fail(`Cannot record ${type} while run is ${run.state}.`)
|
|
378
|
-
const approvedIntent = intent(rawIntent)
|
|
379
|
-
const dir = runDirFor(root, run.runId)
|
|
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) }
|
|
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.')
|
|
393
|
-
if (type === 'tracking-authorization' && run.state === 'AWAITING_AUTHORIZATION') run = transition(run, 'COMPLETE', `Tracking authorization recorded for ${run.tracking.target}.`)
|
|
394
|
-
saveRun(root, run)
|
|
395
|
-
return run
|
|
396
|
-
}
|
|
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
|
-
|
|
417
|
-
const clean = (root, config, periodic) => {
|
|
418
|
-
const manifestPath = join(stateDirFor(root), 'owned-artifacts.json')
|
|
419
|
-
if (!existsSync(manifestPath)) return { removed: [], skipped: [], periodic, message: 'No task-owned artifacts are registered.' }
|
|
420
|
-
const manifest = readJson(manifestPath)
|
|
421
|
-
if (!Array.isArray(manifest)) fail('owned-artifacts.json must contain an array.')
|
|
422
|
-
const removed = []
|
|
423
|
-
const skipped = []
|
|
424
|
-
for (const entry of manifest) {
|
|
425
|
-
const path = resolve(root, entry.path)
|
|
426
|
-
const allowedRoots = (config.cleanup?.roots ?? ['.codex/verification/tmp']).map((item) => resolve(root, item))
|
|
427
|
-
if (!entry.taskOwned || !inside(root, path) || !allowedRoots.some((allowed) => inside(allowed, path))) { skipped.push({ path: entry.path, reason: 'not task-owned or outside cleanup roots' }); continue }
|
|
428
|
-
if (existsSync(path)) { rmSync(path, { recursive: true, force: true }); removed.push(entry.path) }
|
|
429
|
-
}
|
|
430
|
-
return { removed, skipped, periodic }
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const output = (value, json) => process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : `Run: ${value.runId ?? '-'}\nState: ${value.state ?? value.status ?? '-'}\n${value.reason ? `Reason: ${value.reason}\n` : ''}`)
|
|
434
|
-
|
|
435
|
-
const main = async (argv) => {
|
|
436
|
-
const { flags, values, positional } = parseArgs(argv)
|
|
437
|
-
const command = positional[0] ?? 'help'
|
|
438
|
-
if (flags.has('help') || command === 'help') {
|
|
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')
|
|
440
|
-
return 0
|
|
441
|
-
}
|
|
442
|
-
const configPath = resolve(values.get('config') ?? DEFAULT_CONFIG)
|
|
443
|
-
if (!existsSync(configPath)) fail(`Verification contract not found: ${configPath}`)
|
|
444
|
-
const root = projectRoot(configPath, readJson(configPath))
|
|
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
|
-
}
|
|
452
|
-
if (command === 'run') {
|
|
453
|
-
const run = await runVerification(root, config, values.get('run-id'))
|
|
454
|
-
output(run, flags.has('json'))
|
|
455
|
-
return ['COMPLETE', 'AWAITING_HUMAN_APPROVAL', 'AWAITING_AUTHORIZATION'].includes(run.state) ? 0 : 1
|
|
456
|
-
}
|
|
457
|
-
if (command === 'status') {
|
|
458
|
-
const latest = loadLatest(root)
|
|
459
|
-
if (!latest) fail('No verification run exists.')
|
|
460
|
-
const run = readJson(join(root, latest.path))
|
|
461
|
-
output(run, flags.has('json'))
|
|
462
|
-
return run.state === 'COMPLETE' ? 0 : 1
|
|
463
|
-
}
|
|
464
|
-
if (command === 'clean') { output(clean(root, config, flags.has('periodic')), flags.has('json')); return 0 }
|
|
465
|
-
const latest = loadLatest(root)
|
|
466
|
-
if (!latest) fail('No verification run exists.')
|
|
467
|
-
let run = readJson(join(root, latest.path))
|
|
468
|
-
const targetRunId = positional[1]
|
|
469
|
-
if (targetRunId !== run.runId) fail(`Run id mismatch. Latest run is ${run.runId}.`)
|
|
470
|
-
if (command === 'approve') run = updateApproval(root, run, 'human-approval', positional[2], values.get('by'))
|
|
471
|
-
else if (command === 'authorize') run = updateApproval(root, run, 'tracking-authorization', positional[2], values.get('by'))
|
|
472
|
-
else fail(`Unknown command "${command}".`)
|
|
473
|
-
output(run, flags.has('json'))
|
|
474
|
-
return run.state === 'COMPLETE' ? 0 : 1
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
478
|
-
try { process.exitCode = await main(process.argv.slice(2)) }
|
|
479
|
-
catch (error) { process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); process.exitCode = 2 }
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
export { INTENTS, LEGAL_TRANSITIONS, PROFILES, STATES, main, transition, validateConfig }
|