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
+644
View File
@@ -0,0 +1,644 @@
/**
* S3: Init command for APOPHIS CLI
* Scaffold config, scripts, and example usage in one pass.
*/
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import type { CliContext } from '../../core/types.js';
import { USAGE_ERROR, SUCCESS } from '../../core/exit-codes.js';
import { getScaffoldForPreset, getPresetNames, type ScaffoldResult } from './scaffolds/index.js';
// ─────────────────────────────────────────────────────────────────────────────
// Types
// ─────────────────────────────────────────────────────────────────────────────
export interface InitOptions {
preset?: string;
force?: boolean;
noninteractive?: boolean;
cwd?: string;
}
export interface InitResult {
exitCode: number;
message: string;
filesWritten: string[];
nextCommand: string;
}
const DEFAULT_INSTALL_PM: Exclude<CliContext['packageManager'], 'unknown'> = 'npm';
function normalizePackageManager(packageManager: CliContext['packageManager'] | undefined): Exclude<CliContext['packageManager'], 'unknown'> {
if (!packageManager || packageManager === 'unknown') {
return DEFAULT_INSTALL_PM;
}
return packageManager;
}
function renderInstallCommand(
packageManager: CliContext['packageManager'] | undefined,
packages: string[],
): string {
const normalized = normalizePackageManager(packageManager);
if (normalized === 'yarn') {
return `yarn add ${packages.join(' ')}`;
}
if (normalized === 'pnpm') {
return `pnpm add ${packages.join(' ')}`;
}
if (normalized === 'bun') {
return `bun add ${packages.join(' ')}`;
}
return `npm install ${packages.join(' ')}`;
}
// ─────────────────────────────────────────────────────────────────────────────
// Fastify detection
// ─────────────────────────────────────────────────────────────────────────────
/**
* Detect if the project is a Fastify app by looking for:
* - fastify imports in JS/TS files
* - Common server file names (server.js, app.js, index.js, etc.)
*/
export async function detectFastifyEntrypoint(cwd: string): Promise<string | null> {
const candidates = [
'app.js',
'app.ts',
'server.js',
'server.ts',
'index.js',
'index.ts',
'src/app.js',
'src/app.ts',
'src/server.js',
'src/server.ts',
'src/index.js',
'src/index.ts',
];
for (const candidate of candidates) {
const fullPath = resolve(cwd, candidate);
if (!existsSync(fullPath)) continue;
const content = readFileSync(fullPath, 'utf-8');
// Look for fastify import patterns
if (
content.includes('fastify') ||
content.includes('Fastify') ||
content.includes('@fastify') ||
content.includes('fastify-plugin')
) {
return candidate;
}
}
return null;
}
/**
* Check if @fastify/swagger is registered in the project.
* We check package.json dependencies and the entrypoint file.
*/
export async function checkSwaggerRegistration(cwd: string, entrypoint: string | null): Promise<{
hasSwaggerDep: boolean;
hasSwaggerImport: boolean;
}> {
const pkgPath = resolve(cwd, 'package.json');
let hasSwaggerDep = false;
if (existsSync(pkgPath)) {
try {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
const deps = {
...pkg.dependencies,
...pkg.devDependencies,
};
hasSwaggerDep = '@fastify/swagger' in deps;
} catch {
// Ignore parse errors
}
}
let hasSwaggerImport = false;
if (entrypoint) {
const entryPath = resolve(cwd, entrypoint);
if (existsSync(entryPath)) {
const content = readFileSync(entryPath, 'utf-8');
hasSwaggerImport =
content.includes('@fastify/swagger') ||
content.includes('fastify-swagger');
}
}
return { hasSwaggerDep, hasSwaggerImport };
}
/**
* Detect if the project uses TypeScript.
*/
export function detectTypeScript(cwd: string): boolean {
return (
existsSync(resolve(cwd, 'tsconfig.json')) ||
existsSync(resolve(cwd, 'src/app.ts')) ||
existsSync(resolve(cwd, 'src/server.ts')) ||
existsSync(resolve(cwd, 'src/index.ts'))
);
}
// ─────────────────────────────────────────────────────────────────────────────
// Package.json script merging
// ─────────────────────────────────────────────────────────────────────────────
/**
* Merge apophis scripts into package.json without clobbering existing scripts.
*/
export function mergePackageScripts(pkg: Record<string, unknown>): Record<string, unknown> {
const scripts = (pkg.scripts as Record<string, string>) || {};
const apophisScripts: Record<string, string> = {
'apophis:verify': 'apophis verify --profile quick',
'apophis:doctor': 'apophis doctor',
};
const mergedScripts = { ...scripts };
for (const [key, value] of Object.entries(apophisScripts)) {
// Only add if not already present
if (!(key in mergedScripts)) {
mergedScripts[key] = value;
}
}
return {
...pkg,
scripts: mergedScripts,
};
}
// ─────────────────────────────────────────────────────────────────────────────
// File writing
// ─────────────────────────────────────────────────────────────────────────────
/**
* Write the config file (apophis.config.js or .ts).
*/
export function writeConfigFile(
cwd: string,
scaffold: ScaffoldResult,
isTypeScript: boolean,
force: boolean,
): { path: string; existed: boolean } {
const ext = isTypeScript ? 'ts' : 'js';
const configPath = resolve(cwd, `apophis.config.${ext}`);
const existed = existsSync(configPath);
if (existed && !force) {
return { path: configPath, existed: true };
}
const configContent = generateConfigContent(scaffold.config, isTypeScript);
writeFileSync(configPath, configContent, 'utf-8');
return { path: configPath, existed: false };
}
/**
* Generate config file content as a formatted string.
*/
function generateConfigContent(config: ScaffoldResult['config'], isTypeScript: boolean): string {
const lines: string[] = [];
lines.push('/**');
lines.push(' * APOPHIS configuration');
lines.push(' * Generated by `apophis init`');
lines.push(' */');
lines.push('');
if (isTypeScript) {
lines.push('import type { ApophisConfig } from "apophis-fastify/cli";');
lines.push('');
lines.push('const config: ApophisConfig = ' + stringifyConfig(config) + ';');
lines.push('');
lines.push('export default config;');
} else {
lines.push('export default ' + stringifyConfig(config) + ';');
}
lines.push('');
return lines.join('\n');
}
/**
* Stringify a config object with proper indentation.
*/
function stringifyConfig(obj: unknown, indent = 2): string {
if (obj === null) return 'null';
if (typeof obj === 'string') return JSON.stringify(obj);
if (typeof obj === 'number') return String(obj);
if (typeof obj === 'boolean') return String(obj);
if (Array.isArray(obj)) {
if (obj.length === 0) return '[]';
const items = obj.map(item => stringifyConfig(item, indent + 2)).join(',\n' + ' '.repeat(indent));
return '[\n' + ' '.repeat(indent) + items + '\n' + ' '.repeat(indent - 2) + ']';
}
if (typeof obj === 'object') {
const entries = Object.entries(obj as Record<string, unknown>);
if (entries.length === 0) return '{}';
const items = entries
.map(([key, value]) => {
const keyStr = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
return `${keyStr}: ${stringifyConfig(value, indent + 2)}`;
})
.join(',\n' + ' '.repeat(indent));
return '{\n' + ' '.repeat(indent) + items + '\n' + ' '.repeat(indent - 2) + '}';
}
return String(obj);
}
/**
* Write the README guidance file.
*/
export function writeReadmeFile(
cwd: string,
scaffold: ScaffoldResult,
force: boolean,
): { path: string; existed: boolean } {
const readmePath = resolve(cwd, 'APOPHIS.md');
const existed = existsSync(readmePath);
if (existed && !force) {
return { path: readmePath, existed: true };
}
writeFileSync(readmePath, scaffold.readmeContent.trim() + '\n', 'utf-8');
return { path: readmePath, existed: false };
}
/**
* Update package.json with merged scripts.
*/
export function updatePackageJson(cwd: string): { path: string; modified: boolean; error?: string } {
const pkgPath = resolve(cwd, 'package.json');
if (!existsSync(pkgPath)) {
const bootstrapPackage = {
name: 'apophis-app',
version: '0.1.0',
private: true,
type: 'module',
scripts: {
'apophis:doctor': 'apophis doctor',
'apophis:verify': 'apophis verify --profile quick',
},
dependencies: {
fastify: '^5.0.0',
'@fastify/swagger': '^9.0.0',
},
};
try {
writeFileSync(pkgPath, JSON.stringify(bootstrapPackage, null, 2) + '\n', 'utf-8');
return { path: pkgPath, modified: true };
} catch (err) {
return { path: pkgPath, modified: false, error: String(err) };
}
}
try {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
const merged = mergePackageScripts(pkg);
// Check if anything changed
const originalScripts = JSON.stringify(pkg.scripts || {});
const mergedScripts = JSON.stringify(merged.scripts || {});
if (originalScripts === mergedScripts) {
return { path: pkgPath, modified: false };
}
writeFileSync(pkgPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
return { path: pkgPath, modified: true };
} catch (err) {
return { path: pkgPath, modified: false, error: String(err) };
}
}
export function writeBootstrapAppFile(
cwd: string,
existingEntrypoint: string | null,
): { path: string; created: boolean } {
const appPath = resolve(cwd, 'app.js');
if (existingEntrypoint || existsSync(appPath)) {
return { path: appPath, created: false };
}
const appContent = `/**
* Generated by \`apophis init\`.
* This is a minimal Fastify-like app that is runnable with \`apophis verify\`.
*/
const routes = [
{
method: 'POST',
url: '/users',
schema: {
body: {
type: 'object',
required: ['name'],
properties: {
name: { type: 'string' },
},
},
response: {
201: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
},
},
},
'x-ensures': [
'response_code(this) == 201',
],
},
},
];
const app = {
routes,
async ready() {},
hasRoute({ method, url }) {
const normalizedMethod = String(method || '').toUpperCase();
return routes.some(route => route.method === normalizedMethod && route.url === url);
},
async inject({ method, url, payload }) {
const normalizedMethod = String(method || '').toUpperCase();
if (normalizedMethod === 'POST' && url === '/users') {
const body = {
id: 'usr-1',
name: payload && typeof payload === 'object' && 'name' in payload
? String(payload.name)
: 'test',
};
return {
statusCode: 201,
headers: { 'content-type': 'application/json' },
body,
json() {
return body;
},
};
}
const body = { message: 'not found' };
return {
statusCode: 404,
headers: { 'content-type': 'application/json' },
body,
json() {
return body;
},
};
},
};
export default app;
`;
writeFileSync(appPath, appContent, 'utf-8');
return { path: appPath, created: true };
}
// ─────────────────────────────────────────────────────────────────────────────
// Interactive prompts (lazy-loaded)
// ─────────────────────────────────────────────────────────────────────────────
interface PromptsModule {
select: (opts: { message: string; options: { value: string; label: string }[] }) => Promise<string>;
confirm: (opts: { message: string }) => Promise<boolean>;
text: (opts: { message: string; placeholder?: string }) => Promise<string>;
}
async function loadPrompts(): Promise<PromptsModule> {
// Lazy-load @clack/prompts only when interactive
const mod = await import('@clack/prompts');
return mod as unknown as PromptsModule;
}
// ─────────────────────────────────────────────────────────────────────────────
// Main init handler
// ─────────────────────────────────────────────────────────────────────────────
export async function initHandler(args: string[], ctx: CliContext): Promise<InitResult> {
const options = parseInitOptions(args, ctx);
const cwd = options.cwd || ctx.cwd;
// Detect project structure
const isTypeScript = detectTypeScript(cwd);
const fastifyEntry = await detectFastifyEntrypoint(cwd);
const swaggerCheck = await checkSwaggerRegistration(cwd, fastifyEntry);
// Determine preset
let preset = options.preset;
if (!preset) {
if (options.noninteractive) {
return {
exitCode: USAGE_ERROR,
message: 'Missing required --preset flag in non-interactive mode. Use one of: ' + getPresetNames().join(', '),
filesWritten: [],
nextCommand: '',
};
}
// Interactive mode: prompt for preset
if (ctx.isTTY && !ctx.isCI) {
try {
const prompts = await loadPrompts();
const presetNames = getPresetNames();
const choice = await prompts.select({
message: 'Choose a preset:',
options: presetNames.map(name => ({
value: name,
label: name,
})),
});
preset = choice;
} catch {
// Fallback if prompts fail
return {
exitCode: USAGE_ERROR,
message: 'Failed to prompt for preset. Use --preset <name> in non-interactive mode.',
filesWritten: [],
nextCommand: '',
};
}
} else {
// Non-TTY, non-CI: default to safe-ci
preset = 'safe-ci';
}
}
// Validate preset
const scaffold = getScaffoldForPreset(preset);
if (!scaffold) {
return {
exitCode: USAGE_ERROR,
message: `Unknown preset "${preset}". Available presets: ${getPresetNames().join(', ')}`,
filesWritten: [],
nextCommand: '',
};
}
// Check for existing config
const configExt = isTypeScript ? 'ts' : 'js';
const configPath = resolve(cwd, `apophis.config.${configExt}`);
const configExisted = existsSync(configPath);
if (configExisted && !options.force) {
return {
exitCode: USAGE_ERROR,
message: `Config file already exists: apophis.config.${configExt}. Use --force to overwrite.`,
filesWritten: [],
nextCommand: '',
};
}
// Write files
const filesWritten: string[] = [];
const forceWrite = options.force ?? false;
const configResult = writeConfigFile(cwd, scaffold, isTypeScript, forceWrite);
if (configResult.existed && !forceWrite) {
return {
exitCode: USAGE_ERROR,
message: `Config file already exists: ${configResult.path}. Use --force to overwrite.`,
filesWritten: [],
nextCommand: '',
};
}
filesWritten.push(configResult.path);
const readmeResult = writeReadmeFile(cwd, scaffold, forceWrite);
if (!readmeResult.existed || forceWrite) {
filesWritten.push(readmeResult.path);
}
const pkgResult = updatePackageJson(cwd);
if (pkgResult.modified) {
filesWritten.push(pkgResult.path);
}
const bootstrapAppResult = writeBootstrapAppFile(cwd, fastifyEntry);
if (bootstrapAppResult.created) {
filesWritten.push(bootstrapAppResult.path);
}
// Build next command
const profileName = scaffold.config.profile || 'quick';
const routeHint = scaffold.config.routes?.[0] || '';
const nextCommand = routeHint
? `apophis verify --profile ${profileName} --routes "${routeHint}"`
: `apophis verify --profile ${profileName}`;
// Build message
const lines: string[] = [];
lines.push(`Initialized APOPHIS with preset "${preset}"`);
lines.push('');
lines.push('Files written:');
for (const file of filesWritten) {
lines.push(` ${file}`);
}
const installPeerDepsCommand = renderInstallCommand(ctx.packageManager, ['fastify', '@fastify/swagger']);
const installSwaggerCommand = renderInstallCommand(ctx.packageManager, ['@fastify/swagger']);
lines.push('');
lines.push('First success path:');
lines.push(` 1. ${installPeerDepsCommand}`);
lines.push(' 2. apophis doctor');
lines.push(` 3. ${nextCommand}`);
lines.push('');
lines.push('If verify says "No behavioral contracts found", add x-ensures 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('See APOPHIS.md and docs/getting-started.md for full examples.');
if (!swaggerCheck.hasSwaggerDep && !bootstrapAppResult.created) {
lines.push('');
lines.push('Warning: @fastify/swagger not found in dependencies.');
lines.push('APOPHIS requires @fastify/swagger to discover routes.');
lines.push('Install it with:');
lines.push(` ${installSwaggerCommand}`);
} else if (!bootstrapAppResult.created && !swaggerCheck.hasSwaggerImport) {
lines.push('');
lines.push('Warning: @fastify/swagger is installed but not imported in your entrypoint.');
lines.push('Register it in your Fastify app:');
lines.push(` await app.register(import("@fastify/swagger"), { openapi: { info: { title: "API", version: "1.0.0" } } });`);
}
if (fastifyEntry) {
lines.push('');
lines.push(`Detected Fastify entrypoint: ${fastifyEntry}`);
}
lines.push('');
lines.push('Next command:');
lines.push(` ${nextCommand}`);
return {
exitCode: SUCCESS,
message: lines.join('\n'),
filesWritten,
nextCommand,
};
}
// ─────────────────────────────────────────────────────────────────────────────
// Option parsing
// ─────────────────────────────────────────────────────────────────────────────
function parseInitOptions(args: string[], ctx: CliContext): InitOptions {
const options: InitOptions = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--preset' || arg === '-p') {
options.preset = args[++i];
} else if (arg === '--force' || arg === '-f') {
options.force = true;
} else if (arg === '--noninteractive') {
options.noninteractive = true;
} else if (arg === '--cwd') {
options.cwd = args[++i];
}
}
// Non-interactive if CI or not TTY
if (ctx.isCI || !ctx.isTTY) {
options.noninteractive = true;
}
return options;
}
// ─────────────────────────────────────────────────────────────────────────────
// CLI adapter
// ─────────────────────────────────────────────────────────────────────────────
export async function handleInit(args: string[], ctx: CliContext): Promise<number> {
const result = await initHandler(args, ctx);
if (result.message) {
console.log(result.message);
}
return result.exitCode;
}
+374
View File
@@ -0,0 +1,374 @@
/**
* S3: Init command scaffold templates
* Each preset returns a config object and file contents for the init command.
*/
import type { ApophisConfig, PresetDefinition, ProfileDefinition, EnvironmentPolicy } from '../../../core/types.js';
export interface ScaffoldResult {
config: ApophisConfig;
readmeContent: string;
}
// ─────────────────────────────────────────────────────────────────────────────
// safe-ci: Minimal CI-safe preset (default)
// ─────────────────────────────────────────────────────────────────────────────
export function safeCiScaffold(): ScaffoldResult {
const preset: PresetDefinition = {
name: 'safe-ci',
depth: 'quick',
timeout: 5000,
parallel: false,
chaos: false,
observe: false,
};
const profile: ProfileDefinition = {
name: 'quick',
mode: 'verify',
preset: 'safe-ci',
routes: ['POST /users'],
};
const envLocal: EnvironmentPolicy = {
name: 'local',
allowVerify: true,
allowObserve: true,
allowQualify: false,
allowChaos: false,
allowBlocking: true,
requireSink: false,
};
const config: ApophisConfig = {
mode: 'verify',
profiles: { quick: profile },
presets: { 'safe-ci': preset },
environments: { local: envLocal },
};
const readmeContent = `
# APOPHIS Setup — safe-ci preset
This project was scaffolded with \`apophis init --preset safe-ci\`.
## Quick Start
1. Ensure you have a Fastify app with @fastify/swagger registered.
2. Add behavioral contracts to your route schemas using \`x-ensures\`.
3. Run: apophis verify --profile quick
## What This Preset Does
- Runs only behavioral contracts (not schema-only routes).
- No chaos, no observe, no stateful testing.
- Safe for CI pipelines.
- Timeout: 5s per route.
## Example Behavioral Contract
Add this inside your route schema to check that a created resource is retrievable:
\`\`\`javascript
"x-ensures": [
"response_code(GET /users/{response_body(this).id}) == 200"
]
\`\`\`
If \`apophis verify\` says "No behavioral contracts found", it means your routes have schemas but no \`x-ensures\` or \`x-requires\` clauses. Add at least one clause per route you want to verify.
## Next Steps
- Add more routes to the \`routes\` array in your profile.
- Try \`apophis init --preset platform-observe\` for production readiness.
- Try \`apophis init --preset protocol-lab\` for multi-step flows.
`;
return { config, readmeContent };
}
// ─────────────────────────────────────────────────────────────────────────────
// platform-observe: Production-ready with observe mode
// ─────────────────────────────────────────────────────────────────────────────
export function platformObserveScaffold(): ScaffoldResult {
const preset: PresetDefinition = {
name: 'platform-observe',
depth: 'standard',
timeout: 10000,
parallel: true,
chaos: false,
observe: true,
};
const profile: ProfileDefinition = {
name: 'staging-observe',
mode: 'observe',
preset: 'platform-observe',
routes: [],
};
const envStaging: EnvironmentPolicy = {
name: 'staging',
allowVerify: true,
allowObserve: true,
allowQualify: true,
allowChaos: false,
allowBlocking: false,
requireSink: true,
};
const envProduction: EnvironmentPolicy = {
name: 'production',
allowVerify: true,
allowObserve: true,
allowQualify: false,
allowChaos: false,
allowBlocking: false,
requireSink: true,
};
const config: ApophisConfig = {
mode: 'observe',
profile: 'staging-observe',
profiles: { 'staging-observe': profile },
presets: { 'platform-observe': preset },
environments: {
staging: envStaging,
production: envProduction,
},
};
const readmeContent = `
# APOPHIS Setup — platform-observe preset
This project was scaffolded with \`apophis init --preset platform-observe\`.
## Quick Start
1. Ensure you have a Fastify app with @fastify/swagger registered.
2. Configure your reporting sink (see environments.staging.requireSink).
3. Run: apophis observe --profile staging-observe
## What This Preset Does
- Enables observe mode for production readiness checks.
- Validates non-blocking semantics and sink configuration.
- Parallel execution for faster feedback.
- Requires sink config in staging/production.
## Safety
- Observe mode is non-blocking by default.
- Production requires explicit policy to enable blocking.
- Chaos is disabled in this preset.
## Next Steps
- Add a sink configuration to your environment policy.
- Run \`apophis doctor\` to validate the full setup.
`;
return { config, readmeContent };
}
// ─────────────────────────────────────────────────────────────────────────────
// llm-safe: Minimal preset for LLM-generated codebases
// ─────────────────────────────────────────────────────────────────────────────
export function llmSafeScaffold(): ScaffoldResult {
const preset: PresetDefinition = {
name: 'llm-safe',
depth: 'quick',
timeout: 3000,
parallel: false,
chaos: false,
observe: false,
};
const profile: ProfileDefinition = {
name: 'llm-check',
mode: 'verify',
preset: 'llm-safe',
routes: [],
};
const envLocal: EnvironmentPolicy = {
name: 'local',
allowVerify: true,
allowObserve: false,
allowQualify: false,
allowChaos: false,
allowBlocking: false,
requireSink: false,
};
const config: ApophisConfig = {
mode: 'verify',
profile: 'llm-check',
profiles: { 'llm-check': profile },
presets: { 'llm-safe': preset },
environments: { local: envLocal },
};
const readmeContent = `
# APOPHIS Setup — llm-safe preset
This project was scaffolded with \`apophis init --preset llm-safe\`.
## Quick Start
1. Ensure you have a Fastify app with @fastify/swagger registered.
2. Add behavioral contracts to your route schemas using \`x-ensures\`.
3. Run: apophis verify --profile llm-check
## What This Preset Does
- Ultra-minimal preset for LLM-generated codebases.
- 3s timeout per route (fast feedback).
- No observe, no qualify, no chaos — verify only.
- Conservative defaults to avoid surprising failures.
## Example Behavioral Contract
Add this inside your route schema to check that a created resource is retrievable:
\`\`\`javascript
"x-ensures": [
"response_code(GET /users/{response_body(this).id}) == 200"
]
\`\`\`
If \`apophis verify\` says "No behavioral contracts found", it means your routes have schemas but no \`x-ensures\` or \`x-requires\` clauses. Add at least one clause per route you want to verify.
## Next Steps
- Add routes to the \`routes\` array once you have behavioral contracts.
- Run \`apophis doctor\` to check for missing dependencies.
`;
return { config, readmeContent };
}
// ─────────────────────────────────────────────────────────────────────────────
// protocol-lab: Multi-step flow and stateful testing
// ─────────────────────────────────────────────────────────────────────────────
export function protocolLabScaffold(): ScaffoldResult {
const preset: PresetDefinition = {
name: 'protocol-lab',
depth: 'deep',
timeout: 15000,
parallel: false,
chaos: true,
observe: false,
};
const profile: ProfileDefinition = {
name: 'oauth-nightly',
mode: 'qualify',
preset: 'protocol-lab',
routes: [],
seed: 42,
};
const envLocal: EnvironmentPolicy = {
name: 'local',
allowVerify: true,
allowObserve: true,
allowQualify: true,
allowChaos: true,
allowBlocking: true,
requireSink: false,
};
const envTest: EnvironmentPolicy = {
name: 'test',
allowVerify: true,
allowObserve: true,
allowQualify: true,
allowChaos: true,
allowBlocking: true,
requireSink: false,
};
const config: ApophisConfig = {
mode: 'qualify',
profile: 'oauth-nightly',
profiles: { 'oauth-nightly': profile },
presets: { 'protocol-lab': preset },
environments: {
local: envLocal,
test: envTest,
},
};
const readmeContent = `
# APOPHIS Setup — protocol-lab preset
This project was scaffolded with \`apophis init --preset protocol-lab\`.
## Quick Start
1. Ensure you have a Fastify app with @fastify/swagger registered.
2. Define multi-step flows in your route schemas.
3. Run: apophis qualify --profile oauth-nightly --seed 42
## What This Preset Does
- Enables qualify mode for stateful and scenario testing.
- Chaos engineering enabled (local/test only).
- Deep depth for thorough exploration.
- 15s timeout per route.
## Safety
- Chaos is blocked in production by default.
- Use \`apophis doctor\` to validate environment safety before qualifying.
## Machine Output in CI
Qualify can produce large output. In CI, use machine-readable formats and filter events:
- \`--format json\` emits a single stable JSON artifact (good for small-to-medium runs).
- \`--format ndjson\` emits one event per line (good for streaming parsers).
- Use \`--quiet\` to suppress human progress text.
- Pipe ndjson to \`jq\` or a custom filter to extract only failures:
\`\`\`bash
apophis qualify --profile oauth-nightly --format ndjson | jq 'select(.type == "route.failed")'
\`\`\`
- For very large runs, consider writing artifacts to a directory and parsing the JSON file instead of stdout:
\`\`\`bash
apophis qualify --profile oauth-nightly --format json --artifact-dir reports/apophis
\`\`\`
## Next Steps
- Define scenario sequences in your config.
- Add route allowlists for chaos if needed.
- Run \`apophis replay --artifact <path>\` to debug failures.
`;
return { config, readmeContent };
}
// ─────────────────────────────────────────────────────────────────────────────
// Preset registry
// ─────────────────────────────────────────────────────────────────────────────
export const PRESETS: Record<string, () => ScaffoldResult> = {
'safe-ci': safeCiScaffold,
'platform-observe': platformObserveScaffold,
'llm-safe': llmSafeScaffold,
'protocol-lab': protocolLabScaffold,
};
export function getPresetNames(): string[] {
return Object.keys(PRESETS);
}
export function getScaffoldForPreset(preset: string): ScaffoldResult | null {
const fn = PRESETS[preset];
return fn ? fn() : null;
}