2026-03-10 00:00:00 -07:00
/**
* S4: Verify thread - Deterministic contract verification command
*
* Responsibilities:
* - Load config and resolve profile
* - Discover routes from Fastify app
* - Filter routes by --routes flag (supports wildcards/patterns)
* - Run deterministic contract verification
* - Generate seed if omitted, always print it
* - Produce canonical failure output matching golden snapshot
* - Emit artifact JSON
* - Print replay command
* - Support --changed for git-based filtering
* - Exit 0 on pass, 1 on behavioral failure, 2 on config error
*
* Architecture:
* - Dependency injection: all dependencies passed explicitly
* - No optional imports — everything is required or injected
* - Inline comments for documentation
*/
import type { CliContext } from '../../core/context.js'
import { loadConfig , findWorkspacePackages } from '../../core/config-loader.js'
import { PolicyEngine , detectEnvironment } from '../../core/policy-engine.js'
2026-03-10 00:00:00 -07:00
2026-03-10 00:00:00 -07:00
import { SUCCESS , BEHAVIORAL_FAILURE , USAGE_ERROR , INTERNAL_ERROR } from '../../core/exit-codes.js'
import type { CommandResult , Artifact , FailureRecord , RouteResult , WorkspaceRun , WorkspaceResult } from '../../core/types.js'
import { classifyError , ErrorTaxonomy } from '../../core/error-taxonomy.js'
import { runVerify , type VerifyRunResult } from './runner.js'
import { renderCanonicalFailure , renderHumanArtifact } from '../../renderers/human.js'
import { renderJson , renderJsonArtifact , renderJsonSummaryArtifact } from '../../renderers/json.js'
import { renderNdjsonArtifact , renderNdjsonSummaryArtifact } from '../../renderers/ndjson.js'
import type { OutputContext } from '../../renderers/shared.js'
import { resolve , basename } from 'node:path'
const ROUTE_IDENTITY_PATTERN = /^[A-Z]+\s+\/\S*$/
function normalizeRouteIdentity ( route : string ) : string {
const normalized = route . trim ( ) . replace ( /\s+/g , ' ' )
const [ method , . . . pathParts ] = normalized . split ( ' ' )
if ( ! method || pathParts . length === 0 ) {
return normalized
}
return ` ${ method . toUpperCase ( ) } ${ pathParts . join ( ' ' ) } `
}
function isReplayCompatibleRoute ( route : string ) : boolean {
return ROUTE_IDENTITY_PATTERN . test ( route )
}
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface VerifyOptions {
profile? : string
routes? : string
seed? : number
changed? : boolean
config? : string
cwd? : string
format ? : 'human' | 'json' | 'ndjson'
quiet? : boolean
verbose? : boolean
artifactDir? : string
}
// ---------------------------------------------------------------------------
// Seed generation
// ---------------------------------------------------------------------------
/**
* Generate a deterministic seed if none provided.
* Uses current time + process pid for uniqueness.
*/
export function generateSeed ( ) : number {
return Date . now ( ) + ( process . pid || 0 )
}
// ---------------------------------------------------------------------------
// Route filter parsing
// ---------------------------------------------------------------------------
/**
* Parse --routes flag into filter patterns.
* Supports comma-separated patterns with wildcards.
*/
function parseRouteFilters ( routesFlag : string | undefined ) : string [ ] | undefined {
if ( ! routesFlag ) return undefined
return routesFlag . split ( ',' ) . map ( r = > r . trim ( ) ) . filter ( Boolean )
}
// ---------------------------------------------------------------------------
// Artifact builder
// ---------------------------------------------------------------------------
/**
* Build artifact document from verify results.
*/
function buildArtifact (
runResult : VerifyRunResult ,
options : {
cwd : string
configPath? : string
profile? : string
preset? : string
env : string
seed : number
routeFilters? : string [ ]
} ,
) : Artifact {
const warnings : string [ ] = [ ]
const failures : FailureRecord [ ] = runResult . failures . map ( f = > {
const route = normalizeRouteIdentity ( f . route )
if ( ! isReplayCompatibleRoute ( route ) ) {
warnings . push ( ` Failure route " ${ f . route } " is not in METHOD /path format; replay matching may be less precise. ` )
}
return {
route ,
2026-05-21 20:39:36 -07:00
contract : f.formula || f . contract ,
2026-03-10 00:00:00 -07:00
expected : f.expected ,
observed : f.observed ,
seed : options.seed ,
replayCommand : ` apophis replay --artifact ${ f . artifactPath || '<artifact-path-unavailable>' } ` ,
2026-05-21 20:39:36 -07:00
category : f.category ? ? ( f . observed ? classifyError ( f . observed ) : ErrorTaxonomy . RUNTIME ) ,
2026-03-10 00:00:00 -07:00
}
} )
if ( runResult . noContractsFound ) {
warnings . push ( 'No behavioral contracts found. Schema-only routes are not enough for verify. Add x-ensures or x-requires to route schemas. See docs/getting-started.md for examples.' )
2026-05-21 20:39:36 -07:00
if ( runResult . discoveryWarnings && runResult . discoveryWarnings . length > 0 ) {
warnings . push ( . . . runResult . discoveryWarnings )
}
2026-03-10 00:00:00 -07:00
}
if ( runResult . noRoutesMatched ) {
warnings . push ( ` No routes matched the filter. Available routes: ${ runResult . availableRoutes ? . join ( ', ' ) || 'none' } ` )
2026-05-21 20:39:36 -07:00
if ( runResult . discoveryWarnings && runResult . discoveryWarnings . length > 0 ) {
warnings . push ( . . . runResult . discoveryWarnings )
}
2026-03-10 00:00:00 -07:00
}
if ( runResult . notGitRepo ) {
warnings . push ( '--changed requires a git repository. Current directory is not inside a git repo.' )
}
if ( runResult . noRelevantChanges ) {
warnings . push ( 'No relevant changes detected. Git shows no modified files that match any route.' )
}
if ( runResult . failures . length > 0 ) {
const profileFlag = options . profile ? ` --profile ${ options . profile } ` : ''
const routesFlag = options . routeFilters && options . routeFilters . length > 0
? ` --routes " ${ options . routeFilters . join ( ',' ) } " `
: ''
warnings . push ( ` Deterministic rerun: apophis verify --seed ${ options . seed } ${ profileFlag } ${ routesFlag } ` )
warnings . push ( 'If rerun output differs with same seed, stabilize app state/data and isolate time/external dependencies.' )
}
return {
version : 'apophis-artifact/1' ,
2026-05-21 20:39:36 -07:00
cliVersion : '2.7.0' ,
2026-03-10 00:00:00 -07:00
command : 'verify' ,
mode : 'verify' ,
cwd : options.cwd ,
configPath : options.configPath ,
profile : options.profile ,
preset : options.preset ,
env : options.env ,
seed : options.seed ,
startedAt : new Date ( Date . now ( ) - runResult . durationMs ) . toISOString ( ) ,
durationMs : runResult.durationMs ,
summary : {
total : runResult.total ,
passed : runResult.passedCount ,
failed : runResult.failed ,
} ,
deterministicParams : {
seed : options.seed ,
routeFilters : options.routeFilters ? ? [ ] ,
} ,
failures ,
artifacts : runResult.artifactPaths ,
warnings ,
exitReason : runResult.passed ? 'success' : 'behavioral_failure' ,
}
}
function attachReplayCommands ( artifact : Artifact , artifactPath : string ) : void {
for ( const failure of artifact . failures ) {
failure . replayCommand = ` apophis replay --artifact ${ artifactPath } `
}
}
async function emitArtifact (
artifact : Artifact ,
options : {
command : 'verify'
cwd : string
preferredDir? : string
force : boolean
} ,
) : Promise < string | undefined > {
if ( ! options . force && ! options . preferredDir ) {
return undefined
}
const defaultDir = resolve ( options . cwd , 'reports' , 'apophis' )
const candidateDirs = [ options . preferredDir , defaultDir ] . filter ( Boolean ) as string [ ]
const attempted = new Set < string > ( )
for ( const dir of candidateDirs ) {
if ( attempted . has ( dir ) ) continue
attempted . add ( dir )
try {
const { mkdirSync , writeFileSync } = await import ( 'node:fs' )
const artifactPath = resolve ( dir , ` ${ options . command } - ${ new Date ( ) . toISOString ( ) . replace ( /[:.]/g , '-' ) } .json ` )
mkdirSync ( dir , { recursive : true } )
attachReplayCommands ( artifact , artifactPath )
writeFileSync ( artifactPath , JSON . stringify ( artifact , null , 2 ) )
if ( ! artifact . artifacts . includes ( artifactPath ) ) {
artifact . artifacts . push ( artifactPath )
}
return artifactPath
} catch {
// Try fallback directory if available.
}
}
artifact . warnings . push ( 'Failed to write artifact to disk' )
return undefined
}
// ---------------------------------------------------------------------------
// Human output formatting
// ---------------------------------------------------------------------------
/**
* Format canonical failure output matching golden snapshot.
*/
function formatHumanFailure ( failure : FailureRecord , profile? : string ) : string {
const lines : string [ ] = [ ]
lines . push ( 'Contract violation' )
lines . push ( failure . route )
lines . push ( ` Profile: ${ profile || 'default' } ` )
lines . push ( ` Seed: ${ failure . seed } ` )
lines . push ( '' )
lines . push ( 'Expected' )
lines . push ( ` ${ failure . contract } ` )
lines . push ( '' )
lines . push ( 'Observed' )
lines . push ( ` ${ failure . observed } ` )
lines . push ( '' )
lines . push ( 'Why this matters' )
lines . push ( ` The resource created by ${ failure . route . split ( ' ' ) [ 1 ] } is not retrievable. ` )
lines . push ( '' )
lines . push ( 'Replay' )
lines . push ( ` ${ failure . replayCommand } ` )
lines . push ( '' )
lines . push ( 'Next' )
lines . push ( ` Check the create/read consistency for ${ failure . route } and GET ${ failure . route . split ( ' ' ) [ 1 ] } /{id}. ` )
return lines . join ( '\n' )
}
/**
// ---------------------------------------------------------------------------
// Main command handler
// ---------------------------------------------------------------------------
/**
* Main verify command handler.
*
* Flow:
* 1. Load and resolve config
* 2. Run policy engine checks
* 3. Generate seed if omitted, always print it
* 4. Parse route filters
* 5. Load Fastify app and discover routes
* 6. Run deterministic contract verification
* 7. Build artifact
* 8. Format output
* 9. Write artifact if artifactDir specified
* 10. Return appropriate exit code
*/
export async function verifyCommand (
options : VerifyOptions ,
ctx : CliContext ,
) : Promise < CommandResult > {
const {
profile ,
routes : routesFlag ,
seed : explicitSeed ,
changed ,
config : configPath ,
cwd ,
artifactDir ,
} = options
const workingDir = cwd || ctx . cwd
const format = options . format || ctx . options . format || 'human'
// Detect environment
const env = detectEnvironment ( )
try {
// 1. Load config
const loadResult = await loadConfig ( {
cwd : workingDir ,
configPath ,
profileName : profile ,
env ,
} )
if ( ! loadResult . configPath ) {
return {
exitCode : USAGE_ERROR ,
message : 'No config found. Run "apophis init" to create one.' ,
}
}
const config = loadResult . config
// 2a. Resolve profile — if explicitly requested but missing, list available ones
if ( profile && ! config . profiles ? . [ profile ] ) {
const available = Object . keys ( config . profiles ? ? { } ) . join ( ', ' ) || 'none'
return {
exitCode : USAGE_ERROR ,
message : ` Unknown profile " ${ profile } ". Available profiles: ${ available } . \ n \ nNext: \ n Run \` apophis init \` to scaffold a new profile, or use one of the profiles listed above. ` ,
}
}
// 2. Run policy engine checks
const policyEngine = new PolicyEngine ( {
config ,
env ,
mode : 'verify' ,
profileName : profile || undefined ,
presetName : loadResult.presetName || undefined ,
} )
const policyResult = policyEngine . check ( )
if ( ! policyResult . allowed ) {
const message = [
'Policy check failed:' ,
. . . policyResult . errors . map ( e = > ` ✗ ${ e } ` ) ,
] . join ( '\n' )
return {
exitCode : USAGE_ERROR ,
message ,
}
}
// 3. Generate seed if omitted
const seed = explicitSeed ? ? generateSeed ( )
if ( ! ctx . options . quiet && format === 'human' ) {
console . log ( ` Seed: ${ seed } ` )
}
// 4. Parse route filters
const routeFilters = parseRouteFilters ( routesFlag )
// 5. Load the Fastify app
let fastify : unknown
2026-05-21 20:39:36 -07:00
let appEntrypoint : string | undefined
2026-03-10 00:00:00 -07:00
try {
const { loadApp } = await import ( '../../core/app-loader.js' )
const loaded = await loadApp ( workingDir )
fastify = loaded . fastify
2026-05-21 20:39:36 -07:00
appEntrypoint = loaded . entrypoint
2026-03-10 00:00:00 -07:00
if ( fastify && typeof ( fastify as any ) . ready === 'function' ) {
await ( fastify as any ) . ready ( )
}
} catch ( err ) {
const errorMessage = err instanceof Error ? err.message : String ( err )
return {
exitCode : USAGE_ERROR ,
2026-05-21 20:39:36 -07:00
message : ` Failed to load Fastify app. \ n \ nError: ${ errorMessage } \ n \ nApp file candidates (searched in order): \ n app.js, app.ts, server.js, server.ts, index.js, index.ts \ n src/app.js, src/app.ts, src/server.js, src/server.ts, src/index.js, src/index.ts \ n \ nNext: \ n Run \` apophis init \` to scaffold a working app.js and config. ` ,
2026-03-10 00:00:00 -07:00
}
}
// 6. Run verify execution
const runResult = await runVerify ( {
fastify : fastify as any ,
seed ,
timeout : typeof config . presets ? . [ loadResult . presetName || '' ] ? . timeout === 'number'
? ( config . presets [ loadResult . presetName || '' ] as { timeout? : number } ) . timeout
: undefined ,
routeFilters ,
changed ,
profileRoutes : config.profiles?. [ profile || '' ] ? . routes ,
2026-05-21 20:39:36 -07:00
runs : typeof config . presets ? . [ loadResult . presetName || '' ] ? . runs === 'number'
? ( config . presets [ loadResult . presetName || '' ] as { runs? : number } ) . runs
: undefined ,
2026-03-10 00:00:00 -07:00
} )
// 7. Build artifact
const artifact = buildArtifact ( runResult , {
cwd : workingDir ,
configPath : loadResult.configPath ,
profile : profile || undefined ,
preset : loadResult.presetName || undefined ,
env ,
seed ,
routeFilters ,
} )
// 8. Write artifact if configured or on failure
const shouldEmitArtifact = Boolean ( artifactDir || config . artifactDir || ! runResult . passed )
await emitArtifact ( artifact , {
command : 'verify' ,
cwd : workingDir ,
preferredDir : artifactDir || config . artifactDir ,
force : shouldEmitArtifact ,
} )
// 9. Format output based on format option
const outputCtx : OutputContext = {
isTTY : ctx.isTTY ,
isCI : ctx.isCI ,
colorMode : ctx.options.color ,
}
let message : string
if ( format === 'json' ) {
message = renderJsonArtifact ( artifact )
} else if ( format === 'json-summary' ) {
message = renderJsonSummaryArtifact ( artifact )
} else if ( format === 'ndjson' ) {
// For ndjson, we don't return a message string; events are streamed
message = ''
} else if ( format === 'ndjson-summary' ) {
// Concise ndjson: only summary events
message = ''
} else {
// human format
message = renderHumanArtifact ( artifact , outputCtx )
}
// Determine exit code
let exitCode : number = SUCCESS
if ( runResult . noRoutesMatched || runResult . noContractsFound || runResult . notGitRepo ) {
exitCode = USAGE_ERROR
} else if ( ! runResult . passed ) {
exitCode = BEHAVIORAL_FAILURE
}
return {
exitCode : exitCode as import ( '../../core/types.js' ) . ExitCode ,
artifact ,
message ,
warnings : artifact.warnings ,
}
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error )
// Config validation errors are usage errors, not internal errors
if ( error instanceof Error && error . name === 'ConfigValidationError' ) {
return {
exitCode : USAGE_ERROR ,
message : ` Config validation failed: ${ message } ` ,
}
}
2026-03-10 00:00:00 -07:00
2026-03-10 00:00:00 -07:00
return {
exitCode : INTERNAL_ERROR ,
message : ` Internal error in verify command: ${ message } ` ,
}
}
}
// ---------------------------------------------------------------------------
// CLI adapter
// ---------------------------------------------------------------------------
/**
* Adapter that bridges the CLI framework (cac) to the verify command handler.
* This function signature matches what the CLI core expects.
*/
export async function handleVerify (
args : string [ ] ,
ctx : CliContext ,
) : Promise < number > {
const options : VerifyOptions = {
profile : ctx.options.profile || undefined ,
routes : undefined ,
seed : undefined ,
changed : false ,
config : ctx.options.config || undefined ,
cwd : ctx.cwd ,
format : ctx.options.format as VerifyOptions [ 'format' ] ,
quiet : ctx.options.quiet ,
verbose : ctx.options.verbose ,
artifactDir : ctx.options.artifactDir || undefined ,
}
// Parse command-specific flags from args (passed by CLI dispatcher)
const routesIdx = args . indexOf ( '--routes' )
if ( routesIdx !== - 1 && args [ routesIdx + 1 ] ) {
options . routes = args [ routesIdx + 1 ]
}
const seedIdx = args . indexOf ( '--seed' )
if ( seedIdx !== - 1 && args [ seedIdx + 1 ] ) {
const parsed = parseInt ( args [ seedIdx + 1 ] ! , 10 )
if ( ! isNaN ( parsed ) ) {
options . seed = parsed
}
}
options . seed = options . seed as number | undefined
if ( args . includes ( '--changed' ) ) {
options . changed = true
}
const workspaceMode = args . includes ( '--workspace' )
if ( workspaceMode ) {
const packages = findWorkspacePackages ( ctx . cwd )
if ( packages . length === 0 ) {
if ( ! ctx . options . quiet ) {
console . error ( 'No workspace packages found. Ensure workspaces are defined in root package.json or pnpm-workspace.yaml.' )
}
return USAGE_ERROR
}
const runs : WorkspaceRun [ ] = [ ]
let overallExitCode = SUCCESS
const allWarnings : string [ ] = [ ]
for ( const pkgPath of packages ) {
const pkgName = basename ( pkgPath )
const pkgOptions = { . . . options , cwd : pkgPath }
const pkgCtx : CliContext = { . . . ctx , cwd : pkgPath }
const pkgResult = await verifyCommand ( pkgOptions , pkgCtx )
if ( pkgResult . artifact ) {
pkgResult . artifact . package = pkgName
runs . push ( { package : pkgName , cwd : pkgPath , artifact : pkgResult.artifact } )
}
if ( pkgResult . exitCode !== SUCCESS ) {
overallExitCode = pkgResult . exitCode
}
if ( pkgResult . warnings ) {
allWarnings . push ( . . . pkgResult . warnings . map ( w = > ` [ ${ pkgName } ] ${ w } ` ) )
}
}
const workspaceResult : WorkspaceResult = {
exitCode : overallExitCode as import ( '../../core/types.js' ) . ExitCode ,
runs ,
warnings : allWarnings ,
}
const format = options . format || ctx . options . format || 'human'
if ( ! ctx . options . quiet ) {
if ( format === 'json' ) {
console . log ( renderJson ( {
exitCode : workspaceResult.exitCode ,
runs : workspaceResult.runs.map ( r = > ( {
package : r . package ,
cwd : r.cwd ,
artifact : r.artifact ,
} ) ) ,
warnings : workspaceResult.warnings ,
} ) )
} else if ( format === 'json-summary' ) {
console . log ( renderJson ( {
exitCode : workspaceResult.exitCode ,
runs : workspaceResult.runs.map ( r = > ( {
package : r . package ,
cwd : r.cwd ,
summary : r.artifact.summary ,
exitReason : r.artifact.exitReason ,
} ) ) ,
warnings : workspaceResult.warnings ,
} ) )
} else if ( format === 'ndjson' ) {
for ( const run of workspaceResult . runs ) {
process . stdout . write ( JSON . stringify ( {
type : 'workspace.run.completed' ,
package : run . package ,
cwd : run.cwd ,
summary : run.artifact.summary ,
exitReason : run.artifact.exitReason ,
} ) + '\n' )
}
process . stdout . write ( JSON . stringify ( {
type : 'workspace.completed' ,
exitCode : workspaceResult.exitCode ,
packages : workspaceResult.runs.length ,
} ) + '\n' )
} else if ( format === 'ndjson-summary' ) {
for ( const run of workspaceResult . runs ) {
process . stdout . write ( JSON . stringify ( {
type : 'workspace.run.completed' ,
package : run . package ,
cwd : run.cwd ,
summary : run.artifact.summary ,
exitReason : run.artifact.exitReason ,
} ) + '\n' )
}
process . stdout . write ( JSON . stringify ( {
type : 'workspace.completed' ,
exitCode : workspaceResult.exitCode ,
packages : workspaceResult.runs.length ,
} ) + '\n' )
} else {
// Human format
const lines : string [ ] = [ ]
lines . push ( 'Workspace verify results' )
lines . push ( '' )
for ( const run of workspaceResult . runs ) {
const a = run . artifact
const status = a . exitReason === 'success' ? '✓' : '✗'
lines . push ( ` ${ status } ${ run . package } : ${ a . summary . passed } / ${ a . summary . total } passed ` )
if ( a . summary . failed > 0 ) {
lines . push ( ` ${ a . summary . failed } failed ` )
}
}
lines . push ( '' )
lines . push ( ` Overall: ${ workspaceResult . exitCode === SUCCESS ? 'passed' : 'failed' } ` )
console . log ( lines . join ( '\n' ) )
}
}
if ( format !== 'json' && format !== 'ndjson' && format !== 'json-summary' && format !== 'ndjson-summary' && allWarnings . length > 0 && ! ctx . options . quiet ) {
for ( const warning of allWarnings ) {
console . warn ( ` Warning: ${ warning } ` )
}
}
return workspaceResult . exitCode
}
const result = await verifyCommand ( options , ctx )
const format = options . format || ctx . options . format || 'human'
const machineMode = format === 'json' || format === 'ndjson' || format === 'json-summary' || format === 'ndjson-summary'
if ( ! ctx . options . quiet ) {
if ( format === 'json' ) {
if ( result . artifact ) {
console . log ( renderJsonArtifact ( result . artifact ) )
} else {
console . log ( renderJson ( {
exitCode : result.exitCode ,
message : result.message ,
warnings : result.warnings ,
} ) )
}
} else if ( format === 'json-summary' ) {
if ( result . artifact ) {
console . log ( renderJsonSummaryArtifact ( result . artifact ) )
} else {
console . log ( renderJson ( {
exitCode : result.exitCode ,
message : result.message ,
warnings : result.warnings ,
} ) )
}
} else if ( format === 'ndjson' ) {
if ( result . artifact ) {
renderNdjsonArtifact ( result . artifact )
} else {
process . stdout . write ( JSON . stringify ( {
type : 'run.completed' ,
command : 'verify' ,
exitCode : result.exitCode ,
message : result.message ,
warnings : result.warnings ,
} ) + '\n' )
}
} else if ( format === 'ndjson-summary' ) {
if ( result . artifact ) {
renderNdjsonSummaryArtifact ( result . artifact )
} else {
process . stdout . write ( JSON . stringify ( {
type : 'run.completed' ,
command : 'verify' ,
exitCode : result.exitCode ,
message : result.message ,
warnings : result.warnings ,
} ) + '\n' )
}
} else if ( result . message ) {
console . log ( result . message )
}
}
// Print warnings in human mode only
if ( ! machineMode && result . warnings && result . warnings . length > 0 && ! ctx . options . quiet ) {
for ( const warning of result . warnings ) {
console . warn ( ` Warning: ${ warning } ` )
}
}
return result . exitCode
}