Files

672 lines
49 KiB
Markdown
Raw Permalink Normal View History

# agents/APISkill.md — NEXUS API AGENT SKILL (CANONICAL OPERATIONAL CONTRACT)
> **Machine-oriented contract for Hermes/Nexus agents** working with the Nexus
> Scalp Engine APIs. Read this file before touching any API: it tells you WHAT
> exists, WHICH endpoint answers WHICH question, HOW to call it, WHAT it
> depends on, WHAT can fail, HOW to test it, and WHAT MUST NEVER be done.
>
> - Spec of record (design): `docs/api/API_PLATFORM_V1.md`
> - Developer reference (usage): `docs/api/API_REFERENCE.md`
> - **This file**: agent-facing operational contract + drift-protected registry
> - Drift validator: `scripts/dev/api_skill_drift_check.py` (exit 0 = in sync)
>
> Maintainer: Hermes-Main (API Platform owner, TASK-API-PLATFORM, CHG-0043).
> Companion file convention: see `agents/skill.md` (architecture map).
---
## 0. STATE AT A GLANCE (verify before trusting)
| fact | value | how to re-verify |
|---|---|---|
| canonical tree | `src/nexus_scalp/web/api_v1/` | `ls src/nexus_scalp/web/api_v1/` |
| mount point | ONE call: `web/api_v1_wiring.register_api_v1(app)` at end of `web/server.create_app()` | `grep -n "register_api_v1" src/nexus_scalp/web/server.py` |
| live operation count | **68** (63 GET + 5 POST) | `.venv/Scripts/python.exe scripts/dev/api_contract_check.py` |
| legacy dashboard routes | **280** decorators, still served, additive-untouched | grep `@(app\|router).(get\|post...)` over `web/*.py` excluding `api_v1*` |
| superseded tree | `src/nexus_scalp/api/v1` — **REMOVED** (was 45 routes, commit `e51a910` removed it) | must NOT exist; do not resurrect |
| envelope | `{data, meta}` / `{error{...}}` | §4 |
| auth | none (localhost bind; no broker mutations by design) | §7 |
| platform owner registry | TASK-API-PLATFORM → VERIFIED (CHG-0043) | `agents/taskboard.md` |
---
## 1. API LANDSCAPE: FOUR STATUS CLASSES
Every API path in this repository is exactly ONE of these. Never blur them.
| class | meaning | examples |
|---|---|---|
| **CURRENT / CANONICAL** | `/api/v1/*` — the platform of record. Build here. | all 68 operations in §4/§5 |
| **LEGACY / HISTORICAL** | the ~280 dashboard routes (`/api/status`, `/api/live/state`, `/api/models/*`, `/api/research/*`, `/api/diagnostics/*`, `/api/db/console/*`, …). Still served for the Web UI. Do NOT build new client work on them; consume via `/api/v1` equivalents where they exist. | §2 |
| **DEPRECATED / SUPERSEDED** | legacy paths whose truth now has a canonical v1 endpoint. Keep working (UI depends on them) but prefer v1. | `/api/status` → `/api/v1/system/status`; `/api/models/champion` → `/api/v1/model/champion` |
| **REMOVED** | trees deleted from the codebase. They do not exist. Any doc claiming otherwise is a defect. | `src/nexus_scalp/api/v1/*` (the first 45-route v1, superseded envelope) — removed in consolidation commit `e51a910` |
**Hard rule**: an agent must never resurrect `src/nexus_scalp/api/v1`, never
mount a second v1 tree, and never add routes outside `web/api_v1/` for new
platform capabilities. Extension workflow: §14.
---
## 2. LEGACY / HISTORICAL INVENTORY (verified from source, not memory)
Current decorator scan of `web/*.py` (excluding `api_v1*`): **280 routes** across
10 route modules + `server.py` static/JS file routes. Families (count = decorators):
| module | count | family / path pattern | status | canonical v1 replacement |
|---|---|---|---|---|
| `debug_research_routes.py` | 50 | `/api/debug/*`, `/api/research/*`, `/api/experience/*`, `/api/account/*` | LEGACY (UI-owned) | `/api/v1/research/*`, `/api/v1/decisions/*`, `/api/v1/audit/*` |
| `model_governance_routes.py` | 45 | `/api/models/*` (train/worker/shadow/governance/promotion/emergency) | LEGACY — operator+UI only | `/api/v1/model/*` (read-only truth); POST governance actions stay legacy |
| `diagnostics_state_routes.py` | 43 | `/api/status`, `/health`, `/api/live/*`, `/api/account/*`, `/api/engine/*`, `/api/config`, `/api/settings/*`, `/api/db/*`, `/api/rules/*` | LEGACY — UI control plane | `/api/v1/system/*`, `/api/v1/config/*`, `/api/v1/database/*` |
| `news_liquidity_mslie_routes.py` | 23 | `/api/news/*`, `/api/liquidity/*`, `/api/mslie/*` | LEGACY | none in v1 (news/liquidity domain not yet versioned) |
| `intelligence_routes.py` | 10 | `/api/intelligence/*` | LEGACY | none in v1 |
| `server.py` (inline) | 9 | `/api/ticks/stream` (SSE), `/api/chart`, `/api/mt5`, `/api/positions`, `/api/status` helpers, `/ws` | LEGACY | `/api/v1/market/*`, `/api/v1/positions` (polling; SSE remains UI-owned) |
| `command_center_integration.py` | 8 | `/api/command-center/*` | LEGACY | none in v1 |
| `operator_routes.py` | 6 | operator console routes | LEGACY | none in v1 |
| `factory_routes.py` | ~18 | `/api/factory/*`, `/loop/*`, `/generations/*`, `/llm-config/*`, `/analyze/*` | LEGACY | none in v1 |
| `replay_routes.py` | 5 | `/api/replay/*` | LEGACY | none in v1 (replay sessions are CHG-0043-adjacent, not yet versioned) |
| `db_console.py` | 10 | `/api/db/console/*` (SQL console, API keys) | LEGACY — read-only by contract, UI-owned | `/api/v1/database/*` (metadata only; v1 NEVER executes user SQL) |
| `dependency_routes.py` | 9 | `/api/dependency/*` | LEGACY | none in v1 |
| `news_intelligence_routes.py` | 7 | `/pro/*`, `/apikey/*`, `/vendor/*` | LEGACY | none in v1 |
| `server.py` static files | ~10 | `/app.js`, `/api_client.js`, `*.css` | static assets (not APIs) | n/a |
**Migration rule**: v1 duplicates legacy **read** truths only where a stable
contract was worth publishing; legacy **control** endpoints (engine toggle,
model training, promotion, SQL console) are deliberately NOT in v1 — they stay
operator-console/CLI owned. Do not search for them under `/api/v1`.
---
## 3. CURRENT CANONICAL API MAP (per-domain operational entries)
Format per operation: Purpose / Returns / Dependencies / Errors / Agent use /
CLI / Tests / Smoke. All GETs unless marked POST. All paths below are verified
against the live OpenAPI (`scripts/dev/api_skill_drift_check.py` enforces this).
### 3.1 SYSTEM — module `web/api_v1/system.py`
#### GET /api/v1/system/status
- Purpose: one-shot operational snapshot (health verdict + version + mode + freshness + check counts).
- Returns: `data{health_verdict, critical_failures[], version{product,version,commit,channel}, runtime{engine_attached,engine_running,mode,freshness_overall}, checks_count}`.
- Dependencies: HealthEngine (3s TTL cache + prewarm thread); engine attrs if attached.
- Errors: 503 `DEPENDENCY_UNAVAILABLE` (health engine itself failed).
- Agent use: FIRST call to answer "what is the engine doing right now?" — but read §11 truth rules: `health_verdict` is a layered verdict, not a trading guarantee.
- CLI: `nexus api status` (human rendering).
- Tests: `tests/unit/test_api_v1_contracts.py`, `tests/integration/test_api_v1_platform.py`.
- Smoke: `python scripts/dev/api_smoke.py --embedded|--live`.
#### GET /api/v1/system/health
- Purpose: full HealthEngine check list (verbatim per-layer entries).
- Returns: `data{verdict, checks[{category,verdict,...}], critical_failures[]}`.
- Dependencies: HealthEngine (cached 3s).
- Errors: 503 `DEPENDENCY_UNAVAILABLE`.
- Agent use: WHEN a status layer failed and you need the per-layer detail.
- CLI: `nexus api health`.
#### GET /api/v1/system/readiness
- Purpose: can the system accept work? Required layers (SYSTEM/RUNTIME/CONFIGURATION/DATABASE/MODEL/FEATURE_SCHEMA) must have no FAIL.
- Returns: `data{ready: bool, verdict, required_layers[], optional_layers[]}`.
- Errors: 503 `DEPENDENCY_UNAVAILABLE`.
- Agent use: gate any automation on `data.ready` — never on HTTP 200 alone.
#### GET /api/v1/system/version
- Purpose: build/revision identity.
- Returns: full `get_version_info()` dict (product, version, commit, channel, python, platform, mode…).
- Dependencies: build metadata (memoized).
- Errors: 503 `DEPENDENCY_UNAVAILABLE`.
- CLI: `nexus api version`.
#### GET /api/v1/system/runtime
- Purpose: runtime environment + mode + warmup + inference flag + freshness contract.
- Returns: `data{engine_attached, engine_running, warmup_state, inference_enabled, freshness, mode, effective_mode}`.
- Errors: truthful nulls when engine absent (HTTP 200, `engine_attached:false`).
#### GET /api/v1/system/capabilities
- Purpose: platform discovery — domains + endpoint counts + pagination model + read_only flag; built from the REAL mounted route table.
- Returns: `data{api_version, spec, read_only, domains{}, domain_count, endpoint_count, pagination{...}, generated_at}`.
- Agent use: always the first discovery step on an unfamiliar deployment.
- CLI: `nexus api capabilities`.
#### GET /api/v1/system/workers
- Purpose: worker lifecycle (accounting/incident/hygiene/model-lifecycle workers).
- Returns: `data{workers[{name,state,attached}], engine_attached}`; honest `NOT_ATTACHED` when absent.
#### POST /api/v1/system/refresh
- Purpose: safe refresh of runtime-observable state (re-read account/symbol snapshots, bump versioner).
- Body: none. Headers: optional `Idempotency-Key` (echoed in `meta.idempotency_key`).
- Returns: `data{refreshed{account{available}}, at}`.
- Dependencies: **ENGINE + adapter** → 503 `ENGINE_UNAVAILABLE` when absent.
- Side effects: NONE that reach a broker (snapshot reads only).
#### POST /api/v1/system/diagnostics/run
- Purpose: run the bounded observability selftest; caches result on app.state for `/diagnostics`.
- Returns: selftest result dict. Idempotency-Key echoed.
#### GET /api/v1/system/diagnostics
- Purpose: latest diagnostics = adapter `diagnostics_summary()` (or null) + last selftest + version.
- Returns: `data{mt5, last_selftest, version}`; never 503 — truthful nulls when no adapter.
### 3.2 RUNTIME — module `web/api_v1/runtime.py`
#### GET /api/v1/runtime/mode
- Purpose: configured mode + effective `_runtime_mode` + replay flag.
- Returns: `data{mode, effective_mode, engine_attached, replaying}`; HTTP 200 with nulls when engine absent.
- CLI: `nexus api mode`.
#### GET /api/v1/runtime/freshness
- Purpose: the live-freshness contract (market/inference stages + overall) from `engine.compute_live_freshness()`.
- Returns: `data{freshness, available}`.
- Errors: 503 `ENGINE_UNAVAILABLE`.
#### POST /api/v1/runtime/mode/validate
- Purpose: pure transition validation of a PROPOSED mode — **never applies**.
- Body: `{"mode": "PAPER"}` (ModeProposal). Returns `data{valid, current_mode, proposed_mode, errors[], warnings[]}`; invalid → HTTP 422 with the same body (valid:false).
- Agent use: pre-flight any mode reasoning; `LIVE` proposals always carry a warning that v1 has no mutation path.
#### POST /api/v1/runtime/mode/preview
- Purpose: impact matrix of a proposed mode (which subsystems it would touch).
- Body: ModeProposal. Returns `data{validation, impact{touches[],matrix{...}}}`; 422 on invalid mode.
### 3.3 MARKET — module `web/api_v1/market.py` (ALL → 503 `ENGINE_UNAVAILABLE` without engine)
#### GET /api/v1/market/snapshot — `data{symbol, account, symbol_spec}` from real adapter snapshots (`?symbol=` override).
#### GET /api/v1/market/quote — `data` = broker tick (bid/ask/spread/freshness/stale) from `get_broker_tick`.
#### GET /api/v1/market/bars — `?limit≤500` completed M-bars from the engine aggregator (engine-local scope, stated in response).
#### GET /api/v1/market/regime — `data{regime, evidence}` from `_last_regime_state`; `None` before first inference (truthful, not 404).
#### GET /api/v1/market/symbols — configured symbol(s) + spec block.
### 3.4 SIGNALS — module `web/api_v1/signals.py` (audit_signals DB)
#### GET /api/v1/signals/latest — most recent audit_signals row (ORDER BY id DESC) + sanitized parsed payload; 404 truthful when table empty.
#### GET /api/v1/signals/history — paginated; filters `symbol`, `action` (upper-cased), `hours_back` (≤720); 422 pagination errors.
### 3.5 DECISIONS — module `web/api_v1/decisions.py` (audit_signals; decision_id = request_id)
#### GET /api/v1/decisions — paginated history; filters `symbol/action/execution_mode/decision_stage/hours_back`.
#### GET /api/v1/decisions/latest — full summary (`decision_id`, action, confidence, gates-adjacent fields, risk_checks).
#### GET /api/v1/decisions/stats — `?hours_back` window; `data{window_hours,total,by_action{},by_stage{}}`.
#### GET /api/v1/decisions/no-trade — NO_TRADE analytics: `data{total,reasons[],latest}`.
#### GET /api/v1/decisions/no-trade/reasons — `data{total, reasons{reason_code:count}}`.
#### GET /api/v1/decisions/{decision_id} — detail by request_id; 404 truthful.
#### GET /api/v1/decisions/{decision_id}/evidence — raw payload (sanitized of secret-shaped keys).
#### GET /api/v1/decisions/{decision_id}/gates — ordered gate trace: `decision_stage`, `blocked_by`, `reason_code`, `guardian_status`, `risk_allowed` each with `{value,passed:bool}`.
#### GET /api/v1/decisions/{decision_id}/explanation — human-readable sentence composed ONLY from real fields.
### 3.6 POSITIONS / EXECUTION / MODEL / FEATURES — module `web/api_v1/positions.py`
#### GET /api/v1/positions — real `adapter.get_all_positions()` snapshots (MT5 field contract); 503 `ENGINE_UNAVAILABLE` without engine.
#### GET /api/v1/positions/{ticket} — one open position; 404 truthful if ticket not open.
#### GET /api/v1/positions/history/{ticket} — ledger row + broker deals for a historical ticket (DB-backed; works without engine).
#### GET /api/v1/execution/status — `data{adapter_present, connection_state, connected, engine_running, mode}`; 503 without engine.
#### GET /api/v1/execution/history — paginated `audit_executions` (order events), filters `status,symbol`.
#### GET /api/v1/model/status — `data{bundle_loaded, inference_enabled, warmup_state, runtime_mode}`; 503 without engine.
#### GET /api/v1/model/identity — bundle manifest identity + `feature_schema_hash`; `{available:false}` truthful when no bundle.
#### GET /api/v1/model/contracts — `expected_schema_ids()` + `compatible_model_schemas()` matrix.
#### GET /api/v1/model/champion — champion identity from the SERVING bundle (authoritative for "what is actually serving"); not the governance registry.
#### GET /api/v1/features/status — warmup + last-vector availability + missing features from last proposal.
#### GET /api/v1/features/contract — canonical 70D SSoT: SCHEMA_ID, feature_count, schema hash, family groups, registry_canonical.
#### GET /api/v1/features/groups — family → names/counts.
#### GET /api/v1/features/current — last computed vector (dimension + named dict or honest absence).
### 3.7 RISK — module `web/api_v1/risk.py`
#### GET /api/v1/risk/status — last proposal `risk_checks` (sanitized) + risk config block; 503 without engine.
#### GET /api/v1/risk/summary — open exposure by symbol (volume/profit) + account margin metrics from the adapter; `exposure.available:false` truthful.
### 3.8 RESEARCH — module `web/api_v1/research.py` (research store/registry over audit DB)
#### GET /api/v1/research/status — health + registry summaries; 503 `DEPENDENCY_UNAVAILABLE` if stores unreadable.
#### GET /api/v1/research/strategies — paginated registry entries; `?lifecycle=`.
#### GET /api/v1/research/strategies/{strategy_id} — detail + invariant_check; 404 truthful.
#### GET /api/v1/research/runs — paginated run inventory; `?strategy_id=`.
#### GET /api/v1/research/datasets — distinct dataset_ids + run counts (real rows only).
### 3.9 SHADOW — module `web/api_v1/shadow.py` (ShadowStore + Shadow70Store)
#### GET /api/v1/shadow/status — both stores' `summary()`; per-block failure isolation (`null` block ≠ fake zero).
#### GET /api/v1/shadow/runs — paginated PHASE-11 run inventory.
#### GET /api/v1/shadow/runs/{run_id} — run + comparison + promotion; 404 truthful.
#### GET /api/v1/shadow/decisions — paginated decision records; `?run_id=`.
#### GET /api/v1/shadow/70d — 70D observer: summary + disagreement counts (valid-only) + drift alerts + feature health.
### 3.10 OBSERVABILITY / AUDIT / INCIDENTS / DATABASE / CONFIG — module `web/api_v1/incidents.py`
#### GET /api/v1/observability/events — paginated audit_events tail; `?event_type=`.
#### GET /api/v1/observability/metrics — real process counters (threads; POSIX rusage; `UNAVAILABLE` on Windows — never fabricated) + SSE diag + engine peak equity.
#### GET /api/v1/audit/events — paginated audit_ledger tail; `?status=`.
#### GET /api/v1/incidents — paginated incident inventory; filters `status/severity/category/component`.
#### GET /api/v1/incidents/stats — counts + by-component + recurring fingerprints.
#### GET /api/v1/incidents/{incident_id} — full Incident.as_dict(); 404 truthful.
#### GET /api/v1/incidents/{incident_id}/timeline — timeline events; 404 truthful.
#### GET /api/v1/database/status — O(1) metadata (filename, size, table_count); `integrity_endpoint` pointer. Never per-call PRAGMA.
#### GET /api/v1/database/integrity — `quick_check` + bounded row counts (explicit heavier route).
#### GET /api/v1/database/tables — table names only (no row dumps ever).
#### GET /api/v1/config — full engine config **sanitized** (secret-shaped keys → `***`); 503 without engine.
#### GET /api/v1/config/schema — AppConfig section/field inventory (pydantic-derived).
#### POST /api/v1/config/validate — validate a partial config dict against real pydantic models; **no apply** (422 with field errors on invalid; `applied:false` always).
---
## 4. MACHINE-READABLE REGISTRY (drift-protected)
Every current operation. Columns: METHOD | PATH | DOMAIN | MODULE | READ/WRITE | ENGINE | DB | primary errors | tests | CLI.
`E` = requires engine (503 otherwise) · `E-soft` = degrades truthfully without engine · `S` = static/config/schema-backed.
| METHOD | PATH | DOMAIN | MODULE | READ/WRITE | ENGINE | DB | ERRORS | TESTS | CLI |
|---|---|---|---|---|---|---|---|---|---|
| GET | /api/v1/system/status | system | system.py | READ_ONLY | E-soft | S | 503 DEP | contracts+integration | `nexus api status` |
| GET | /api/v1/system/health | system | system.py | READ_ONLY | E-soft | S | 503 DEP | contracts+integration | `nexus api health` |
| GET | /api/v1/system/readiness | system | system.py | READ_ONLY | E-soft | S | 503 DEP | contracts | — |
| GET | /api/v1/system/version | system | system.py | READ_ONLY | N | S | 503 DEP | contracts+integration | `nexus api version` |
| GET | /api/v1/system/runtime | system | system.py | READ_ONLY | E-soft | N | — | contracts | — |
| GET | /api/v1/system/capabilities | system | system.py | READ_ONLY | N | N | — | contracts+integration | `nexus api capabilities` |
| GET | /api/v1/system/workers | system | system.py | READ_ONLY | E-soft | N | — | contracts | — |
| POST | /api/v1/system/refresh | system | system.py | REFRESH | E (adapter) | N | 503 ENGINE | integration | — |
| POST | /api/v1/system/diagnostics/run | system | system.py | DIAGNOSTIC | N | N | 500 | contracts | `nexus api diagnostics --run` |
| GET | /api/v1/system/diagnostics | system | system.py | READ_ONLY | N (adapter null) | N | — | integration | `nexus api diagnostics` |
| GET | /api/v1/runtime/mode | runtime | runtime.py | READ_ONLY | E-soft | N | — | contracts+integration | `nexus api mode` |
| GET | /api/v1/runtime/freshness | runtime | runtime.py | READ_ONLY | E | N | 503 ENGINE | contracts | — |
| POST | /api/v1/runtime/mode/validate | runtime | runtime.py | SAFE_VALIDATION | E-soft | N | 422 | contracts | — |
| POST | /api/v1/runtime/mode/preview | runtime | runtime.py | SAFE_VALIDATION | E-soft | N | 422 | contracts | — |
| GET | /api/v1/market/snapshot | market | market.py | READ_ONLY | E | N | 503 ENGINE | integration | — |
| GET | /api/v1/market/quote | market | market.py | READ_ONLY | E | N | 503 ENGINE | contracts+integration | — |
| GET | /api/v1/market/bars | market | market.py | READ_ONLY | E | N | 503 ENGINE | contracts | — |
| GET | /api/v1/market/regime | market | market.py | READ_ONLY | E | N | 503 ENGINE | contracts | — |
| GET | /api/v1/market/symbols | market | market.py | READ_ONLY | E | N | 503 ENGINE | integration | — |
| GET | /api/v1/signals/latest | signals | signals.py | READ_ONLY | N | Y | 404 | contracts+integration | `nexus api signals` |
| GET | /api/v1/signals/history | signals | signals.py | READ_ONLY | N | Y | 422 | contracts+integration | — |
| GET | /api/v1/decisions | decisions | decisions.py | READ_ONLY | N | Y | 422 | contracts+integration | `nexus api decisions` |
| GET | /api/v1/decisions/latest | decisions | decisions.py | READ_ONLY | N | Y | 404 | contracts+integration | `nexus api decisions --latest` |
| GET | /api/v1/decisions/stats | decisions | decisions.py | READ_ONLY | N | Y | — | integration | — |
| GET | /api/v1/decisions/no-trade | decisions | decisions.py | READ_ONLY | N | Y | — | integration | — |
| GET | /api/v1/decisions/no-trade/reasons | decisions | decisions.py | READ_ONLY | N | Y | — | integration | — |
| GET | /api/v1/decisions/{decision_id} | decisions | decisions.py | READ_ONLY | N | Y | 404 | contracts+integration | `nexus api decisions --id X` |
| GET | /api/v1/decisions/{decision_id}/evidence | decisions | decisions.py | READ_ONLY | N | Y | 404 | integration | — |
| GET | /api/v1/decisions/{decision_id}/gates | decisions | decisions.py | READ_ONLY | N | Y | 404 | contracts+integration | `nexus api decisions --id X --gates` |
| GET | /api/v1/decisions/{decision_id}/explanation | decisions | decisions.py | READ_ONLY | N | Y | 404 | contracts | — |
| GET | /api/v1/positions | positions | positions.py | READ_ONLY | E | N | 503 ENGINE, 503 DEP | contracts+integration | — |
| GET | /api/v1/positions/{ticket} | positions | positions.py | READ_ONLY | E | N | 404, 503 ENGINE | integration | — |
| GET | /api/v1/positions/history/{ticket} | positions | positions.py | READ_ONLY | N | Y | 404 | integration | — |
| GET | /api/v1/execution/status | execution | positions.py | READ_ONLY | E | N | 503 ENGINE | contracts+integration | — |
| GET | /api/v1/execution/history | execution | positions.py | READ_ONLY | N | Y | 422 | contracts+integration | — |
| GET | /api/v1/model/status | model | positions.py | READ_ONLY | E | N | 503 ENGINE | contracts+integration | — |
| GET | /api/v1/model/identity | model | positions.py | READ_ONLY | E | N | 503 ENGINE | contracts+integration | `nexus api model` |
| GET | /api/v1/model/contracts | model | positions.py | READ_ONLY | E-soft | N | — | contracts | — |
| GET | /api/v1/model/champion | model | positions.py | READ_ONLY | E-soft | N | — | contracts | — |
| GET | /api/v1/features/status | features | positions.py | READ_ONLY | E | N | 503 ENGINE | contracts | — |
| GET | /api/v1/features/contract | features | positions.py | READ_ONLY | N | S | 503 DEP | contracts+integration | `nexus api features` |
| GET | /api/v1/features/groups | features | positions.py | READ_ONLY | N | S | 503 DEP | contracts | — |
| GET | /api/v1/features/current | features | positions.py | READ_ONLY | E | N | 503 ENGINE | contracts | — |
| GET | /api/v1/risk/status | risk | risk.py | READ_ONLY | E | N | 503 ENGINE | contracts+integration | — |
| GET | /api/v1/risk/summary | risk | risk.py | READ_ONLY | E | N | 503 ENGINE | contracts | — |
| GET | /api/v1/research/status | research | research.py | READ_ONLY | N | Y | 503 DEP | contracts+integration | — |
| GET | /api/v1/research/strategies | research | research.py | READ_ONLY | N | Y | 422, 503 DEP | contracts | — |
| GET | /api/v1/research/strategies/{strategy_id} | research | research.py | READ_ONLY | N | Y | 404, 503 DEP | contracts | — |
| GET | /api/v1/research/runs | research | research.py | READ_ONLY | N | Y | 422, 503 DEP | contracts+integration | — |
| GET | /api/v1/research/datasets | research | research.py | READ_ONLY | N | Y | 503 DEP | contracts | — |
| GET | /api/v1/shadow/status | shadow | shadow.py | READ_ONLY | N | Y | — | contracts+integration | — |
| GET | /api/v1/shadow/runs | shadow | shadow.py | READ_ONLY | N | Y | 422 | contracts+integration | — |
| GET | /api/v1/shadow/runs/{run_id} | shadow | shadow.py | READ_ONLY | N | Y | 404 | contracts | — |
| GET | /api/v1/shadow/decisions | shadow | shadow.py | READ_ONLY | N | Y | 422 | contracts | — |
| GET | /api/v1/shadow/70d | shadow | shadow.py | READ_ONLY | N | Y | — | contracts | — |
| GET | /api/v1/observability/events | observability | incidents.py | READ_ONLY | N | Y | 422 | contracts | — |
| GET | /api/v1/observability/metrics | observability | incidents.py | READ_ONLY | E-soft | N | — | contracts+integration | — |
| GET | /api/v1/audit/events | audit | incidents.py | READ_ONLY | N | Y | 422 | contracts+integration | — |
| GET | /api/v1/incidents | incidents | incidents.py | READ_ONLY | N | Y | 422 | contracts+integration | — |
| GET | /api/v1/incidents/stats | incidents | incidents.py | READ_ONLY | N | Y | — | contracts | — |
| GET | /api/v1/incidents/{incident_id} | incidents | incidents.py | READ_ONLY | N | Y | 404 | contracts+integration | — |
| GET | /api/v1/incidents/{incident_id}/timeline | incidents | incidents.py | READ_ONLY | N | Y | 404 | contracts | — |
| GET | /api/v1/database/status | database | incidents.py | READ_ONLY | N | Y | — | contracts+integration | — |
| GET | /api/v1/database/integrity | database | incidents.py | READ_ONLY | N | Y | 503 DEP | contracts | — |
| GET | /api/v1/database/tables | database | incidents.py | READ_ONLY | N | Y | 503 DEP | contracts | — |
| GET | /api/v1/config | config | incidents.py | READ_ONLY | E | N | 503 ENGINE | contracts | — |
| GET | /api/v1/config/schema | config | incidents.py | READ_ONLY | N | S | — | contracts+integration | — |
| POST | /api/v1/config/validate | config | incidents.py | SAFE_VALIDATION | N | S | 422 | contracts+integration | — |
**68 documented operations** (63 GET + 5 POST). Drift-protected by
`scripts/dev/api_skill_drift_check.py`.
---
## 5. ENVELOPE CONTRACT (§3 of spec of record)
Success — `data` present:
```json
{"data": <payload>, "meta": {"request_id": "req_ab12cd34", "generated_at": "2026-09-02T10:00:00+00:00"}}
```
Error — `error` present, `data` absent:
```json
{"error": {"code": "RESOURCE_NOT_FOUND", "message": "...", "details": {}, "request_id": "req_...", "retryable": false}}
```
Rules:
- Exactly one of `data`/`error` at top level. Never both, never neither (2xx always has `data`).
- `meta.request_id`: echoes the client `X-Request-ID` when supplied (truncated 64 chars), else server-generated `req_<10hex>`.
- `meta.generated_at`: server wall clock at response build, UTC ISO-8601 (`+00:00`). It is a RESPONSE time, not an event time — never use it to order domain events (use the domain's own timestamps, e.g. `generated_at` on audit rows).
- Serialization: enums → `.value` strings; datetimes → UTC-aware ISO-8601 (naive datetimes are stamped UTC); Pydantic models → `model_dump()`. Handled centrally in `common.jsonable()`/`ok()` — routes never hand-roll it.
- `null` semantics: `None` means **unknown/unavailable/not-yet-computed** (e.g. `regime:null` before first inference, `mt5:null` without adapter). It NEVER means zero/empty/healthy.
- Unavailable subsystem blocks inside a 200 body (e.g. shadow store blocks) carry their own `null`/`available:false` and are failure-isolated — a `null` block is not a platform error.
## 6. ERROR CONTRACT (all real codes — none invented)
| code | HTTP | retryable | server condition | client/agent action |
|---|---|---|---|---|
| VALIDATION_ERROR | 422 | no | bad query/pagination/body (incl. FastAPI request validation) | fix the request; `error.details.errors[]` names fields |
| RESOURCE_NOT_FOUND | 404 | no | id unknown OR dataset legitimately empty (e.g. no incidents yet) | do NOT retry blindly; distinguish empty-vs-missing via list endpoints |
| METHOD_NOT_ALLOWED | 405 | no | wrong verb on v1 path | use the documented method |
| PAYLOAD_TOO_LARGE | 413 | no | oversized body | shrink request |
| CONFLICT | 409 | no | state conflict (reserved) | re-read state first |
| ENGINE_UNAVAILABLE | 503 | **yes** | API server running without attached engine/adapter | NOT an empty dataset, NOT zero positions, NOT a healthy engine — a missing dependency; retry after engine start or use DB-backed routes |
| DEPENDENCY_UNAVAILABLE | 503 | **yes** | backing store/registry/health-engine read failed | retry with backoff; check logs via `request_id` |
| RESOURCE_UNAVAILABLE | 503 | yes | transient resource (reserved, same family) | retry |
| TIMEOUT | 504 | yes | upstream timeout (reserved) | retry bounded |
| INTERNAL_ERROR | 500 | no | unhandled exception (full detail logged under `request_id`) | report bug with `request_id`; never assume data validity |
Handlers: `web/api_v1/errors.py` (path-guarded — legacy routes keep legacy bodies).
## 7. ENGINE-DEPENDENT BEHAVIOR MATRIX
Dependency classes (verified per route, §4 registry):
| class | behavior when ENGINE absent (server up, engine=None) | routes |
|---|---|---|
| **E** (engine/adapter hard) | **503 `ENGINE_UNAVAILABLE`** | market/* (5), positions (2 GET on adapter), risk/* (2), execution/status, model/status, model/identity, features/status, features/current, runtime/freshness, system/refresh |
| **E-soft** (engine fields degrade truthfully) | HTTP 200 with `engine_attached:false` / nulls | system/status·health·readiness·runtime·workers, runtime/mode, model/contracts, model/champion, observability/metrics |
| **DB** (audit DB-backed) | fully functional (real SQLite reads); empty tables → truthful 404/empty pages | signals/*, decisions/*, execution/history, positions/history/{ticket}, research/*, shadow/*, incidents/*, observability/events, audit/events, database/* |
| **STATIC** (schema/config/build) | fully functional offline | features/contract·groups, config/schema, system/version |
Interpretation rules for agents:
- `503 ENGINE_UNAVAILABLE` ≠ empty dataset ≠ zero positions ≠ healthy engine ≠ successful operation. It means: **the dependency was absent at request time**.
- Engine states: READY (all E routes 200), STOPPED (server may still be up; E routes may attach but return stopped-state truthfully), NOT_INITIALIZED/UNAVAILABLE (E routes → 503), DEGRADED (see `/system/health` layers).
- Environment matrix (verified):
| environment | behavior |
|---|---|
| EMBEDDED (`create_v1_app()` + TestClient) | no engine → E routes 503; DB routes need a repo on `app.state.audit_v1_repo` or default-path artifacts DB |
| LIVE uvicorn (dashboard `create_app`) | full surface; engine-dependent routes reflect the attached engine |
| PAPER/REPLAY/SHADOW/LIVE engine modes | no route-class change — modes affect engine payload content, not API availability |
| NO ENGINE at all | v1 standalone app still serves DB/STATIC routes (CLI + tests rely on this) |
## 8. READ/WRITE & SIDE-EFFECT CLASSIFICATION
| class | count | operations | broker reach |
|---|---|---|---|
| READ_ONLY | 63 | all GETs in §4 | none |
| SAFE_VALIDATION | 3 | `runtime/mode/validate`, `runtime/mode/preview`, `config/validate` — **pure functions, zero state change** | none |
| REFRESH | 1 | `system/refresh` — re-reads adapter snapshots, bumps UI versioner | none (snapshot reads only) |
| DIAGNOSTIC | 1 | `system/diagnostics/run` — bounded offline selftest, caches result | none |
| STATE_AFFECTING | 0 | — | none |
| EXECUTION_CAPABLE | 0 | **v1 can never submit/modify/close an order** | none |
**ABSOLUTE RULE: NO REAL TRADING FROM AGENT API TESTS.** The v1 platform has no
order path at all; still, tests must use TestClient / embedded apps / PAPER /
REPLAY / MOCK fixtures only. Live-mode selection, engine toggling, model
training/promotion and SQL consoles remain legacy-console/CLI owned and are
out of scope for agent API testing.
## 9. PAGINATION CONTRACT
- Params: `page` (default 1, ≥1), `page_size` (default 50, 1..200). Violations → 422 `VALIDATION_ERROR`.
- Shape: `data{items[], page, page_size, has_more}`. `has_more` is the ONLY continuation signal; **no total counts** (full-scan protection).
- Ordering: stable (`id DESC` or documented store order); repeated identical queries return identical `data` (meta timestamp varies).
- Invariants proven by tests: no duplicates across pages, no skipped rows, page k = exact slice `rows[(k-1)*s : k*s]` of the unpaginated order.
- Filters compose with pagination; indexed columns only (see §10).
Example:
```
GET /api/v1/decisions?page=2&page_size=5
→ {"data": {"items": [...5...], "page": 2, "page_size": 5, "has_more": true}, "meta": {...}}
```
## 10. FILTER CONTRACT (verified bounds)
| filter | routes | type | semantics | bound |
|---|---|---|---|---|
| symbol | signals/history, decisions, execution/history, market/quote·snapshot, positions | str ≤32 | exact match | — |
| action | signals/history, decisions | str | upper-cased exact (BUY/SELL/NO_TRADE) | — |
| status | execution/history, audit/events, incidents | str ≤24 | exact match | — |
| severity/category/component | incidents | str ≤24/32/48 | exact match | — |
| execution_mode, decision_stage | decisions | str | exact match | — |
| event_type | observability/events | str ≤48 | exact match | — |
| lifecycle | research/strategies | str ≤32 | exact match | — |
| strategy_id | research/runs | str ≤64 | exact match | — |
| run_id | shadow/decisions | str ≤64 | exact match | — |
| hours_back | signals/history, decisions | int | generated_at lower bound (UTC) | 1..720 (decisions enforce via Query ge/le; signals caps server-side) |
| limit | market/bars | int | bar count | ≤500 |
All filters are parameterized SQL/store parameters — never string-interpolated. Unbounded queries are impossible (server-side LIMIT always injected).
## 11. TIME CONTRACT
- Transport: ISO-8601, timezone-aware, **UTC** (`+00:00`). Naive datetimes are stamped UTC by `common.jsonable()` before serialization.
- `meta.generated_at` = response build time — NOT event time.
- Domain event times (e.g. `generated_at` on decisions, `timestamp` on ledger rows) are the ONLY ordering truth for domain events.
- `freshness` endpoints carry engine-model stage ages — the authoritative "is data fresh" signal.
- **WARNING**: never mix local time, UTC, and broker/server time. MT5 epochs are SERVER-LOCAL (GMT+3, BUG-070) — raw epoch fields from broker snapshots are not UTC. Convert only with the broker-aware providers; do not epoch-math in agents.
## 12. REQUEST ID / CORRELATION / IDEMPOTENCY
- `X-Request-ID` (any HTTP request): accepted on ALL v1 routes; echoed as `meta.request_id` + `X-Request-ID` response header + server log correlation. Max 64 chars.
- `Idempotency-Key` (POSTs): echoed as `meta.idempotency_key` (≤128 chars). v1 POSTs are naturally idempotent (validate/refresh/diagnostics) — no server-side dedup store; the echo is for caller-side correlation.
- Retry guidance: retryable codes (503/504) may be retried with backoff using the SAME request id for traceability; non-retryable (4xx/500) must not be retried unchanged.
- All failures are logged internally with the same `request_id` — quote it in bug reports.
## 13. VERSIONING + OPENAPI
- Single mount: `/api/v1` from `web/api_v1_wiring.py`. Incompatible changes → `/api/v2` (never break v1 in place).
- OpenAPI: `GET /openapi.json` on the v1 app (or dashboard app) — 100% operation coverage enforced (68/68).
- Regenerate snapshot: `.venv/Scripts/python.exe scripts/dev/api_openapi_snapshot.py` → `artifacts/api/openapi_snapshot.json` (sorted, timestamp-free, deterministic).
- Detect drift: `.venv/Scripts/python.exe scripts/dev/api_openapi_diff.py OLD.json NEW.json --fail-on-breaking` — BREAKING = removed endpoint/method or request-schema change → **exit 2**; additions → note, exit 0.
- APISkill drift: `.venv/Scripts/python.exe scripts/dev/api_skill_drift_check.py` — this file ↔ live OpenAPI (missing/extra ops, count mismatch) → exit 1 on drift.
- Breaking changes REQUIRE explicit review (change-control entry + taskboard row) before merge.
## 14. EXTENSION WORKFLOW (adding an API — the ONLY sanctioned path)
1. Define the contract (purpose, request, response, errors) in `docs/api/API_PLATFORM_V1.md` §7 inventory.
2. Implement the route in the matching `web/api_v1/<domain>.py` (reuse `common.ok/fail/parse_pagination/build_page/fetch_rows_bounded`; no god router, no inline SQL from user input).
3. Wire nothing new if the domain router already exists (single mount covers it); new domains require a new module + wiring import in `_include_routers`.
4. Add contract tests (`tests/unit/test_api_v1_contracts.py`) + integration tests (`tests/integration/test_api_v1_platform.py`) incl. engineless 503 or DB fixture paths.
5. `ruff check --fix && ruff format && mypy` on touched files.
6. Regenerate snapshot; run `api_openapi_diff.py OLD NEW --fail-on-breaking`; run `api_contract_check.py` (expect N+1 ops documented).
7. Update `docs/api/API_REFERENCE.md` + §3/§4 of THIS file (drift check must PASS).
8. `api_smoke.py --embedded` PASS; benchmark if performance-sensitive.
9. Commit (`<AGENT>:<task>` convention), push, verify `origin/main == HEAD`.
Removal workflow: deprecate first (mark in docs + snapshot note), name the replacement, keep serving through the compatibility window, then remove code + docs + snapshot entries + add a regression test asserting the removal — never leave dead documentation claiming the route exists.
## 15. TOOLING REFERENCE (scripts/dev/)
| tool | purpose | input | output | exit codes |
|---|---|---|---|---|
| `api_smoke.py` | per-domain reachability + truthfulness | `--embedded` (no server) or `--live BASE_URL` | per-domain ✓/○/✗ + `API_SMOKE = PASS\|FAIL` | 0/1 |
| `api_contract_check.py` | OpenAPI quality gate: route↔spec parity, summaries/tags, schema refs, secret-shape scan, min-capability floor | none (embedded app) | `API CONTRACT CHECK = PASS (68 operations…)` | 0/1 |
| `api_openapi_snapshot.py` | deterministic artifact for drift detection | `--out PATH` | sorted JSON snapshot | 0 |
| `api_openapi_diff.py` | snapshot-vs-snapshot breaking-change classifier | OLD.json NEW.json [--fail-on-breaking] | BREAKING/note lines + verdict | 0 compatible, 2 breaking, 1 IO |
| `api_benchmark.py` | bounded local perf (median/p95/p99, ≤300 req/route, read-only) | `--requests N` | per-route table + `API_BENCHMARK = PASS\|PASS_WITH_NOTES` (p95 budget 250ms) | 0 |
| `api_skill_drift_check.py` | THIS file ↔ live OpenAPI sync | none | `APISKILL DRIFT = PASS\|FAIL` | 0/1 |
## 16. CLI → API MAP (verified from `cli/api_commands.py`)
| command | endpoint(s) |
|---|---|
| `nexus api status` | `/api/v1/system/status` (human rendering) |
| `nexus api health` | `/api/v1/system/health` |
| `nexus api version` | `/api/v1/system/version` |
| `nexus api capabilities` | `/api/v1/system/capabilities` |
| `nexus api mode` | `/api/v1/runtime/mode` |
| `nexus api signals` | `/api/v1/signals/latest` |
| `nexus api decisions [--latest \| --id X [--gates]]` | `/api/v1/decisions/latest`, `/api/v1/decisions`, `/api/v1/decisions/{id}`, `/decisions/{id}/gates` |
| `nexus api model` | `/api/v1/model/identity` |
| `nexus api features` | `/api/v1/features/contract` |
| `nexus api diagnostics [--run]` | `POST /api/v1/system/diagnostics/run`, `GET /api/v1/system/diagnostics` |
| `nexus api get PATH [--page --page-size]` | any `/api/v1/*` path |
| `nexus api smoke` | all domains via the Python client → `API_SMOKE = PASS` |
All CLI commands consume `NexusApiClient` over real HTTP (`--base-url` / `NEXUS_API_BASE`, default `http://127.0.0.1:8080`) — the SAME contracts as external clients; zero duplicated logic.
## 17. NEXUSAPICLIENT CONTRACT
```python
from nexus_scalp.api_client import NexusApiClient, NexusApiError, DEFAULT_BASE_URL
client = NexusApiClient("http://127.0.0.1:8080", timeout=10.0, headers={})
client.get(
path, params=...
) # → envelope dict {"data","meta"} (+ "pagination" keys inside data on lists)
client.post(path, json_body=..., idempotency_key=...)
# typed shortcuts: system_status/health/version, capabilities, runtime_mode,
# market_quote, signals_latest/history, decisions_latest/decisions/
# decision_detail/decision_gates, positions, risk_status, execution_status,
# model_status/identity, features_contract, research_status, shadow_status,
# incidents, database_status, observability_metrics, run_diagnostics
```
- Path normalization: leading `/api/v1` auto-prepended (`client.get("incidents")` works).
- **Errors raise** `NexusApiError(code, message, request_id, retryable, status, details)` — parsed from the v1 error envelope; transport failures raise the same type with `code=DEPENDENCY_UNAVAILABLE, retryable=True`.
- No automatic retries inside the client (agents decide policy per `retryable`).
- No pagination helpers beyond passing params — the envelope IS the contract.
## 18. AGENT CALLING PATTERNS (verified commands)
```bash
# discovery
nexus api capabilities
curl -s http://127.0.0.1:8080/api/v1/system/capabilities
# operational truth
nexus api status
curl -s -H "X-Request-ID: req_agent_001" http://127.0.0.1:8080/api/v1/system/status
# decision forensics
nexus api decisions --latest
curl -s http://127.0.0.1:8080/api/v1/decisions/req_ab12cd34/gates
# paginated + filtered
curl -s "http://127.0.0.1:8080/api/v1/decisions?page=2&page_size=50&symbol=XAUUSD&action=NO_TRADE"
# validation POST (no state change)
curl -s -X POST http://127.0.0.1:8080/api/v1/runtime/mode/validate \
-H "Content-Type: application/json" -d '{"mode": "PAPER"}'
# test batteries (NO live trading — embedded/TestClient/PAPER only)
python scripts/dev/api_smoke.py --embedded
python scripts/dev/api_smoke.py --live http://127.0.0.1:8080
python scripts/dev/api_contract_check.py
python scripts/dev/api_skill_drift_check.py
.venv/Scripts/python.exe -m pytest tests/unit/test_api_v1_contracts.py tests/integration/test_api_v1_platform.py -q
```
## 19. TEST MAP (real files — do not invent others)
| domain/aspect | tests |
|---|---|
| envelope/pagination/sanitize/errors/capabilities/versioning | `tests/unit/test_api_v1_contracts.py` |
| full request paths + fixtures + fuzz + security + additive wiring | `tests/integration/test_api_v1_platform.py` |
| cross-surface state truth (CLI/API/runtime parity) | `tests/unit/test_state_truth_parity.py` |
| client E2E golden journeys (live HTTP against the API) | `tests/e2e_client/` (`e2e_harness.py`, `j1_golden.py`, `j2_mode_switch.py`, `j3_signal_decisions.py`, `j4_resilience.py`) |
| perf | `scripts/dev/api_benchmark.py` (manual, bounded) |
## 20. SECURITY RULES FOR AGENTS
MUST NOT:
- expose secrets/tokens/passwords/broker credentials (sanitizer masks `token|password|secret|apikey|api_key|credential|login|account_number`-shaped keys → `***`; report any leak as a BUG)
- expect or extract stack traces/filesystem paths/SQL from error bodies (they are contractually absent; internal detail lives in logs only)
- attempt SQL/shell/command injection via params or IDs — hostile-input handling is tested (bounded fuzz: 4xx envelope, never 500); do not try to bypass it
- execute arbitrary SQL anywhere (v1 has no SQL endpoint; the legacy db-console SQL route is UI-owned and out of scope for agents)
- call anything that could reach a broker in tests (§8: v1 has zero execution path; keep it that way — TestClient/PAPER/REPLAY only)
- hammer endpoints: bounded requests only (pagination caps exist for a reason; benchmark tool is the sanctioned load path)
## 21. PERFORMANCE CONTRACT
Protections (verified): health 3s TTL cache + daemon prewarm at mount; version memoization; O(1) `/database/status` (integrity is explicit); pagination caps; server-side LIMITs; `hours_back` windows.
- Run `api_benchmark.py` after adding routes or touching stores. Regression = p95 > 250ms on a previously-green route, or any route newly over budget.
- Do not hard-code old numbers in agent logic; measure current.
## 22. DOMAIN GUIDANCE (avoid asking the wrong endpoint)
| domain | USE WHEN | does NOT answer | dependencies | common misuse |
|---|---|---|---|---|
| decisions | why was X traded/rejected; NO_TRADE analysis; evidence replay | live market state, model weights | audit DB | treating `decision_id` as broker ticket (that's positions/{ticket}) |
| model | which artifact is SERVING (`/model/identity`), warmup/inference state | training/provenance lineage (use `/research/runs`, legacy `/api/models/*`) | engine bundle | conflating champion-registry with serving-bundle truth — `/model/champion` is bundle-authoritative |
| features | active 70D contract (SSoT), last vector | historical vectors (not stored per-tick for API) | schema contract / engine | expecting per-feature history |
| risk | last proposal risk checks, current exposure | policy edits (none exist in v1) | engine + adapter | reading `risk_checks` as a guarantee of future acceptance |
| runtime | mode/freshness truth | mode CHANGES (validate-only; apply is console/CLI) | engine | applying a mode via API (impossible by design) |
| shadow | 70D observer health/disagreements/drift | promotion decisions (governance legacy) | audit DB | interpreting observer counters as champion impact |
| research | runs/strategies/datasets lineage | live training control | audit DB | using `/research/runs` as model registry |
| incidents | incident inventory/timelines/stats | real-time alerting (that's Telegram notifier) | audit DB | polling faster than bounded pages allow |
## 23. ONE QUESTION → ONE API (routing table)
| question | endpoint |
|---|---|
| What is the engine currently doing? | `/api/v1/system/status` |
| Can the system accept work? | `/api/v1/system/readiness` (`data.ready`) |
| Which layers failed and why? | `/api/v1/system/health` (checks) |
| What model is actually serving? | `/api/v1/model/identity` |
| What feature contract is active? | `/api/v1/features/contract` |
| Why was a decision rejected? | `/api/v1/decisions/{id}/gates` (+ `/evidence`) |
| What NO_TRADE reasons occurred? | `/api/v1/decisions/no-trade` (+ `/no-trade/reasons`) |
| Is market data fresh? | `/api/v1/runtime/freshness` |
| What capabilities exist? | `/api/v1/system/capabilities` |
| What incidents exist? | `/api/v1/incidents` (+ `/stats`) |
| Current exposure/margin? | `/api/v1/risk/summary` |
| What changed in the audit ledger? | `/api/v1/audit/events` |
| Is the broker connected? | `/api/v1/execution/status` |
| Database healthy? | `/api/v1/database/status` (+ `/integrity`) |
| What config is effective (sanitized)? | `/api/v1/config` |
| Would a mode change be valid? | `POST /api/v1/runtime/mode/validate` (never apply) |
## 24. TRUTH / CONSISTENCY RULES
- Never infer READY/HEALTHY/LIVE/TRADING/MODEL_VALID/DATA_FRESH from a single field unless the contract states it. Compose: `readiness.ready` + `health.checks` + `runtime/freshness`.
- Authoritative endpoints per fact: serving model → `/model/identity` (bundle), NOT governance pages; mode → `/runtime/mode`; data freshness → `/runtime/freshness`; decision truth → audit-backed `/decisions/*`.
- Compare snapshots via `meta.request_id`/`generated_at`; different endpoints sampled at different times legitimately differ — do not manufacture contradictions from timestamp skew.
- 503 ≠ empty. null ≠ zero. `available:false` ≠ error.
## 25. DISCOVERY WORKFLOW (recommended for a fresh agent)
1. `nexus api capabilities` (or read §4 registry here)
2. `nexus api status` (operational posture)
3. consult §23 routing table → call the specific endpoint
4. verify assumptions with `api_smoke.py --embedded` / `--live`
5. extend? → §14 workflow; check drift after docs edits: `api_skill_drift_check.py`
## 26. HISTORICAL RECONCILIATION (why the old tree is gone)
`legacy 257→280 dashboard routes (UI-owned, still served)`
→ `first /api/v1 tree (src/nexus_scalp/api/v1, 45 routes, superseded envelope — committed mid-flight by a parallel session)`
→ `spec-of-record authored (docs/api/API_PLATFORM_V1.md, 65 capabilities)`
→ **consolidation decision**: the WIP `web/api_v1` tree matched the spec's envelope/pagination/error contracts, so it became canonical; the 45-route tree was DELETED (`e51a910`) and its salvable patterns (store queries, exception handlers) were ported
→ current **68 operations**, drift-protected.
**Never resurrect `src/nexus_scalp/api/v1`.** Its paths are NOT current. Any
agent finding references to it (old docs, old sessions) must treat them as
historical context only.
## 27. STALENESS / DRIFT DETECTION (contract)
Sync axes: implementation ↔ OpenAPI ↔ API reference ↔ APISkill ↔ tests.
Any mismatch = documentation/contract defect (file it, don't paper over it).
```bash
python scripts/dev/api_contract_check.py # implementation ↔ OpenAPI
python scripts/dev/api_openapi_snapshot.py # regenerate artifact
python scripts/dev/api_openapi_diff.py OLD NEW --fail-on-breaking # snapshot ↔ snapshot
python scripts/dev/api_skill_drift_check.py # THIS FILE ↔ OpenAPI
```
Update order when changing APIs: code → tests → snapshot → docs → THIS file → re-run all four checks.
## 28. SKILL SELF-AUDIT (Phase 33 — answered)
1. discover every current API? ✅ §3 + §4 registry (drift-enforced 68/68)
2. current vs legacy? ✅ §1/§2 status classes + §26 reconciliation
3. right endpoint for common questions? ✅ §23
4. engine-dependency per endpoint? ✅ §4 E/E-soft/N columns + §7 matrix
5. error semantics? ✅ §6 (all real codes)
6. construct a valid request? ✅ §5/§9/§10/§18
7. correct smoke/contract test? ✅ §15/§19
8. safe testing (no live trading)? ✅ §8/§20
9. identify breaking change? ✅ §13 (diff tool + exit codes)
10. extend without a second API tree? ✅ §14 (single-mount, single-tree rule)