@stravica-ai/rcf-verify-lite 0.1.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/src/cli/mcp.js ADDED
@@ -0,0 +1,98 @@
1
+ // `rcf-verify mcp` subcommand (spec §10 MCP; mirrors build's cli/mcp.js).
2
+ // Wires process stdin/stdout/stderr into the core MCP protocol shell and
3
+ // serves the verify tool registry. stdout carries MCP messages and NOTHING
4
+ // else; all logging goes to stderr.
5
+
6
+ import { readFile } from 'node:fs/promises';
7
+ import { dirname, resolve as resolvePath } from 'node:path';
8
+ import { parseArgs } from 'node:util';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ import { createMcpServer, serveStreams } from '@stravica-ai/rcf-lite-core/mcp-shell';
12
+
13
+ import { createToolRegistry } from '../mcp/tools.js';
14
+
15
+ const here = dirname(fileURLToPath(import.meta.url));
16
+
17
+ export const HELP = `Usage: rcf-verify mcp [options]
18
+
19
+ Serve verify over the Model Context Protocol (local stdio). Exposes one tool,
20
+ rcf_verify_run, over the same in-process engine the CLI uses. stdout carries
21
+ protocol messages only; logging goes to stderr.
22
+
23
+ Options:
24
+ --verbose Per-request logging on stderr
25
+ --help Print this help
26
+ `;
27
+
28
+ const INSTRUCTIONS = 'Fresh-context adversarial verifier for RCF chains. rcf_verify_run takes an RCF '
29
+ + 'chain (the acceptance contract) and a running app under a declared runtime profile, launches an '
30
+ + 'isolated verifier agent that tries to DISPROVE the app against its acceptance criteria, and '
31
+ + 'returns a structured verdict stamped with the runtime it ran against. Only a "deployed" profile '
32
+ + '(or a declared --parity-env runtime) yields a SHIP verdict. This is an independent ship-readiness '
33
+ + 'signal, not a correctness guarantee.';
34
+
35
+ /**
36
+ * @param {string[]} argv - argv slice after `mcp`
37
+ * @param {object} [deps]
38
+ * @returns {Promise<number>}
39
+ */
40
+ export async function main(argv, deps = {}) {
41
+ const stdout = deps.stdout ?? process.stdout;
42
+ const stderr = deps.stderr ?? process.stderr;
43
+ const stdin = deps.stdin ?? process.stdin;
44
+ const onSignal = deps.onSignal ?? ((sig, fn) => process.on(sig, fn));
45
+
46
+ let parsed;
47
+ try {
48
+ parsed = parseArgs({ args: argv, options: { verbose: { type: 'boolean' }, help: { type: 'boolean' } }, allowPositionals: false, strict: true });
49
+ } catch (err) {
50
+ stderr.write(`[error] usage ${err.message}\n`);
51
+ stderr.write(HELP);
52
+ return 2;
53
+ }
54
+ if (parsed.values.help) {
55
+ stdout.write(HELP);
56
+ return 0;
57
+ }
58
+
59
+ const verbose = Boolean(parsed.values.verbose);
60
+ const log = {
61
+ info: verbose ? (line) => stderr.write(`[rcf-verify mcp] ${line}\n`) : () => {},
62
+ error: (line) => stderr.write(`${line}\n`),
63
+ };
64
+
65
+ const tools = createToolRegistry(deps);
66
+
67
+ const server = createMcpServer({
68
+ serverInfo: { name: 'rcf-verify-lite', version: await readPackageVersion() },
69
+ instructions: INSTRUCTIONS,
70
+ capabilities: { tools: {} },
71
+ handlers: {
72
+ 'tools/list': async () => ({ tools: tools.definitions }),
73
+ 'tools/call': async (params) => {
74
+ log.info(`tools/call ${params.name}`);
75
+ return await tools.call(params.name, params.arguments);
76
+ },
77
+ },
78
+ log,
79
+ });
80
+
81
+ stderr.write('rcf-verify mcp: serving\n');
82
+
83
+ const { done, stop } = serveStreams(server, { input: stdin, output: stdout, log });
84
+ onSignal('SIGINT', () => { log.info('received SIGINT, shutting down'); stop(); });
85
+ onSignal('SIGTERM', () => { log.info('received SIGTERM, shutting down'); stop(); });
86
+
87
+ return await done;
88
+ }
89
+
90
+ async function readPackageVersion() {
91
+ try {
92
+ const pkgPath = resolvePath(here, '..', '..', 'package.json');
93
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
94
+ return pkg.version ?? '0.0.0';
95
+ } catch {
96
+ return '0.0.0';
97
+ }
98
+ }
@@ -0,0 +1,71 @@
1
+ // `rcf-verify provision` (spec §3, §6). Stand up prerequisites standalone and
2
+ // write credentials to the --provision file. Derives what to provision from
3
+ // the chain's ACs (needs --repo). The concrete signup route is app-specific
4
+ // and agent-driven; where it can't be derived, this BLOCKS honestly rather
5
+ // than silently skipping (§6).
6
+
7
+ import { parseArgs } from 'node:util';
8
+
9
+ import { formatError, isRcfError } from '@stravica-ai/rcf-lite-core/errors';
10
+
11
+ import { readChain } from '../chain/index.js';
12
+ import { runProvisioning } from '../provision/index.js';
13
+
14
+ export const HELP = `Usage: rcf-verify provision --repo <path> --url <app-url> --provision <file>
15
+
16
+ Stand up prerequisite accounts / sandboxes / seed data for a later run.
17
+ Credentials are written to the --provision FILE only, never echoed.
18
+
19
+ Required:
20
+ --repo <path> RCF chain source (to derive what to provision)
21
+ --url <app-url> The running app
22
+ --provision <file> Where to write provisioned credentials
23
+
24
+ Exit codes:
25
+ 0 provisioning attempted; see the report of what was provisioned/blocked
26
+ 2 usage error
27
+ 3 chain could not be loaded
28
+ `;
29
+
30
+ /**
31
+ * @param {string[]} argv
32
+ * @param {object} [deps]
33
+ * @returns {Promise<number>}
34
+ */
35
+ export async function main(argv, deps = {}) {
36
+ const stdout = deps.stdout ?? process.stdout;
37
+ const stderr = deps.stderr ?? process.stderr;
38
+
39
+ let parsed;
40
+ try {
41
+ parsed = parseArgs({ args: argv, options: { repo: { type: 'string' }, url: { type: 'string' }, provision: { type: 'string' }, help: { type: 'boolean' } }, allowPositionals: false, strict: true });
42
+ } catch (err) {
43
+ stderr.write(`[error] usage ${err.message}\n`);
44
+ stderr.write(HELP);
45
+ return 2;
46
+ }
47
+ const flags = parsed.values;
48
+ if (flags.help) { stdout.write(HELP); return 0; }
49
+ if (!flags.repo || !flags.url || !flags.provision) {
50
+ stderr.write('[error] usage provision requires --repo, --url and --provision\n');
51
+ stderr.write(HELP);
52
+ return 2;
53
+ }
54
+
55
+ const chain = await (deps.readChain ?? readChain)({ repo: flags.repo });
56
+ if (isRcfError(chain)) {
57
+ stderr.write(`${formatError(chain, { verbose: true })}\n`);
58
+ return 3;
59
+ }
60
+
61
+ const { provisioning } = await runProvisioning({
62
+ acs: chain.acs,
63
+ url: flags.url,
64
+ provisionPath: flags.provision,
65
+ mode: 'run',
66
+ signup: deps.signup,
67
+ });
68
+
69
+ stdout.write(`${JSON.stringify(provisioning, null, 2)}\n`);
70
+ return 0;
71
+ }
@@ -0,0 +1,71 @@
1
+ // `rcf-verify report <report-path>` (spec §3, parallels `rcf view`).
2
+ // Re-render / summarise a prior report artifact using verify's own
3
+ // self-contained finding-renderer (§7.2 — no core view dependency).
4
+
5
+ import { readFile } from 'node:fs/promises';
6
+ import { resolve as resolvePath } from 'node:path';
7
+ import { parseArgs } from 'node:util';
8
+
9
+ import { formatError, isRcfError } from '@stravica-ai/rcf-lite-core/errors';
10
+
11
+ import { parseReport } from '../report/index.js';
12
+ import { renderReport } from '../report/renderer.js';
13
+
14
+ export const HELP = `Usage: rcf-verify report <report-path> [--json]
15
+
16
+ Re-render a prior report artifact. Default output is the human-readable
17
+ summary; --json re-emits the parsed artifact as JSON.
18
+
19
+ Exit codes:
20
+ 0 rendered
21
+ 2 usage error (no path)
22
+ 3 the report could not be read / parsed / is an unsupported schema
23
+ `;
24
+
25
+ /**
26
+ * @param {string[]} argv - argv slice after `report`
27
+ * @param {object} [deps]
28
+ * @returns {Promise<number>}
29
+ */
30
+ export async function main(argv, deps = {}) {
31
+ const stdout = deps.stdout ?? process.stdout;
32
+ const stderr = deps.stderr ?? process.stderr;
33
+ const cwd = deps.cwd ?? process.cwd();
34
+
35
+ let parsed;
36
+ try {
37
+ parsed = parseArgs({ args: argv, options: { json: { type: 'boolean' }, help: { type: 'boolean' } }, allowPositionals: true, strict: true });
38
+ } catch (err) {
39
+ stderr.write(`[error] usage ${err.message}\n`);
40
+ stderr.write(HELP);
41
+ return 2;
42
+ }
43
+ if (parsed.values.help) {
44
+ stdout.write(HELP);
45
+ return 0;
46
+ }
47
+ const target = parsed.positionals[0];
48
+ if (!target) {
49
+ stderr.write('[error] usage report: a <report-path> is required\n');
50
+ stderr.write(HELP);
51
+ return 2;
52
+ }
53
+
54
+ const reader = deps.readFile ?? readFile;
55
+ let raw;
56
+ try {
57
+ raw = await reader(resolvePath(cwd, target), 'utf8');
58
+ } catch (err) {
59
+ stderr.write(`[error] missingFile ${target}: ${err.message}\n`);
60
+ return 3;
61
+ }
62
+
63
+ const report = parseReport(raw);
64
+ if (isRcfError(report)) {
65
+ stderr.write(`${formatError(report, { verbose: true })}\n`);
66
+ return 3;
67
+ }
68
+
69
+ stdout.write(parsed.values.json ? `${JSON.stringify(report, null, 2)}\n` : renderReport(report));
70
+ return 0;
71
+ }
package/src/cli/run.js ADDED
@@ -0,0 +1,155 @@
1
+ // `rcf-verify run` (spec §3 primary verb). Parses flags, runs the
2
+ // verification orchestrator, writes the report artifact (always, regardless of
3
+ // gate — §3 rule 5), and sets the process exit code from the severity gate
4
+ // (§8.2: exit 0 below the gate, non-zero at/above it, and always non-zero for
5
+ // NOT-DEPLOYED / BLOCKED). This is the exact call-path build-lite's finalise
6
+ // step spawns (§8.2).
7
+
8
+ import { writeFile } from 'node:fs/promises';
9
+ import { resolve as resolvePath } from 'node:path';
10
+ import { parseArgs } from 'node:util';
11
+
12
+ import { formatError, isRcfError } from '@stravica-ai/rcf-lite-core/errors';
13
+
14
+ import { runVerification } from '../engine/index.js';
15
+ import { serialiseReport } from '../report/index.js';
16
+ import { gateTripped, FINDING_SEVERITIES } from '../verdict/index.js';
17
+
18
+ const OPTION_SPEC = {
19
+ repo: { type: 'string' },
20
+ chain: { type: 'string' },
21
+ profile: { type: 'string' },
22
+ url: { type: 'string' },
23
+ 'parity-env': { type: 'boolean' },
24
+ provision: { type: 'string' },
25
+ out: { type: 'string' },
26
+ 'severity-gate': { type: 'string' },
27
+ 'provision-mode': { type: 'string' },
28
+ persona: { type: 'string' },
29
+ help: { type: 'boolean' },
30
+ };
31
+
32
+ export const HELP = `Usage: rcf-verify run [options]
33
+
34
+ Launch a fresh-context adversarial verifier against a running app and emit a
35
+ structured verdict stamped with the runtime it ran against.
36
+
37
+ Required:
38
+ --repo <path-or-ref> RCF chain source (the acceptance contract)
39
+ --profile <p> Runtime profile: deployed | ci | local-dev
40
+ --url <app-url> The running app for this profile
41
+ --out <report-path> Where to write the structured report artifact
42
+
43
+ Optional:
44
+ --chain <PRD/ref> Which PRD/chain to verify against (default: the repo's)
45
+ --parity-env Assert this ci/local-dev runtime is edge-identical
46
+ to prod — the only path to a SHIP verdict from a
47
+ non-deployed profile. Logged in the report.
48
+ --provision <path> Provisioning spec / prerequisite credentials FILE
49
+ (never inline; secrets never echoed)
50
+ --severity-gate <sev> Exit non-zero at/above this severity:
51
+ PASS | COSMETIC | DEGRADED | BROKEN
52
+ --provision-mode <m> run | skip (default: run)
53
+ --persona <name> Adversarial persona flavour (default: generic-sceptic)
54
+ --help Print this help
55
+
56
+ Exit codes:
57
+ 0 report written, verdict below the severity gate
58
+ 1 IO / unexpected runtime failure (e.g. the report could not be written)
59
+ 2 usage error (missing/invalid flags)
60
+ 3 chain could not be loaded (the acceptance contract is unreadable)
61
+ 5 severity gate tripped, or NOT-DEPLOYED / BLOCKED / LAUNCH-FAILURE
62
+ (ship cannot be confirmed — a report is still written)
63
+ `;
64
+
65
+ function exitForError(err) {
66
+ switch (err.kind) {
67
+ case 'usage': return 2;
68
+ case 'missingFile':
69
+ case 'parseFailure':
70
+ case 'validation': return 3;
71
+ default: return 1; // ioFailure + anything unexpected
72
+ }
73
+ }
74
+
75
+ /**
76
+ * @param {string[]} argv - argv slice after `run`
77
+ * @param {object} [deps]
78
+ * @returns {Promise<number>}
79
+ */
80
+ export async function main(argv, deps = {}) {
81
+ const stdout = deps.stdout ?? process.stdout;
82
+ const stderr = deps.stderr ?? process.stderr;
83
+ const cwd = deps.cwd ?? process.cwd();
84
+
85
+ let parsed;
86
+ try {
87
+ parsed = parseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: false, strict: true });
88
+ } catch (err) {
89
+ stderr.write(`[error] usage ${err.message}\n`);
90
+ stderr.write(HELP);
91
+ return 2;
92
+ }
93
+ const flags = parsed.values;
94
+ if (flags.help) {
95
+ stdout.write(HELP);
96
+ return 0;
97
+ }
98
+
99
+ const gate = flags['severity-gate'];
100
+ if (gate && !FINDING_SEVERITIES.includes(gate)) {
101
+ stderr.write(`[error] usage --severity-gate must be one of ${FINDING_SEVERITIES.join(' | ')}\n`);
102
+ return 2;
103
+ }
104
+ const provisionMode = flags['provision-mode'] ?? 'run';
105
+ if (provisionMode !== 'run' && provisionMode !== 'skip') {
106
+ stderr.write('[error] usage --provision-mode must be run | skip\n');
107
+ return 2;
108
+ }
109
+ if (!flags.out) {
110
+ stderr.write('[error] usage --out <report-path> is required\n');
111
+ return 2;
112
+ }
113
+ // Dash-footgun (§3 rule 3, mirrors build's --body-file discipline): a
114
+ // --provision value that looks like a flag is almost always a swallowed next
115
+ // option or an attempt to pass a credential inline. Refuse it — credentials
116
+ // are FILE-only, never inline, never echoed.
117
+ if (typeof flags.provision === 'string' && flags.provision.startsWith('-')) {
118
+ stderr.write('[error] usage --provision takes a file path, not a flag or inline value (credentials are never accepted inline)\n');
119
+ return 2;
120
+ }
121
+
122
+ const result = await runVerification({
123
+ repo: flags.repo,
124
+ chainRef: flags.chain,
125
+ profile: flags.profile,
126
+ url: flags.url,
127
+ parityEnv: Boolean(flags['parity-env']),
128
+ provision: flags.provision,
129
+ provisionMode,
130
+ persona: flags.persona,
131
+ severityGate: gate,
132
+ }, deps);
133
+
134
+ if (isRcfError(result)) {
135
+ stderr.write(`${formatError(result, { verbose: true })}\n`);
136
+ return exitForError(result);
137
+ }
138
+
139
+ const { report } = result;
140
+
141
+ // The report is written ALWAYS, regardless of the gate (§3 rule 5).
142
+ const outPath = resolvePath(cwd, flags.out);
143
+ const writer = deps.writeFile ?? writeFile;
144
+ try {
145
+ await writer(outPath, serialiseReport(report), 'utf8');
146
+ } catch (err) {
147
+ stderr.write(`[rcf-verify] unexpected failure: could not write report: ${err.message}\n`);
148
+ return 1;
149
+ }
150
+
151
+ stderr.write(`[rcf-verify] verdict ${report.verdict} [${report.verdictAuthority}] -> ${flags.out}\n`);
152
+
153
+ // Exit code is the gate (§8.2). NOT-DEPLOYED / BLOCKED always trip.
154
+ return gateTripped({ verdict: report.verdict, findings: report.findings, gate }) ? 5 : 0;
155
+ }
@@ -0,0 +1,59 @@
1
+ // Adversarial brief composition (spec §5, §9 guarantee 4). Verify is thin: it
2
+ // reads the chain and composes an adversarial brief from the ACs, then hands
3
+ // that brief to a fresh isolated agent (engine/launcher.js). The stance is
4
+ // DISPROOF — an adversarial walk derived independently from the ACs does not
5
+ // inherit the build's framing. The brief NEVER references the source tree, the
6
+ // build transcript, or a "this was verified" claim (§9 guarantee 1-2).
7
+
8
+ /** Default adversarial persona flavour (spec §3 --persona default). */
9
+ export const DEFAULT_PERSONA = 'generic-sceptic';
10
+
11
+ /**
12
+ * Compose the adversarial brief the verifier agent is launched with. The brief
13
+ * is built purely from the acceptance contract (ACs) + the live URL — the only
14
+ * two inputs (§9 guarantee 2). Returned as structured data so tests can assert
15
+ * the ACs drove it and no build context leaked in.
16
+ *
17
+ * @param {object} opts
18
+ * @param {Array<object>} opts.acs - flattened ACs from the chain
19
+ * @param {string} opts.url - the running app under test
20
+ * @param {string} [opts.persona]
21
+ * @param {string} [opts.chainRef]
22
+ * @returns {{ persona: string, url: string, chainRef: string, stance: string, acCount: number, journeys: object[], instructions: string }}
23
+ */
24
+ export function composeBrief({ acs = [], url, persona = DEFAULT_PERSONA, chainRef } = {}) {
25
+ const testable = acs.filter((ac) => ac.testable !== false);
26
+ const journeys = testable.map((ac) => ({
27
+ acId: ac.acId,
28
+ usId: ac.usId,
29
+ journey: ac.title || ac.usId || ac.acId,
30
+ given: ac.given,
31
+ when: ac.when,
32
+ then: ac.then,
33
+ // The disproof prompt for THIS criterion: try to make `then` false.
34
+ disprove: `Attempt to make the app FAIL "${ac.then}" starting from "${ac.given}" by doing "${ac.when}", and adversarial variations of it.`,
35
+ }));
36
+
37
+ const instructions = [
38
+ 'You are an adversarial verifier. Your job is to DISPROVE the application against its acceptance criteria, not to confirm it works.',
39
+ 'You have NOT seen how this app was built, its source, or any claim that it was verified. Judge only the running app against the contract below.',
40
+ `Drive the running app at ${url} through each journey using your browser tooling. For each acceptance criterion, actively try to break it: edge inputs, boundary conditions, isolation between users, error paths, and the exact security/quality floors the criterion promises.`,
41
+ 'For every defect, record: the acId it maps to, the journey, exact reproduction steps against the live URL, and evidence (screenshot path, response body, or runtime error).',
42
+ 'Classify each finding: BROKEN (a journey is dead or wrong), DEGRADED (works but a criterion is materially weakened / a false promise / a missed floor), or COSMETIC (hygiene, no AC touched). Report PASS only for criteria you actively tried and could not break.',
43
+ 'Do NOT claim the app is "fully verified" or "safe" — you are producing an independent ship-readiness signal, not a correctness guarantee.',
44
+ '',
45
+ 'OUTPUT CONTRACT (mandatory). After any analysis, the FINAL thing you emit must be a single JSON object of exactly this shape, and nothing after it:',
46
+ '{"findings":[{"severity":"BROKEN|DEGRADED|COSMETIC|PASS","acId":"<AC id>","journey":"<journey name>","reproSteps":["..."],"evidence":{"kind":"<http_response|runtimeError|screenshot|note>","detail":"..."}}]}',
47
+ 'Emit exactly one finding per acceptance criterion (a PASS finding for any you tried and could not break). The object must be valid JSON on its own — the harness extracts the last {"findings":[...]} object from your reply, so make sure your final object is complete and well-formed.',
48
+ ].join('\n');
49
+
50
+ return {
51
+ persona,
52
+ url,
53
+ chainRef: chainRef ?? 'PRD-UNKNOWN',
54
+ stance: 'disprove',
55
+ acCount: testable.length,
56
+ journeys,
57
+ instructions,
58
+ };
59
+ }
@@ -0,0 +1,162 @@
1
+ // Verification orchestrator (spec §5, §8.2). Ties the pieces together in the
2
+ // exact order build-lite's finalise step would drive them: resolve the runtime
3
+ // profile → read the acceptance contract off the chain → (deployed) gate on
4
+ // reachability → provision prerequisites → compose the adversarial brief →
5
+ // launch the isolated verifier agent → validate/aggregate/stamp the verdict →
6
+ // build the ingestible report. Every failure is returned as data (RcfError),
7
+ // never a fabricated PASS (§9 false-confidence prohibition).
8
+
9
+ import { rcfError, isRcfError } from '@stravica-ai/rcf-lite-core/errors';
10
+ import { isolationProvenance } from '@stravica-ai/rcf-lite-core/isolation';
11
+
12
+ import {
13
+ resolveProfile,
14
+ checkDeployedReachability,
15
+ isNotDeployed,
16
+ verdictAuthorityFor,
17
+ } from '../profile/index.js';
18
+ import { readChain as defaultReadChain } from '../chain/index.js';
19
+ import { runProvisioning, cleanup as defaultCleanup } from '../provision/index.js';
20
+ import { composeBrief } from './brief.js';
21
+ import { resolveLauncher } from './launcher.js';
22
+ import { aggregateVerdict, validateFinding } from '../verdict/index.js';
23
+ import { buildReport } from '../report/index.js';
24
+
25
+ /**
26
+ * Validate + normalise the raw findings an agent returned. A malformed finding
27
+ * is an error-as-data, not a silent drop — the report contract (§5.2) requires
28
+ * every finding to be chain-node-addressed.
29
+ *
30
+ * @param {unknown} rawFindings
31
+ * @returns {{ findings: object[] } | import('@stravica-ai/rcf-lite-core/errors').RcfError}
32
+ */
33
+ export function normaliseFindings(rawFindings) {
34
+ if (!Array.isArray(rawFindings)) {
35
+ return rcfError({ kind: 'validation', message: 'verifier agent must return a findings array' });
36
+ }
37
+ const findings = [];
38
+ for (const raw of rawFindings) {
39
+ const err = validateFinding(raw);
40
+ if (err) return err;
41
+ findings.push({
42
+ severity: raw.severity,
43
+ acId: raw.acId,
44
+ journey: raw.journey,
45
+ reproSteps: raw.reproSteps,
46
+ evidence: raw.evidence,
47
+ });
48
+ }
49
+ return { findings };
50
+ }
51
+
52
+ /**
53
+ * Run a full verification. Returns `{ report }` on completion (including the
54
+ * NOT-DEPLOYED / BLOCKED refusals, which are legitimate reports), or an
55
+ * RcfError for usage / chain-load / agent failures the CLI maps to a non-zero
56
+ * exit.
57
+ *
58
+ * @param {object} opts - the CLI-parsed run options
59
+ * @param {object} [deps] - injectable seams (launchAgent, fetchImpl, signup, teardown, now, readChain)
60
+ * @returns {Promise<{ report: object } | import('@stravica-ai/rcf-lite-core/errors').RcfError>}
61
+ */
62
+ export async function runVerification(opts = {}, deps = {}) {
63
+ const now = deps.now ?? (() => new Date().toISOString());
64
+ const readChain = deps.readChain ?? defaultReadChain;
65
+ const startedAt = now();
66
+
67
+ // 1. Resolve + validate the runtime declaration (§4).
68
+ const resolved = resolveProfile({ profile: opts.profile, url: opts.url, parityEnv: opts.parityEnv });
69
+ if (isRcfError(resolved)) return resolved;
70
+ const { profile, url, parityEnv } = resolved;
71
+
72
+ // 2. Read the acceptance contract off the chain (the ONLY structural input, §9).
73
+ const chain = await readChain({ repo: opts.repo, chainRef: opts.chainRef });
74
+ if (isRcfError(chain)) return chain;
75
+
76
+ const verdictAuthority = verdictAuthorityFor(profile, parityEnv);
77
+
78
+ // 3. Deployed reachability gate (§4). Only consulted for profile==='deployed'.
79
+ let reachability = null;
80
+ if (profile === 'deployed') {
81
+ reachability = await checkDeployedReachability(url, { fetchImpl: deps.fetchImpl });
82
+ if (isNotDeployed(profile, reachability)) {
83
+ // Refusal to issue a verdict — never a soft pass (§4, §5.1).
84
+ const report = buildReport({
85
+ profile, url, parityEnv, reachability, chainRef: chain.chainRef, repo: opts.repo,
86
+ persona: opts.persona, startedAt, finishedAt: now(),
87
+ verifierIsolation: isolationProvenance(),
88
+ verdict: 'NOT-DEPLOYED', verdictAuthority,
89
+ findings: [], blockedAcs: [], provisioning: null,
90
+ });
91
+ return { report };
92
+ }
93
+ }
94
+
95
+ // 4. Provision prerequisites (§6). Unprovisionable → BLOCKED (named), dependent ACs BLOCKED.
96
+ const { provisioning, blockedAcs } = await runProvisioning({
97
+ acs: chain.acs,
98
+ url,
99
+ provisionPath: opts.provision,
100
+ mode: opts.provisionMode ?? 'run',
101
+ signup: deps.signup,
102
+ });
103
+
104
+ // 5. Compose the adversarial brief from the chain ACs (§5, §9 guarantee 4).
105
+ const brief = composeBrief({ acs: chain.acs, url, persona: opts.persona, chainRef: chain.chainRef });
106
+
107
+ // Cleanup runs whether or not the launch succeeds — provisioned artefacts
108
+ // must not be orphaned by a launch failure (§6 cleanup contract).
109
+ const runCleanup = async () => {
110
+ const cleanupFn = deps.cleanup ?? defaultCleanup;
111
+ const cleanupResult = await cleanupFn({ provisioned: provisioning.provisioned, teardown: deps.teardown });
112
+ provisioning.cleanupRan = cleanupResult.cleanupRan;
113
+ provisioning.cleanupRemoved = cleanupResult.cleanupRemoved;
114
+ if (cleanupResult.cleanupBlocked?.length) provisioning.cleanupBlocked = cleanupResult.cleanupBlocked;
115
+ };
116
+
117
+ // 6. Launch the isolated verifier agent (§7.3 isolation env, §9 fresh session).
118
+ let launchResult;
119
+ try {
120
+ const launchAgent = await resolveLauncher(deps);
121
+ launchResult = await launchAgent({ brief, url, profile });
122
+ } catch (err) {
123
+ // A verifier agent that could not run — or whose output could not be
124
+ // ingested — is NEVER a fabricated PASS (§9). But the report is still
125
+ // build-lite's next input (§5.4, §10 --out-always-written), so we build a
126
+ // LAUNCH-FAILURE report carrying the error + preserved-transcript path so
127
+ // the fix loop has something to ingest. Exit stays non-zero (gate trips).
128
+ await runCleanup();
129
+ const report = buildReport({
130
+ profile, url, parityEnv, reachability, chainRef: chain.chainRef, repo: opts.repo,
131
+ persona: opts.persona, startedAt, finishedAt: now(),
132
+ verifierIsolation: isolationProvenance(),
133
+ verdict: 'LAUNCH-FAILURE', verdictAuthority,
134
+ findings: [], blockedAcs, provisioning,
135
+ launchFailure: { message: err.message, rawOutputPath: err.rawOutputPath ?? null },
136
+ });
137
+ return { report };
138
+ }
139
+
140
+ // 7. Validate + stamp the findings (§5.2 chain-node addressing).
141
+ const normalised = normaliseFindings(launchResult?.findings);
142
+ if (isRcfError(normalised)) return normalised;
143
+ const { findings } = normalised;
144
+
145
+ // 8. Cleanup provisioned artefacts (§6 cleanup contract).
146
+ await runCleanup();
147
+
148
+ // 9. Aggregate the verdict — split, never averaged (§5.1).
149
+ const verdict = aggregateVerdict({ findings, blockedAcs, notDeployed: false });
150
+
151
+ // 10. Build the ingestible report (§5.3).
152
+ const report = buildReport({
153
+ profile, url, parityEnv, reachability, chainRef: chain.chainRef, repo: opts.repo,
154
+ persona: opts.persona, startedAt, finishedAt: now(),
155
+ verifierIsolation: isolationProvenance(),
156
+ verdict, verdictAuthority,
157
+ findings, blockedAcs, provisioning,
158
+ runStats: launchResult?.runStats ?? null,
159
+ });
160
+
161
+ return { report };
162
+ }