stemedb/docs/planning/aphoria-claims-api.md
jml 3b5f88b4f0 feat(aphoria): implement claims architecture (A1-A5) with verify engine, corpus, coverage, and explain
Complete Aphoria claims system overhaul:
- A1: Rename ExtractedClaim to Observation (extractors produce observations, not claims)
- A2: Add AuthoredClaim with full provenance, invariants, and authority tiers
- A3: Verify engine comparing observations against authored claims, CLI + formatters
- A4: Corpus as first-class assertions with predicate indexing, authority lens, trust packs
- A5: Coverage analysis, explain/docs generation, self-audit extractor, claim suggester skill

Also includes: 42 extractors updated for Observation type, verifiable_predicates trait,
conflict detection with comparison modes, claims TOML persistence, Grafana dashboard,
backup/restore scripts, and comprehensive test coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 09:11:47 +00:00

449 lines
14 KiB
Markdown

# Aphoria Claims API — Sidecar Service
> **Goal:** A lightweight HTTP API that exposes the same claim operations the `aphoria-claims` Claude Code skill performs — review diffs, identify claimable patterns, check existing claims, suggest new claims, create/update/verify claims — so any tool, agent, or CI pipeline can build claim authoring flows.
>
> **Key insight:** The skill is just an LLM calling CLI commands. This API replaces the CLI calls with HTTP endpoints and replaces the Claude skill prompt with a Gemini call for the reasoning. Same workflow, any client.
---
## Architecture
```
┌──────────────────────────┐
│ Any Client │
│ (CI, IDE, ADK agent, │
│ custom UI, webhook) │
└───────────┬──────────────┘
│ HTTP
┌──────────────────────────┐
│ aphoria-claims-api │
│ (Rust, axum, port 18189) │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ Claims │ │ Gemini │ │
│ │ Engine │ │ Client │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ ┌────▼───────────▼────┐ │
│ │ aphoria lib crate │ │
│ │ (ClaimsFile, │ │
│ │ verify_claims, │ │
│ │ extract_claims, │ │
│ │ run_scan) │ │
│ └─────────────────────┘ │
└──────────────────────────┘
┌───────────▼──────────────┐
│ .aphoria/claims.toml │
│ (project claim store) │
└──────────────────────────┘
```
The sidecar calls `aphoria` as a library crate (not shelling out to CLI). It links against the same types: `ClaimsFile`, `AuthoredClaim`, `Observation`, `verify_claims()`, `extract_claims()`.
For the reasoning parts (identifying claimable patterns in diffs, suggesting claims), it calls Gemini via the HTTP API.
---
## Why a Sidecar, Not Extending stemedb-api
Aphoria claims are file-based (`.aphoria/claims.toml`) and project-scoped. The StemeDB API serves the knowledge graph (assertions, lenses, queries). These are different concerns:
| | stemedb-api (18180) | aphoria-claims-api (18189) |
|---|---|---|
| **Data** | Episteme assertions (append-only DAG) | Authored claims (TOML file) |
| **Scope** | Cluster-wide knowledge | Single project |
| **Storage** | WAL + KV | `.aphoria/claims.toml` |
| **Auth** | API keys, per-agent | Local only (or simple token) |
The sidecar runs alongside a project checkout. It needs filesystem access to the project root (for claims.toml, extractors, git).
---
## API Surface
### Claims CRUD
These mirror `aphoria claims create|list|explain|update|supersede|deprecate`:
```
POST /v1/claims Create a claim
GET /v1/claims List claims (filter by ?category=&status=&format=json)
GET /v1/claims/:id Get a single claim
PATCH /v1/claims/:id Update claim fields
POST /v1/claims/:id/supersede Create superseding claim
DELETE /v1/claims/:id Deprecate a claim (body: {reason})
```
**Request/response types use the existing `AuthoredClaim` struct** serialized as JSON. The API reads/writes `.aphoria/claims.toml` via `ClaimsFile`.
### Verification
Mirrors `aphoria verify run`:
```
POST /v1/verify Run verification (claims vs observations)
GET /v1/verify/map Show claim-to-extractor mapping
```
**POST /v1/verify** body:
```json
{
"path": ".",
"show_unclaimed": true,
"categories": ["safety", "imports"],
"claims": ["wallet-seqcst-001"]
}
```
**Response:** `VerifyReport` as JSON — per-claim verdicts (pass/conflict/missing/unclaimed) + summary counts.
### Coverage (A5.1)
```
GET /v1/coverage Coverage metrics per module
```
**Response:**
```json
{
"project": "maxwell",
"summary": {
"total_observations": 67,
"total_claims": 12,
"claimed_percentage": 45.2,
"unclaimed_count": 37
},
"modules": [
{
"module_path": "wallet/atomics",
"observation_count": 5,
"claim_count": 3,
"density": 0.6
}
]
}
```
### Diff Review (A5.3 — the reasoning endpoint)
This is the one that calls Gemini. It does what the `aphoria-claims` skill does:
```
POST /v1/review Review a diff for claimable patterns
```
**Request:**
```json
{
"diff": "... unified diff text ...",
"context": {
"repo": "maxwell",
"branch": "feat/new-ordering"
}
}
```
Internally:
1. Load existing claims from `.aphoria/claims.toml`
2. Run extractors on changed files to get observations
3. Run `verify_claims()` to check for violations
4. Send diff + existing claims + observations to Gemini with the claim-identification prompt
5. Return structured suggestions
**Response:**
```json
{
"violations": [
{
"claim_id": "wallet-seqcst-001",
"invariant": "All wallet atomics MUST use SeqCst",
"violation": "Ordering::Relaxed at sync.rs:42",
"action": "fix_code_or_supersede"
}
],
"suggestions": [
{
"observation": {
"file": "src/pool.rs",
"line": 15,
"matched_text": "const MAX_POOL_SIZE: u32 = 50;"
},
"suggested_claim": {
"id": "maxwell-pool-max-001",
"concept_path": "maxwell/db/pool/max_size",
"predicate": "max_value",
"value": "50",
"category": "constants",
"invariant": "Database pool size MUST NOT exceed 50",
"consequence": "OOM under sustained load",
"authority_tier": "observational"
},
"reason": "New constant with non-obvious value. Similar to 2 existing claims about pool configuration.",
"confidence": 0.85
}
],
"no_claim_needed": [
{
"pattern": "whitespace change in types.rs",
"reason": "Internal refactor, no behavioral change"
}
]
}
```
### Docs Generation (A5.2)
```
POST /v1/docs/generate Generate claims-explained documentation
```
**Response:** Markdown string or JSON with full provenance chains, verification status, coverage gaps.
### Onboarding (A5.4)
```
GET /v1/explain Narrative project overview from claims
```
**Response:** Markdown narrative: architectural boundaries, safety invariants, key constants with provenance, coverage gaps.
---
## Gemini Integration
The reasoning endpoints (`/v1/review`, docs generation, onboarding narrative) call Gemini for LLM tasks.
**Config:**
```toml
# In aphoria.toml or env vars
[claims_api]
gemini_model = "gemini-2.5-flash"
```
```
GEMINI_API_KEY=AIzaSy... # env var, never in config files
```
**Gemini calls are structured, not conversational.** Each call has:
1. A system prompt (the same logic from the `aphoria-claims` SKILL.md — claimability rules, category reference, authority tier guide)
2. Structured input (diff text, existing claims as JSON, observations as JSON)
3. Structured output (JSON schema for suggestions)
The prompt is essentially the skill document converted to a system prompt, with the human-in-the-loop parts replaced by structured JSON output.
### Prompt Structure for `/v1/review`
```
System: You are an expert at identifying architectural decisions, safety
invariants, and policy requirements in code changes.
Given:
- A unified diff
- Existing authored claims (JSON)
- Observations extracted from changed files (JSON)
Identify:
1. Violations: Does the diff contradict any existing claim?
2. Suggestions: What new claims should be authored? (Only if a violation
would break something, a new team member would need to know, or there's
a non-obvious reason for the choice)
3. No-claim-needed: What patterns don't need claims and why?
For each suggestion, provide:
- id, concept_path, predicate, value, category
- invariant (what MUST be true)
- consequence (what breaks if violated)
- authority_tier (regulatory/clinical/observational/expert/community)
- reason (why this needs a claim)
- confidence (0.0-1.0)
Respond in JSON matching this schema: { ... }
```
---
## Implementation
### Crate Structure
New binary in `applications/aphoria-claims-api/`:
```
applications/aphoria-claims-api/
Cargo.toml # depends on aphoria (lib), axum, reqwest, serde_json
src/
main.rs # axum server setup, routes
routes/
claims.rs # CRUD endpoints
verify.rs # verification endpoint
coverage.rs # coverage metrics
review.rs # diff review (calls Gemini)
docs.rs # docs generation
explain.rs # onboarding narrative
gemini/
client.rs # Gemini API client (generateContent)
prompts.rs # Prompt templates for each reasoning task
types.rs # Request/response types for Gemini API
state.rs # AppState (project_root, config, gemini client)
error.rs # Error types -> axum responses
```
### Dependencies
- `aphoria` (path dependency) — all the domain logic
- `axum` — HTTP framework (already used by stemedb-api)
- `reqwest` — Gemini API calls
- `serde_json` — JSON serialization
- `tokio` — async runtime
- `tower-http` — CORS middleware
- `tracing` — structured logging
### Key Design Decisions
**Library, not shell-out.** The API imports `aphoria` as a crate and calls `ClaimsFile::load()`, `verify_claims()`, `extract_claims()` directly. No `Command::new("aphoria")`.
**Stateless per request.** Each request reads `.aphoria/claims.toml` fresh. No in-memory cache of claims (the file is small, TOML parsing is fast). This means multiple clients can't corrupt each other's state.
**File locking for writes.** `POST /v1/claims` and `PATCH /v1/claims/:id` acquire a file lock on `claims.toml` before read-modify-write. Use `fs2::FileExt` or `fd-lock`.
**Gemini is optional.** The CRUD, verify, and coverage endpoints work without Gemini. Only `/v1/review`, `/v1/docs/generate`, and `/v1/explain` need LLM reasoning. If `GEMINI_API_KEY` is not set, these return 503 with a clear message.
---
## Port Assignment
Following the existing 181XX scheme:
| Offset | Service | Port |
|--------|---------|------|
| +9 | Claims API | 18189 |
Env var: `APHORIA_CLAIMS_API_BIND_ADDR` (default `127.0.0.1:18189`)
---
## Example Flows
### CI Pipeline: Block PR if Claims Violated
```bash
# In CI script
DIFF=$(git diff origin/main...HEAD)
RESULT=$(curl -s -X POST http://localhost:18189/v1/review \
-H "Content-Type: application/json" \
-d "{\"diff\": $(echo "$DIFF" | jq -Rs .)}")
VIOLATIONS=$(echo "$RESULT" | jq '.violations | length')
if [ "$VIOLATIONS" -gt 0 ]; then
echo "Claims violated:"
echo "$RESULT" | jq '.violations[]'
exit 1
fi
```
### IDE Extension: Suggest Claims on Save
```typescript
// VS Code extension pseudocode
const diff = await getDiffSinceLastSave();
const response = await fetch('http://localhost:18189/v1/review', {
method: 'POST',
body: JSON.stringify({ diff })
});
const { suggestions } = await response.json();
for (const s of suggestions) {
showInlineHint(s.observation.file, s.observation.line,
`Claim suggested: ${s.suggested_claim.invariant}`);
}
```
### ADK-Go Agent: Claims-Aware Code Generation
```go
// Before generating code, check constraints via claims
resp, _ := http.Post("http://localhost:18189/v1/verify",
"application/json",
bytes.NewReader([]byte(`{"show_unclaimed": false}`)))
var report VerifyReport
json.NewDecoder(resp.Body).Decode(&report)
if report.Summary.Conflict > 0 {
// Don't generate code that conflicts with claims
return fmt.Errorf("existing claims would be violated")
}
```
### New Developer Onboarding
```bash
curl -s http://localhost:18189/v1/explain | less
```
Gets the narrative: what this codebase claims about itself, why, and where to find evidence.
---
## Gemini API Integration Details
### Endpoint
```
POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=$GEMINI_API_KEY
```
### Request Shape
```json
{
"contents": [{
"parts": [{
"text": "System prompt + structured input"
}]
}],
"generationConfig": {
"responseMimeType": "application/json",
"responseSchema": { ... }
}
}
```
Using `responseMimeType: "application/json"` with a schema forces Gemini to return structured output matching our types. No parsing needed.
### Cost Estimate
Diff review for a typical PR (~500 lines):
- Input: ~2K tokens (prompt) + ~1K (diff) + ~1K (existing claims JSON) = ~4K tokens
- Output: ~500 tokens (suggestions JSON)
- At Gemini 2.5 Flash pricing: ~$0.001 per review
Negligible. Run it on every PR.
---
## What This Enables
1. **Any LLM can author claims.** Not just Claude Code. Gemini, GPT, local models — they call the API.
2. **CI enforcement.** Block PRs that violate claims. No human needs to remember.
3. **IDE integration.** Inline suggestions as you type, not just at review time.
4. **ADK-Go agents.** Agents that generate code can check claims before writing, and author claims after.
5. **Custom dashboards.** Coverage metrics as a web service. Build whatever UI you want.
6. **The flywheel without the skill.** The `aphoria-claims` skill is great for Claude Code users. This API is for everyone else.
---
## Implementation Order
1. **Claims CRUD endpoints** — Wrap `ClaimsFile` in axum routes. No LLM needed. Test with curl.
2. **Verify endpoint** — Call `verify_claims()`, return JSON. No LLM needed.
3. **Coverage endpoint** — Compute from verify report. No LLM needed.
4. **Gemini client**`reqwest` + structured output schema.
5. **Review endpoint** — The reasoning endpoint. Diff + claims + observations -> Gemini -> suggestions.
6. **Docs + Explain endpoints** — Narrative generation via Gemini.
Steps 1-3 are pure engineering (wrap existing Rust functions in HTTP). Steps 4-6 add the Gemini reasoning layer.