chore: crush git history - reborn from consolidation on 2026-03-10

This commit is contained in:
John Dvorak
2026-03-10 00:00:00 -07:00
commit d278c4b105
313 changed files with 87549 additions and 0 deletions
+803
View File
@@ -0,0 +1,803 @@
/**
* 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'
import { resolveGenerationProfileOverride, GenerationProfileResolutionError } from '../../core/generation-profile.js'
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
generationProfile?: 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,
contract: f.contract,
expected: f.expected,
observed: f.observed,
seed: options.seed,
replayCommand: `apophis replay --artifact ${f.artifactPath || '<artifact-path-unavailable>'}`,
category: f.observed ? classifyError(f.observed) : ErrorTaxonomy.RUNTIME,
}
})
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.')
}
if (runResult.noRoutesMatched) {
warnings.push(`No routes matched the filter. Available routes: ${runResult.availableRoutes?.join(', ') || 'none'}`)
}
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',
cliVersion: '2.0.0',
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')
}
/**
* Format human-readable output for verify results.
*/
function formatHumanOutput(
runResult: VerifyRunResult,
options: { profile?: string; seed: number; env: string; routeFilters?: string[] },
): string {
const lines: string[] = []
if (runResult.notGitRepo) {
lines.push(`--changed requires a git repository.`)
lines.push(`Current directory is not inside a git repo.`)
lines.push('')
lines.push('Next:')
lines.push(` Initialize git with \`git init\`, or run verify without --changed.`)
lines.push('')
return lines.join('\n')
}
if (runResult.noRelevantChanges) {
lines.push(`No relevant changes detected.`)
lines.push(`Git shows no modified files that match any route.`)
lines.push('')
return lines.join('\n')
}
if (runResult.noRoutesMatched) {
lines.push(`No routes matched the filter.`)
lines.push(`Filters applied: ${options.routeFilters?.join(', ') || 'none'}`)
lines.push(`Available routes:`)
for (const r of runResult.availableRoutes || []) {
lines.push(` ${r}`)
}
lines.push('')
lines.push('Next:')
lines.push(` Adjust --routes filter or add routes to your app.`)
lines.push('')
return lines.join('\n')
}
if (runResult.noContractsFound) {
lines.push('No behavioral contracts found.')
lines.push('')
lines.push('APOPHIS discovered routes, but none have behavioral contracts.')
lines.push('Schema-only routes (with response schemas) are not enough.')
lines.push('You must add x-ensures or x-requires clauses that check behavior.')
lines.push('')
lines.push('Example — add this to your route schema:')
lines.push(' "x-ensures": [')
lines.push(' "response_code(GET /users/{response_body(this).id}) == 200"')
lines.push(' ]')
lines.push('')
lines.push('Next steps:')
lines.push(' 1. Open your route file (e.g., app.js or src/routes/users.js)')
lines.push(' 2. Find the route you want to test')
lines.push(' 3. Add an "x-ensures" array inside the schema object')
lines.push(' 4. Run: apophis verify --profile quick --routes "POST /users"')
lines.push('')
lines.push('For more examples, see docs/getting-started.md')
lines.push('')
return lines.join('\n')
}
// Print failures using canonical format
for (const failure of runResult.failures) {
const failureRecord: FailureRecord = {
route: failure.route,
contract: failure.contract,
expected: failure.expected,
observed: failure.observed,
seed: options.seed,
replayCommand: `apophis replay --artifact ${failure.artifactPath || 'reports/apophis/failure-*.json'}`,
}
lines.push(formatHumanFailure(failureRecord, options.profile))
lines.push('')
}
// Summary
if (runResult.passed) {
lines.push(`All ${runResult.total} contract(s) passed.`)
} else {
lines.push(`Failed: ${runResult.failed} of ${runResult.total} contract(s) failed.`)
}
lines.push(`Seed: ${options.seed}`)
// Replay command on failure
if (!runResult.passed && runResult.failures.length > 0) {
lines.push('')
lines.push('Replay')
lines.push(` apophis replay --artifact <path-to-artifact>`)
lines.push('')
lines.push('Determinism')
lines.push(` This run used seed ${options.seed}.`)
lines.push(` Same seed + same app state = same results.`)
lines.push(` If results differ on re-run, the app has nondeterministic behavior.`)
lines.push(` Stabilize: reset app state, mock external services, avoid time-dependent logic.`)
}
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,
generationProfile,
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
const resolvedGenerationProfile = resolveGenerationProfileOverride(generationProfile, 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
try {
const { loadApp } = await import('../../core/app-loader.js')
const loaded = await loadApp(workingDir)
fastify = loaded.fastify
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,
message: `No Fastify app found. Ensure app.js exports a Fastify instance.\n\nError: ${errorMessage}\n\nNext:\n Run \`apophis init\` to scaffold a working app.js and config.`,
}
}
// 6. Run verify execution
const runResult = await runVerify({
fastify: fastify as any,
seed,
generationProfile: resolvedGenerationProfile,
timeout: typeof config.presets?.[loadResult.presetName || '']?.timeout === 'number'
? (config.presets[loadResult.presetName || ''] as { timeout?: number }).timeout
: undefined,
routeFilters,
changed,
profileRoutes: config.profiles?.[profile || '']?.routes,
})
// 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}`,
}
}
if (error instanceof GenerationProfileResolutionError) {
return {
exitCode: USAGE_ERROR,
message,
}
}
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,
generationProfile: ctx.options.generationProfile,
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 generationProfileIdx = args.indexOf('--generation-profile')
if (generationProfileIdx !== -1 && args[generationProfileIdx + 1]) {
options.generationProfile = args[generationProfileIdx + 1]
}
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
}
+490
View File
@@ -0,0 +1,490 @@
/**
* S4: Verify thread - Runner for deterministic contract verification
*
* Responsibilities:
* - Route discovery from Fastify app
* - Route filtering by patterns and git changes
* - Contract execution using existing plugin/evaluator code
* - Deterministic execution with seed
* - Result aggregation
*
* Architecture:
* - Pure execution functions that accept injected dependencies
* - Reuses existing APOPHIS plugin and formula code
* - No reimplementation of parser/evaluator
*/
import { discoverRoutes } from '../../../domain/discovery.js'
import { extractContract } from '../../../domain/contract.js'
import { executeHttp } from '../../../infrastructure/http-executor.js'
import { parse } from '../../../formula/parser.js'
import { evaluateAsync } from '../../../formula/evaluator.js'
import { createOperationResolver } from '../../../formula/runtime.js'
import type { EvalContext, RouteContract, FastifyInjectInstance } from '../../../types.js'
import type { RouteResult } from '../../core/types.js'
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface VerifyFailure {
route: string
contract: string
expected: string
observed: string
artifactPath?: string
}
export interface VerifyRunResult {
passed: boolean
total: number
passedCount: number
failed: number
failures: VerifyFailure[]
durationMs: number
noRoutesMatched: boolean
noContractsFound: boolean
notGitRepo?: boolean
noRelevantChanges?: boolean
availableRoutes?: string[]
artifactPaths: string[]
}
export interface VerifyRunnerDeps {
fastify: FastifyInjectInstance
seed: number
generationProfile?: 'quick' | 'standard' | 'thorough'
timeout?: number
routeFilters?: string[]
changed?: boolean
profileRoutes?: string[]
}
// ---------------------------------------------------------------------------
// Route discovery
// ---------------------------------------------------------------------------
/**
* Discover routes from a Fastify instance.
* Uses the existing discovery module.
*/
export async function discoverAppRoutes(fastify: FastifyInjectInstance): Promise<RouteContract[]> {
return discoverRoutes(fastify)
}
/**
* Check if specific routes exist in a Fastify instance using hasRoute.
* Used when the APOPHIS plugin wasn't registered before routes.
*/
export async function discoverSpecificRoutes(
fastify: FastifyInjectInstance,
routePatterns: string[],
): Promise<RouteContract[]> {
if (typeof fastify.hasRoute !== 'function') {
return []
}
const routes: RouteContract[] = []
const seen = new Set<string>()
for (const pattern of routePatterns) {
// Parse pattern like "GET /users" or "POST /api/*"
const parts = pattern.split(' ')
const method = parts[0] || 'GET'
const path = parts.slice(1).join(' ')
// For exact routes (no wildcards), check if route exists
if (!pattern.includes('*') && !pattern.includes('?')) {
try {
if (fastify.hasRoute({ url: path, method })) {
const key = `${method} ${path}`
if (!seen.has(key)) {
seen.add(key)
routes.push({
method: method as RouteContract['method'],
path,
category: 'observer',
schema: {},
requires: [],
ensures: [],
invariants: [],
regexPatterns: {},
validateRuntime: false,
})
}
}
} catch {
// Route doesn't exist
}
}
}
return routes
}
// ---------------------------------------------------------------------------
// Route filtering
// ---------------------------------------------------------------------------
/**
* Check if a route matches a filter pattern.
* Supports wildcards: * matches any characters.
*/
function matchRoutePattern(route: string, pattern: string): boolean {
// Convert pattern to regex
const regexPattern = pattern
.replace(/\*/g, '.*')
.replace(/\?/g, '.')
const regex = new RegExp(`^${regexPattern}$`, 'i')
return regex.test(route)
}
/**
* Filter routes by patterns.
*/
function filterRoutesByPatterns(routes: RouteContract[], patterns: string[]): RouteContract[] {
return routes.filter(route => {
const routeStr = `${route.method} ${route.path}`
return patterns.some(pattern => matchRoutePattern(routeStr, pattern))
})
}
/**
* Check if cwd is inside a git repository.
*/
async function isGitRepo(cwd: string): Promise<boolean> {
try {
const { execSync } = await import('node:child_process')
execSync('git rev-parse --git-dir', { cwd, encoding: 'utf-8', stdio: 'pipe' })
return true
} catch {
return false
}
}
/**
* Get git-modified files for --changed filtering.
*/
async function getGitChangedFiles(cwd: string): Promise<string[]> {
try {
const { execSync } = await import('node:child_process')
const output = execSync('git diff --name-only HEAD', { cwd, encoding: 'utf-8' })
return output.split('\n').filter(Boolean)
} catch {
return []
}
}
/**
* Filter routes to only those modified in git.
*/
async function filterChangedRoutes(
routes: RouteContract[],
cwd: string,
): Promise<RouteContract[]> {
const changedFiles = await getGitChangedFiles(cwd)
// Map route paths to potential file paths (heuristic)
return routes.filter(route => {
const routePath = route.path
// Check if any changed file might contain this route
return changedFiles.some(file => {
// Simple heuristic: check if route path segments appear in file path
const segments = routePath.split('/').filter(Boolean)
return segments.some(segment => file.includes(segment))
})
})
}
// ---------------------------------------------------------------------------
// Contract execution
// ---------------------------------------------------------------------------
/**
* Build a request for a route.
*/
function buildRouteRequest(route: RouteContract): {
method: string
url: string
body?: unknown
headers: Record<string, string>
} {
const headers: Record<string, string> = {
'content-type': 'application/json',
}
// Build body from schema if available
let body: unknown = undefined
const bodySchema = route.schema?.body as Record<string, unknown> | undefined
if (bodySchema && route.method === 'POST') {
body = buildExampleBody(bodySchema)
}
return {
method: route.method,
url: route.path,
body,
headers,
}
}
/**
* Build an example body from JSON Schema.
*/
function buildExampleBody(schema: Record<string, unknown>): unknown {
if (schema.type === 'object' && schema.properties) {
const obj: Record<string, unknown> = {}
const properties = schema.properties as Record<string, Record<string, unknown>>
for (const [key, propSchema] of Object.entries(properties)) {
obj[key] = buildExampleValue(propSchema)
}
return obj
}
return undefined
}
/**
* Build an example value from a property schema.
*/
function buildExampleValue(schema: Record<string, unknown>): unknown {
if (schema.type === 'string') {
if (schema.enum && Array.isArray(schema.enum) && schema.enum.length > 0) {
return schema.enum[0]
}
return 'test'
}
if (schema.type === 'number' || schema.type === 'integer') {
return 1
}
if (schema.type === 'boolean') {
return true
}
if (schema.type === 'array') {
return []
}
if (schema.type === 'object' && schema.properties) {
return buildExampleBody(schema)
}
return undefined
}
/**
* Execute a single contract for a route.
* Returns the evaluation context and any failure.
*/
async function executeContract(
fastify: FastifyInjectInstance,
route: RouteContract,
contract: string,
timeout?: number,
variant?: { name: string; headers?: Record<string, string> },
): Promise<{ ctx: EvalContext; failure?: VerifyFailure }> {
const request = buildRouteRequest(route)
// Merge variant headers if provided
const headers = variant?.headers
? { ...request.headers, ...variant.headers }
: request.headers
// Execute the primary request
const ctx = await executeHttp(fastify, route, {
method: request.method,
url: request.url,
body: request.body,
headers,
query: {},
}, undefined, timeout)
// Build eval context with operation resolver for cross-operation calls
const evalCtx: EvalContext = {
...ctx,
operationResolver: createOperationResolver(fastify, headers, ctx),
}
// Parse and evaluate the contract
try {
const parsed = parse(contract)
const result = await evaluateAsync(parsed.ast, evalCtx)
if (!result.success || !result.value) {
return {
ctx: evalCtx,
failure: {
route: variant && variant.name !== 'default'
? `[variant:${variant.name}] ${route.method} ${route.path}`
: `${route.method} ${route.path}`,
contract,
expected: 'true',
observed: result.success ? String(result.value) : result.error,
},
}
}
return { ctx: evalCtx }
} catch (error) {
return {
ctx: evalCtx,
failure: {
route: variant && variant.name !== 'default'
? `[variant:${variant.name}] ${route.method} ${route.path}`
: `${route.method} ${route.path}`,
contract,
expected: 'true',
observed: error instanceof Error ? error.message : String(error),
},
}
}
}
// ---------------------------------------------------------------------------
// Main verify runner
// ---------------------------------------------------------------------------
/**
* Run deterministic contract verification.
*
* Flow:
* 1. Discover routes from Fastify app
* 2. Apply route filters (patterns, changed, profile routes)
* 3. Check for behavioral contracts
* 4. Execute each contract deterministically
* 5. Aggregate results
*/
export async function runVerify(deps: VerifyRunnerDeps): Promise<VerifyRunResult> {
const started = Date.now()
const { fastify, routeFilters, changed, profileRoutes } = deps
// 1. Discover routes
let allRoutes = await discoverAppRoutes(fastify)
// If no routes discovered (plugin not registered before routes),
// try to discover specific routes from filters
if (allRoutes.length === 0 && (routeFilters?.length || profileRoutes?.length)) {
const patternsToCheck = [
...(routeFilters || []),
...(profileRoutes || []),
]
allRoutes = await discoverSpecificRoutes(fastify, patternsToCheck)
}
const availableRoutes = allRoutes.map(r => `${r.method} ${r.path}`)
// 2. Apply filters
let routes = allRoutes
// Apply profile routes filter first
if (profileRoutes && profileRoutes.length > 0) {
routes = filterRoutesByPatterns(routes, profileRoutes)
}
// Apply --routes flag filter
if (routeFilters && routeFilters.length > 0) {
routes = filterRoutesByPatterns(routes, routeFilters)
}
// Apply --changed filter
if (changed) {
const cwd = process.cwd()
const inGit = await isGitRepo(cwd)
if (!inGit) {
return {
passed: false,
total: 0,
passedCount: 0,
failed: 0,
failures: [],
durationMs: Date.now() - started,
noRoutesMatched: false,
noContractsFound: false,
availableRoutes,
artifactPaths: [],
notGitRepo: true,
}
}
routes = await filterChangedRoutes(routes, cwd)
}
// Check if any routes matched
if (routes.length === 0) {
return {
passed: false,
total: 0,
passedCount: 0,
failed: 0,
failures: [],
durationMs: Date.now() - started,
noRoutesMatched: true,
noContractsFound: false,
availableRoutes,
artifactPaths: [],
}
}
// 3. Check for behavioral contracts
const routesWithContracts = routes.filter(route =>
route.ensures.length > 0 || route.requires.length > 0
)
if (routesWithContracts.length === 0) {
return {
passed: false,
total: 0,
passedCount: 0,
failed: 0,
failures: [],
durationMs: Date.now() - started,
noRoutesMatched: false,
noContractsFound: true,
availableRoutes,
artifactPaths: [],
}
}
// 4. Execute contracts (with variant expansion)
const failures: VerifyFailure[] = []
let total = 0
let passedCount = 0
for (const route of routesWithContracts) {
const contracts = [...route.requires, ...route.ensures]
const variants = route.variants && route.variants.length > 0
? route.variants
: [{ name: 'default' }]
for (const variant of variants) {
for (const contract of contracts) {
total++
const result = await executeContract(fastify, route, contract, deps.timeout, variant)
if (result.failure) {
failures.push(result.failure)
} else {
passedCount++
}
}
}
}
const durationMs = Date.now() - started
// Sort failures deterministically by route then contract for stable output
const sortedFailures = failures.sort((a, b) => {
const routeCmp = a.route.localeCompare(b.route)
if (routeCmp !== 0) return routeCmp
return a.contract.localeCompare(b.contract)
})
return {
passed: failures.length === 0,
total,
passedCount,
failed: failures.length,
failures: sortedFailures,
durationMs,
noRoutesMatched: false,
noContractsFound: false,
availableRoutes,
artifactPaths: [],
}
}