610 lines
18 KiB
Markdown
610 lines
18 KiB
Markdown
|
|
# APOPHIS DX Improvement Plan
|
||
|
|
## Getting Started, Error Context, Cache/CI Docs, and Human-Readable Output
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. GETTING STARTED GUIDE
|
||
|
|
|
||
|
|
### Goal
|
||
|
|
A complete "Hello World" to "Production Ready" guide that a developer can follow in 15 minutes.
|
||
|
|
|
||
|
|
### Structure
|
||
|
|
|
||
|
|
#### 1.1 Installation (30 seconds)
|
||
|
|
```bash
|
||
|
|
npm install apophis-fastify
|
||
|
|
# peer deps: fastify, @fastify/swagger
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 1.2 Minimal Setup (2 minutes)
|
||
|
|
```typescript
|
||
|
|
import Fastify from 'fastify'
|
||
|
|
import apophisPlugin from 'apophis-fastify'
|
||
|
|
|
||
|
|
const fastify = Fastify()
|
||
|
|
|
||
|
|
// APOPHIS needs @fastify/swagger for spec generation
|
||
|
|
await fastify.register(import('@fastify/swagger'), {})
|
||
|
|
await fastify.register(apophisPlugin, {
|
||
|
|
validateRuntime: true, // optional: validates contracts on every request
|
||
|
|
})
|
||
|
|
|
||
|
|
fastify.get('/health', {
|
||
|
|
schema: {
|
||
|
|
response: {
|
||
|
|
200: {
|
||
|
|
type: 'object',
|
||
|
|
properties: { status: { type: 'string' } }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, async () => ({ status: 'ok' }))
|
||
|
|
|
||
|
|
await fastify.ready()
|
||
|
|
|
||
|
|
// Run contract tests
|
||
|
|
const result = await fastify.apophis.test({ mode: 'all', depth: 'quick' })
|
||
|
|
console.log(result.summary)
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 1.3 Your First Contract (5 minutes)
|
||
|
|
Explain the mental model:
|
||
|
|
- **Requires** (preconditions): What must be true BEFORE the request
|
||
|
|
- **Ensures** (postconditions): What must be true AFTER the response
|
||
|
|
- **Invariants**: What must ALWAYS be true across requests
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
fastify.post('/users', {
|
||
|
|
schema: {
|
||
|
|
'x-category': 'constructor', // creates a resource
|
||
|
|
'x-requires': [], // no preconditions
|
||
|
|
'x-ensures': [
|
||
|
|
'status:201',
|
||
|
|
'response_body(this).id != null',
|
||
|
|
'response_body(this).email == request_body(this).email',
|
||
|
|
],
|
||
|
|
body: {
|
||
|
|
type: 'object',
|
||
|
|
properties: {
|
||
|
|
email: { type: 'string', format: 'email' },
|
||
|
|
name: { type: 'string', minLength: 1 }
|
||
|
|
},
|
||
|
|
required: ['email', 'name']
|
||
|
|
},
|
||
|
|
response: {
|
||
|
|
201: {
|
||
|
|
type: 'object',
|
||
|
|
properties: {
|
||
|
|
id: { type: 'string' },
|
||
|
|
email: { type: 'string' },
|
||
|
|
name: { type: 'string' }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, async (req, reply) => {
|
||
|
|
reply.status(201)
|
||
|
|
return { id: 'user-123', email: req.body.email, name: req.body.name }
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 1.4 Complete CRUD Example (7 minutes)
|
||
|
|
Show a full resource lifecycle:
|
||
|
|
- POST /users (constructor)
|
||
|
|
- GET /users/:id (observer — reads the resource)
|
||
|
|
- PUT /users/:id (mutator — updates the resource)
|
||
|
|
- DELETE /users/:id (destructor — deletes the resource)
|
||
|
|
|
||
|
|
Demonstrate:
|
||
|
|
- How constructors populate the state
|
||
|
|
- How observers verify state
|
||
|
|
- How mutators maintain invariants
|
||
|
|
- How cleanup works
|
||
|
|
|
||
|
|
#### 1.5 Running in CI (1 minute)
|
||
|
|
```yaml
|
||
|
|
# .github/workflows/contracts.yml
|
||
|
|
- run: npm test
|
||
|
|
env:
|
||
|
|
APOPHIS_CHANGED_ROUTES: "${{ steps.changes.outputs.routes }}"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Files to Create
|
||
|
|
- `docs/getting-started.md` — Full guide
|
||
|
|
- `docs/examples/crud-api.ts` — Complete working example
|
||
|
|
- `docs/examples/minimal.ts` — Single route example
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. RICH ERROR CONTEXT SYSTEM
|
||
|
|
|
||
|
|
### Current State (Bad)
|
||
|
|
```
|
||
|
|
Contract violation: response_body(this).id != null
|
||
|
|
```
|
||
|
|
|
||
|
|
No context. No request body. No response body. No status code. No suggestion.
|
||
|
|
|
||
|
|
### Target State (Good)
|
||
|
|
```
|
||
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
|
CONTRACT VIOLATION: POST /users
|
||
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
|
|
||
|
|
Formula:
|
||
|
|
response_body(this).id != null
|
||
|
|
|
||
|
|
Expected:
|
||
|
|
id to be non-null
|
||
|
|
|
||
|
|
Actual:
|
||
|
|
id = undefined
|
||
|
|
|
||
|
|
Request:
|
||
|
|
POST /users
|
||
|
|
Content-Type: application/json
|
||
|
|
|
||
|
|
{
|
||
|
|
"email": "alice@example.com",
|
||
|
|
"name": "Alice"
|
||
|
|
}
|
||
|
|
|
||
|
|
Response:
|
||
|
|
HTTP/1.1 201 Created
|
||
|
|
content-type: application/json
|
||
|
|
|
||
|
|
{
|
||
|
|
"email": "alice@example.com",
|
||
|
|
"name": "Alice"
|
||
|
|
// id is MISSING
|
||
|
|
}
|
||
|
|
|
||
|
|
Suggestion:
|
||
|
|
Your handler returned a 201 but forgot to include 'id' in the
|
||
|
|
response body. Ensure your constructor routes return the created
|
||
|
|
resource with its generated identifier.
|
||
|
|
|
||
|
|
Stack:
|
||
|
|
at validatePostconditions (src/domain/contract-validation.ts:39)
|
||
|
|
at runSequence (src/test/stateful-runner.ts:167)
|
||
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
|
```
|
||
|
|
|
||
|
|
### Implementation Plan
|
||
|
|
|
||
|
|
#### Phase 1: Structured Error Objects
|
||
|
|
Replace string errors with rich error types:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/types.ts
|
||
|
|
export interface ContractViolation {
|
||
|
|
readonly type: 'contract-violation'
|
||
|
|
readonly route: { method: string; path: string }
|
||
|
|
readonly formula: string
|
||
|
|
readonly formulaType: 'status' | 'apostl'
|
||
|
|
readonly request: {
|
||
|
|
body: unknown
|
||
|
|
headers: Record<string, string>
|
||
|
|
query: Record<string, unknown>
|
||
|
|
params: Record<string, unknown>
|
||
|
|
}
|
||
|
|
readonly response: {
|
||
|
|
statusCode: number
|
||
|
|
headers: Record<string, string>
|
||
|
|
body: unknown
|
||
|
|
}
|
||
|
|
readonly context: {
|
||
|
|
expected: string
|
||
|
|
actual: string
|
||
|
|
diff?: string
|
||
|
|
}
|
||
|
|
readonly suggestion?: string
|
||
|
|
readonly stack?: string
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Phase 2: Smart Suggestions Engine
|
||
|
|
Add a suggestions module that maps common failures to actionable fixes:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/domain/error-suggestions.ts
|
||
|
|
export const getSuggestion = (violation: ContractViolation): string | undefined => {
|
||
|
|
// Status code mismatch
|
||
|
|
if (violation.formulaType === 'status') {
|
||
|
|
return `Expected status ${violation.context.expected}, got ${violation.context.actual}. Check your route handler's reply.status() call.`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Null field
|
||
|
|
if (violation.formula.includes('!= null') && violation.context.actual === 'undefined') {
|
||
|
|
const field = extractField(violation.formula)
|
||
|
|
return `Field '${field}' is missing from the response. Ensure your handler returns all required fields.`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Equality mismatch
|
||
|
|
if (violation.formula.includes('==')) {
|
||
|
|
return `Expected values to match. Check for typos, case sensitivity, or missing transformations.`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Authorization
|
||
|
|
if (violation.formula.includes('authorization') || violation.formula.includes('tenant')) {
|
||
|
|
return `This route may require authentication headers. Check your scope configuration.`
|
||
|
|
}
|
||
|
|
|
||
|
|
return undefined
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Phase 3: Diff Generation
|
||
|
|
For equality comparisons, show a visual diff:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/domain/error-formatter.ts
|
||
|
|
export const formatDiff = (expected: unknown, actual: unknown): string => {
|
||
|
|
if (typeof expected === 'string' && typeof actual === 'string') {
|
||
|
|
// String diff
|
||
|
|
return `Expected: "${expected}"\nActual: "${actual}"\nDiff: ${generateCharDiff(expected, actual)}`
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof expected === 'number' && typeof actual === 'number') {
|
||
|
|
return `Expected: ${expected}\nActual: ${actual}\nDelta: ${actual - expected}`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Object diff (shallow)
|
||
|
|
return `Expected: ${JSON.stringify(expected, null, 2)}\nActual: ${JSON.stringify(actual, null, 2)}`
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Phase 4: Stack Traces
|
||
|
|
Capture the call stack at the point of failure:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// In validatePostconditions
|
||
|
|
const stack = new Error().stack
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
error: new ContractViolation({
|
||
|
|
// ... fields
|
||
|
|
stack: cleanStack(stack),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Files to Create/Modify
|
||
|
|
- `src/types.ts` — Add `ContractViolation` interface
|
||
|
|
- `src/domain/error-suggestions.ts` — Suggestion engine
|
||
|
|
- `src/domain/error-formatter.ts` — Human-readable formatter
|
||
|
|
- `src/domain/contract-validation.ts` — Return structured errors
|
||
|
|
- `src/test/tap-formatter.ts` — Format violations in TAP output
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. CACHE/CI DOCUMENTATION
|
||
|
|
|
||
|
|
### Goal
|
||
|
|
Clear documentation for CI/CD integration with practical examples.
|
||
|
|
|
||
|
|
### Content
|
||
|
|
|
||
|
|
#### 3.1 Cache Overview
|
||
|
|
Explain:
|
||
|
|
- What gets cached (schema → arbitrary mappings, generated commands)
|
||
|
|
- Where it lives (`.apophis-cache.json` in project root)
|
||
|
|
- When it invalidates (schema hash mismatch, explicit hints)
|
||
|
|
- Performance impact (12x speedup on warm cache)
|
||
|
|
|
||
|
|
#### 3.2 CI/CD Integration Patterns
|
||
|
|
|
||
|
|
**Pattern A: Git-based Route Detection**
|
||
|
|
```bash
|
||
|
|
# Detect changed routes from git diff
|
||
|
|
CHANGED=$(git diff --name-only HEAD~1 | grep 'routes/' | sed 's|routes/||' | paste -sd ',' -)
|
||
|
|
APOPHIS_CHANGED_ROUTES="$CHANGED" npm test
|
||
|
|
```
|
||
|
|
|
||
|
|
**Pattern B: Manual Hints File**
|
||
|
|
```json
|
||
|
|
// .apophis-hints.json
|
||
|
|
{
|
||
|
|
"changed": ["/users", "POST /orders"],
|
||
|
|
"reason": "PR #123: Updated user and order endpoints"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Pattern C: Full Cache Reset**
|
||
|
|
```bash
|
||
|
|
# Nuclear option: rebuild everything
|
||
|
|
rm .apophis-cache.json
|
||
|
|
npm test
|
||
|
|
```
|
||
|
|
|
||
|
|
**Pattern D: Monorepo Support**
|
||
|
|
```bash
|
||
|
|
# Per-package cache
|
||
|
|
APOPHIS_CACHE_FILE="./packages/api/.apophis-cache.json" npm test
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 3.3 GitHub Actions Example
|
||
|
|
```yaml
|
||
|
|
name: Contract Tests
|
||
|
|
|
||
|
|
on: [push, pull_request]
|
||
|
|
|
||
|
|
jobs:
|
||
|
|
contracts:
|
||
|
|
runs-on: ubuntu-latest
|
||
|
|
steps:
|
||
|
|
- uses: actions/checkout@v4
|
||
|
|
|
||
|
|
- name: Detect changed routes
|
||
|
|
id: changes
|
||
|
|
run: |
|
||
|
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||
|
|
CHANGED=$(git diff --name-only ${{ github.base_ref }} | grep -E 'routes/|schema/' || true)
|
||
|
|
echo "routes=$CHANGED" >> $GITHUB_OUTPUT
|
||
|
|
fi
|
||
|
|
|
||
|
|
- name: Run contract tests
|
||
|
|
run: npm test
|
||
|
|
env:
|
||
|
|
APOPHIS_CHANGED_ROUTES: ${{ steps.changes.outputs.routes }}
|
||
|
|
|
||
|
|
- name: Upload cache artifact
|
||
|
|
uses: actions/upload-artifact@v4
|
||
|
|
with:
|
||
|
|
name: apophis-cache
|
||
|
|
path: .apophis-cache.json
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 3.4 Cache Configuration API
|
||
|
|
```typescript
|
||
|
|
// Programmatic control
|
||
|
|
import { invalidateRoutes, invalidateCache } from 'apophis-fastify/incremental/cache'
|
||
|
|
|
||
|
|
// Before test run
|
||
|
|
invalidateRoutes(['/users']) // Invalidate specific routes
|
||
|
|
invalidateCache() // Clear everything
|
||
|
|
```
|
||
|
|
|
||
|
|
### Files to Create
|
||
|
|
- `docs/cache-and-ci.md` — Complete guide
|
||
|
|
- `docs/examples/github-actions.yml` — Working workflow
|
||
|
|
- `docs/examples/gitlab-ci.yml` — GitLab example
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. HUMAN-READABLE FAST-CHECK OUTPUT
|
||
|
|
|
||
|
|
### Current State (Bad)
|
||
|
|
```
|
||
|
|
Property failed after 42 tests
|
||
|
|
Counterexample: [{"name":"","email":"a@b.c"}]
|
||
|
|
```
|
||
|
|
|
||
|
|
### Target State (Good)
|
||
|
|
```
|
||
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
|
PROPERTY TEST FAILURE: POST /users
|
||
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
|
|
||
|
|
Fast-check found a counterexample after 42 generated test cases:
|
||
|
|
|
||
|
|
Generated Input:
|
||
|
|
{
|
||
|
|
"name": "", ← empty string (violates minLength: 1)
|
||
|
|
"email": "a@b.c" ← valid email format
|
||
|
|
}
|
||
|
|
|
||
|
|
Request:
|
||
|
|
POST /users
|
||
|
|
Content-Type: application/json
|
||
|
|
|
||
|
|
{ "name": "", "email": "a@b.c" }
|
||
|
|
|
||
|
|
Response:
|
||
|
|
HTTP/1.1 400 Bad Request
|
||
|
|
|
||
|
|
{ "error": "Name is required" }
|
||
|
|
|
||
|
|
Contract Violation:
|
||
|
|
Postcondition: status:201
|
||
|
|
Expected: 201 Created
|
||
|
|
Actual: 400 Bad Request
|
||
|
|
|
||
|
|
Analysis:
|
||
|
|
Your schema requires name to have minLength: 1, but the
|
||
|
|
generated test case produced an empty string. Your handler
|
||
|
|
correctly rejected it with 400, but the contract expects 201.
|
||
|
|
|
||
|
|
Fix: Either:
|
||
|
|
1. Remove minLength constraint from schema if empty names are valid
|
||
|
|
2. Update contract to expect 400 for invalid input
|
||
|
|
3. Add x-category: 'utility' if this is a validation endpoint
|
||
|
|
|
||
|
|
Shrunk: 3 times (from 128-character string to empty string)
|
||
|
|
Seed: 12345 (re-run with APOPHIS_SEED=12345 to reproduce)
|
||
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
|
```
|
||
|
|
|
||
|
|
### Implementation Plan
|
||
|
|
|
||
|
|
#### Phase 1: Counterexample Formatter
|
||
|
|
```typescript
|
||
|
|
// src/test/counterexample-formatter.ts
|
||
|
|
export interface FormattedCounterexample {
|
||
|
|
readonly route: { method: string; path: string }
|
||
|
|
readonly generatedInput: Record<string, unknown>
|
||
|
|
readonly request: { body: unknown; headers: Record<string, string> }
|
||
|
|
readonly response: { statusCode: number; body: unknown }
|
||
|
|
readonly contractViolation: ContractViolation
|
||
|
|
readonly shrinkCount: number
|
||
|
|
readonly seed: number
|
||
|
|
}
|
||
|
|
|
||
|
|
export const formatCounterexample = (example: FormattedCounterexample): string => {
|
||
|
|
// Build human-readable output
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Phase 2: Route Context in Errors
|
||
|
|
When fast-check finds a failure, include the route context:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// In stateful-runner.ts, catch fast-check errors
|
||
|
|
try {
|
||
|
|
await fc.assert(prop, { numRuns, seed })
|
||
|
|
} catch (err) {
|
||
|
|
if (err instanceof fc.Error) {
|
||
|
|
const formatted = formatFastCheckError(err, results)
|
||
|
|
console.error(formatted)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Phase 3: Analysis Engine
|
||
|
|
Auto-analyze failures and suggest fixes:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/test/failure-analyzer.ts
|
||
|
|
export const analyzeFailure = (
|
||
|
|
cmd: ApiOperation,
|
||
|
|
ctx: EvalContext,
|
||
|
|
violation: ContractViolation
|
||
|
|
): string => {
|
||
|
|
// 400 status with 201 expectation
|
||
|
|
if (ctx.response.statusCode === 400 && violation.formula === 'status:201') {
|
||
|
|
return `Your handler rejected valid input. Check schema constraints match contract expectations.`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Missing field
|
||
|
|
if (violation.formula.includes('!= null') && violation.context.actual === 'undefined') {
|
||
|
|
const field = extractField(violation.formula)
|
||
|
|
return `Response missing '${field}'. Check your handler returns all required fields.`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Schema mismatch
|
||
|
|
return `Schema and contract may be out of sync. Review both for consistency.`
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Files to Create
|
||
|
|
- `src/test/counterexample-formatter.ts` — Format fast-check failures
|
||
|
|
- `src/test/failure-analyzer.ts` — Auto-analyze and suggest fixes
|
||
|
|
- `src/test/error-renderer.ts` — Terminal-friendly rendering with box drawing
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. ERROR SYSTEM ARCHITECTURE
|
||
|
|
|
||
|
|
### Design Principles
|
||
|
|
1. **Structured over String**: All errors are objects, not strings
|
||
|
|
2. **Context-Rich**: Every error includes request, response, and contract context
|
||
|
|
3. **Actionable**: Every error includes a suggestion for how to fix it
|
||
|
|
4. **Traceable**: Every error includes a stack trace and route identifier
|
||
|
|
5. **Diff-Friendly**: Equality failures show visual diffs
|
||
|
|
6. **Reproducible**: Every error includes the seed needed to reproduce
|
||
|
|
|
||
|
|
### Error Flow
|
||
|
|
```
|
||
|
|
Test Execution
|
||
|
|
↓
|
||
|
|
Contract Validation (contract-validation.ts)
|
||
|
|
↓
|
||
|
|
Structured Error Object (ContractViolation)
|
||
|
|
↓
|
||
|
|
Suggestion Engine (error-suggestions.ts)
|
||
|
|
↓
|
||
|
|
Diff Generation (error-formatter.ts)
|
||
|
|
↓
|
||
|
|
TAP Output (tap-formatter.ts)
|
||
|
|
↓
|
||
|
|
Console/CI Reporter
|
||
|
|
```
|
||
|
|
|
||
|
|
### Error Types
|
||
|
|
```typescript
|
||
|
|
export type ApophisError =
|
||
|
|
| ContractViolation
|
||
|
|
| FormulaParseError
|
||
|
|
| FormulaEvalError
|
||
|
|
| PreconditionError
|
||
|
|
| InvariantError
|
||
|
|
| TestGenerationError
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. IMPLEMENTATION ORDER
|
||
|
|
|
||
|
|
### Week 1: Foundation ✅ COMPLETE
|
||
|
|
- [x] Create `ContractViolation` type in `src/types.ts`
|
||
|
|
- [x] Update `contract-validation.ts` to return structured errors
|
||
|
|
- [x] Create `error-suggestions.ts` with basic suggestion engine
|
||
|
|
- [x] Update `tap-formatter.ts` to render rich diagnostics
|
||
|
|
- [x] Add tests for new error system
|
||
|
|
- [x] Fix `extractContract` null schema crash (`contract.ts:21`)
|
||
|
|
- [x] Fix `hashSchema` circular reference stack overflow (`hash.ts:24`)
|
||
|
|
- [x] Fix cleanup manager signal listener leak (`cleanup-manager.ts:48`)
|
||
|
|
- [x] Block dangerous accessors (`__proto__`, `constructor`, `prototype`) in formula evaluator
|
||
|
|
- [x] Normalize empty arrays to singletons in `extractContract`
|
||
|
|
- [x] Fix build output path (`tsconfig.json` rootDir)
|
||
|
|
- [x] Document route registration order requirement in README
|
||
|
|
- [x] Add violation deduplication in test output (PETIT + stateful runners)
|
||
|
|
- [x] Fix HEAD route noise in test generation
|
||
|
|
- [x] Add clean stack traces filtered to user code
|
||
|
|
|
||
|
|
**Status**: Error type chain tightened. `EvalResult` uses `error: string` with optional `violation?: ContractViolation`. Runners check `post.violation`. All 246 tests passing. Hardened against null schemas, circular references, prototype pollution, signal leaks, and duplicate failures.
|
||
|
|
|
||
|
|
### Week 2: Getting Started ✅ COMPLETE
|
||
|
|
- [x] Write `docs/getting-started.md`
|
||
|
|
- [x] Create `docs/examples/minimal.ts`
|
||
|
|
- [x] Create `docs/examples/crud-api.ts`
|
||
|
|
- [ ] Add screenshots/GIFs of test output
|
||
|
|
- [x] Update README with quick-start section
|
||
|
|
|
||
|
|
### Week 3: Cache/CI Docs
|
||
|
|
- [ ] Write `docs/cache-and-ci.md`
|
||
|
|
- [ ] Create GitHub Actions example
|
||
|
|
- [ ] Create GitLab CI example
|
||
|
|
- [ ] Document `APOPHIS_CHANGED_ROUTES`
|
||
|
|
- [ ] Document `.apophis-hints.json`
|
||
|
|
|
||
|
|
### Week 4: Fast-Check Formatter ✅ COMPLETE
|
||
|
|
- [x] Create `counterexample-formatter.ts`
|
||
|
|
- [x] Create `failure-analyzer.ts`
|
||
|
|
- [x] Create `error-renderer.ts` with box drawing
|
||
|
|
- [x] Integrate with stateful runner
|
||
|
|
- [x] Add tests for formatting
|
||
|
|
|
||
|
|
### Week 5: Production Hardening ✅ COMPLETE
|
||
|
|
- [x] Regex DoS protection with `safe-regex`
|
||
|
|
- [x] Standard logging with `pino` (APOPHIS_LOG_LEVEL)
|
||
|
|
- [x] Environment-aware cache (disabled in production/test)
|
||
|
|
- [x] Lazy cache loading (no sync file I/O at module load)
|
||
|
|
- [x] Fastify prefix support in route discovery
|
||
|
|
- [x] Signal handler deduplication (global Map)
|
||
|
|
- [x] Add `dispose()` method to CleanupManager
|
||
|
|
- [x] Remove all `console.log` from production code
|
||
|
|
- [x] Stryker mutation testing (contract-validation: 70%, error-suggestions: 68.7%)
|
||
|
|
- [x] Fix flaky property test (schema-to-arbitrary)
|
||
|
|
- [x] 345 tests passing
|
||
|
|
|
||
|
|
### Week 6: Scope Isolation ✅ COMPLETE
|
||
|
|
- [x] Implement scope filtering in `petit-runner.ts`
|
||
|
|
- [x] Implement scope filtering in `stateful-runner.ts`
|
||
|
|
- [x] Add scope headers to test requests via `buildRequest`
|
||
|
|
- [x] Tests for multi-scope scenarios
|
||
|
|
|
||
|
|
**Status**: Scope isolation fully implemented. Routes with `x-scope` annotation are filtered by the `scope` test parameter. Scope headers from `ScopeRegistry` are passed to test requests. 249 tests passing.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. SUCCESS METRICS
|
||
|
|
|
||
|
|
- [ ] New user can go from `npm install` to passing contract tests in < 15 minutes
|
||
|
|
- [ ] Error messages include request/response context 100% of the time
|
||
|
|
- [ ] 80% of contract violations include an actionable suggestion
|
||
|
|
- [ ] CI integration documented for GitHub Actions, GitLab CI, and CircleCI
|
||
|
|
- [ ] Fast-check failures formatted with route context and analysis
|
||
|
|
- [ ] All examples in documentation are tested and working
|
||
|
|
- [ ] README has a "Getting Started" section above the fold
|