2026-03-10 00:00:00 -07:00
/**
* S6: Qualify thread - Qualify command handler
*
* Responsibilities:
* - Load config and resolve profile
* - Block prod runs by default (policy engine)
* - Run scenario/stateful/chaos based on profile
* - Generate seed if omitted, always print it
* - Rich artifact emission with step traces
* - Handle cleanup failures separately
* - Exit 0 on pass, 1 on qualification failure, 2 on safety violation
*
* 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 } 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 } from '../../core/types.js'
import { classifyError , ErrorTaxonomy } from '../../core/error-taxonomy.js'
import {
runQualify ,
resolveProfileGates ,
type QualifyRunResult ,
type StepTrace ,
type CleanupFailure ,
} from './runner.js'
import { SeededRng } from '../../../infrastructure/seeded-rng.js'
import type { ScenarioConfig , TestConfig , RouteContract , ChaosConfig } from '../../../types.js'
import { 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 } from 'node:path'
import { pathToFileURL } from 'node:url'
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 )
}
function coerceTimeout ( value : unknown ) : number | undefined {
return typeof value === 'number' ? value : undefined
}
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface QualifyOptions {
profile? : string
seed? : number
config? : string
cwd? : string
format ? : 'human' | 'json' | 'ndjson'
quiet? : boolean
verbose? : boolean
artifactDir? : string
}
interface FastifyAppLike {
ready ? : ( ) = > Promise < void >
close ? : ( ) = > Promise < void >
}
// ---------------------------------------------------------------------------
// Seed generation
// ---------------------------------------------------------------------------
/**
* Generate a deterministic seed if none provided.
* Uses current time + process pid + counter for uniqueness.
*/
let seedCounter = 0
export function generateSeed ( ) : number {
seedCounter ++
return Date . now ( ) + ( process . pid || 0 ) + seedCounter
}
// ---------------------------------------------------------------------------
// Route discovery helper
// ---------------------------------------------------------------------------
/**
* Discover routes from the Fastify app for chaos execution.
* Injected fastify instance must have routes registered.
*/
async function discoverAppRoutes ( fastify : unknown ) : Promise < RouteContract [ ] > {
// Cast to access routes
const app = fastify as { routes? : Array < { method : string ; url : string ; schema? : Record < string , unknown > } > }
if ( ! app . routes ) return [ ]
return app . routes . map ( r = > ( {
path : r.url ,
method : r.method as RouteContract [ 'method' ] ,
category : 'observer' ,
requires : [ ] ,
ensures : [ ] ,
invariants : [ ] ,
regexPatterns : { } ,
validateRuntime : false ,
schema : r.schema ,
} ) )
}
// ---------------------------------------------------------------------------
// Scenario builder from profile
// ---------------------------------------------------------------------------
/**
* Build scenario configs from profile routes for protocol-lab fixture.
* Creates an OAuth-like multi-step scenario.
*/
function buildScenarioConfigs ( routes : string [ ] , seed : number ) : ScenarioConfig [ ] {
// For the protocol-lab fixture, build the OAuth scenario
const hasOAuth = routes . some ( r = > r . includes ( '/oauth/authorize' ) )
if ( ! hasOAuth ) return [ ]
const rng = new SeededRng ( seed )
const clientId = ` client- ${ Math . floor ( rng . next ( ) * 10000 ) } `
return [ {
name : 'oauth-flow' ,
steps : [
{
name : 'authorize' ,
request : {
method : 'POST' ,
url : '/oauth/authorize' ,
body : {
client_id : clientId ,
redirect_uri : 'http://localhost/callback' ,
scope : 'read' ,
} ,
} ,
expect : [ 'status:200' , 'response_body(this).code != null' ] ,
capture : { code : 'response_body(this).code' } ,
} ,
{
name : 'token' ,
request : {
method : 'POST' ,
url : '/oauth/token' ,
body : {
code : '$authorize.code' ,
client_id : clientId ,
client_secret : 'secret' ,
redirect_uri : 'http://localhost/callback' ,
} ,
} ,
expect : [ 'status:200' , 'response_body(this).access_token != null' ] ,
capture : { accessToken : 'response_body(this).access_token' } ,
} ,
{
name : 'user' ,
request : {
method : 'GET' ,
url : '/api/user' ,
headers : {
authorization : 'Bearer $token.accessToken' ,
} ,
} ,
expect : [ 'status:200' , 'response_body(this).id != null' ] ,
} ,
] ,
} ]
}
// ---------------------------------------------------------------------------
// Artifact builder
// ---------------------------------------------------------------------------
/**
* Build a rich artifact document from qualify results.
* Includes step traces, cleanup failures, and replay info.
*/
export function buildArtifact (
runResult : QualifyRunResult ,
options : {
cwd : string
configPath? : string
profile? : string
preset? : string
env : string
seed : number
} ,
) : Artifact {
const failures : FailureRecord [ ] = [ ]
const warnings : string [ ] = [ ]
const replayCompatibleExecutedRoutes = ( runResult . executedRoutes || [ ] )
. map ( normalizeRouteIdentity )
. filter ( isReplayCompatibleRoute )
// Collect scenario failures
for ( const scenario of runResult . scenarioResults ) {
if ( ! scenario . ok ) {
for ( let stepIdx = 0 ; stepIdx < scenario . steps . length ; stepIdx ++ ) {
const step = scenario . steps [ stepIdx ] !
if ( ! step . ok && step . diagnostics ) {
// Use actual HTTP route from step trace for stable replay identity
const trace = runResult . stepTraces . find (
t = > t . name === step . name && t . status === 'failed'
)
const route = normalizeRouteIdentity ( trace ? . route || ` ${ scenario . name } / ${ step . name } ` )
if ( ! isReplayCompatibleRoute ( route ) ) {
warnings . push ( ` Scenario step " ${ scenario . name } / ${ step . name } " did not resolve to METHOD /path route identity. ` )
}
failures . push ( {
route ,
contract : step.diagnostics.formula || 'scenario-step' ,
expected : step.diagnostics.expected || 'success' ,
observed : step.diagnostics.error || 'failure' ,
seed : runResult.seed ,
replayCommand : ` apophis replay --artifact <artifact-path-unavailable> ` ,
category : step.diagnostics.error ? classifyError ( step . diagnostics . error ) : ErrorTaxonomy . RUNTIME ,
diff : step.diagnostics.diff ? ? undefined ,
actual : step.diagnostics.actual ? ? undefined ,
} )
}
}
}
}
// Collect stateful failures
if ( runResult . statefulResult ) {
let fallbackRouteIdx = 0
for ( const test of runResult . statefulResult . tests ) {
if ( ! test . ok ) {
let route = normalizeRouteIdentity ( test . name )
if ( ! isReplayCompatibleRoute ( route ) ) {
route = replayCompatibleExecutedRoutes [ fallbackRouteIdx ] || route
fallbackRouteIdx ++
}
if ( ! isReplayCompatibleRoute ( route ) ) {
warnings . push ( ` Stateful failure " ${ test . name } " did not resolve to METHOD /path route identity. ` )
}
failures . push ( {
route ,
contract : test.diagnostics?.formula || 'stateful-test' ,
expected : test.diagnostics?.expected || 'success' ,
observed : test.diagnostics?.error || 'failure' ,
seed : runResult.seed ,
replayCommand : ` apophis replay --artifact <artifact-path-unavailable> ` ,
category : test.diagnostics?.error ? classifyError ( test . diagnostics . error ) : ErrorTaxonomy . RUNTIME ,
diff : test.diagnostics?.diff ? ? undefined ,
actual : test.diagnostics?.actual ? ? undefined ,
} )
}
}
}
const totalTests =
runResult . scenarioResults . reduce ( ( sum , s ) = > sum + s . steps . length , 0 ) +
( runResult . statefulResult ? . tests . length ? ? 0 )
const passedTests =
runResult . scenarioResults . reduce ( ( sum , s ) = > sum + s . summary . passed , 0 ) +
( runResult . statefulResult ? . summary . passed ? ? 0 )
if ( runResult . cleanupFailures . length > 0 ) {
warnings . push (
` Cleanup failures: ${ runResult . cleanupFailures . map ( c = > ` ${ c . resource } : ${ c . error } ` ) . join ( ', ' ) } `
)
}
// Build cleanup outcomes from cleanup failures
const cleanupOutcomes = runResult . cleanupFailures . map ( cf = > ( {
resource : cf.resource ,
cleaned : false ,
error : cf.error ,
} ) )
// Build execution summary from runner result
const executionSummary = runResult . executionSummary
// Build profile gates from the result context
// We need to pass gates through or infer from results
const profileGates = {
scenario : runResult.scenarioResults.length > 0 || executionSummary . scenariosRun > 0 ,
stateful : ( runResult . statefulResult ? . tests . length ? ? 0 ) > 0 || executionSummary . statefulTestsRun > 0 ,
chaos : ( runResult . chaosResult !== undefined ) || executionSummary . chaosRunsRun > 0 ,
}
// Deterministic parameters for audit
const deterministicParams = {
seed : runResult.seed ,
profileGates ,
}
return {
version : 'apophis-artifact/1' ,
command : 'qualify' ,
mode : 'qualify' ,
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 : totalTests ,
passed : passedTests ,
failed : failures.length ,
} ,
executionSummary ,
executedRoutes : ( runResult . executedRoutes || [ ] ) . map ( normalizeRouteIdentity ) ,
skippedRoutes : ( runResult . skippedRoutes || [ ] ) . map ( sr = > ( {
route : sr.route ,
executed : false ,
reason : sr.reason ,
} ) ) ,
stepTraces : runResult.stepTraces ,
cleanupOutcomes ,
profileGates ,
deterministicParams ,
failures ,
artifacts : [ ] ,
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 : 'qualify'
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
}
// ---------------------------------------------------------------------------
// Output formatting
// ---------------------------------------------------------------------------
function formatHumanOutput (
result : QualifyRunResult ,
options : { profile? : string ; seed : number ; env : string } ,
) : string {
const lines : string [ ] = [ ]
lines . push ( ` Qualify run for environment " ${ options . env } " ` )
if ( options . profile ) {
lines . push ( ` Profile: ${ options . profile } ` )
}
lines . push ( ` Seed: ${ options . seed } ` )
lines . push ( '' )
// Scenario results
for ( const scenario of result . scenarioResults ) {
lines . push ( ` Scenario: ${ scenario . name } ` )
for ( const step of scenario . steps ) {
const icon = step . ok ? '✓' : '✗'
lines . push ( ` ${ icon } ${ step . name } ( ${ step . statusCode ? ? 'no-status' } ) ` )
if ( ! step . ok && step . diagnostics ) {
lines . push ( ` Expected: ${ step . diagnostics . expected || 'success' } ` )
lines . push ( ` Observed: ${ step . diagnostics . error || 'failure' } ` )
if ( step . diagnostics . actual ) {
lines . push ( ` Actual: ${ step . diagnostics . actual } ` )
}
if ( step . diagnostics . diff ) {
lines . push ( ` Diff: ` )
for ( const line of String ( step . diagnostics . diff ) . split ( '\n' ) ) {
lines . push ( ` ${ line } ` )
}
}
}
}
lines . push ( '' )
}
// Stateful results
if ( result . statefulResult ) {
lines . push ( ` Stateful: ${ result . statefulResult . summary . passed } passed, ${ result . statefulResult . summary . failed } failed ` )
lines . push ( '' )
}
// Chaos results
if ( result . chaosResult ) {
lines . push ( ` Chaos: ${ result . chaosResult . applied ? 'applied' : 'none' } ` )
if ( result . chaosResult . events . length > 0 ) {
for ( const event of result . chaosResult . events ) {
lines . push ( ` ${ event } ` )
}
}
lines . push ( '' )
}
// Step traces
if ( result . stepTraces . length > 0 ) {
lines . push ( 'Step traces:' )
for ( const trace of result . stepTraces . slice ( 0 , 20 ) ) {
const icon = trace . status === 'passed' ? '✓' : trace . status === 'skipped' ? '⊘' : '✗'
lines . push ( ` ${ icon } ${ trace . name } ( ${ trace . durationMs } ms) ` )
}
if ( result . stepTraces . length > 20 ) {
lines . push ( ` ... and ${ result . stepTraces . length - 20 } more ` )
}
lines . push ( '' )
}
// Cleanup failures
if ( result . cleanupFailures . length > 0 ) {
lines . push ( 'Cleanup failures (reported separately):' )
for ( const cf of result . cleanupFailures ) {
lines . push ( ` ⚠ ${ cf . resource } : ${ cf . error } ` )
}
lines . push ( '' )
}
// Per-profile gate execution counts
lines . push ( 'Profile gate execution counts:' )
lines . push ( ` Scenario: ${ result . executionSummary . scenariosRun } run ` )
lines . push ( ` Stateful: ${ result . executionSummary . statefulTestsRun } tests run ` )
lines . push ( ` Chaos: ${ result . executionSummary . chaosRunsRun } runs run ` )
lines . push ( '' )
// Executed routes
if ( result . executedRoutes . length > 0 ) {
lines . push ( ` Executed routes ( ${ result . executedRoutes . length } ): ` )
for ( const route of result . executedRoutes ) {
lines . push ( ` ${ route } ` )
}
lines . push ( '' )
}
// Skipped routes
if ( result . skippedRoutes . length > 0 ) {
lines . push ( ` Skipped routes ( ${ result . skippedRoutes . length } ): ` )
for ( const sr of result . skippedRoutes ) {
lines . push ( ` ${ sr . route } : ${ sr . reason } ` )
}
lines . push ( '' )
}
// Summary
if ( result . passed ) {
lines . push ( 'All qualifications passed.' )
} else {
lines . push ( 'Qualification failed.' )
lines . push ( ` Replay: apophis replay --artifact <artifact-path> ` )
}
return lines . join ( '\n' )
}
// ---------------------------------------------------------------------------
// Main command handler
// ---------------------------------------------------------------------------
/**
* Main qualify command handler.
*
* Flow:
* 1. Load and resolve config
* 2. Run policy engine checks (block prod by default)
* 3. Generate seed if omitted, always print it
* 4. Resolve profile gates (scenario/stateful/chaos)
* 5. Build scenario configs from profile routes
* 6. Run execution modes
* 7. Build rich artifact with step traces
* 8. Handle cleanup failures separately
* 9. Return appropriate exit code
*/
export async function qualifyCommand (
options : QualifyOptions ,
ctx : CliContext ,
) : Promise < CommandResult > {
const {
profile ,
seed : explicitSeed ,
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
// 2. Run policy engine checks
const policyEngine = new PolicyEngine ( {
config ,
env ,
mode : 'qualify' ,
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. Resolve profile gates
const profileDef = profile ? config . profiles ? . [ profile ] : undefined
const gates = resolveProfileGates ( profileDef ? . features )
// 5. Build scenario configs from profile routes
const routes = profileDef ? . routes ? ? [ ]
const scenarios = buildScenarioConfigs ( routes , seed )
// 6. Build stateful config
const presetName = profileDef ? . preset
const preset = presetName ? config . presets ? . [ presetName ] : undefined
const presetTimeout = coerceTimeout ( ( preset as { timeout? : unknown } | undefined ) ? . timeout )
const statefulConfig : TestConfig | undefined = gates . stateful
? {
seed ,
timeout : presetTimeout ,
routes : profileDef?.routes ,
}
: undefined
// 7. Build chaos config
const chaosConfig : ChaosConfig | undefined = gates . chaos && preset ? . chaos
? {
probability : 0.5 ,
delay : { probability : 0.3 , minMs : 100 , maxMs : 500 } ,
error : { probability : 0.2 , statusCode : 503 } ,
dropout : { probability : 0.2 , statusCode : 504 } ,
corruption : { probability : 0.1 } ,
}
: undefined
// 8. Load the Fastify app for execution
// Try to import the app from the fixture
let fastify : FastifyAppLike | undefined
try {
const appPath = resolve ( workingDir , 'app.js' )
const appUrl = pathToFileURL ( appPath )
appUrl . searchParams . set ( 'apophisRun' , String ( Date . now ( ) ) )
const appModule = await import ( appUrl . href )
fastify = ( appModule . default || appModule ) as FastifyAppLike
if ( fastify && typeof fastify . ready === 'function' ) {
await fastify . ready ( )
}
} catch ( err ) {
// App not available — return a result indicating no app to test
if ( process . env . APOPHIS_DEBUG === '1' ) {
console . error ( 'Failed to load app:' , err )
}
return {
exitCode : USAGE_ERROR ,
2026-03-10 00:00:00 -07:00
message : 'No Fastify app found. Ensure app.js exports a Fastify instance or a factory function.\n\nSupported patterns:\n export default app\n export const createApp = () => app\n module.exports = app' ,
2026-03-10 00:00:00 -07:00
}
}
try {
// 9. Discover routes for chaos
const appRoutes = await discoverAppRoutes ( fastify )
// 10. Run qualify execution
const deps = {
fastify : fastify as any ,
seed ,
timeout : presetTimeout ,
}
const runResult = await runQualify ( deps , gates , scenarios , statefulConfig , chaosConfig , appRoutes )
// 11. Build artifact first so we can reference it for guardrails
const artifact = buildArtifact ( runResult , {
cwd : workingDir ,
configPath : loadResult.configPath ,
profile : profile || undefined ,
preset : presetName ,
env ,
seed ,
} )
// 12. Signal quality guardrails — fail if zero checks executed
const execSummary = runResult . executionSummary
const warnings : string [ ] = [ . . . artifact . warnings ]
if ( execSummary . totalExecuted === 0 ) {
await emitArtifact ( artifact , {
command : 'qualify' ,
cwd : workingDir ,
preferredDir : artifactDir || config . artifactDir ,
force : true ,
} )
return {
exitCode : BEHAVIORAL_FAILURE ,
message : 'Qualify failed: zero checks executed. No scenarios, stateful tests, or chaos runs were performed. Verify profile gates and app configuration.' ,
artifact ,
warnings : artifact.warnings ,
}
}
// Warn if execution counts are suspiciously low
if ( gates . scenario && execSummary . scenariosRun === 0 ) {
warnings . push ( 'WARNING: scenario gate enabled but zero scenarios executed. Check route configuration.' )
}
if ( gates . stateful && execSummary . statefulTestsRun === 0 ) {
warnings . push ( 'WARNING: stateful gate enabled but zero stateful tests executed. Check app routes and schema.' )
}
if ( gates . chaos && execSummary . chaosRunsRun === 0 ) {
warnings . push ( 'WARNING: chaos gate enabled but zero chaos runs executed. Check chaos config and route availability.' )
}
// 12. Write artifact if configured or on failure
const shouldEmitArtifact = Boolean ( artifactDir || config . artifactDir || ! runResult . passed )
await emitArtifact ( artifact , {
command : 'qualify' ,
cwd : workingDir ,
preferredDir : artifactDir || config . artifactDir ,
force : shouldEmitArtifact ,
} )
// 13. Format output based on format option
const outputCtx : OutputContext = {
isTTY : ctx.isTTY ,
isCI : ctx.isCI ,
colorMode : ctx.options.color ,
}
let message = ''
if ( ! ctx . options . quiet ) {
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 )
}
}
return {
exitCode : runResult.passed ? SUCCESS : BEHAVIORAL_FAILURE ,
artifact ,
message ,
warnings : artifact.warnings ,
}
} finally {
if ( fastify && typeof fastify . close === 'function' ) {
try {
await fastify . close ( )
} catch ( closeErr ) {
if ( process . env . APOPHIS_DEBUG === '1' ) {
console . error ( 'Failed to close Fastify app after qualify run:' , closeErr )
}
}
}
}
} catch ( error ) {
const message = error instanceof Error ? error.message : String ( error )
return {
exitCode : INTERNAL_ERROR ,
message : ` Internal error in qualify command: ${ message } ` ,
}
}
}
// ---------------------------------------------------------------------------
// CLI adapter
// ---------------------------------------------------------------------------
/**
* Adapter that bridges the CLI framework (cac) to the qualify command handler.
* This function signature matches what the CLI core expects.
*/
export async function handleQualify (
args : string [ ] ,
ctx : CliContext ,
) : Promise < number > {
const options : QualifyOptions = {
profile : ctx.options.profile || undefined ,
seed : undefined ,
config : ctx.options.config || undefined ,
cwd : ctx.cwd ,
format : ctx.options.format as QualifyOptions [ 'format' ] ,
quiet : ctx.options.quiet ,
verbose : ctx.options.verbose ,
artifactDir : ctx.options.artifactDir || undefined ,
}
const seedIdx = args . indexOf ( '--seed' )
if ( seedIdx !== - 1 && args [ seedIdx + 1 ] ) {
const parsed = parseInt ( args [ seedIdx + 1 ] ! , 10 )
if ( ! isNaN ( parsed ) ) {
options . seed = parsed
}
}
const result = await qualifyCommand ( 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 : 'qualify' ,
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 : 'qualify' ,
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
}