# Feedback for Apophis Team: Cross-Route Relationships and Hypermedia Validation **From:** Arbiter Team (Multi-tenant identity platform with LDF+Action hypermedia architecture) **Date:** 2026-04-26 **Status:** ✅ **IMPLEMENTED in v2.1** — All P0/P1 features complete --- ## 1. Executive Summary **The Gap (v2.0):** Apophis validated routes as independent entities. Real-world APIs have relationships: - **Parent-child**: Tenant owns Applications, Application owns Users - **Hypermedia links**: Resources expose `controls` with URLs to related resources - **Cascade behavior**: Deleting a parent should make children inaccessible - **Path correlation**: Child routes use parent IDs from path parameters **The Solution (v2.1):** All cross-route validation is now expressed through APOSTL formulas using extension predicates. No imperative APIs or special endpoints. --- ## 2. What Was Implemented ### 2.1 Extension Predicate: `route_exists()` ✅ Check that hypermedia links resolve to registered routes: ```apostl 'route_exists(this).controls.self.href == true' 'route_exists(this).controls.tenant.href == true' 'route_exists(this).controls.applications.href == true' ``` **File**: `src/extensions/relationships.ts` **Tests**: `src/test/relationships.test.ts`, `src/test/cross-operation-support.test.ts` ### 2.2 Extension Predicate: `relationship_valid()` ✅ Validate parent-child consistency: ```apostl 'relationship_valid("parent", request_params(this).tenantId, response_body(this).tenantId) == true' ``` **File**: `src/extensions/relationships.ts` **Tests**: `src/test/relationships.test.ts` ### 2.3 Extension Predicate: `cascade_valid()` ✅ Verify cascade after DELETE: ```apostl 'cascade_valid("tenant", request_params(this).id, ["application", "user"]) == true' ``` **File**: `src/extensions/relationships.ts` **Tests**: `src/test/relationships.test.ts` ### 2.4 Automatic Path Substitution in Stateful Tests ✅ When generating commands for routes with path params (e.g., `:tenantId`): - Checks if resource type `tenant` exists in state - If yes, substitutes with a known ID from state - If no, falls back to arbitrary generation **File**: `src/domain/request-builder.ts` (enhanced `substitutePathParams()`) **Tests**: `src/test/stateful-runner.test.ts` ### 2.5 Cascade Validator ✅ After DELETE commands, automatically discovers child routes and verifies they return 404: ```typescript const validator = createCascadeValidator(routes) const report = await validator.validateAfterDelete( '/tenants/tenant:acme', { id: 'tenant:acme' }, { maxDepth: 2 } ) ``` **File**: `src/test/cascade-validator.ts` **Tests**: `src/test/cascade-validator.test.ts` ### 2.6 Hypermedia Link Extraction ✅ Utility for extracting links from response bodies (controls, _links, links array): ```typescript const links = extractLinks(response.body, 'GET /users/:id') // Returns: [{ route: 'GET /users/:id', control: 'self', href: '/users/123' }, ...] ``` **File**: `src/test/hypermedia-validator.ts` **Tests**: `src/test/hypermedia-validator.test.ts` --- ## 3. Design Philosophy: APOSTL-First **We rejected the imperative API approach.** Instead of: ```typescript // ❌ WRONG: Imperative API const report = await fastify.apophis.validateHypermedia({ checkLinks: true, checkDescriptors: true }) ``` We use declarative APOSTL contracts: ```apostl // ✅ CORRECT: Declarative contracts 'route_exists(this).controls.self.href == true' 'route_exists(this).controls.tenant.href == true' ``` **Why?** - Contracts are evaluated during all test phases (petit, stateful, runtime) - No special endpoints or hooks needed - Consistent with APOPHIS's design philosophy - Self-documenting in route schemas --- ## 4. Usage Examples ### 4.1 Hypermedia Controls ```typescript fastify.get('/tenants/:id', { schema: { 'x-category': 'observer', 'x-ensures': [ 'route_exists(this).controls.self.href == true', 'route_exists(this).controls.applications.href == true', ], response: { 200: { type: 'object', properties: { id: { type: 'string' }, controls: { type: 'object', properties: { self: { type: 'object', properties: { href: { type: 'string' } } }, applications: { type: 'object', properties: { href: { type: 'string' } } }, }, }, }, }, }, }, }) ``` ### 4.2 Parent-Child Validation ```typescript fastify.post('/tenants/:tenantId/applications', { schema: { 'x-category': 'constructor', 'x-ensures': [ 'response_body(this).tenantId == request_params(this).tenantId', 'response_code(GET /tenants/{request_params(this).tenantId}/applications/{response_body(this).id}) == 200', ], }, }) ``` ### 4.3 Cascade Validation ```typescript fastify.delete('/tenants/:id', { schema: { 'x-category': 'destructor', 'x-ensures': [ 'cascade_valid("tenant", request_params(this).id, ["application", "user"]) == true', ], }, }) ``` --- ## 5. Test Results | Feature | Tests | Status | |---------|-------|--------| | `route_exists()` predicate | 5 tests | ✅ Passing | | `relationship_valid()` predicate | 2 tests | ✅ Passing | | `cascade_valid()` predicate | 2 tests | ✅ Passing | | Path substitution | 1 test | ✅ Passing | | Cascade validator | 6 tests | ✅ Passing | | Hypermedia extraction | 9 tests | ✅ Passing | | **Total** | **487 tests** | **✅ All passing** | --- ## 6. What We Learned ### 6.1 APOSTL is Sufficient We initially proposed imperative APIs (`validateHypermedia()`, `x-relationships` annotations). Through implementation, we discovered that APOSTL predicates are more powerful and consistent: - **Composability**: `route_exists()` can be combined with any other APOSTL expression - **Test coverage**: Works in petit, stateful, and runtime validation without extra code - **Clarity**: Contracts are self-documenting in route schemas ### 6.2 Extension Predicates are the Right Abstraction The extension system (predicates + headers + hooks) provides exactly the right level of flexibility: - **Domain-specific**: Each predicate solves one problem well - **Composable**: Multiple extensions work together - **Testable**: Pure functions with clear inputs/outputs ### 6.3 State Tracking is Key Automatic path substitution requires tracking resource state across test commands. The `ModelState` with `ResourceHierarchy` provides the right structure: ```typescript interface ModelState { resources: Map> // resourceType → resourceId → { id, type, parentId, parentType, ... } } ``` --- ## 7. Remaining Work (Out of Scope for v2.1) | Feature | Status | Reason | |---------|--------|--------| | `x-relationships` schema annotation | ❌ Not implemented | Replaced by APOSTL predicates | | Full graph traversal | ❌ Out of scope | Complex graph algorithms belong in application tests | | Database foreign key validation | ❌ Out of scope | Apophis shouldn't access databases directly | | Cross-service link validation | ❌ Out of scope | Microservice links require running external services | --- ## 8. References - **Implementation**: `src/extensions/relationships.ts` - **Route Matcher**: `src/infrastructure/route-matcher.ts` - **Cascade Validator**: `src/test/cascade-validator.ts` - **Hypermedia Validator**: `src/test/hypermedia-validator.ts` - **Tests**: `src/test/relationships.test.ts`, `src/test/cross-operation-support.test.ts` - **Extension System**: `docs/extensions/EXTENSION-PLUGIN-SYSTEM.md` --- **Contact:** Arbiter Team — We'd love to hear how these features work for your use cases!