308 lines
8.4 KiB
Markdown
308 lines
8.4 KiB
Markdown
|
|
# FEEDBACK: APOSTL Parser Limitations Blocking Behavioral Contracts
|
||
|
|
|
||
|
|
**From:** Arbiter Team (opencode integration)
|
||
|
|
**Date:** 2026-04-28
|
||
|
|
**Severity:** High - prevents adoption of Silver/Gold behavioral contracts
|
||
|
|
**Apophis Version:** 2.x (latest)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Executive Summary
|
||
|
|
|
||
|
|
We've spent significant effort upgrading our route contracts from Bronze (tautological) to Silver/Gold (behavioral with cross-operation causality, data integrity, and state transitions). However, **multiple documented APOSTL features fail at parse time**, forcing us to strip contracts back to Bronze level or remove features entirely.
|
||
|
|
|
||
|
|
We cannot leverage the full power of Apophis as documented. This feedback documents exact parser failures with minimal reproductions.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Issue 1: `x-requires` Resource Identifier Syntax Fails to Parse
|
||
|
|
|
||
|
|
### Documented Syntax (from getting-started.md line 227)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
'x-requires': ['users:id'] // requires a user resource to exist
|
||
|
|
```
|
||
|
|
|
||
|
|
### Actual Behavior
|
||
|
|
|
||
|
|
**Parse Error:**
|
||
|
|
```
|
||
|
|
Parse error at position 5: (found ':')
|
||
|
|
users:userKey
|
||
|
|
^
|
||
|
|
Unexpected token
|
||
|
|
```
|
||
|
|
|
||
|
|
### Impact
|
||
|
|
|
||
|
|
We cannot declare route preconditions. This breaks:
|
||
|
|
- Observer routes that need resources to exist before testing
|
||
|
|
- Mutator routes that should only run on existing resources
|
||
|
|
- Destructor routes that require resources to delete
|
||
|
|
|
||
|
|
### Workaround
|
||
|
|
|
||
|
|
We stripped ALL `x-requires` from our contracts. This means Apophis cannot know which routes depend on which resources, likely breaking stateful test generation.
|
||
|
|
|
||
|
|
### Minimal Reproduction
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
app.get('/users/:id', {
|
||
|
|
schema: {
|
||
|
|
'x-requires': ['users:id'], // FAILS
|
||
|
|
'x-ensures': ['status:200']
|
||
|
|
}
|
||
|
|
}, handler)
|
||
|
|
```
|
||
|
|
|
||
|
|
### Expected Behavior
|
||
|
|
|
||
|
|
Either:
|
||
|
|
1. The `resource:id` syntax should parse correctly, OR
|
||
|
|
2. Documentation should show the correct APOSTL expression format for preconditions
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Issue 2: `route_exists()` Inside Conditionals Fails to Parse
|
||
|
|
|
||
|
|
### Documented Syntax (from getting-started.md line 742)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
'route_exists(this).controls.self.href == true'
|
||
|
|
```
|
||
|
|
|
||
|
|
### Actual Behavior
|
||
|
|
|
||
|
|
When used inside an `if` conditional (which is necessary since we only want to check hypermedia on success):
|
||
|
|
|
||
|
|
```
|
||
|
|
Parse error at position 31: (found '(')
|
||
|
|
if status:200 then route_exists(this).controls.self.href == true else true
|
||
|
|
^
|
||
|
|
Expected "else"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Impact
|
||
|
|
|
||
|
|
We cannot validate hypermedia links in success cases. This breaks:
|
||
|
|
- HATEOAS contract verification
|
||
|
|
- Self-link validation
|
||
|
|
- Action descriptor integrity checks
|
||
|
|
|
||
|
|
### Workaround
|
||
|
|
|
||
|
|
Strip all `route_exists()` calls from contracts.
|
||
|
|
|
||
|
|
### Minimal Reproduction
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
app.get('/users/:id', {
|
||
|
|
schema: {
|
||
|
|
'x-ensures': [
|
||
|
|
// FAILS - parser chokes on route_exists inside conditional
|
||
|
|
'if status:200 then route_exists(this).controls.self.href == true else true'
|
||
|
|
]
|
||
|
|
}
|
||
|
|
}, handler)
|
||
|
|
```
|
||
|
|
|
||
|
|
### Expected Behavior
|
||
|
|
|
||
|
|
`route_exists()` should be valid inside `if` expressions, or the docs should show the correct nesting syntax.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Issue 3: `response_body(GET /path/{id})` Inside Conditionals May Fail
|
||
|
|
|
||
|
|
### Observed Pattern
|
||
|
|
|
||
|
|
Cross-operation calls like:
|
||
|
|
```typescript
|
||
|
|
'response_code(GET /users/{response_body(this).id}) == 200'
|
||
|
|
```
|
||
|
|
|
||
|
|
Work fine as top-level expressions. But we suspect nesting them inside conditionals may also fail (we haven't tested extensively due to Issues 1 and 2 blocking progress).
|
||
|
|
|
||
|
|
### Question for Apophis Team
|
||
|
|
|
||
|
|
Are cross-operation calls valid inside `if` expressions? Example:
|
||
|
|
```typescript
|
||
|
|
'if status:201 then response_code(GET /users/{response_body(this).id}) == 200 else true'
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Issue 4: Lack of Clear Error Context
|
||
|
|
|
||
|
|
### Problem
|
||
|
|
|
||
|
|
Parse errors show:
|
||
|
|
```
|
||
|
|
Parse error at position 5: (found ':')
|
||
|
|
users:userKey
|
||
|
|
```
|
||
|
|
|
||
|
|
But they do NOT show:
|
||
|
|
- Which route file caused the error
|
||
|
|
- Which route definition (path/method)
|
||
|
|
- Which specific contract clause failed
|
||
|
|
|
||
|
|
With 100+ routes, debugging requires binary search through files.
|
||
|
|
|
||
|
|
### Expected Behavior
|
||
|
|
|
||
|
|
```
|
||
|
|
Parse error in route GET /tenant/users/:userKey
|
||
|
|
File: src/routes/user-directory/index.js:150
|
||
|
|
Contract: x-requires[0]
|
||
|
|
Expression: 'users:userKey'
|
||
|
|
Parse error at position 5: (found ':')
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## What We Had to Remove
|
||
|
|
|
||
|
|
Here's the complete list of behavioral contracts we WROTE but had to DELETE due to parser failures:
|
||
|
|
|
||
|
|
### From `user-directory/index.js`:
|
||
|
|
```javascript
|
||
|
|
// All x-requires (6 routes affected):
|
||
|
|
'x-requires': ['users:userKey']
|
||
|
|
|
||
|
|
// Hypermedia validation (2 routes affected):
|
||
|
|
'if status:200 then route_exists(this).controls.self.href == true else true'
|
||
|
|
```
|
||
|
|
|
||
|
|
### From `billing/subscriptions.js`:
|
||
|
|
```javascript
|
||
|
|
// x-requires (2 routes):
|
||
|
|
'x-requires': ['subscriptions:subscriptionId']
|
||
|
|
```
|
||
|
|
|
||
|
|
### From `billing/invoices.js`:
|
||
|
|
```javascript
|
||
|
|
// x-requires (3 routes):
|
||
|
|
'x-requires': ['invoices:invoiceId']
|
||
|
|
```
|
||
|
|
|
||
|
|
### From `notifications/email-routes.js`:
|
||
|
|
```javascript
|
||
|
|
// x-requires (3 routes):
|
||
|
|
'x-requires': ['notifications:notificationId']
|
||
|
|
|
||
|
|
// Hypermedia:
|
||
|
|
'if status:200 then route_exists(this).controls.self.href == true else true'
|
||
|
|
```
|
||
|
|
|
||
|
|
### From `webhooks-management/index.js`:
|
||
|
|
```javascript
|
||
|
|
// x-requires (12 routes):
|
||
|
|
'x-requires': ['webhooks:id']
|
||
|
|
```
|
||
|
|
|
||
|
|
### From `sessions-management/index.js`:
|
||
|
|
```javascript
|
||
|
|
// x-requires (3 routes):
|
||
|
|
'x-requires': ['sessions:jti']
|
||
|
|
```
|
||
|
|
|
||
|
|
### From `devices/*.js`:
|
||
|
|
```javascript
|
||
|
|
// x-requires (4 routes):
|
||
|
|
'x-requires': ['devices:id']
|
||
|
|
```
|
||
|
|
|
||
|
|
### From `workflow/index.js`:
|
||
|
|
```javascript
|
||
|
|
// x-requires (3 routes):
|
||
|
|
'x-requires': ['workflow_handoffs:id']
|
||
|
|
'x-requires': ['workflow_lineages:lineageId']
|
||
|
|
```
|
||
|
|
|
||
|
|
**Total: 39 routes had behavioral contracts stripped due to parser limitations.**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Current State After Workarounds
|
||
|
|
|
||
|
|
We've kept the behavioral contracts that DO work:
|
||
|
|
|
||
|
|
✅ **Cross-operation causality** (top-level):
|
||
|
|
```javascript
|
||
|
|
'response_code(GET /resource/{response_body(this).data.id}) == 200'
|
||
|
|
```
|
||
|
|
|
||
|
|
✅ **Data integrity** (top-level):
|
||
|
|
```javascript
|
||
|
|
'response_body(GET /resource/{response_body(this).data.id}).data.name == request_body(this).name'
|
||
|
|
```
|
||
|
|
|
||
|
|
✅ **Collection consistency** (top-level):
|
||
|
|
```javascript
|
||
|
|
'exists item in response_body(GET /resource).data: item.id == response_body(this).data.id'
|
||
|
|
```
|
||
|
|
|
||
|
|
✅ **State transitions** (top-level):
|
||
|
|
```javascript
|
||
|
|
'previous(response_body(GET /resource/{id}).data.status) != response_body(GET /resource/{id}).data.status'
|
||
|
|
```
|
||
|
|
|
||
|
|
✅ **Tenant isolation** (top-level):
|
||
|
|
```javascript
|
||
|
|
'for item in response_body(this).data: item.tenantId == request_headers(this)["x-tenant-id"]'
|
||
|
|
```
|
||
|
|
|
||
|
|
✅ **Deletion semantics** (top-level):
|
||
|
|
```javascript
|
||
|
|
'response_code(GET /resource/{request_params(this).id}) == 404'
|
||
|
|
```
|
||
|
|
|
||
|
|
❌ **All `x-requires` removed** (39 routes affected)
|
||
|
|
❌ **All `route_exists()` removed** (6 routes affected)
|
||
|
|
❌ **Cannot nest cross-operation calls inside conditionals** (untested but suspected)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Recommendations
|
||
|
|
|
||
|
|
### Immediate (P0)
|
||
|
|
|
||
|
|
1. **Fix `x-requires` parsing**: Either support `resource:id` syntax or document the correct APOSTL expression format
|
||
|
|
2. **Fix nested expression parsing**: Allow `route_exists()`, `response_code(GET ...)`, etc. inside `if` conditionals
|
||
|
|
3. **Improve error messages**: Include file path, route method/path, and contract clause index in parse errors
|
||
|
|
|
||
|
|
### Short-term (P1)
|
||
|
|
|
||
|
|
4. **Add a contract validator CLI**: `npx apophis validate-contracts src/routes/**/*.js` that reports all parse errors without running tests
|
||
|
|
5. **Document parser limitations**: Clearly state which APOSTL features work in which contexts (top-level vs nested)
|
||
|
|
|
||
|
|
### Long-term (P2)
|
||
|
|
|
||
|
|
6. **Consider JSON Schema integration**: Auto-derive `x-requires` from `required` params fields
|
||
|
|
7. **Add IDE support**: VS Code extension that highlights invalid APOSTL expressions at write-time
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Context
|
||
|
|
|
||
|
|
We operate a large Fastify API (40+ route families, 200+ routes). Our goal is to have Gold-level behavioral contracts on every route. We've completed:
|
||
|
|
|
||
|
|
- ✅ Explicit JSON Schema on all routes
|
||
|
|
- ✅ `x-category` classification (constructor/observer/mutator/destructor)
|
||
|
|
- ✅ Bronze-level contracts (status codes, error consistency)
|
||
|
|
- ✅ Silver/Gold cross-operation contracts (where parser allows)
|
||
|
|
- ❌ `x-requires` preconditions (blocked by Issue 1)
|
||
|
|
- ❌ Hypermedia validation (blocked by Issue 2)
|
||
|
|
|
||
|
|
We want to be an Apophis success story. These parser issues are the only blockers.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Contact
|
||
|
|
|
||
|
|
This feedback was generated during active route decoration work. We're available to test fixes, provide more reproductions, or discuss syntax design.
|
||
|
|
|
||
|
|
**Priority:** Blocking production adoption of behavioral contracts
|
||
|
|
**Impact:** 39 routes cannot express preconditions; 6 routes cannot validate hypermedia
|