15 KiB
NEXT_STEPS_426.md — Post-v2.x APOSTL Restoration & Remaining Work
Status: v2.2 Complete (2026-04-27)
Test count: 503 passing, 0 failures
New in v2.x: Justin removed, APOSTL restored as primary and only contract language, cross-operation behavioral contracts re-enabled, all documentation updated
Completed (v2.x)
Justin Removal & APOSTL Restoration
- Removed
subscriptdependency from package.json - Deleted
src/formula/justin.ts— Justin wrapper with compile cache - Deleted
src/formula/context-builder.ts— EvalContext → Justin context mapping - Restored APOSTL types in
src/types.ts - Updated
src/infrastructure/hook-validator.ts— APOSTL-only evaluation - Updated
src/domain/contract-validation.ts— APOSTL-only evaluation - Updated
src/domain/schema-to-contract.ts— generates APOSTL syntax - Updated
src/domain/error-suggestions.ts— matches APOSTL syntax - Restored parser/evaluator files for APOSTL
- Hand-converted all test schema annotations (~40 test files) from Justin back to APOSTL
- Fixed APOSTL cross-operation support (pure GET calls,
previous(...), guarded prefetch) - Fixed
validateRouteContractsto iteratefastify.routesdirectly - Fixed build errors across all modules
- Fixed runtime validation: Dynamic contract lookup from
routeContractStoreat request time
Behavioral Contract Documentation
README.md— v2.x rewrite with behavioral contract focusdocs/getting-started.md— behavioral examples + APOSTL referencedocs/PLUGIN_CONTRACTS_SPEC.md— APOSTL syntaxdocs/extensions/QUICK-REFERENCE.md— APOSTL extension predicatesdocs/extensions/EXTENSION-PLUGIN-SYSTEM.md— APOSTL predicate examplesskills.md— behavioral contract focusCHANGELOG.md— v2.1.0 section documenting Justin removal
Critical Safety Fixes (from Expert Assessments)
- C1: Chaos two-level probability bug removed
- C2:
Math.random()in corruption — now requires injected RNG - C3: Seed collision — FNV-1a hash combine
- H1: Hook validator 500s — formulas validated at registration time
- H2: env-guard runtime throws — now validated at plugin registration
- P4: Promise.race leak — timer cleanup added
- P9: safe-regex false positives — actual execution timeout test added
- P11: PARAM_PATTERN injection — URL encoding + validation added
Architecture Extraction
src/test/command-generator.tssrc/test/precondition-checker.tssrc/test/result-deduplicator.tssrc/test/route-filter.tssrc/test/plugin-contract-composer.tssrc/test/result-formatter.tssrc/test/api-operations.ts(shared between petit and stateful runners)src/plugin/swagger.tssrc/plugin/spec-builder.tssrc/plugin/contract-builder.tssrc/plugin/stateful-builder.tssrc/plugin/check-builder.tssrc/plugin/cleanup-builder.ts
Remaining from v1.3 (Carried Forward)
Medium Priority
- F6: CI/CD examples (
docs/ci-cd.md) — GitHub Actions, GitLab CI, CircleCI workflows
Quality Features
- Flake Detection — Auto-rerun failing tests with varied seeds
- Mutation Testing (
src/quality/mutation.ts) — Synthetic bug injection, contract strength scoring
Performance & Implementation (John Carmack)
- P2:
hashSchematruncated to 16 chars — use full SHA-256 (64 hex chars) - P3:
PARSE_CACHEMap has no TTL — add LRU cache with configurable max size - P5: Streaming NDJSON loads entire response — add chunked processing with limits
- P6:
request-builder.tsusesMath.random()fallback — deterministic fallback + warning (already clean in production) - P8:
topologicalSortre-sorts on everyregister()— lazy sorting
Observability (Charity Majors)
- O1: Zero OpenTelemetry integration — add tracing, metrics, correlation (deferred — not appropriate for test framework)
- O2: No per-route chaos granularity — route overrides, include/exclude patterns
- O3: No resilience verification after chaos — recovery check post-injection
- O4: Runtime hooks evaluate on every request — pre-filter routes with contracts
- O5: Arbiter Bug #1 — ScopeRegistry default scope ignored configured
default - O6: Arbiter Bug #2 —
routesoption dropped in plugin contract builder
Type Safety (Uncle Bob)
- T1:
OperationHeaderunion withstring— use branded type for extensions - T2:
RequestStructure.body?: unknown— discriminated union for body types
Category Inference (Martin Fowler)
- Cat1: Hardcoded exact paths miss prefixed variants — regex/prefix matching
New for v2.2: Arbiter Integration Stabilization (2026-04-27)
P0: Targeted Chaos Testing ✅ COMPLETE
- Per-route include/exclude patterns for chaos injection
- Route-level chaos config overrides global config
- Resilience verification (retry after chaos injection)
P1: Arbiter Bug Fixes ✅ COMPLETE
- Bug #1: ScopeRegistry default scope — now respects configured
defaultscope - Bug #2: Plugin contract builder —
routesoption now propagated to test runner - Bug #3: Configurable invariants — deferred to v2.3
P2: Schema Inference Fixes ✅ COMPLETE
- Disabled aggressive array-of-objects schema inference (was generating invalid
[]accessors) - Reduced false-positive contract violations from inferred schemas
New for v2.1: Cross-Route Relationships (Arbiter Feedback)
Design Decision: Relationships are expressed as APOSTL predicates inside x-ensures. No new schema annotation needed — relationships are just postconditions.
schema: {
'x-ensures': [
// Parent consistency
'response_body(this).tenantId == request_params(this).tenantId',
// Hypermedia link validation
'route_exists(response_body(this).controls.tenant.href) == true',
// Relationship validation
'relationship_valid("parent", request_params(this).tenantId, response_body(this).tenantId) == true'
]
}
P0: Core Relationship Predicates ✅ COMPLETE
R1: route_exists() Extension Predicate ✅
File: src/extensions/relationships.ts
Description: Check that a URL resolves to a registered route.
// Basic: check route exists
'route_exists(response_body(this).controls.tenant.href) == true'
// With method check:
'route_exists(response_body(this).controls.edit.href, response_body(this).controls.edit.method) == true'
// Negative: ensure link is NOT a route (external URL)
'route_exists(response_body(this).externalUrl) == false'
Implementation:
- Use
discoverRoutes()to get all registered routes - Match concrete URLs against route patterns (
/users/:idmatches/users/user:alice) - Support method validation
Invariants:
- MUST: Pattern matching handle
:paramsyntax - MUST: Return
falsefor unregistered routes, never throw - MUST: Cache route discovery results per test run
- MAY NEVER: Match against routes registered after the check
R2: Route Pattern Matcher ✅ COMPLETE
File: src/infrastructure/route-matcher.ts
Description: Utility to match concrete URLs against Fastify route patterns.
function matchRoutePattern(pattern: string, concreteUrl: string): {
matched: boolean
params: Record<string, string>
}
// Example:
matchRoutePattern('/users/:id', '/users/user:alice')
// → { matched: true, params: { id: 'user:alice' } }
Invariants:
- MUST: Support Fastify's
:paramand*wildcard syntax - MUST: Return extracted parameters
- MUST: Handle trailing slashes consistently
- MAY NEVER: Match partial segments (e.g.,
/users/:idshould NOT match/users/admin/settings)
P1: Relationship & Cascade Validation ✅ COMPLETE
R3: relationship_valid() Extension Predicate ✅
File: src/extensions/relationships.ts
Description: Validate parent-child consistency.
// Verify resource belongs to parent from path
'relationship_valid("parent", request_params(this).tenantId, response_body(this).tenantId) == true'
// Verify arbitrary relationship type
'relationship_valid("owner", request_params(this).userId, response_body(this).ownerId) == true'
Implementation:
- Track resource creation/deletion in test state
- Check that child resources reference existing parents
Invariants:
- MUST: Track resource lifecycle across test commands
- MUST: Support arbitrary relationship types (not hardcoded)
- MAY NEVER: Report false positives due to test isolation issues
R4: cascade_valid() Extension Predicate ✅
File: src/extensions/relationships.ts
Description: Verify that deleting a parent resource makes children inaccessible.
// After DELETE /tenants/:id, verify cascade
'cascade_valid("tenant", request_params(this).id, ["application", "user"]) == true'
Implementation:
- Track DELETE operations in test state
- For deleted resources, check child routes return 404
- Accept array of child resource types to validate
Invariants:
- MUST: Verify HTTP 404 for child resources after parent deletion
- MUST: Support soft-delete (200 with deleted flag) vs hard-delete (404)
- MUST: Accept list of child types to check
- MAY NEVER: Assume all DELETEs are hard deletes
R5: Hypermedia Validation Phase ✅ COMPLETE
File: src/test/hypermedia-validator.ts
Description: Post-test validation that checks all hypermedia links across responses.
const result = await fastify.apophis.contract({ depth: 'standard' });
// Optional: Run hypermedia validation
const hypermediaReport = await fastify.apophis.validateHypermedia({
checkLinks: true, // verify hrefs resolve to routes
checkDescriptors: true, // verify action descriptors exist
checkMethods: true, // verify methods match route definitions
checkRelationships: true // verify parent-child consistency
});
Output format:
{
"brokenLinks": [
{
"route": "GET /users/user:alice",
"control": "tenant",
"href": "/tenants/tenant:acme",
"issue": "route_not_found",
"suggestion": "Route GET /tenants/:id is not registered"
}
],
"orphanResources": [
{
"route": "GET /applications/app:123",
"field": "tenantId",
"value": "tenant:deleted",
"issue": "parent_not_found"
}
]
}
Invariants:
- MUST: Collect all hypermedia links from test responses
- MUST: Validate links against registered routes
- MUST: Report per-route summaries
- MAY NEVER: Fail the main contract test suite due to hypermedia issues (separate report)
P2: Stateful Test Enhancement ✅ COMPLETE
R6: Automatic Path Substitution in Stateful Tests ✅
File: src/domain/request-builder.ts
Description: Infer path parameters from previously created resources.
// Apophis generates:
// Step 1: POST /tenants → { id: 'tenant:acme' }
// Step 2: POST /tenants/tenant:acme/applications → { id: 'app:123' }
// Step 3: GET /tenants/tenant:acme/applications/app:123
Implementation ✅:
- Enhanced
substitutePathParams()insrc/domain/request-builder.ts - Supports patterns:
tenantId,tenant_id,userId - Uses
inferResourceTypeFromParam()to map param names to resource types - Falls back to arbitrary generation if no matching resource in state
- Added test:
stateful runner substitutes path params from resource state
Invariants:
- ✅ MUST: Only substitute when resource type matches param name
- ✅ MUST: Fall back to random/arbitrary generation if no matching resource
- ✅ MUST: Not break existing stateful tests
- ✅ MAY NEVER: Generate invalid URLs due to substitution errors
R7: Cascade Validation in Stateful Tests ✅
File: src/test/cascade-validator.ts
Description: After DELETE commands, automatically verify children are inaccessible.
// Stateful test runs DELETE /tenants/tenant:acme
// Cascade validator then checks:
// - GET /tenants/tenant:acme → 404
// - GET /tenants/tenant:acme/applications → 404
// - GET /tenants/tenant:acme/users → 404
Implementation ✅:
- Created
src/test/cascade-validator.tswithcreateCascadeValidator() findChildRoutes()discovers nested routes under a parent patternvalidateAfterDelete()generates cascade checks with configurable depthextractPathParamsFromUrl()extracts params for URL substitution- Added comprehensive tests for cascade validation
Invariants:
- ✅ MUST: Only trigger after DELETE commands that return 2xx/204
- ✅ MUST: Use route pattern matching to find child routes
- ✅ MUST: Configurable (on/off, max depth)
- ✅ MAY NEVER: Cause test suite to fail due to cascade check timing issues
- MAY NEVER: Cause test suite to fail due to cascade check timing issues
Implementation Order
Phase 1: Foundation (P0) ✅ COMPLETE
- ✅ Create
src/infrastructure/route-matcher.ts— pattern matching utility - ✅ Create
src/extensions/relationships.ts—route_exists()predicate - ✅ Add tests for route pattern matcher
- ✅ Add tests for
route_exists()predicate
Phase 2: Relationship Predicates (P1) ✅ COMPLETE
- ✅ Add
relationship_valid()predicate - ✅ Add
cascade_valid()predicate - ✅ Create
src/test/hypermedia-validator.ts— collect and validate links - ✅ Hypermedia validation via APOSTL
route_exists()predicate (no imperative API needed) - ✅ Add tests for all predicates and hypermedia validation
Phase 3: Stateful Enhancement (P2) ✅ COMPLETE
- ✅ Enhanced
src/domain/request-builder.ts— automatic path substitution from resource state - ✅ Created
src/test/cascade-validator.ts— automatic cascade checks after DELETE - ✅ Added tests for automatic path substitution
- ✅ Added tests for cascade validation
Phase 4: Integration & Polish ✅ COMPLETE
- ✅ Update documentation with relationship predicate examples
- ✅ Update
FEEDBACK-cross-route-relationships.mdwith implementation status - ⏳ Performance testing with Arbiter's 30+ route families (deferred)
- ✅ Release v2.1
Metrics
| Metric | v2.0 | v2.x | v2.1 |
|---|---|---|---|
| Tests passing | 343 | 476 | 503 |
| Contract language | Justin | APOSTL | APOSTL |
| Cross-operation support | ❌ | ✅ | ✅ |
| Cross-route predicates | 0 | 0 | 3 (route_exists, relationship_valid, cascade_valid) |
| Hypermedia validation | ❌ | ❌ | ✅ |
| Automatic path substitution | ❌ | ❌ | ✅ |
| Cascade validation | ❌ | ❌ | ✅ |
Reference
- Cross-Route Feedback:
FEEDBACK-cross-route-relationships.md - Cross-Operation Feedback:
FEEDBACK-cross-operation-expressiveness.md - Previous Steps:
NEXT_STEPS_425.md - Plugin Contracts Spec:
docs/PLUGIN_CONTRACTS_SPEC.md - Extension System:
docs/extensions/EXTENSION-PLUGIN-SYSTEM.md - Arbiter Collaboration: Contact via GitHub issues/PRs