Skip to main content

axon/
axon_server.rs

1//! AxonServer — native reactive daemon platform.
2//!
3//! HTTP server for the AXON runtime, implementing the `/v1/` API surface:
4//!   - `/v1/health`       — full health report with component checks
5//!   - `/v1/health/live`  — liveness probe
6//!   - `/v1/health/ready` — readiness probe
7//!   - `/v1/health/components` — component-level health (trace_store, event_bus, supervisor, schedules, audit_log, rate_limiter)
8//!   - `/v1/health/gates` — configurable readiness gates (GET/PUT)
9//!   - `/v1/health/history` — health transition history (GET ?limit=N&component=...)
10//!   - `/v1/health/check-and-record` — evaluate health and record transitions (POST)
11//!   - `/v1/alerts/rules` — operational alert rules CRUD (GET/POST/DELETE)
12//!   - `/v1/alerts/evaluate` — evaluate rules against current metrics (POST)
13//!   - `/v1/alerts/history` — fired alert history (GET ?limit=N)
14//!   - `/v1/version`      — AXON version info
15//!   - `/v1/uptime`       — detailed server uptime with hourly buckets
16//!   - `/v1/dashboard`    — comprehensive server status overview (all subsystems)
17//!   - `/v1/primitives`   — cognitive primitive inventory (47 primitives, wired/pending status, ΛD alignment)
18//!   - `/v1/docs`         — API documentation with route listing and categories
19//!   - `/v1/metrics`  — execution metrics (deployments, latency, errors)
20//!   - `/v1/metrics/export` — export metrics snapshot to disk as Prometheus/JSON (POST)
21//!   - `/v1/deploy`   — compile and deploy .axon source
22//!   - `/v1/deploy/reload` — hot-reload all flows by re-reading source files (POST)
23//!   - `/v1/execute`  — execute a deployed flow (auto-records trace)
24//!   - `/v1/execute/enqueue` — enqueue flow execution with priority (POST)
25//!   - `/v1/execute/queue` — view execution queue (GET)
26//!   - `/v1/execute/dequeue` — take next item from queue (POST)
27//!   - `/v1/execute/drain` — process all pending queue items sequentially (POST)
28//!   - `/v1/execute/sandbox` — isolated execution with resource limits (POST)
29//!   - `/v1/execute/process` — dequeue+execute+trace in one atomic operation (POST)
30//!   - `/v1/execute/dry-run` — compile and validate without executing (POST)
31//!   - `/v1/execute/pipeline` — multi-flow sequential orchestration (POST)
32//!   - `/v1/execute/stream` — algebraic effect streaming execution (POST) — bridges flow.stream.{trace_id} to SSE
33//!   - `/v1/execute/cache` — execution result cache with TTL and ΛD epistemic state (GET/PUT/DELETE)
34//!   - `/v1/execute/cached` — cache-aware execution: check cache → execute if miss → auto-cache (POST)
35//!   - `/v1/execute/stream/:trace_id/consume` — stream consumer with cursor pagination and output reconstruction (GET)
36//!   - `/v1/execute/batch` — batch multiple flow executions in one request (POST)
37//!   - `/v1/execute/batch-cached` — batch execution with per-item cache awareness (POST)
38//!   - `/v1/execute/cache-replay` — re-execute cached flow and compare results (POST)
39//!   - `/v1/execute/pinned` — execute a specific version of a flow (POST)
40//!   - `/v1/execute/ab-test` — execute two versions and compare results (POST)
41//!   - `/v1/execute/warm` — pre-execute flows to prime cache (POST)
42//!   - `/v1/estimate`   — estimate execution cost (tokens/USD)
43//!   - `/v1/costs`      — cumulative per-flow cost tracking (GET all, GET /:flow, PUT /pricing)
44//!   - `/v1/costs/:flow/budget` — per-flow cost budget (PUT set, DELETE remove)
45//!   - `/v1/costs/alerts` — check flows against cost budgets (GET)
46//!   - `/v1/costs/forecast` — predict future costs via linear regression (GET ?flow=X&days=N)
47//!   - `/v1/axonstore`  — AxonStore cognitive persistence (create/list/get/delete, persist/retrieve/mutate/purge/transact with ΛD envelopes)
48//!   - `/v1/dataspace`  — Dataspace cognitive navigation (create/list/delete, ingest/focus/associate/aggregate/explore with ΛD)
49//!   - `/v1/backends`   — LLM backend registry (GET list, PUT register, DELETE, POST check)
50//!   - `/v1/backends/dashboard` — aggregate backend fleet dashboard (calls, cost, limits, circuit, ranking)
51//!   - `/v1/mcp`        — MCP server endpoint (JSON-RPC 2.0: initialize, tools/list, tools/call)
52//!   - `/v1/mcp/tools`  — list exposed MCP tools (convenience, non-JSON-RPC)
53//!   - `/v1/rate-limit` — rate limit status for calling client
54//!   - `/v1/rate-limit/endpoints` — per-endpoint rate limits (GET/PUT/DELETE)
55//!   - `/v1/keys`       — API key management (list/create/revoke/rotate)
56//!   - `/v1/webhooks`   — webhook management (register/list/delete/toggle/deliveries/stats)
57//!   - `/v1/webhooks/retry-queue` — pending webhook retries with exponential backoff
58//!   - `/v1/webhooks/dead-letters` — permanently failed webhook deliveries
59//!   - `/v1/webhooks/:id/template` — payload template management (GET/PUT)
60//!   - `/v1/webhooks/:id/render` — preview rendered payload with template (POST)
61//!   - `/v1/webhooks/:id/simulate` — dry-run delivery with signature computation (POST)
62//!   - `/v1/webhooks/:id/filters` — per-webhook event topic filters (GET/PUT)
63//!   - `/v1/config`     — runtime server configuration (GET/PUT/save/load)
64//!   - `/v1/config/snapshots` — config snapshot management (GET list/POST save)
65//!   - `/v1/config/snapshots/restore` — restore from named snapshot (POST)
66//!   - `/v1/audit`      — audit trail (query entries, stats, export)
67//!   - `/v1/shutdown`   — initiate graceful server shutdown (admin)
68//!   - `/v1/server/backup` — export server configuration as JSON (POST)
69//!   - `/v1/server/restore` — import server configuration from backup (POST)
70//!   - `/v1/server/persist` — save state to disk for crash recovery (POST)
71//!   - `/v1/server/recover` — load state from disk after restart (POST)
72//!   - `/v1/server/auto-persist` — toggle auto-persist on shutdown (GET/PUT)
73//!   - `/v1/cors`       — CORS configuration (GET/PUT)
74//!   - `/v1/middleware`  — request middleware config/stats (GET/PUT)
75//!   - `/v1/inspect`    — list deployed flows / introspect by name / graph export / dependency analysis
76//!   - `/v1/flows/:name/rules` — pre-execution validation rules (GET/PUT/DELETE)
77//!   - `/v1/flows/:name/validate` — validate flow against configured rules (POST)
78//!   - `/v1/flows/:name/quota` — execution quotas per flow (GET/PUT/DELETE)
79//!   - `/v1/flows/:name/quota/check` — check and record quota usage (POST)
80//!   - `/v1/flows/:name/dashboard` — per-flow execution dashboard (GET)
81//!   - `/v1/flows/:name/sla` — SLA definitions (GET/PUT/DELETE)
82//!   - `/v1/flows/:name/sla/check` — check SLA compliance (GET)
83//!   - `/v1/flows/:name/canary` — canary deployment config (GET/PUT/DELETE)
84//!   - `/v1/flows/:name/canary/route` — route request through canary logic (POST)
85//!   - `/v1/flows/compare` — compare multiple flows side-by-side (POST)
86//!   - `/v1/flows/:name/tags` — flow tagging for grouping (GET/PUT/DELETE)
87//!   - `/v1/flows/by-tag` — find flows by tag (GET ?tag=...)
88//!   - `/v1/flows/group/:tag/execute` — execute all flows in a tag group (POST)
89//!   - `/v1/flows/group/:tag/dashboard` — aggregate dashboard for tag group (GET)
90//!   - `/v1/versions/:name/rollback/check` — pre-rollback safety validation (POST)
91//!   - `/v1/traces`     — query execution traces (list/filter)
92//!   - `/v1/traces/:id` — get a specific trace by ID
93//!   - `/v1/traces/stats` — aggregate trace analytics
94//!   - `/v1/traces/export` — export traces as JSONL/CSV/Prometheus
95//!   - `/v1/traces/export/custom` — export traces with custom template (GET ?template=...)
96//!   - `/v1/traces/diff` — compare two traces side-by-side (GET ?a=X&b=Y)
97//!   - `/v1/traces/search` — full-text search across traces (GET ?q=...)
98//!   - `/v1/traces/aggregate` — aggregated metrics with percentiles (GET ?window=N)
99//!   - `/v1/traces/retention` — trace retention policy (GET/PUT max_age_secs)
100//!   - `/v1/traces/evict` — manually trigger TTL-based eviction (POST)
101//!   - `/v1/traces/bulk` — bulk delete traces by IDs (DELETE)
102//!   - `/v1/traces/bulk/annotate` — bulk annotate traces by IDs (POST)
103//!   - `/v1/traces/compare` — compare N traces across metrics (POST with ids array)
104//!   - `/v1/traces/timeline` — merged chronological timeline across traces (POST)
105//!   - `/v1/traces/heatmap` — latency/error heatmap across time buckets (GET ?bucket_secs=N&window=N)
106//!   - `/v1/traces/:id/annotate` — add annotation (note + tags) to a trace
107//!   - `/v1/traces/:id/annotations` — list annotations for a trace
108//!   - `/v1/traces/:id/replay` — re-execute trace's flow and compare results
109//!   - `/v1/traces/:id/flamegraph` — flamegraph-style span tree from trace events
110//!   - `/v1/traces/:id/profile` — per-step timing breakdown with hotspot detection (GET)
111//!   - `/v1/traces/:id/correlate` — set correlation ID on a trace (POST)
112//!   - `/v1/traces/:id/annotate-from-template` — apply annotation template (POST ?template=...)
113//!   - `/v1/traces/annotation-templates` — built-in and custom annotation templates (GET/PUT)
114//!   - `/v1/traces/correlated` — find traces by correlation ID (GET ?correlation_id=...)
115//!   - `/v1/session/:scope/export` — export scoped session data as JSON/CSV
116//!   - `/v1/logs`       — query recent request logs
117//!   - `/v1/logs/stats` — aggregate request statistics
118//!   - `/v1/logs/export` — export request logs as JSONL/CSV with filtering
119//!   - `/v1/daemons`    — list registered daemons
120//!   - `/v1/daemons/:name` — get/delete individual daemon
121//!   - `/v1/daemons/:name/run` — execute daemon's flow with lifecycle management
122//!   - `/v1/daemons/:name/pause` — pause a daemon (POST)
123//!   - `/v1/daemons/:name/resume` — resume a paused daemon (POST)
124//!   - `/v1/daemons/:name/events` — lifecycle events for a daemon (GET ?limit=N)
125//!   - `/v1/daemons/dependencies` — inferred daemon dependency graph from chain topology
126//!   - `/v1/daemons/autoscale` — auto-scaling configuration and evaluation (GET/PUT)
127//!   - `/v1/daemons/:name/trigger` — GET/PUT/DELETE daemon event trigger binding
128//!   - `/v1/triggers`   — list all daemon trigger bindings
129//!   - `/v1/triggers/dispatch` — dispatch event to matching triggered daemons
130//!   - `/v1/triggers/replay` — replay historical events to re-trigger daemons (POST)
131//!   - `/v1/events/history` — view recent event bus history (GET ?limit=N&topic=...)
132//!   - `/v1/events/stream` — poll-based SSE event stream (GET ?since=N&limit=N&topic=...)
133//!   - `/v1/daemons/:name/chain` — GET/PUT/DELETE daemon output chain binding
134//!   - `/v1/chains`   — list all daemon chain bindings (trigger → daemon → output)
135//!   - `/v1/chains/graph` — export chain topology as DOT or Mermaid graph
136//!   - `/v1/schedules`   — list/create scheduled flow executions
137//!   - `/v1/schedules/:name` — get/delete individual schedule
138//!   - `/v1/schedules/:name/toggle` — enable/disable a schedule
139//!   - `/v1/schedules/:name/history` — execution history for a schedule (GET ?limit=N)
140//!   - `/v1/schedules/tick` — poll-based tick to execute due schedules
141//!
142//! Built on tokio + axum for async HTTP handling.
143//! Auth: role-based via ApiKeyManager (Admin/Operator/ReadOnly) with auth_middleware.
144//!
145//! This is the Rust-native replacement for the Python AxonServer (uvicorn).
146//!
147//! # §Fase 33.x.i — `crate::backend` deprecation
148//!
149//! This file uses the deprecated synchronous `crate::backend`
150//! surface (`call`, `SUPPORTED_BACKENDS`, `get_api_key`) on the
151//! legacy JSON-mode dispatch path + on backend-management
152//! endpoints. The `#![allow(deprecated)]` below silences the
153//! deprecation warnings while the deeper async migration progresses
154//! under followup sub-fase Fase 33.x.i.2 (sync→async migration
155//! of the 4 callers, separate cycle).
156
157#![allow(deprecated)]
158
159use axum::{
160    Router,
161    Json,
162    extract::State,
163    extract::Path,
164    extract::Query,
165    http::StatusCode,
166    http::HeaderMap,
167    routing::{get, post, put, delete},
168};
169use serde::{Deserialize, Serialize};
170use std::collections::HashMap;
171use std::sync::{Arc, Mutex};
172use std::time::Instant;
173
174use crate::api_keys::ApiKeyManager;
175use crate::audit_trail::{AuditLog, AuditAction, AuditFilter};
176use crate::auth_middleware::{self, AccessLevel};
177use crate::cors::CorsConfig;
178use crate::request_middleware::{RequestIdGenerator, MiddlewareConfig};
179use crate::trace_store::{TraceStore, TraceStoreConfig, TraceFilter};
180use crate::event_bus::{DaemonSupervisor, EventBus, RestartPolicy};
181use crate::flow_version::VersionRegistry;
182use crate::rate_limiter::{RateLimiter, RateLimitConfig, TenantRateLimiter};
183use crate::request_log::{RequestLogger, RequestLogConfig, LogFilter};
184use crate::runner::AXON_VERSION;
185use crate::session_scope::ScopedSessionManager;
186use crate::session_store::SessionStore;
187use crate::webhook_delivery::{self, DeliveryConfig};
188use crate::webhooks::WebhookRegistry;
189
190// ── Server configuration ──────────────────────────────────────────────────
191
192/// Server configuration — mirrors CLI args for `axon serve`.
193#[derive(Debug, Clone)]
194pub struct ServerConfig {
195    pub host: String,
196    pub port: u16,
197    pub channel: String,
198    pub auth_token: String,
199    pub log_level: String,
200    /// Log output format: "json" or "pretty".
201    pub log_format: String,
202    /// Optional directory for daily-rotated log files.
203    pub log_file: Option<String>,
204    /// PostgreSQL connection URL (for persistent storage).
205    pub database_url: Option<String>,
206    /// Optional path for persisted config file.
207    pub config_path: Option<String>,
208    /// §Fase 31.d (D6) — Type-Driven Wire Inference activation flag.
209    ///
210    /// When `true`, `POST /v1/execute` promotes to SSE for any flow
211    /// the type-checker inferred as stream-producing (i.e. the
212    /// AxonEndpoint AST node carries `implicit_transport == "sse"`)
213    /// REGARDLESS of the `Accept:` header. The D1 inference becomes
214    /// the wire's authoritative source.
215    ///
216    /// When `false` (D6 default in v1.22.x — backwards-compat first),
217    /// the Fase 30.e D4 + D5 negotiation matrix is preserved verbatim.
218    /// Only the additive 31.e diagnostic header is observable for
219    /// existing clients.
220    ///
221    /// D9 ratified — this flag flips to default `true` in v2.0.0
222    /// (Fase 35+ candidate) with the full migration guide already
223    /// published in `docs/MIGRATION_v1.22.md` (31.h).
224    ///
225    /// Adopters opt in via three converging surfaces (31.f):
226    ///   * CLI: `axon serve --strict-type-driven-transport`
227    ///   * Config file: `[server] strict_type_driven_transport = true`
228    ///   * Env var:    `AXON_STRICT_TYPE_DRIVEN_TRANSPORT=1`
229    pub strict_type_driven_transport: bool,
230    /// §Fase 36.g (D7) — Server-wide default execution backend.
231    ///
232    /// Rung 3 of the Fase 36 Backend Resolution Contract ladder: when
233    /// a request behind an `axonendpoint` names no backend (rung 1)
234    /// and the route declares none (rung 2), this server default is
235    /// consulted before the environment-available `auto` rungs.
236    ///
237    /// `None` ≡ no server default — resolution falls straight through
238    /// to the `auto` rungs (operator-tuned registry scores → the
239    /// providers with an API key in the environment). A `Some("auto")`
240    /// value is transparent — it behaves as `None` at the ladder. Lets
241    /// an operator pin a fleet-wide default without editing a single
242    /// `.axon`.
243    ///
244    /// Adopters set it via three converging surfaces (D7 — mirrors the
245    /// `strict_type_driven_transport` precedence):
246    ///   * CLI:     `axon serve --backend <name>`
247    ///   * Env var: `AXON_DEFAULT_BACKEND=<name>`
248    ///   * Programmatic: this field.
249    /// The CLI flag wins when both CLI and env are set. The value is
250    /// validated against the closed catalog
251    /// (`parser::AXONENDPOINT_BACKEND_VALUES`) at `run_serve` startup —
252    /// an unknown name fails fast, before the first request is served.
253    pub default_backend: Option<String>,
254    /// §Fase 38.j (D3 + D7 + D8) — Directory containing declared store-
255    /// schema manifests (`*.axon-schema.json` files at the project root
256    /// and/or under a `schemas/` subdirectory).
257    ///
258    /// When `Some(path)`, `POST /v1/deploy` loads and merges every
259    /// manifest under the directory before running the deploy-time
260    /// store verification pass. The declared columns of every Fase 38
261    /// `schema:` form (a/b/c) are then proven AGAINST the live Postgres
262    /// introspection — any drift fails the deploy with structured
263    /// axon-T807 (DeclaredVsLiveDrift) diagnostics. axon-T805 (manifest
264    /// hash mismatch) and axon-T806 (missing per-tenant env var) also
265    /// surface here.
266    ///
267    /// `None` ≡ no manifest loading — the v1.37.0 verify behavior is
268    /// preserved verbatim (D5 absolute backwards-compat). An adopter
269    /// who never adopts Fase 38's compile-time schema observes ZERO
270    /// behavior change at deploy.
271    ///
272    /// Adopters set it via three converging surfaces (D7 — mirrors the
273    /// `default_backend` precedence):
274    ///   * CLI:     `axon serve --schemas-dir <path>`
275    ///   * Env var: `AXON_SCHEMAS_DIR=<path>`
276    ///   * Programmatic: this field.
277    /// The CLI flag wins when both CLI and env are set. An empty value
278    /// collapses to `None`. The directory's existence is NOT verified
279    /// at startup — a missing dir resolves to "no manifest files" the
280    /// same way an empty dir does (`load_and_merge_manifests` is total).
281    pub schemas_dir: Option<String>,
282}
283
284impl ServerConfig {
285    /// Whether authentication is enabled.
286    pub fn auth_enabled(&self) -> bool {
287        !self.auth_token.is_empty()
288    }
289
290    /// The bind address string (host:port).
291    pub fn bind_addr(&self) -> String {
292        format!("{}:{}", self.host, self.port)
293    }
294}
295
296/// §Fase 50.d/2 (v2.4.0) — minimal `ServerConfig` for embedded
297/// callers (notably `axon-enterprise`'s runtime executor that
298/// constructs `ServerState` to drive `server_execute_streaming`).
299///
300/// Defaults: in-memory channel (no DB), no auth token, INFO logs to
301/// stdout, no persisted state path. Adopters that need persistence
302/// or auth construct `ServerConfig` explicitly. This default is
303/// total + side-effect-free: `ServerState::new(ServerConfig::default())`
304/// produces a fresh in-memory state with no on-disk recovery
305/// attempted (when `config_path` is `None`).
306impl Default for ServerConfig {
307    fn default() -> Self {
308        Self {
309            host: "127.0.0.1".to_string(),
310            port: 0,
311            channel: "memory".to_string(),
312            auth_token: String::new(),
313            log_level: "INFO".to_string(),
314            log_format: "json".to_string(),
315            log_file: None,
316            database_url: None,
317            config_path: None,
318            strict_type_driven_transport: false,
319            default_backend: None,
320            schemas_dir: None,
321        }
322    }
323}
324
325/// §Fase 31.f (D7) — Parse a truthy env var per the cross-stack
326/// contract. Truthy values (case-insensitive): "1", "true", "yes",
327/// "on". Empty / unset / any other value → false.
328///
329/// Public so the Python CLI can call out to the same canonical
330/// parser via FFI if needed (though Python ships its own parser
331/// of the same shape per `axon/cli/serve_cmd.py` — the contract
332/// is the VALUE SET, not a binary link).
333///
334/// Examples:
335///   `AXON_STRICT_TYPE_DRIVEN_TRANSPORT=1`      → true
336///   `AXON_STRICT_TYPE_DRIVEN_TRANSPORT=true`   → true
337///   `AXON_STRICT_TYPE_DRIVEN_TRANSPORT=YES`    → true
338///   `AXON_STRICT_TYPE_DRIVEN_TRANSPORT=on`     → true
339///   `AXON_STRICT_TYPE_DRIVEN_TRANSPORT=0`      → false
340///   `AXON_STRICT_TYPE_DRIVEN_TRANSPORT=false`  → false
341///   `AXON_STRICT_TYPE_DRIVEN_TRANSPORT=`       → false (empty)
342///   (unset)                                    → false
343pub fn parse_truthy_env(name: &str) -> bool {
344    std::env::var(name)
345        .ok()
346        .map(|v| {
347            matches!(
348                v.trim().to_ascii_lowercase().as_str(),
349                "1" | "true" | "yes" | "on",
350            )
351        })
352        .unwrap_or(false)
353}
354
355// ── Server state ──────────────────────────────────────────────────────────
356
357/// Shared server state, wrapped in Arc<Mutex<>> for thread safety.
358pub struct ServerState {
359    pub config: ServerConfig,
360    pub daemons: HashMap<String, DaemonInfo>,
361    pub metrics: ServerMetrics,
362    pub started_at: Instant,
363    pub deploy_count: u64,
364    pub event_bus: EventBus,
365    pub supervisor: DaemonSupervisor,
366    pub versions: VersionRegistry,
367    /// §Fase 32.b — Dynamic HTTP routes registered from axonendpoint
368    /// declarations at deploy time (D1, D11). Key is
369    /// `(method_uppercase, path)`; value carries the metadata needed
370    /// for request-time dispatch (flow name + transport + keepalive +
371    /// source file for the negotiation classifier).
372    ///
373    /// Populated by `register_axonendpoint_routes` after every
374    /// successful `/v1/deploy`. Path conflicts detected at deploy
375    /// time per D2 (deploy fails with 409 when the same
376    /// `(method, path)` tuple is already owned by a different
377    /// `(flow_name, source_file)` pair). Re-deploying the same flow
378    /// updates its routes in place.
379    ///
380    /// Cross-stack contract (D11): the Python `AxonServer` mirrors
381    /// this state via FastAPI route registration; both stacks
382    /// produce byte-identical route sets from the same source.
383    pub dynamic_routes: HashMap<(String, String), DynamicEndpointRoute>,
384    /// §Fase 32.c — Per-name `type T { … }` snapshot consulted by the
385    /// dynamic-route fallback handler at request time to validate the
386    /// HTTP request body against the axonendpoint's declared
387    /// `body: T`. Populated alongside `dynamic_routes` at every
388    /// successful `/v1/deploy`. Last-wins on cross-deploy type name
389    /// collision (a known limitation of the 32.c surface; a future
390    /// type-registry fase will add deploy-scoped namespacing).
391    pub dynamic_types: HashMap<String, crate::route_schema::TypeSchema>,
392    /// §Fase 32.f — Idempotency-Key store for POST/PUT axonendpoint
393    /// routes. Stripe-compatible. Indexed by `(client_id,
394    /// endpoint_path, idempotency_key)`. Cross-tenant isolation is a
395    /// property of the composite key — two adopters cannot collide.
396    /// Default 24h retention; eviction is lazy on lookup with a
397    /// `reap_expired` helper available for periodic background sweeps.
398    pub idempotency_store: crate::idempotency::IdempotencyStore,
399    /// §Fase 32.h — Axonendpoint replay log. Append-only, keyed by
400    /// trace_id. Populated on every successful 2xx response when the
401    /// route's `replay_enabled` is true. Consulted by
402    /// `GET /v1/replay/<trace_id>` for regulatory audit.
403    pub axonendpoint_replay: crate::axonendpoint_replay::AxonendpointReplayLog,
404    pub session: SessionStore,
405    pub scoped_sessions: ScopedSessionManager,
406    pub rate_limiter: RateLimiter,
407    /// Per-tenant request-rate + daily token quota enforcement (M4).
408    pub tenant_rate_limiter: TenantRateLimiter,
409    pub request_logger: RequestLogger,
410    pub api_keys: ApiKeyManager,
411    pub webhooks: WebhookRegistry,
412    pub delivery_config: DeliveryConfig,
413    pub cors_config: CorsConfig,
414    pub middleware_config: MiddlewareConfig,
415    pub request_id_gen: RequestIdGenerator,
416    pub audit_log: AuditLog,
417    pub trace_store: TraceStore,
418    pub schedules: HashMap<String, ScheduleEntry>,
419    pub config_snapshots: Vec<NamedConfigSnapshot>,
420    pub execution_queue: Vec<QueuedExecution>,
421    pub execution_queue_next_id: u64,
422    pub cost_pricing: CostPricing,
423    pub cost_budgets: HashMap<String, CostBudget>,
424    pub flow_rules: HashMap<String, FlowValidationRules>,
425    pub flow_quotas: HashMap<String, FlowQuota>,
426    pub readiness_gates: ReadinessGates,
427    pub autoscale_config: AutoscaleConfig,
428    pub auto_persist_on_shutdown: bool,
429    pub flow_tags: HashMap<String, Vec<String>>,
430    pub flow_slas: HashMap<String, FlowSLA>,
431    pub canary_configs: HashMap<String, CanaryConfig>,
432    pub alert_rules: Vec<AlertRule>,
433    pub fired_alerts: Vec<FiredAlert>,
434    pub alert_silences: Vec<AlertSilence>,
435    pub health_history: Vec<HealthTransition>,
436    pub endpoint_rate_limits: HashMap<String, EndpointRateLimit>,
437    pub execution_cache: Vec<CachedResult>,
438    pub backend_registry: HashMap<String, BackendRegistryEntry>,
439    pub axon_stores: HashMap<String, AxonStoreInstance>,
440    pub dataspaces: HashMap<String, DataspaceInstance>,
441    pub shields: HashMap<String, ShieldInstance>,
442    pub corpora: HashMap<String, CorpusInstance>,
443    pub mandates: HashMap<String, MandatePolicy>,
444    pub refine_sessions: HashMap<String, RefineSession>,
445    pub trails: HashMap<String, TrailRecord>,
446    pub probes: HashMap<String, ProbeSession>,
447    pub weaves: HashMap<String, WeaveSession>,
448    pub corroborations: HashMap<String, CorroborateSession>,
449    pub drills: HashMap<String, DrillSession>,
450    pub forges: HashMap<String, ForgeSession>,
451    pub deliberations: HashMap<String, DeliberateSession>,
452    pub consensus_sessions: HashMap<String, ConsensusSession>,
453    pub hibernations: HashMap<String, HibernateSession>,
454    pub ots_secrets: HashMap<String, OtsSecret>,
455    pub psyche_sessions: HashMap<String, PsycheSession>,
456    pub axon_endpoints: HashMap<String, EndpointBinding>,
457    pub endpoint_calls: Vec<EndpointCallRecord>,
458    pub pix_sessions: HashMap<String, PixSession>,
459    pub backend_health_probes: HashMap<String, BackendHealthProbe>,
460    pub backend_health_history: HashMap<String, Vec<HealthCheckRecord>>,
461    pub shutdown: Option<Arc<crate::graceful_shutdown::ShutdownCoordinator>>,
462    /// Persistent storage backend (PostgreSQL or InMemory).
463    pub storage: Arc<crate::storage::StorageDispatcher>,
464    /// Resilient backend for LLM calls with retry, circuit breaker, and fallback.
465    pub resilient_backend: Arc<crate::resilient_backend::ResilientBackend>,
466    /// Per-tenant API key resolver (AWS Secrets Manager + in-memory cache).
467    pub tenant_secrets: Arc<crate::tenant_secrets::TenantSecretsClient>,
468}
469
470/// A queued flow execution request with priority.
471#[derive(Debug, Clone, Serialize)]
472pub struct QueuedExecution {
473    /// Queue item ID.
474    pub id: u64,
475    /// Flow name to execute.
476    pub flow_name: String,
477    /// Backend override (or "stub").
478    pub backend: String,
479    /// Priority (lower = higher priority, default 5).
480    pub priority: u32,
481    /// Client who enqueued.
482    pub client_key: String,
483    /// Unix timestamp when enqueued.
484    pub enqueued_at: u64,
485    /// Status: "pending", "processing", "completed", "failed".
486    pub status: String,
487}
488
489/// Configurable pricing per backend (USD per 1M tokens).
490#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct CostPricing {
492    /// Price per 1M input tokens by backend.
493    pub input_per_million: HashMap<String, f64>,
494    /// Price per 1M output tokens by backend.
495    pub output_per_million: HashMap<String, f64>,
496}
497
498impl Default for CostPricing {
499    fn default() -> Self {
500        let mut input = HashMap::new();
501        input.insert("anthropic".into(), 3.0);
502        input.insert("openai".into(), 2.5);
503        input.insert("stub".into(), 0.0);
504
505        let mut output = HashMap::new();
506        output.insert("anthropic".into(), 15.0);
507        output.insert("openai".into(), 10.0);
508        output.insert("stub".into(), 0.0);
509
510        CostPricing { input_per_million: input, output_per_million: output }
511    }
512}
513
514/// Per-flow cost summary.
515#[derive(Debug, Clone, Serialize)]
516pub struct FlowCostSummary {
517    pub flow_name: String,
518    pub executions: u64,
519    pub total_input_tokens: u64,
520    pub total_output_tokens: u64,
521    pub estimated_cost_usd: f64,
522}
523
524/// Per-flow cost budget configuration.
525#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct CostBudget {
527    /// Maximum allowed cost in USD.
528    pub max_cost_usd: f64,
529    /// Warning threshold (0.0–1.0, e.g. 0.8 = alert at 80%).
530    pub warn_threshold: f64,
531}
532
533/// A cost budget alert.
534#[derive(Debug, Clone, Serialize)]
535pub struct CostAlert {
536    pub flow_name: String,
537    pub current_cost_usd: f64,
538    pub budget_usd: f64,
539    pub usage_pct: f64,
540    pub level: String, // "warning" or "exceeded"
541}
542
543/// Pre-execution validation rules for a flow.
544#[derive(Debug, Clone, Serialize, Deserialize)]
545pub struct FlowValidationRules {
546    /// Maximum allowed steps (0 = no limit).
547    #[serde(default)]
548    pub max_steps: usize,
549    /// Required anchor names (must be present in flow).
550    #[serde(default)]
551    pub required_anchors: Vec<String>,
552    /// Banned tool names (must not be used).
553    #[serde(default)]
554    pub banned_tools: Vec<String>,
555    /// Allowed backends (empty = all allowed).
556    #[serde(default)]
557    pub allowed_backends: Vec<String>,
558    /// Maximum estimated cost in USD (0.0 = no limit).
559    #[serde(default)]
560    pub max_cost_usd: f64,
561}
562
563/// Result of validating a flow against its rules.
564#[derive(Debug, Clone, Serialize)]
565pub struct ValidationResult {
566    pub valid: bool,
567    pub violations: Vec<String>,
568}
569
570/// Per-flow execution quota with hourly/daily limits.
571#[derive(Debug, Clone, Serialize, Deserialize)]
572pub struct FlowQuota {
573    /// Maximum executions per hour (0 = unlimited).
574    #[serde(default)]
575    pub max_per_hour: u64,
576    /// Maximum executions per day (0 = unlimited).
577    #[serde(default)]
578    pub max_per_day: u64,
579    /// Executions in the current hour window.
580    #[serde(default)]
581    pub current_hour_count: u64,
582    /// Executions in the current day window.
583    #[serde(default)]
584    pub current_day_count: u64,
585    /// Hour window start (Unix seconds, aligned to hour).
586    #[serde(default)]
587    pub hour_window_start: u64,
588    /// Day window start (Unix seconds, aligned to day).
589    #[serde(default)]
590    pub day_window_start: u64,
591}
592
593impl FlowQuota {
594    /// Check if an execution is allowed and record it if so.
595    pub fn check_and_record(&mut self) -> (bool, Vec<String>) {
596        let now = std::time::SystemTime::now()
597            .duration_since(std::time::UNIX_EPOCH)
598            .unwrap_or_default()
599            .as_secs();
600
601        // Reset hour window if expired
602        let hour_start = (now / 3600) * 3600;
603        if self.hour_window_start != hour_start {
604            self.hour_window_start = hour_start;
605            self.current_hour_count = 0;
606        }
607
608        // Reset day window if expired
609        let day_start = (now / 86400) * 86400;
610        if self.day_window_start != day_start {
611            self.day_window_start = day_start;
612            self.current_day_count = 0;
613        }
614
615        let mut violations = Vec::new();
616        if self.max_per_hour > 0 && self.current_hour_count >= self.max_per_hour {
617            violations.push(format!("hourly quota exceeded ({}/{})", self.current_hour_count, self.max_per_hour));
618        }
619        if self.max_per_day > 0 && self.current_day_count >= self.max_per_day {
620            violations.push(format!("daily quota exceeded ({}/{})", self.current_day_count, self.max_per_day));
621        }
622
623        if violations.is_empty() {
624            self.current_hour_count += 1;
625            self.current_day_count += 1;
626            (true, Vec::new())
627        } else {
628            (false, violations)
629        }
630    }
631}
632
633/// Configurable readiness gates for the /v1/health/ready probe.
634#[derive(Debug, Clone, Serialize, Deserialize)]
635pub struct ReadinessGates {
636    /// Minimum number of registered daemons required.
637    #[serde(default)]
638    pub min_daemons: usize,
639    /// Required flow names that must be deployed.
640    #[serde(default)]
641    pub required_flows: Vec<String>,
642    /// Maximum error rate (total_errors/total_requests) allowed (0.0 = no limit).
643    #[serde(default)]
644    pub max_error_rate: f64,
645    /// Minimum uptime in seconds before ready (0 = immediate).
646    #[serde(default)]
647    pub min_uptime_secs: u64,
648}
649
650impl Default for ReadinessGates {
651    fn default() -> Self {
652        ReadinessGates {
653            min_daemons: 0,
654            required_flows: Vec::new(),
655            max_error_rate: 0.0,
656            min_uptime_secs: 0,
657        }
658    }
659}
660
661/// Result of evaluating readiness gates.
662#[derive(Debug, Clone, Serialize)]
663pub struct GateCheckResult {
664    pub gate: String,
665    pub passed: bool,
666    pub detail: String,
667}
668
669/// Auto-scaling configuration for daemons.
670#[derive(Debug, Clone, Serialize, Deserialize)]
671pub struct AutoscaleConfig {
672    /// Whether auto-scaling is enabled.
673    #[serde(default)]
674    pub enabled: bool,
675    /// Minimum daemons to keep active.
676    #[serde(default = "default_min_daemons")]
677    pub min_daemons: usize,
678    /// Maximum daemons allowed.
679    #[serde(default = "default_max_daemons")]
680    pub max_daemons: usize,
681    /// Queue depth threshold to trigger scale-up.
682    #[serde(default = "default_scale_up_threshold")]
683    pub scale_up_queue_depth: usize,
684    /// Events/sec threshold to trigger scale-up.
685    #[serde(default = "default_scale_up_events")]
686    pub scale_up_events_per_sec: u64,
687    /// Idle seconds before scale-down.
688    #[serde(default = "default_scale_down_idle_secs")]
689    pub scale_down_idle_secs: u64,
690}
691
692fn default_min_daemons() -> usize { 1 }
693fn default_max_daemons() -> usize { 10 }
694fn default_scale_up_threshold() -> usize { 5 }
695fn default_scale_up_events() -> u64 { 100 }
696fn default_scale_down_idle_secs() -> u64 { 300 }
697
698impl Default for AutoscaleConfig {
699    fn default() -> Self {
700        AutoscaleConfig {
701            enabled: false,
702            min_daemons: 1,
703            max_daemons: 10,
704            scale_up_queue_depth: 5,
705            scale_up_events_per_sec: 100,
706            scale_down_idle_secs: 300,
707        }
708    }
709}
710
711/// Result of an autoscale evaluation.
712#[derive(Debug, Clone, Serialize)]
713pub struct AutoscaleDecision {
714    pub current_daemons: usize,
715    pub active_daemons: usize,
716    pub queue_depth: usize,
717    pub events_per_sec: f64,
718    pub recommendation: String,
719    pub reason: String,
720}
721
722/// Per-endpoint rate limit with independent sliding window.
723#[derive(Debug, Clone, Serialize, Deserialize)]
724pub struct EndpointRateLimit {
725    /// Path prefix to match (e.g. "/v1/execute", "/v1/deploy").
726    pub path_prefix: String,
727    /// Max requests per window.
728    pub max_requests: u64,
729    /// Window size in seconds.
730    pub window_secs: u64,
731    /// Current request count in window.
732    #[serde(default)]
733    pub current_count: u64,
734    /// Window start timestamp (Unix seconds).
735    #[serde(default)]
736    pub window_start: u64,
737}
738
739impl EndpointRateLimit {
740    /// Check if a request to this path is allowed. Auto-resets window.
741    pub fn check(&mut self, path: &str) -> bool {
742        if !path.starts_with(&self.path_prefix) {
743            return true; // doesn't match this limit
744        }
745        let now = std::time::SystemTime::now()
746            .duration_since(std::time::UNIX_EPOCH)
747            .unwrap_or_default()
748            .as_secs();
749
750        // Reset window if expired
751        if now >= self.window_start + self.window_secs || self.window_start == 0 {
752            self.window_start = now;
753            self.current_count = 0;
754        }
755
756        if self.current_count >= self.max_requests {
757            return false;
758        }
759        self.current_count += 1;
760        true
761    }
762}
763
764/// A named server configuration snapshot for save/restore.
765#[derive(Debug, Clone, Serialize)]
766pub struct NamedConfigSnapshot {
767    pub name: String,
768    pub created_at: u64,
769    pub snapshot: crate::server_config::ConfigSnapshot,
770}
771
772/// A lifecycle event for a daemon (state transition record).
773#[derive(Debug, Clone, Serialize)]
774pub struct DaemonLifecycleEvent {
775    /// Unix timestamp of the event.
776    pub timestamp: u64,
777    /// Previous state.
778    pub from_state: DaemonState,
779    /// New state.
780    pub to_state: DaemonState,
781    /// Reason for the transition (if applicable).
782    #[serde(skip_serializing_if = "Option::is_none")]
783    pub reason: Option<String>,
784}
785
786/// Information about a registered daemon.
787#[derive(Debug, Clone, Serialize)]
788pub struct DaemonInfo {
789    pub name: String,
790    pub state: DaemonState,
791    pub source_file: String,
792    pub flow_name: String,
793    pub event_count: u64,
794    pub restart_count: u32,
795    /// Event topic pattern that triggers this daemon (None = manual only).
796    pub trigger_topic: Option<String>,
797    /// Topic to publish execution result to (enables daemon chaining).
798    #[serde(skip_serializing_if = "Option::is_none")]
799    pub output_topic: Option<String>,
800    /// Lifecycle events (state transitions), capped at 100.
801    #[serde(skip_serializing_if = "Vec::is_empty")]
802    pub lifecycle_events: Vec<DaemonLifecycleEvent>,
803}
804
805/// Daemon lifecycle states.
806#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
807#[serde(rename_all = "lowercase")]
808pub enum DaemonState {
809    Idle,
810    Running,
811    Hibernating,
812    Paused,
813    Stopped,
814    Crashed,
815}
816
817/// A single execution record in a schedule's history.
818#[derive(Debug, Clone, Serialize, Deserialize)]
819pub struct ScheduleRun {
820    /// Unix timestamp of this execution.
821    pub timestamp: u64,
822    /// Whether the execution succeeded.
823    pub success: bool,
824    /// Trace ID for this execution (0 if unavailable).
825    pub trace_id: u64,
826    /// Latency in milliseconds.
827    pub latency_ms: u64,
828    /// Error message if failed.
829    #[serde(skip_serializing_if = "Option::is_none")]
830    pub error: Option<String>,
831}
832
833/// A scheduled flow execution entry.
834#[derive(Debug, Clone, Serialize)]
835pub struct ScheduleEntry {
836    /// Flow name to execute on schedule.
837    pub flow_name: String,
838    /// Interval in seconds between executions.
839    pub interval_secs: u64,
840    /// Whether the schedule is active.
841    pub enabled: bool,
842    /// Backend for execution (default: "stub").
843    pub backend: String,
844    /// Unix timestamp of last execution (0 = never).
845    pub last_run: u64,
846    /// Unix timestamp of next scheduled execution.
847    pub next_run: u64,
848    /// Total executions performed by this schedule.
849    pub run_count: u64,
850    /// Total errors from scheduled executions.
851    pub error_count: u64,
852    /// Execution history (most recent last, capped at 50).
853    #[serde(skip_serializing_if = "Vec::is_empty")]
854    pub history: Vec<ScheduleRun>,
855}
856
857/// Aggregated server metrics.
858#[derive(Debug, Clone, Serialize)]
859pub struct ServerMetrics {
860    pub total_requests: u64,
861    pub total_deployments: u64,
862    pub total_errors: u64,
863    pub active_daemons: u32,
864}
865
866impl ServerMetrics {
867    fn new() -> Self {
868        ServerMetrics {
869            total_requests: 0,
870            total_deployments: 0,
871            total_errors: 0,
872            active_daemons: 0,
873        }
874    }
875}
876
877impl ServerState {
878    // §Fase 50.d/2 (v2.4.0) — publicized so external crates (notably
879    // `axon-enterprise`'s runtime executor) can construct a
880    // `ServerState` without reimplementing the persistence-recovery
881    // dance. The constructor is total + safe for callers without
882    // on-disk persistence (the recovery branches no-op when
883    // `config.config_path` is unset + no STATE_PERSIST_PATH file
884    // exists).
885    pub fn new(config: ServerConfig) -> Self {
886        let event_bus = EventBus::new();
887        let supervisor = DaemonSupervisor::new(event_bus.clone());
888        let master_token = if config.auth_token.is_empty() { None } else { Some(config.auth_token.clone()) };
889
890        let mut rate_limiter = RateLimiter::new(RateLimitConfig::default_config());
891        let mut request_logger = RequestLogger::new(RequestLogConfig::default_config());
892
893        // Restore persisted config if available
894        let config_path = crate::config_persistence::resolve_path(config.config_path.as_deref());
895        if crate::config_persistence::exists(&config_path) {
896            if let Ok(persisted) = crate::config_persistence::load(&config_path) {
897                let update = crate::config_persistence::snapshot_to_update(&persisted.config);
898                if let Some(ref rl) = update.rate_limit {
899                    crate::server_config::apply_rate_limit(rl, &mut rate_limiter);
900                }
901                if let Some(ref log) = update.request_log {
902                    crate::server_config::apply_request_log(log, &mut request_logger);
903                }
904                eprintln!("  Restored config from {} (save #{})", config_path.display(), persisted.save_count);
905            }
906        }
907
908        // Try auto-recover from ΛD-format state file
909        let state_path = config.config_path.as_deref()
910            .map(|p| std::path::Path::new(p).parent().unwrap_or(std::path::Path::new(".")).join(STATE_PERSIST_PATH))
911            .unwrap_or_else(|| std::path::PathBuf::from(STATE_PERSIST_PATH));
912
913        let mut cost_pricing = CostPricing::default();
914        let mut cost_budgets = HashMap::new();
915        let mut flow_rules = HashMap::new();
916        let mut flow_quotas = HashMap::new();
917        let mut readiness_gates = ReadinessGates::default();
918        let mut autoscale_config = AutoscaleConfig::default();
919        let mut endpoint_rate_limits = HashMap::new();
920        let mut schedules: HashMap<String, ScheduleEntry> = HashMap::new();
921        let mut recovered = false;
922
923        if state_path.exists() {
924            if let Ok(json_str) = std::fs::read_to_string(&state_path) {
925                if let Ok(backup) = serde_json::from_str::<ServerBackup>(&json_str) {
926                    if backup.lambda_d.validate().is_ok() {
927                        cost_pricing = backup.cost_pricing;
928                        cost_budgets = backup.cost_budgets;
929                        flow_rules = backup.flow_rules;
930                        flow_quotas = backup.flow_quotas;
931                        readiness_gates = backup.readiness_gates;
932                        endpoint_rate_limits = backup.endpoint_rate_limits;
933                        for sched in &backup.schedules {
934                            schedules.insert(sched.name.clone(), ScheduleEntry {
935                                flow_name: sched.flow_name.clone(), interval_secs: sched.interval_secs,
936                                enabled: sched.enabled, backend: sched.backend.clone(),
937                                last_run: 0, next_run: sched.interval_secs, run_count: 0, error_count: 0, history: Vec::new(),
938                            });
939                        }
940                        recovered = true;
941                        eprintln!("  Auto-recovered ΛD state from {} (v{})", state_path.display(), backup.version);
942                    }
943                }
944            }
945        }
946
947        let _ = recovered; // used for logging above
948
949        ServerState {
950            config,
951            daemons: HashMap::new(),
952            metrics: ServerMetrics::new(),
953            started_at: Instant::now(),
954            deploy_count: 0,
955            event_bus,
956            supervisor,
957            versions: VersionRegistry::new(),
958            // §Fase 32.b — dynamic_routes is populated lazily by
959            // deploy_handler. Empty at startup; the legacy fallback
960            // handler returns 404 for unrecognized paths until the
961            // first /v1/deploy that registers axonendpoints with
962            // explicit `path:` declarations.
963            dynamic_routes: HashMap::new(),
964            // §Fase 32.c — Per-name type schemas captured alongside the
965            // dynamic routes for body-schema validation at request time.
966            dynamic_types: HashMap::new(),
967            // §Fase 32.f — Idempotency-Key store with 24h default
968            // retention. Populated on first POST/PUT request bearing
969            // the header; consulted on every repeat.
970            idempotency_store: crate::idempotency::IdempotencyStore::default(),
971            // §Fase 32.h — Axonendpoint replay log with 30-day default
972            // retention. Populated on every successful 2xx response
973            // when route.replay_enabled is true; consulted by
974            // `GET /v1/replay/<trace_id>` for regulatory audit.
975            axonendpoint_replay:
976                crate::axonendpoint_replay::AxonendpointReplayLog::default(),
977            session: SessionStore::new("axon-server"),
978            scoped_sessions: ScopedSessionManager::new("axon-server"),
979            rate_limiter,
980            tenant_rate_limiter: TenantRateLimiter::new(),
981            request_logger,
982            api_keys: ApiKeyManager::new(master_token.as_deref()),
983            webhooks: WebhookRegistry::new(),
984            delivery_config: DeliveryConfig::default(),
985            cors_config: CorsConfig::default(),
986            middleware_config: MiddlewareConfig::default(),
987            request_id_gen: RequestIdGenerator::new(),
988            audit_log: AuditLog::new(5000),
989            trace_store: TraceStore::new(TraceStoreConfig::default()),
990            schedules,
991            config_snapshots: Vec::new(),
992            execution_queue: Vec::new(),
993            execution_queue_next_id: 1,
994            cost_pricing,
995            cost_budgets,
996            flow_rules,
997            flow_quotas,
998            readiness_gates,
999            autoscale_config,
1000            auto_persist_on_shutdown: true,
1001            flow_tags: HashMap::new(),
1002            flow_slas: HashMap::new(),
1003            canary_configs: HashMap::new(),
1004            alert_rules: Vec::new(),
1005            fired_alerts: Vec::new(),
1006            alert_silences: Vec::new(),
1007            health_history: Vec::new(),
1008            endpoint_rate_limits,
1009            execution_cache: Vec::new(),
1010            backend_registry: HashMap::new(),
1011            axon_stores: HashMap::new(),
1012            dataspaces: HashMap::new(),
1013            shields: HashMap::new(),
1014            corpora: HashMap::new(),
1015            mandates: HashMap::new(),
1016            refine_sessions: HashMap::new(),
1017            trails: HashMap::new(),
1018            probes: HashMap::new(),
1019            weaves: HashMap::new(),
1020            corroborations: HashMap::new(),
1021            drills: HashMap::new(),
1022            forges: HashMap::new(),
1023            deliberations: HashMap::new(),
1024            consensus_sessions: HashMap::new(),
1025            hibernations: HashMap::new(),
1026            ots_secrets: HashMap::new(),
1027            psyche_sessions: HashMap::new(),
1028            axon_endpoints: HashMap::new(),
1029            endpoint_calls: Vec::new(),
1030            pix_sessions: HashMap::new(),
1031            backend_health_probes: HashMap::new(),
1032            backend_health_history: HashMap::new(),
1033            shutdown: None,
1034            storage: Arc::new(crate::storage::StorageDispatcher::in_memory()),
1035            resilient_backend: Arc::new(crate::resilient_backend::ResilientBackend::new()),
1036            tenant_secrets: Arc::new(crate::tenant_secrets::TenantSecretsClient::new_stub()),
1037        }
1038    }
1039}
1040
1041// §Fase 50.d/2 (v2.4.0) — publicized alongside `server_execute_streaming`
1042// so external crates can construct + thread the shared server state
1043// into the streaming entry point. The wrapper type stays a `type`
1044// alias so adopters that prefer the explicit `Arc<Mutex<ServerState>>`
1045// shape don't pay an indirection cost.
1046pub type SharedState = Arc<Mutex<ServerState>>;
1047
1048// ── Auth middleware ────────────────────────────────────────────────────────
1049
1050/// Check auth + role for a mutable state reference (records usage).
1051fn check_auth(state: &mut ServerState, headers: &HeaderMap, level: AccessLevel) -> Result<(), StatusCode> {
1052    auth_middleware::check(&mut state.api_keys, headers, level)?;
1053    Ok(())
1054}
1055
1056/// Check auth + role without recording usage (for read-only peeks).
1057fn check_auth_peek(state: &ServerState, headers: &HeaderMap, level: AccessLevel) -> Result<(), StatusCode> {
1058    auth_middleware::peek(&state.api_keys, headers, level)?;
1059    Ok(())
1060}
1061
1062// ── Rate limiting ────────────────────────────────────────────────────────
1063
1064/// Record a daemon lifecycle event (state transition).
1065fn record_lifecycle(daemon: &mut DaemonInfo, from: DaemonState, to: DaemonState, reason: Option<String>) {
1066    let ts = std::time::SystemTime::now()
1067        .duration_since(std::time::UNIX_EPOCH)
1068        .unwrap_or_default()
1069        .as_secs();
1070    daemon.lifecycle_events.push(DaemonLifecycleEvent {
1071        timestamp: ts,
1072        from_state: from,
1073        to_state: to,
1074        reason,
1075    });
1076    if daemon.lifecycle_events.len() > 100 {
1077        daemon.lifecycle_events.remove(0);
1078    }
1079}
1080
1081/// Extract client key from headers (Authorization token or fallback to "anonymous").
1082fn client_key_from_headers(headers: &HeaderMap) -> String {
1083    headers
1084        .get("authorization")
1085        .and_then(|v| v.to_str().ok())
1086        .map(|v| v.to_string())
1087        .unwrap_or_else(|| "anonymous".to_string())
1088}
1089
1090/// Check rate limit for a request. Returns Err(429) if over limit.
1091/// Enforces both the global per-client sliding window and the per-tenant plan quota (M4).
1092fn check_rate_limit(state: &mut ServerState, headers: &HeaderMap) -> Result<(), StatusCode> {
1093    // Global per-client limit (existing behavior)
1094    let key = client_key_from_headers(headers);
1095    let result = state.rate_limiter.check(&key);
1096    if !result.allowed {
1097        return Err(StatusCode::TOO_MANY_REQUESTS);
1098    }
1099
1100    // Per-tenant quota enforcement (M4)
1101    let tenant_id = crate::tenant::current_tenant_id();
1102    let plan = crate::tenant::TenantPlan::from_str(
1103        if tenant_id == "default" { "enterprise" } else { "starter" }
1104    );
1105    let tenant_result = state.tenant_rate_limiter.check_request(&tenant_id, &plan);
1106    if !tenant_result.allowed {
1107        tracing::warn!(
1108            tenant_id = %tenant_id,
1109            remaining = tenant_result.remaining,
1110            reset_secs = tenant_result.reset_secs,
1111            "tenant_rate_limit_exceeded"
1112        );
1113        return Err(StatusCode::TOO_MANY_REQUESTS);
1114    }
1115
1116    Ok(())
1117}
1118
1119// ── Webhook async delivery ───────────────────────────────────────────────
1120
1121/// Trigger async webhook delivery for an event.
1122/// Locks state to match webhooks and read config, then spawns tokio tasks.
1123fn trigger_webhook_delivery(
1124    state: &SharedState,
1125    topic: &str,
1126    payload: serde_json::Value,
1127    source: &str,
1128) {
1129    let (matched_ids, targets, config, timestamp) = {
1130        let s = state.lock().unwrap();
1131        let ids = s.webhooks.match_topic(topic);
1132        if ids.is_empty() {
1133            return;
1134        }
1135        let mut targets = Vec::new();
1136        for id in &ids {
1137            if let Some(wh) = s.webhooks.get(id) {
1138                targets.push((wh.id.clone(), wh.url.clone(), wh.secret.clone()));
1139            }
1140        }
1141        let config = s.delivery_config.clone();
1142        let ts = std::time::SystemTime::now()
1143            .duration_since(std::time::UNIX_EPOCH)
1144            .unwrap_or_default()
1145            .as_secs();
1146        (ids, targets, config, ts)
1147    };
1148
1149    let _ = matched_ids; // used for count only
1150
1151    for (webhook_id, url, secret) in targets {
1152        let state = state.clone();
1153        let topic = topic.to_string();
1154        let payload = payload.clone();
1155        let source = source.to_string();
1156        let config = config.clone();
1157
1158        tokio::spawn(async move {
1159            let body = webhook_delivery::WebhookPayload {
1160                event: topic.clone(),
1161                payload,
1162                source,
1163                timestamp,
1164            };
1165
1166            let signature = secret.as_ref().map(|s| {
1167                let body_bytes = serde_json::to_vec(&body).unwrap_or_default();
1168                crate::webhooks::WebhookRegistry::compute_signature(s, &body_bytes)
1169            });
1170
1171            let result = webhook_delivery::deliver_with_retry(
1172                &url,
1173                &body,
1174                signature.as_deref(),
1175                &config,
1176            ).await;
1177
1178            // Record result back in registry
1179            if let Ok(mut s) = state.lock() {
1180                s.webhooks.record_completed(
1181                    &webhook_id,
1182                    &topic,
1183                    result.status_code,
1184                    result.latency_ms,
1185                    result.error,
1186                    result.attempts.saturating_sub(1),
1187                );
1188            }
1189        });
1190    }
1191}
1192
1193// ── Route handlers ────────────────────────────────────────────────────────
1194
1195/// Build a HealthInput snapshot from locked server state.
1196fn build_health_input(s: &ServerState) -> crate::health_check::HealthInput {
1197    let bus_stats = s.event_bus.stats();
1198    let sup_counts = s.supervisor.state_counts();
1199    let mut daemon_state_counts = std::collections::HashMap::new();
1200    for (k, v) in &sup_counts {
1201        daemon_state_counts.insert(k.to_string(), *v);
1202    }
1203
1204    let rl_config = s.rate_limiter.config();
1205    let log_config = s.request_logger.config();
1206    let wh_stats = s.webhooks.stats();
1207
1208    crate::health_check::HealthInput {
1209        uptime_secs: s.started_at.elapsed().as_secs(),
1210        axon_version: AXON_VERSION.to_string(),
1211        daemon_count: s.daemons.len(),
1212        daemon_state_counts,
1213        bus_events_published: bus_stats.events_published,
1214        bus_subscriber_count: bus_stats.active_subscribers as usize,
1215        session_memory_count: s.scoped_sessions.total_memory_count(),
1216        session_store_count: s.scoped_sessions.total_store_count(),
1217        flows_tracked: s.versions.flow_count(),
1218        versions_total: s.versions.total_versions(),
1219        rate_limiter_enabled: rl_config.enabled,
1220        rate_limiter_max_requests: rl_config.max_requests,
1221        rate_limiter_window_secs: rl_config.window.as_secs(),
1222        request_log_enabled: log_config.enabled,
1223        request_log_entries: s.request_logger.len(),
1224        request_log_capacity: log_config.capacity,
1225        api_keys_enabled: s.api_keys.is_enabled(),
1226        api_keys_active: s.api_keys.active_count(),
1227        api_keys_total: s.api_keys.total_count(),
1228        webhooks_active: wh_stats.active_webhooks,
1229        webhooks_total: wh_stats.total_webhooks,
1230        webhooks_total_failures: wh_stats.total_failures,
1231        audit_log_entries: s.audit_log.len(),
1232        audit_log_total_recorded: s.audit_log.total_recorded(),
1233    }
1234}
1235
1236/// GET /v1/health — full health report with component checks.
1237async fn health_handler(State(state): State<SharedState>) -> Json<serde_json::Value> {
1238    let s = state.lock().unwrap();
1239    let input = build_health_input(&s);
1240    let report = crate::health_check::evaluate(&input);
1241    Json(serde_json::to_value(&report).unwrap_or_default())
1242}
1243
1244/// GET /v1/health/live — liveness probe (always alive if responding).
1245async fn health_live_handler() -> Json<serde_json::Value> {
1246    Json(crate::health_check::liveness())
1247}
1248
1249/// GET /v1/health/ready — readiness probe (ready if no component is unhealthy).
1250async fn health_ready_handler(State(state): State<SharedState>) -> Json<serde_json::Value> {
1251    let s = state.lock().unwrap();
1252    let input = build_health_input(&s);
1253    Json(crate::health_check::readiness(&input))
1254}
1255
1256/// GET /v1/health/components — component-level health checks.
1257///
1258/// Reports individual health status for: trace_store, event_bus,
1259/// supervisor, schedules, audit_log, rate_limiter.
1260async fn health_components_handler(
1261    State(state): State<SharedState>,
1262) -> Json<serde_json::Value> {
1263    let s = state.lock().unwrap();
1264
1265    let mut components = Vec::new();
1266    let mut overall = "healthy";
1267
1268    // ── Trace store ──
1269    let ts_status = if !s.trace_store.config().enabled {
1270        "disabled"
1271    } else if s.trace_store.len() >= s.trace_store.config().capacity {
1272        overall = if overall == "healthy" { "degraded" } else { overall };
1273        "degraded"
1274    } else {
1275        "healthy"
1276    };
1277    components.push(serde_json::json!({
1278        "name": "trace_store",
1279        "status": ts_status,
1280        "details": {
1281            "enabled": s.trace_store.config().enabled,
1282            "buffered": s.trace_store.len(),
1283            "capacity": s.trace_store.config().capacity,
1284            "total_recorded": s.trace_store.total_recorded(),
1285            "utilization_pct": if s.trace_store.config().capacity > 0 {
1286                (s.trace_store.len() as f64 / s.trace_store.config().capacity as f64 * 100.0) as u64
1287            } else { 0 },
1288        },
1289    }));
1290
1291    // ── Event bus ──
1292    let bus_stats = s.event_bus.stats();
1293    let bus_status = if bus_stats.events_dropped > 0 { "degraded" } else { "healthy" };
1294    if bus_status == "degraded" && overall == "healthy" {
1295        overall = "degraded";
1296    }
1297    components.push(serde_json::json!({
1298        "name": "event_bus",
1299        "status": bus_status,
1300        "details": {
1301            "topics_seen": bus_stats.topics_seen.len(),
1302            "events_published": bus_stats.events_published,
1303            "events_delivered": bus_stats.events_delivered,
1304            "events_dropped": bus_stats.events_dropped,
1305            "active_subscribers": bus_stats.active_subscribers,
1306        },
1307    }));
1308
1309    // ── Supervisor ──
1310    let sup_counts = s.supervisor.state_counts();
1311    let dead = sup_counts.get("dead").copied().unwrap_or(0);
1312    let sup_status = if dead > 0 {
1313        overall = "degraded";
1314        "degraded"
1315    } else {
1316        "healthy"
1317    };
1318    components.push(serde_json::json!({
1319        "name": "supervisor",
1320        "status": sup_status,
1321        "details": {
1322            "registered": s.supervisor.list().len(),
1323            "state_counts": sup_counts,
1324            "dead": dead,
1325        },
1326    }));
1327
1328    // ── Schedules ──
1329    let sched_total = s.schedules.len();
1330    let sched_enabled = s.schedules.values().filter(|e| e.enabled).count();
1331    let sched_errors: u64 = s.schedules.values().map(|e| e.error_count).sum();
1332    let sched_status = if sched_errors > 0 { "degraded" } else { "healthy" };
1333    if sched_status == "degraded" && overall == "healthy" {
1334        overall = "degraded";
1335    }
1336    components.push(serde_json::json!({
1337        "name": "schedules",
1338        "status": sched_status,
1339        "details": {
1340            "total": sched_total,
1341            "enabled": sched_enabled,
1342            "total_runs": s.schedules.values().map(|e| e.run_count).sum::<u64>(),
1343            "total_errors": sched_errors,
1344        },
1345    }));
1346
1347    // ── Audit log ──
1348    let audit_status = "healthy";
1349    components.push(serde_json::json!({
1350        "name": "audit_log",
1351        "status": audit_status,
1352        "details": {
1353            "buffered": s.audit_log.len(),
1354            "capacity": s.audit_log.capacity(),
1355        },
1356    }));
1357
1358    // ── Rate limiter ──
1359    let rl_config = s.rate_limiter.config();
1360    let rl_status = if rl_config.enabled { "healthy" } else { "disabled" };
1361    components.push(serde_json::json!({
1362        "name": "rate_limiter",
1363        "status": rl_status,
1364        "details": {
1365            "enabled": rl_config.enabled,
1366            "max_requests": rl_config.max_requests,
1367            "window_secs": rl_config.window.as_secs(),
1368        },
1369    }));
1370
1371    let healthy_count = components.iter().filter(|c| c["status"] == "healthy").count();
1372    let degraded_count = components.iter().filter(|c| c["status"] == "degraded").count();
1373    let disabled_count = components.iter().filter(|c| c["status"] == "disabled").count();
1374
1375    Json(serde_json::json!({
1376        "overall": overall,
1377        "components_total": components.len(),
1378        "healthy": healthy_count,
1379        "degraded": degraded_count,
1380        "disabled": disabled_count,
1381        "components": components,
1382    }))
1383}
1384
1385/// GET /v1/version
1386async fn version_handler() -> Json<serde_json::Value> {
1387    Json(serde_json::json!({
1388        "axon_version": AXON_VERSION,
1389        "server": "axon-serve",
1390        "runtime": "native",
1391        "api_version": "v1",
1392    }))
1393}
1394
1395/// GET /v1/uptime — detailed server uptime information.
1396async fn uptime_handler(
1397    State(state): State<SharedState>,
1398) -> Json<serde_json::Value> {
1399    let s = state.lock().unwrap();
1400    let uptime_secs = s.started_at.elapsed().as_secs();
1401    let now_wall = std::time::SystemTime::now()
1402        .duration_since(std::time::UNIX_EPOCH)
1403        .unwrap_or_default()
1404        .as_secs();
1405    let start_timestamp = now_wall.saturating_sub(uptime_secs);
1406
1407    let days = uptime_secs / 86400;
1408    let hours = (uptime_secs % 86400) / 3600;
1409    let minutes = (uptime_secs % 3600) / 60;
1410    let secs = uptime_secs % 60;
1411    let formatted = format!("{}d {}h {}m {}s", days, hours, minutes, secs);
1412
1413    let requests_per_minute = if uptime_secs > 0 {
1414        (s.metrics.total_requests as f64 / uptime_secs as f64) * 60.0
1415    } else {
1416        0.0
1417    };
1418
1419    // Uptime buckets: what percentage of time has been "up" in each hour bracket
1420    let total_hours = (uptime_secs as f64 / 3600.0).ceil() as u64;
1421    let buckets: Vec<serde_json::Value> = (0..total_hours.min(24)).map(|h| {
1422        let bucket_start = h * 3600;
1423        let bucket_end = ((h + 1) * 3600).min(uptime_secs);
1424        let bucket_duration = bucket_end.saturating_sub(bucket_start);
1425        serde_json::json!({
1426            "hour": h,
1427            "duration_secs": bucket_duration,
1428            "pct_of_hour": (bucket_duration as f64 / 3600.0 * 100.0).min(100.0),
1429        })
1430    }).collect();
1431
1432    Json(serde_json::json!({
1433        "uptime_secs": uptime_secs,
1434        "uptime_formatted": formatted,
1435        "start_timestamp": start_timestamp,
1436        "total_requests": s.metrics.total_requests,
1437        "total_errors": s.metrics.total_errors,
1438        "requests_per_minute": (requests_per_minute * 100.0).round() / 100.0,
1439        "daemons_active": s.daemons.len(),
1440        "traces_buffered": s.trace_store.len(),
1441        "schedules_active": s.schedules.values().filter(|e| e.enabled).count(),
1442        "hourly_buckets": buckets,
1443    }))
1444}
1445
1446/// GET /v1/metrics
1447async fn metrics_handler(
1448    State(state): State<SharedState>,
1449    headers: HeaderMap,
1450) -> Result<Json<serde_json::Value>, StatusCode> {
1451    let s = state.lock().unwrap();
1452    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
1453
1454    let uptime = s.started_at.elapsed().as_secs();
1455    let bus_stats = s.event_bus.stats();
1456    Ok(Json(serde_json::json!({
1457        "uptime_secs": uptime,
1458        "total_requests": s.metrics.total_requests,
1459        "total_deployments": s.metrics.total_deployments,
1460        "total_errors": s.metrics.total_errors,
1461        "active_daemons": s.daemons.len(),
1462        "daemon_names": s.daemons.keys().collect::<Vec<_>>(),
1463        "bus_events_published": bus_stats.events_published,
1464        "bus_topics_seen": bus_stats.topics_seen,
1465        "supervisor_summary": s.supervisor.summary(),
1466        "session_memory_count": s.scoped_sessions.total_memory_count(),
1467        "session_store_count": s.scoped_sessions.total_store_count(),
1468    })))
1469}
1470
1471/// GET /v1/metrics/prometheus — server metrics in Prometheus exposition format.
1472async fn metrics_prometheus_handler(
1473    State(state): State<SharedState>,
1474    headers: HeaderMap,
1475) -> Result<String, StatusCode> {
1476    let mut s = state.lock().unwrap();
1477    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
1478
1479    let bus_stats = s.event_bus.stats();
1480
1481    let mut daemon_states: HashMap<String, u32> = HashMap::new();
1482    for d in s.daemons.values() {
1483        let state_name = format!("{:?}", d.state).to_lowercase();
1484        *daemon_states.entry(state_name).or_insert(0) += 1;
1485    }
1486
1487    let snap = crate::server_metrics::ServerSnapshot {
1488        uptime_secs: s.started_at.elapsed().as_secs(),
1489        server_start_timestamp: {
1490            let now_wall = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
1491            now_wall.saturating_sub(s.started_at.elapsed().as_secs())
1492        },
1493        total_requests: s.metrics.total_requests,
1494        total_deployments: s.metrics.total_deployments,
1495        total_errors: s.metrics.total_errors,
1496        active_daemons: s.daemons.len() as u32,
1497        daemon_states,
1498        daemon_metrics: s.daemons.values().map(|d| crate::server_metrics::DaemonMetric {
1499            name: d.name.clone(),
1500            state: format!("{:?}", d.state).to_lowercase(),
1501            event_count: d.event_count,
1502            restart_count: d.restart_count,
1503        }).collect(),
1504        daemon_total_restarts: s.daemons.values().map(|d| d.restart_count as u64).sum(),
1505        daemon_total_events: s.daemons.values().map(|d| d.event_count).sum(),
1506        bus_events_published: bus_stats.events_published,
1507        bus_events_delivered: bus_stats.events_delivered,
1508        bus_events_dropped: bus_stats.events_dropped,
1509        bus_topics_seen: bus_stats.topics_seen.len(),
1510        bus_active_subscribers: bus_stats.active_subscribers as usize,
1511        bus_topic_metrics: bus_stats.topic_publish_counts.iter().map(|(topic, &count)| {
1512            crate::server_metrics::TopicMetric { topic: topic.clone(), published: count }
1513        }).collect(),
1514        flows_tracked: s.versions.flow_count(),
1515        versions_total: s.versions.total_versions(),
1516        session_memory_count: s.scoped_sessions.total_memory_count(),
1517        session_store_count: s.scoped_sessions.total_store_count(),
1518        deploy_count: s.deploy_count,
1519        // Rate limiter
1520        rate_limiter_enabled: s.rate_limiter.config().enabled,
1521        rate_limiter_clients: s.rate_limiter.client_count(),
1522        rate_limiter_max_requests: s.rate_limiter.config().max_requests,
1523        rate_limiter_window_secs: s.rate_limiter.config().window.as_secs(),
1524        rate_limiter_client_metrics: s.rate_limiter.client_metrics().iter().map(|cm| {
1525            crate::server_metrics::ClientRateLimitMetric {
1526                client_key: cm.client_key.clone(),
1527                total_requests: cm.total_requests,
1528                rejected: cm.rejected,
1529            }
1530        }).collect(),
1531        // Request log
1532        request_log_enabled: s.request_logger.config().enabled,
1533        request_log_buffered: s.request_logger.len(),
1534        request_log_capacity: s.request_logger.config().capacity,
1535        request_log_total: s.request_logger.total_requests(),
1536        request_log_errors: s.request_logger.stats().total_errors,
1537        // API keys
1538        api_keys_enabled: s.api_keys.is_enabled(),
1539        api_keys_active: s.api_keys.active_count(),
1540        api_keys_total: s.api_keys.total_count(),
1541        // Webhooks
1542        webhooks_total: s.webhooks.count(),
1543        webhooks_active: s.webhooks.active_count(),
1544        webhooks_deliveries_total: s.webhooks.stats().total_deliveries,
1545        webhooks_failures_total: s.webhooks.stats().total_failures,
1546        // Audit trail
1547        audit_buffered: s.audit_log.len(),
1548        audit_total_recorded: s.audit_log.total_recorded(),
1549        // Request middleware
1550        middleware_enabled: s.middleware_config.enabled,
1551        middleware_requests_total: s.request_id_gen.count(),
1552        middleware_slow_threshold_ms: s.middleware_config.slow_threshold_ms,
1553        // CORS
1554        cors_enabled: s.cors_config.enabled,
1555        cors_permissive: s.cors_config.is_permissive(),
1556        // Trace store
1557        trace_enabled: s.trace_store.config().enabled,
1558        trace_buffered: s.trace_store.len(),
1559        trace_capacity: s.trace_store.config().capacity,
1560        trace_total_recorded: s.trace_store.total_recorded(),
1561        trace_total_executions: s.trace_store.total_recorded(),
1562        trace_total_errors: {
1563            let stats = s.trace_store.stats();
1564            stats.total_errors as u64
1565        },
1566        flow_metrics: {
1567            let entries = s.trace_store.recent(s.trace_store.len(), None);
1568            let mut fm_map: HashMap<String, (u64, u64, u64)> = HashMap::new(); // (count, errors, total_lat)
1569            for e in &entries {
1570                let entry = fm_map.entry(e.flow_name.clone()).or_insert((0, 0, 0));
1571                entry.0 += 1;
1572                entry.1 += e.errors as u64;
1573                entry.2 += e.latency_ms;
1574            }
1575            fm_map.into_iter().map(|(name, (count, errs, lat))| {
1576                crate::server_metrics::FlowMetric {
1577                    flow_name: name,
1578                    executions: count,
1579                    errors: errs,
1580                    avg_latency_ms: if count > 0 { lat / count } else { 0 },
1581                }
1582            }).collect()
1583        },
1584        // Schedules
1585        schedules_total: s.schedules.len(),
1586        schedules_enabled: s.schedules.values().filter(|e| e.enabled).count(),
1587        schedules_total_runs: s.schedules.values().map(|e| e.run_count).sum(),
1588        schedules_total_errors: s.schedules.values().map(|e| e.error_count).sum(),
1589        schedules_avg_interval_secs: if s.schedules.is_empty() {
1590            0
1591        } else {
1592            s.schedules.values().map(|e| e.interval_secs).sum::<u64>() / s.schedules.len() as u64
1593        },
1594        // Shutdown
1595        shutdown_initiated: s.shutdown.as_ref().map_or(false, |c| c.is_triggered()),
1596    };
1597
1598    Ok(crate::server_metrics::to_prometheus(&snap))
1599}
1600
1601/// Deploy request payload.
1602#[derive(Debug, Deserialize)]
1603pub struct DeployRequest {
1604    /// AXON source code to compile and deploy.
1605    pub source: String,
1606    /// Optional filename for error messages.
1607    #[serde(default)]
1608    pub filename: String,
1609    /// Deploy-scoped execution backend. §Fase 36.e (D3) — when set to
1610    /// an explicit, concrete value this becomes the declared backend
1611    /// for every deployed `axonendpoint` route that did not declare
1612    /// its own `backend:`. The default is `"auto"` (transparent — the
1613    /// routes resolve down the Fase 36 D1 ladder; D5 forbids a silent
1614    /// degradation to a no-op). Pre-36 the default was `"anthropic"`,
1615    /// but the dynamic-route execution path never consulted this
1616    /// field, so the change cannot regress a working deploy (D9).
1617    #[serde(default = "default_backend")]
1618    pub backend: String,
1619}
1620
1621fn default_backend() -> String {
1622    "auto".to_string()
1623}
1624
1625/// POST /v1/deploy
1626async fn deploy_handler(
1627    State(state): State<SharedState>,
1628    headers: HeaderMap,
1629    Json(payload): Json<DeployRequest>,
1630) -> Result<Json<serde_json::Value>, StatusCode> {
1631    let req_start = Instant::now();
1632    let client = client_key_from_headers(&headers);
1633    {
1634        let mut s = state.lock().unwrap();
1635        check_auth(&mut s, &headers, AccessLevel::Write)?;
1636        check_rate_limit(&mut s, &headers)?;
1637    }
1638
1639    // Compile the source
1640    let source = payload.source.clone();
1641    let filename = if payload.filename.is_empty() {
1642        "deploy.axon".to_string()
1643    } else {
1644        payload.filename
1645    };
1646
1647    // Lex → Parse → TypeCheck → IR
1648    let tokens = match crate::lexer::Lexer::new(&source, &filename).tokenize() {
1649        Ok(t) => t,
1650        Err(e) => {
1651            let mut s = state.lock().unwrap();
1652            s.metrics.total_errors += 1;
1653            return Ok(Json(serde_json::json!({
1654                "success": false,
1655                "error": format!("lex error: {e:?}"),
1656                "phase": "lexer",
1657            })));
1658        }
1659    };
1660
1661    let mut parser = crate::parser::Parser::new(tokens);
1662    let mut program = match parser.parse() {
1663        Ok(p) => p,
1664        Err(e) => {
1665            let mut s = state.lock().unwrap();
1666            s.metrics.total_errors += 1;
1667            return Ok(Json(serde_json::json!({
1668                "success": false,
1669                "error": format!("parse error: {e:?}"),
1670                "phase": "parser",
1671            })));
1672        }
1673    };
1674
1675    let type_errors = crate::type_checker::TypeChecker::new(&program).check();
1676    if !type_errors.is_empty() {
1677        let mut s = state.lock().unwrap();
1678        s.metrics.total_errors += 1;
1679        let msgs: Vec<String> = type_errors.iter().map(|e| format!("{e:?}")).collect();
1680        return Ok(Json(serde_json::json!({
1681            "success": false,
1682            "error": msgs.join("; "),
1683            "phase": "type_checker",
1684            "error_count": type_errors.len(),
1685        })));
1686    }
1687
1688    // §Fase 31.b — populate AxonEndpoint.implicit_transport on the
1689    // AST so downstream consumers (route registration, runtime
1690    // classifier, audit log) see the type-driven inference. Mutates
1691    // program in place; idempotent.
1692    crate::type_checker::compute_implicit_transports(&mut program);
1693
1694    // §Fase 32.b — Collect dynamic routes from the program's
1695    // AxonEndpoint declarations + detect intra-program path
1696    // collisions (D2 within deploy). The collected table is merged
1697    // into ServerState.dynamic_routes below; cross-deploy collisions
1698    // are detected by the merge step.
1699    let mut incoming_routes = match collect_axonendpoint_routes(&program, &source, &filename) {
1700        Ok(r) => r,
1701        Err(msg) => {
1702            let mut s = state.lock().unwrap();
1703            s.metrics.total_errors += 1;
1704            return Ok(Json(serde_json::json!({
1705                "success": false,
1706                "error": msg,
1707                "phase": "route_registration",
1708                "d_letter": "D2",
1709            })));
1710        }
1711    };
1712
1713    // §Fase 36.e (D3) — stop discarding `DeployRequest.backend`.
1714    apply_deploy_backend_default(&mut incoming_routes, &payload.backend);
1715
1716    // §Fase 36.k (D10) — deploy-time backend resolution check. Run the
1717    // D1 ladder for every collected route against the server's CURRENT
1718    // environment (operator-tuned registry scores + provider API keys
1719    // + server default). A route whose backend cannot be resolved will
1720    // fail with a structured HTTP 503 at request time (36.h);
1721    // surfacing it in the deploy response lets the adopter learn at
1722    // deploy, not at the first production request. Non-blocking — the
1723    // deploy still succeeds (the operator may set a provider key or
1724    // populate the backend registry afterwards).
1725    let backend_deploy_warnings: Vec<serde_json::Value> = {
1726        let (registry_ranked, server_default): (Vec<String>, Option<String>) = {
1727            let s = state.lock().unwrap();
1728            (
1729                compute_backend_scores(&s, "balanced")
1730                    .into_iter()
1731                    .map(|bs| bs.name)
1732                    .collect(),
1733                s.config.default_backend.clone(),
1734            )
1735        };
1736        let env_available = crate::backends::env_available_backends();
1737        let mut warns: Vec<serde_json::Value> = incoming_routes
1738            .iter()
1739            .filter(|(_, route)| {
1740                resolve_route_backend(
1741                    route,
1742                    registry_ranked.clone(),
1743                    env_available.clone(),
1744                    server_default.clone(),
1745                )
1746                .is_err()
1747            })
1748            .map(|((method, path), route)| {
1749                serde_json::json!({
1750                    "code": "no_resolvable_backend",
1751                    "d_letter": "D10",
1752                    "endpoint": route.endpoint_name,
1753                    "method": method,
1754                    "path": path,
1755                    "message": format!(
1756                        "axonendpoint '{}' ({method} {path}) has no \
1757                         resolvable execution backend at deploy time — \
1758                         no `backend:` declaration, no server default, \
1759                         empty backend registry, and no provider API key \
1760                         in the server environment. It will fail with \
1761                         HTTP 503 at request time. Fix: declare \
1762                         `backend:` on the axonendpoint, set a provider \
1763                         API key, pass `--backend` to `axon serve`, or \
1764                         request `backend=stub` explicitly.",
1765                        route.endpoint_name
1766                    ),
1767                })
1768            })
1769            .collect();
1770        // Deterministic order — HashMap iteration is unordered.
1771        warns.sort_by(|a, b| {
1772            let pa = a["path"].as_str().unwrap_or("");
1773            let pb = b["path"].as_str().unwrap_or("");
1774            let ma = a["method"].as_str().unwrap_or("");
1775            let mb = b["method"].as_str().unwrap_or("");
1776            pa.cmp(pb).then(ma.cmp(mb))
1777        });
1778        warns
1779    };
1780
1781    // §Fase 32.c — Capture every `type T { … }` declaration in the
1782    // deployed source. Consulted by the dynamic-route fallback handler
1783    // to validate request bodies against the axonendpoint's declared
1784    // `body:` type. Empty programs produce an empty table; routes with
1785    // no `body:` declaration skip validation entirely (D9 backwards-
1786    // compat).
1787    let incoming_types = crate::route_schema::collect_type_table(&program);
1788
1789    let ir = crate::ir_generator::IRGenerator::new().generate(&program);
1790
1791    // §Fase 37.x.g (D8) — eager deploy-time store-schema verification.
1792    // Every declared `postgresql` axonstore is resolved + introspected
1793    // NOW, against the live database — the failure of a store schema
1794    // moves from the first production request to deploy. A store
1795    // reachable at deploy whose table does not resolve FAILS the
1796    // deploy; an unreachable store is a non-fatal warning (resolution
1797    // defers to the D9 runtime self-heal — deploy stays honest, never
1798    // brittle). The successful resolutions warm the process schema
1799    // cache, so the first runtime operation is a cache hit.
1800    //
1801    // §Fase 38.j (D3 + D8) — When `ServerConfig.schemas_dir` is set,
1802    // load + merge every `*.axon-schema.json` manifest under the
1803    // directory BEFORE verification, and pass the merged manifest into
1804    // `verify_postgres_schemas_with_manifest`. This activates the
1805    // compile-time-declared store schema as the authoritative shape:
1806    // any drift between the declared columns (form a/b/c) and the live
1807    // Postgres introspection raises axon-T807 and fails the deploy.
1808    // Adopters who never set `--schemas-dir` observe ZERO behavior
1809    // change — `None` falls through to the v1.37.0 path verbatim (D5).
1810    let schemas_dir_opt = {
1811        let s = state.lock().unwrap();
1812        s.config.schemas_dir.clone()
1813    };
1814    let loaded_manifest: Option<axon_frontend::store_schema_manifest::Manifest> =
1815        match schemas_dir_opt.as_deref() {
1816            None => None,
1817            Some(dir) => {
1818                let path = std::path::Path::new(dir);
1819                match axon_frontend::store_schema_manifest::load_and_merge_manifests(path) {
1820                    Ok(m) => Some(m),
1821                    Err(e) => {
1822                        let mut s = state.lock().unwrap();
1823                        s.metrics.total_errors += 1;
1824                        return Ok(Json(serde_json::json!({
1825                            "success": false,
1826                            "error": format!(
1827                                "failed to load store-schema manifests from `{}`: {}",
1828                                dir, e,
1829                            ),
1830                            "phase": "store_schema_manifest_load",
1831                            "d_letter": "D3+D8",
1832                            "schemas_dir": dir,
1833                        })));
1834                    }
1835                }
1836            }
1837        };
1838
1839    let store_report = match crate::store::registry::StoreRegistry::build(
1840        &ir.axonstore_specs,
1841    ) {
1842        Ok(registry) => {
1843            registry
1844                .verify_postgres_schemas_with_manifest(loaded_manifest.as_ref())
1845                .await
1846        }
1847        Err(e) => {
1848            let mut s = state.lock().unwrap();
1849            s.metrics.total_errors += 1;
1850            return Ok(Json(serde_json::json!({
1851                "success": false,
1852                "error": e.to_string(),
1853                "phase": "store_registry",
1854                "d_letter": "D8",
1855            })));
1856        }
1857    };
1858    if store_report.has_fatal() {
1859        let mut s = state.lock().unwrap();
1860        s.metrics.total_errors += 1;
1861        return Ok(Json(serde_json::json!({
1862            "success": false,
1863            "error": store_report.fatal_summary(),
1864            "phase": "store_schema_verification",
1865            "d_letter": "D8",
1866            "missing_tables": store_report
1867                .missing
1868                .iter()
1869                .map(|(store, detail)| serde_json::json!({
1870                    "store": store,
1871                    "detail": detail,
1872                }))
1873                .collect::<Vec<serde_json::Value>>(),
1874        })));
1875    }
1876
1877    // Extract flow names from IR and register as daemons
1878    let flow_names: Vec<String> = ir.flows.iter().map(|f| f.name.clone()).collect();
1879    let registered: Vec<String>;
1880
1881    let version_results = {
1882        let mut s = state.lock().unwrap();
1883        s.deploy_count += 1;
1884        s.metrics.total_deployments += 1;
1885
1886        registered = flow_names
1887            .iter()
1888            .map(|name| {
1889                let daemon = DaemonInfo {
1890                    name: name.clone(),
1891                    state: DaemonState::Idle,
1892                    source_file: filename.clone(),
1893                    flow_name: name.clone(),
1894                    event_count: 0,
1895                    restart_count: 0,
1896                    trigger_topic: None,
1897                    output_topic: None,
1898                    lifecycle_events: Vec::new(),
1899                };
1900                s.daemons.insert(name.clone(), daemon);
1901
1902                // Register with supervisor
1903                s.supervisor.register(name, RestartPolicy::default());
1904
1905                name.clone()
1906            })
1907            .collect();
1908
1909        s.metrics.active_daemons = s.daemons.len() as u32;
1910
1911        // §Fase 32.b — Merge dynamic routes into live state. Cross-
1912        // deploy path collisions (D2) detected here; on collision we
1913        // return early before recording the deploy + emitting events
1914        // so the failed deploy doesn't pollute the audit trail.
1915        if let Err(msg) = merge_dynamic_routes(&mut s.dynamic_routes, incoming_routes.clone()) {
1916            s.metrics.total_errors += 1;
1917            return Ok(Json(serde_json::json!({
1918                "success": false,
1919                "error": msg,
1920                "phase": "route_registration",
1921                "d_letter": "D2",
1922            })));
1923        }
1924
1925        // §Fase 32.c — Merge the per-deploy type table into live state.
1926        // Last-wins semantics on cross-deploy name conflict (a future
1927        // type-registry fase will add deploy-scoped namespacing); routes
1928        // are atomic above this point, so the type table only updates
1929        // when route merge succeeded.
1930        for (name, schema) in &incoming_types {
1931            s.dynamic_types.insert(name.clone(), schema.clone());
1932        }
1933
1934        // Record versions
1935        let version_results = s.versions.record_deploy(
1936            &registered,
1937            &source,
1938            &filename,
1939            &payload.backend,
1940        );
1941
1942        // Emit deploy event on bus
1943        s.event_bus.publish(
1944            "deploy",
1945            serde_json::json!({
1946                "flows": &registered,
1947                "source_file": &filename,
1948                "versions": version_results.iter().map(|(n, v)| serde_json::json!({"flow": n, "version": v})).collect::<Vec<_>>(),
1949            }),
1950            "server",
1951        );
1952
1953        // Audit trail
1954        s.audit_log.record(
1955            &client,
1956            AuditAction::Deploy,
1957            &registered.join(","),
1958            serde_json::json!({"flows": &registered, "source_file": &filename}),
1959            true,
1960        );
1961
1962        version_results
1963    };
1964
1965    {
1966        let mut s = state.lock().unwrap();
1967        s.request_logger.record("POST", "/v1/deploy", 200, req_start.elapsed(), &client);
1968    }
1969
1970    // Trigger async webhook delivery for deploy event
1971    trigger_webhook_delivery(
1972        &state,
1973        "deploy",
1974        serde_json::json!({"flows": &registered, "source_file": &filename}),
1975        "server",
1976    );
1977
1978    Ok(Json(serde_json::json!({
1979        "success": true,
1980        "deployed": registered,
1981        "flow_count": registered.len(),
1982        "backend": payload.backend,
1983        "versions": version_results.iter().map(|(n, v)| serde_json::json!({"flow": n, "version": v})).collect::<Vec<serde_json::Value>>(),
1984        // §Fase 36.k (D10) — deploy-time backend resolution warnings.
1985        // Empty when every route has a resolvable backend.
1986        "warnings": backend_deploy_warnings,
1987        // §Fase 37.x.g (D8) — deploy-time store-schema warnings: a
1988        // declared postgresql store unreachable at deploy. Empty when
1989        // every declared store verified. Non-fatal — the D9 runtime
1990        // resolution still applies.
1991        "store_warnings": store_report
1992            .unreachable
1993            .iter()
1994            .map(|(store, detail)| serde_json::json!({
1995                "code": "store_unreachable_at_deploy",
1996                "d_letter": "D8",
1997                "store": store,
1998                "detail": detail,
1999            }))
2000            .collect::<Vec<serde_json::Value>>(),
2001    })))
2002}
2003
2004// ── Execute endpoint ─────────────────────────────────────────────────────
2005
2006/// Execute request payload.
2007#[derive(Debug, Deserialize)]
2008pub struct ExecuteRequest {
2009    /// Name of a deployed flow to execute.
2010    pub flow: String,
2011    /// Backend for execution (default: "stub").
2012    #[serde(default = "default_execute_backend")]
2013    pub backend: String,
2014    /// §Fase 37.b (D1) — the parsed HTTP request body, carried so the
2015    /// flow's declared parameters bind from it (the Request Binding
2016    /// Contract) on the JSON dynamic-route transport. `#[serde(default)]`
2017    /// ⇒ `None` for a legacy `/v1/execute` RPC hit that sends only
2018    /// `{flow, backend}` (D5 backwards-compat).
2019    #[serde(default)]
2020    pub request_body: Option<serde_json::Value>,
2021    /// §Fase 37.y (D3) — URL path captures from the dynamic-route
2022    /// dispatcher (e.g. `{"tenant_id": "acme"}` for a route
2023    /// `/api/tenants/{tenant_id}`). Empty for the legacy
2024    /// `/v1/execute` RPC path (D5 backwards-compat).
2025    #[serde(default)]
2026    pub request_path: HashMap<String, String>,
2027    /// §Fase 37.y (D3) — URL query string parsed name → value.
2028    /// Empty for callers without a query string.
2029    #[serde(default)]
2030    pub request_query: HashMap<String, String>,
2031    /// §Fase 39.b — the declared output type from the originating
2032    /// axonendpoint, propagated through `execute_handler` so the
2033    /// FlowEnvelope's `ontological_type` slot reflects the endpoint
2034    /// contract. Empty for legacy `/v1/execute` calls without a
2035    /// declaring endpoint — the wrapper defaults to `"Any"`.
2036    /// The dynamic-route dispatcher
2037    /// [`dispatch_dynamic_endpoint`] sets this from
2038    /// `route.output_type` so the wire-shape inner-T matches the
2039    /// adopter declaration verbatim.
2040    #[serde(default)]
2041    pub declared_output_type: String,
2042}
2043
2044fn default_execute_backend() -> String {
2045    "stub".to_string()
2046}
2047
2048/// Server-side execution result.
2049///
2050/// §Fase 39.b — promoted from `struct` to `pub struct` (and all fields
2051/// to `pub`) so the new `crate::wire_envelope::FlowEnvelope` module
2052/// can consume it as the converter input. Pre-39.b this type was
2053/// internal to `axon_server`; v2.0.0 elevates it to a crate-public
2054/// shape because it is the canonical input of the wire envelope
2055/// builder. It is intentionally NOT part of the JSON wire (the
2056/// FlowEnvelope is); it remains a runtime-internal aggregation step.
2057#[derive(Debug, Clone, Serialize)]
2058pub struct ServerExecutionResult {
2059    pub success: bool,
2060    pub flow_name: String,
2061    pub source_file: String,
2062    pub backend: String,
2063    pub steps_executed: usize,
2064    pub latency_ms: u64,
2065    pub tokens_input: u64,
2066    pub tokens_output: u64,
2067    pub anchor_checks: usize,
2068    pub anchor_breaches: usize,
2069    pub errors: usize,
2070    pub step_names: Vec<String>,
2071    pub step_results: Vec<String>,
2072    pub trace_id: u64,
2073    /// §Fase 33.e — Per-step stream-effect policies declared in the
2074    /// source. Each entry is `(step_name, policy_slug)` where slug is
2075    /// one of the closed catalog `{drop_oldest, degrade_quality,
2076    /// pause_upstream, fail}`. Empty when no step in the flow declares
2077    /// a `<stream:<policy>>` effect. Surfaced on the SSE
2078    /// `axon.complete` wire envelope so adopters can observe the
2079    /// policy is bound to runtime.
2080    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2081    pub effect_policies: Vec<(String, String)>,
2082
2083    /// §Fase 33.x.d — Per-step `EnforcementSummary` from the
2084    /// `StreamPolicyEnforcer` runs. Empty in two cases:
2085    ///   1. Legacy synchronous path (`run_streaming_legacy_path`) —
2086    ///      the enforcer is not run; the wire stays byte-identical
2087    ///      with v1.24.0 (D4 byte-compat).
2088    ///   2. Async streaming path where no step in the flow has a
2089    ///      declared `<stream:<policy>>` effect — the enforcer is
2090    ///      not constructed (no policy to enforce); D2 contract.
2091    /// Surfaced on the SSE `axon.complete` wire envelope so adopters
2092    /// can observe whether the declared policy actually fired in
2093    /// production (a `drop_oldest` policy that never fires under
2094    /// sustained load is a configuration smell).
2095    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2096    pub enforcement_summaries: Vec<(String, EnforcementSummaryWire)>,
2097
2098    /// §Fase 33.x.g — Closed-catalog runtime warnings. Populated
2099    /// only when `server_execute_streaming` falls back to the
2100    /// legacy synchronous path; carries one `axon-W002
2101    /// streaming-not-supported` warning with the specific
2102    /// `FallbackMode` tag identifying WHY. Empty on the happy
2103    /// (async-streaming-active) path = D4 byte-compat preserved
2104    /// (wire field elided when empty).
2105    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2106    pub runtime_warnings: Vec<crate::runtime_warnings::RuntimeWarning>,
2107
2108    /// §Fase 39.c.y — semantic provenance events from the runtime
2109    /// walk (`retrieve:<store>`, `shield:<name>`, `mutate:<store>`,
2110    /// etc.). Merged into the `FlowEnvelope.provenance_chain` by
2111    /// the converter. Empty for flows with no taxonomy-participating
2112    /// steps.
2113    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2114    pub provenance_events: Vec<String>,
2115
2116    /// §Fase 39.c.z — surfaced blame attribution when the flow
2117    /// proceeded on degraded posture (anchor breach / shield
2118    /// rejection / store breach / backend soft-fail / type mismatch).
2119    /// `None` on clean happy path; the converter writes this slot
2120    /// into the wire envelope's `blame_attribution` field verbatim.
2121    #[serde(default, skip_serializing_if = "Option::is_none")]
2122    pub blame_attribution: Option<crate::wire_envelope::BlameContext>,
2123
2124    /// §Fase 55.b — per-tool epistemic envelopes (`base`, `scope`,
2125    /// `confidence`) for every flow-level `use <Tool>` whose tool declares
2126    /// an `epistemic:<level>` effect. Propagated from the runner's
2127    /// IR-derived capture and written into
2128    /// `FlowEnvelope.epistemic_envelopes` by the converter; the streaming
2129    /// path derives the identical set via
2130    /// `resolve_epistemic_envelopes_for_flow`. Empty (and elided from the
2131    /// wire) for flows that dispatch no epistemic-annotated tool.
2132    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2133    pub epistemic_envelopes: Vec<crate::epistemic_capture::EpistemicEnvelope>,
2134}
2135
2136/// §Fase 33.x.d — Wire-serializable mirror of
2137/// [`crate::stream_effect_dispatcher::EnforcementSummary`] published
2138/// on `axon.complete` per the D2 contract.
2139///
2140/// `policy_slug` is the closed-catalog slug of the policy that the
2141/// enforcer ran (`drop_oldest` / `degrade_quality` / `pause_upstream`
2142/// / `fail`); `pushed`/`delivered` count chunks the enforcer's input
2143/// stream produced + the consumer drained respectively. The four
2144/// `*_hits` / `*_blocks` / `*_overflows` counters surface
2145/// policy-specific activations so adopters can verify the declared
2146/// policy actually fired (D2 contract — declaration ⟺ runtime
2147/// behavior).
2148///
2149/// All counters are `u64` so high-throughput long-running flows
2150/// don't risk overflow. `failed` is set only when the enforcer's
2151/// internal stream surfaced `BackpressurePolicy::Fail` overflow.
2152#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
2153pub struct EnforcementSummaryWire {
2154    pub policy_slug: String,
2155    pub chunks_pushed: u64,
2156    pub chunks_delivered: u64,
2157    pub drop_oldest_hits: u64,
2158    pub degrade_quality_hits: u64,
2159    pub pause_upstream_blocks: u64,
2160    pub fail_overflows: u64,
2161    pub failed: bool,
2162}
2163
2164impl EnforcementSummaryWire {
2165    /// Project from the rich internal `EnforcementSummary` (which has
2166    /// `policy: Option<&'static str>`) into the wire-stable shape.
2167    pub fn from_summary(
2168        s: &crate::stream_effect_dispatcher::EnforcementSummary,
2169    ) -> Self {
2170        Self {
2171            policy_slug: s.policy.unwrap_or("").to_string(),
2172            chunks_pushed: s.chunks_pushed,
2173            chunks_delivered: s.chunks_delivered,
2174            drop_oldest_hits: s.drop_oldest_hits,
2175            degrade_quality_hits: s.degrade_quality_hits,
2176            pause_upstream_blocks: s.pause_upstream_blocks,
2177            fail_overflows: s.fail_overflows,
2178            failed: s.failed,
2179        }
2180    }
2181}
2182
2183/// Compile and execute a deployed flow server-side.
2184///
2185/// Compiles the stored source for the named flow, builds an execution unit,
2186/// executes it via the LLM pipeline, collects metadata, and records a trace.
2187fn server_execute(
2188    source: &str,
2189    source_file: &str,
2190    flow_name: &str,
2191    backend: &str,
2192    api_key_override: Option<&str>,
2193    // §Fase 37.b (D1) — the parsed request body for the Request
2194    // Binding Contract; `None` for callers with no HTTP request body.
2195    request_body: Option<&serde_json::Value>,
2196    // §Fase 37.y (D3) — URL path captures (from axum's dynamic-route
2197    // `Path<HashMap>` extractor). Empty map for callers without a
2198    // dynamic route (D5 backwards-compat).
2199    request_path: &std::collections::HashMap<String, String>,
2200    // §Fase 37.y (D3) — URL query string parsed as name → value.
2201    // Empty map for callers with no query string.
2202    request_query: &std::collections::HashMap<String, String>,
2203) -> Result<ServerExecutionResult, String> {
2204    let start = Instant::now();
2205
2206    // Lex
2207    let tokens = crate::lexer::Lexer::new(source, source_file)
2208        .tokenize()
2209        .map_err(|e| format!("lex error: {e:?}"))?;
2210
2211    // Parse
2212    let mut parser = crate::parser::Parser::new(tokens);
2213    let program = parser
2214        .parse()
2215        .map_err(|e| format!("parse error: {e:?}"))?;
2216
2217    // Type check (non-fatal for execution — collect errors)
2218    let type_errors = crate::type_checker::TypeChecker::new(&program).check();
2219
2220    // Generate IR
2221    let ir = crate::ir_generator::IRGenerator::new().generate(&program);
2222
2223    // Execute via runner
2224    let run_res = crate::runner::execute_server_flow(
2225        &ir,
2226        flow_name,
2227        backend,
2228        source_file,
2229        api_key_override,
2230        request_body,
2231        request_path,
2232        request_query,
2233        // §Fase 58.g (D7) — the OSS server resolves relative tool
2234        // runtimes against the `AXON_TOOL_BASE_URL` env (single-tenant
2235        // per-process); the multi-tenant per-tenant override is the
2236        // enterprise executor's concern. Unset → no resolution (D5).
2237        std::env::var("AXON_TOOL_BASE_URL").ok().as_deref(),
2238    )?;
2239
2240    // Count anchors from IR
2241    let anchor_count = ir.anchors.len();
2242
2243    let latency_ms = start.elapsed().as_millis() as u64;
2244
2245    Ok(ServerExecutionResult {
2246        success: type_errors.is_empty() && run_res.success,
2247        flow_name: flow_name.to_string(),
2248        source_file: source_file.to_string(),
2249        backend: backend.to_string(),
2250        steps_executed: run_res.steps_executed,
2251        latency_ms,
2252        tokens_input: run_res.tokens_input,
2253        tokens_output: run_res.tokens_output,
2254        anchor_checks: anchor_count,
2255        anchor_breaches: run_res.anchor_breaches,
2256        errors: type_errors.len(),
2257        step_names: run_res.step_names,
2258        step_results: run_res.step_results,
2259        trace_id: 0, // set after recording
2260        effect_policies: Vec::new(), // populated by server_execute_streaming
2261        enforcement_summaries: Vec::new(), // populated by 33.x.d async path
2262        runtime_warnings: Vec::new(), // populated by 33.x.g when LEGACY path chosen
2263        // §Fase 39.c.y + 39.c.z — propagate from runtime walk.
2264        provenance_events: run_res.provenance_events,
2265        blame_attribution: run_res.blame_attribution,
2266        // §Fase 55.b — propagate the IR-derived epistemic envelopes.
2267        epistemic_envelopes: run_res.epistemic_envelopes,
2268    })
2269}
2270
2271/// POST /v1/execute — execute a deployed flow and auto-record a trace.
2272async fn execute_handler(
2273    State(state): State<SharedState>,
2274    headers: HeaderMap,
2275    Json(payload): Json<ExecuteRequest>,
2276) -> Result<Json<serde_json::Value>, StatusCode> {
2277    let req_start = Instant::now();
2278    let client = client_key_from_headers(&headers);
2279    {
2280        let mut s = state.lock().unwrap();
2281        check_auth(&mut s, &headers, AccessLevel::Write)?;
2282        check_rate_limit(&mut s, &headers)?;
2283    }
2284
2285    // Look up the deployed flow source, resolve auto-backend and key from registry
2286    let (source, source_file, effective_backend, resolved_key) = {
2287        let s = state.lock().unwrap();
2288        // Auto-backend: if "auto", use optimizer to select best backend
2289        let eff = if payload.backend == "auto" {
2290            let scores = compute_backend_scores(&s, "balanced");
2291            scores.first().map(|sc| sc.name.clone()).unwrap_or_else(|| "stub".to_string())
2292        } else {
2293            payload.backend.clone()
2294        };
2295        let history = s.versions.get_history(&payload.flow);
2296        match history.and_then(|h| h.active()) {
2297            Some(active) => {
2298                let key = resolve_backend_key(&s, &eff).ok();
2299                (active.source.clone(), active.source_file.clone(), eff, key)
2300            }
2301            None => {
2302                return Ok(Json(serde_json::json!({
2303                    "success": false,
2304                    "error": format!("flow '{}' not deployed", payload.flow),
2305                })));
2306            }
2307        }
2308    };
2309
2310    // Execute with fallback chain support (outside lock — CPU-bound compilation)
2311    // §Fase 37.b (D1) — `payload.request_body` carries the parsed
2312    // request body of a JSON dynamic-route hit; the flow's declared
2313    // parameters bind from it (the Request Binding Contract). `None`
2314    // for a legacy `/v1/execute` RPC call (D5 backwards-compat).
2315    // §Fase 37.y (D3) — `payload.request_path` + `payload.request_query`
2316    // carry the URL captures + query string parsed by the dynamic-route
2317    // dispatcher. Both default to empty (D5 backwards-compat) for the
2318    // legacy `/v1/execute` RPC path.
2319    let (result, actual_backend) = execute_with_fallback(
2320        &state, &source, &source_file, &payload.flow,
2321        &effective_backend, resolved_key.as_deref(),
2322        payload.request_body.as_ref(),
2323        &payload.request_path,
2324        &payload.request_query,
2325    );
2326
2327    match result {
2328        Ok(mut exec_result) => {
2329            // Update backend to actual (may differ if fallback was used)
2330            exec_result.backend = actual_backend.clone();
2331            // Build and record trace entry
2332            let trace_entry = crate::trace_store::build_trace(
2333                &exec_result.flow_name,
2334                &exec_result.source_file,
2335                &exec_result.backend,
2336                &client,
2337                if exec_result.success {
2338                    crate::trace_store::TraceStatus::Success
2339                } else {
2340                    crate::trace_store::TraceStatus::Partial
2341                },
2342                exec_result.steps_executed,
2343                exec_result.latency_ms,
2344            );
2345
2346            let trace_id = {
2347                let mut s = state.lock().unwrap();
2348
2349                // Record trace
2350                let mut entry = trace_entry;
2351                entry.tokens_input = exec_result.tokens_input;
2352                entry.tokens_output = exec_result.tokens_output;
2353                entry.anchor_checks = exec_result.anchor_checks;
2354                entry.anchor_breaches = exec_result.anchor_breaches;
2355                entry.errors = exec_result.errors;
2356                let trace_id = s.trace_store.record(entry);
2357
2358                // Update daemon event count
2359                if let Some(daemon) = s.daemons.get_mut(&payload.flow) {
2360                    daemon.event_count += 1;
2361                }
2362
2363                // Audit trail
2364                s.audit_log.record(
2365                    &client,
2366                    AuditAction::Execute,
2367                    &exec_result.flow_name,
2368                    serde_json::json!({
2369                        "flow": &exec_result.flow_name,
2370                        "backend": &exec_result.backend,
2371                        "success": exec_result.success,
2372                        "trace_id": trace_id,
2373                    }),
2374                    exec_result.success,
2375                );
2376
2377                // Backend call metrics
2378                record_backend_metrics(
2379                    &mut s, &exec_result.backend, exec_result.success,
2380                    exec_result.tokens_input, exec_result.tokens_output, exec_result.latency_ms,
2381                );
2382
2383                // Request log
2384                s.request_logger.record("POST", "/v1/execute", 200, req_start.elapsed(), &client);
2385
2386                trace_id
2387            };
2388
2389            exec_result.trace_id = trace_id;
2390
2391            // Emit event and trigger webhooks
2392            {
2393                let s = state.lock().unwrap();
2394                s.event_bus.publish(
2395                    "execute",
2396                    serde_json::json!({
2397                        "flow": &exec_result.flow_name,
2398                        "success": exec_result.success,
2399                        "trace_id": trace_id,
2400                        "latency_ms": exec_result.latency_ms,
2401                    }),
2402                    "server",
2403                );
2404            }
2405
2406            trigger_webhook_delivery(
2407                &state,
2408                "execute",
2409                serde_json::json!({
2410                    "flow": &exec_result.flow_name,
2411                    "success": exec_result.success,
2412                    "trace_id": trace_id,
2413                }),
2414                "server",
2415            );
2416
2417            // §Fase 39.b — wrap the legacy ServerExecutionResult into
2418            // the canonical FlowEnvelope wire shape (ψ-vector
2419            // serialization). Per D2 the wire IS the FlowEnvelope;
2420            // the .seal() invariant structurally enforces Theorem 5.1
2421            // + audit_chain_hash before serialization.
2422            //
2423            // The ontological_type slot reflects the endpoint
2424            // declaration when the call came through a dynamic-route
2425            // dispatcher (which sets `payload.declared_output_type`);
2426            // legacy `/v1/execute` calls without an originating
2427            // endpoint default to `"Any"` (singular catch-all).
2428            // `extract_inner_ontological_type` unwraps an outer
2429            // `FlowEnvelope<T>` declaration so the wire's
2430            // `ontological_type` is the inner T (the adopter's data
2431            // type, not the envelope wrapper).
2432            let ontological_type =
2433                crate::wire_envelope::extract_inner_ontological_type(
2434                    &payload.declared_output_type,
2435                );
2436            let envelope = crate::wire_envelope::FlowEnvelope::from_execution_result(
2437                exec_result,
2438                ontological_type,
2439            )
2440            .seal();
2441            Ok(Json(serde_json::to_value(&envelope).unwrap_or_default()))
2442        }
2443        Err(e) => {
2444            // Record failed trace
2445            let mut entry = crate::trace_store::build_trace(
2446                &payload.flow,
2447                &source_file,
2448                &payload.backend,
2449                &client,
2450                crate::trace_store::TraceStatus::Failed,
2451                0,
2452                req_start.elapsed().as_millis() as u64,
2453            );
2454            entry.errors = 1;
2455
2456            let trace_id = {
2457                let mut s = state.lock().unwrap();
2458                let tid = s.trace_store.record(entry);
2459                s.metrics.total_errors += 1;
2460                s.request_logger.record("POST", "/v1/execute", 500, req_start.elapsed(), &client);
2461                tid
2462            };
2463
2464            Ok(Json(serde_json::json!({
2465                "success": false,
2466                "error": e,
2467                "flow": payload.flow,
2468                "trace_id": trace_id,
2469            })))
2470        }
2471    }
2472}
2473
2474/// Estimate request payload.
2475#[derive(Debug, Deserialize)]
2476pub struct EstimateRequest {
2477    /// AXON source code to analyze.
2478    pub source: String,
2479    /// Pricing model: "sonnet" (default), "opus", or "haiku".
2480    #[serde(default = "default_estimate_model")]
2481    pub model: String,
2482}
2483
2484fn default_estimate_model() -> String {
2485    "sonnet".to_string()
2486}
2487
2488/// POST /v1/estimate — estimate execution cost for AXON source.
2489async fn estimate_handler(
2490    State(state): State<SharedState>,
2491    headers: HeaderMap,
2492    Json(payload): Json<EstimateRequest>,
2493) -> Result<Json<serde_json::Value>, StatusCode> {
2494    let req_start = Instant::now();
2495    let client = client_key_from_headers(&headers);
2496    {
2497        let mut s = state.lock().unwrap();
2498        check_auth(&mut s, &headers, AccessLevel::Write)?;
2499        check_rate_limit(&mut s, &headers)?;
2500    }
2501
2502    // Lex
2503    let tokens = match crate::lexer::Lexer::new(&payload.source, "estimate.axon").tokenize() {
2504        Ok(t) => t,
2505        Err(e) => {
2506            return Ok(Json(serde_json::json!({
2507                "success": false,
2508                "error": format!("lex error: {e:?}"),
2509                "phase": "lexer",
2510            })));
2511        }
2512    };
2513
2514    // Parse
2515    let mut parser = crate::parser::Parser::new(tokens);
2516    let program = match parser.parse() {
2517        Ok(p) => p,
2518        Err(e) => {
2519            return Ok(Json(serde_json::json!({
2520                "success": false,
2521                "error": format!("parse error: {e:?}"),
2522                "phase": "parser",
2523            })));
2524        }
2525    };
2526
2527    // Generate IR
2528    let ir = crate::ir_generator::IRGenerator::new().generate(&program);
2529
2530    // Select pricing model
2531    let pricing = match payload.model.as_str() {
2532        "opus" => crate::cost_estimator::PricingModel::opus(),
2533        "haiku" => crate::cost_estimator::PricingModel::haiku(),
2534        _ => crate::cost_estimator::PricingModel::default_sonnet(),
2535    };
2536
2537    // Estimate
2538    let report = crate::cost_estimator::estimate_program(&ir, &pricing);
2539
2540    {
2541        let mut s = state.lock().unwrap();
2542        s.request_logger.record("POST", "/v1/estimate", 200, req_start.elapsed(), &client);
2543    }
2544
2545    Ok(Json(serde_json::to_value(&report).unwrap_or_default()))
2546}
2547
2548/// GET /v1/rate-limit — check rate limit status for the calling client.
2549async fn rate_limit_status_handler(
2550    State(state): State<SharedState>,
2551    headers: HeaderMap,
2552) -> Json<serde_json::Value> {
2553    let mut s = state.lock().unwrap();
2554    let key = client_key_from_headers(&headers);
2555    let result = s.rate_limiter.peek(&key);
2556    Json(serde_json::json!({
2557        "client_key": key,
2558        "allowed": result.allowed,
2559        "remaining": result.remaining,
2560        "limit": result.limit,
2561        "reset_secs": result.reset_secs,
2562        "enabled": s.rate_limiter.config().enabled,
2563    }))
2564}
2565
2566/// GET /v1/daemons
2567async fn list_daemons_handler(
2568    State(state): State<SharedState>,
2569    headers: HeaderMap,
2570) -> Result<Json<serde_json::Value>, StatusCode> {
2571    let s = state.lock().unwrap();
2572    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
2573
2574    let daemons: Vec<&DaemonInfo> = s.daemons.values().collect();
2575
2576    Ok(Json(serde_json::json!({
2577        "daemons": daemons,
2578        "total": daemons.len(),
2579    })))
2580}
2581
2582/// GET /v1/daemons/:name
2583async fn get_daemon_handler(
2584    State(state): State<SharedState>,
2585    headers: HeaderMap,
2586    Path(name): Path<String>,
2587) -> Result<Json<serde_json::Value>, StatusCode> {
2588    let s = state.lock().unwrap();
2589    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
2590
2591    match s.daemons.get(&name) {
2592        Some(d) => Ok(Json(serde_json::to_value(d).unwrap())),
2593        None => Err(StatusCode::NOT_FOUND),
2594    }
2595}
2596
2597/// DELETE /v1/daemons/:name
2598async fn delete_daemon_handler(
2599    State(state): State<SharedState>,
2600    headers: HeaderMap,
2601    Path(name): Path<String>,
2602) -> Result<Json<serde_json::Value>, StatusCode> {
2603    let mut s = state.lock().unwrap();
2604    check_auth(&mut s, &headers, AccessLevel::Write)?;
2605
2606    let client = client_key_from_headers(&headers);
2607    match s.daemons.remove(&name) {
2608        Some(d) => {
2609            s.metrics.active_daemons = s.daemons.len() as u32;
2610            s.supervisor.unregister(&name);
2611            s.audit_log.record(&client, AuditAction::DaemonDelete, &name, serde_json::json!({"state": d.state}), true);
2612            Ok(Json(serde_json::json!({
2613                "removed": d.name,
2614                "state": d.state,
2615            })))
2616        }
2617        None => Err(StatusCode::NOT_FOUND),
2618    }
2619}
2620
2621/// POST /v1/daemons/:name/pause — pause a daemon (preserves state for resume).
2622async fn daemon_pause_handler(
2623    State(state): State<SharedState>,
2624    headers: HeaderMap,
2625    Path(name): Path<String>,
2626) -> Result<Json<serde_json::Value>, StatusCode> {
2627    let client = client_key_from_headers(&headers);
2628    let mut s = state.lock().unwrap();
2629    check_auth(&mut s, &headers, AccessLevel::Write)?;
2630
2631    match s.daemons.get_mut(&name) {
2632        Some(daemon) => {
2633            if daemon.state == DaemonState::Paused {
2634                return Ok(Json(serde_json::json!({
2635                    "success": false,
2636                    "error": "daemon is already paused",
2637                    "daemon": name,
2638                })));
2639            }
2640            if daemon.state == DaemonState::Crashed || daemon.state == DaemonState::Stopped {
2641                return Ok(Json(serde_json::json!({
2642                    "success": false,
2643                    "error": format!("cannot pause daemon in {:?} state", daemon.state),
2644                    "daemon": name,
2645                })));
2646            }
2647            let prev = daemon.state;
2648            daemon.state = DaemonState::Paused;
2649            record_lifecycle(daemon, prev, DaemonState::Paused, Some("manual pause".into()));
2650
2651            s.audit_log.record(
2652                &client, AuditAction::ConfigUpdate, &name,
2653                serde_json::json!({"action": "daemon_pause", "previous_state": format!("{:?}", prev)}),
2654                true,
2655            );
2656
2657            Ok(Json(serde_json::json!({
2658                "success": true,
2659                "daemon": name,
2660                "previous_state": format!("{:?}", prev).to_lowercase(),
2661                "state": "paused",
2662            })))
2663        }
2664        None => Ok(Json(serde_json::json!({
2665            "error": format!("daemon '{}' not found", name),
2666        }))),
2667    }
2668}
2669
2670/// POST /v1/daemons/:name/resume — resume a paused daemon.
2671async fn daemon_resume_handler(
2672    State(state): State<SharedState>,
2673    headers: HeaderMap,
2674    Path(name): Path<String>,
2675) -> Result<Json<serde_json::Value>, StatusCode> {
2676    let client = client_key_from_headers(&headers);
2677    let mut s = state.lock().unwrap();
2678    check_auth(&mut s, &headers, AccessLevel::Write)?;
2679
2680    match s.daemons.get_mut(&name) {
2681        Some(daemon) => {
2682            if daemon.state != DaemonState::Paused {
2683                return Ok(Json(serde_json::json!({
2684                    "success": false,
2685                    "error": format!("daemon is not paused (current state: {:?})", daemon.state),
2686                    "daemon": name,
2687                })));
2688            }
2689            let prev = daemon.state;
2690            daemon.state = DaemonState::Idle;
2691            record_lifecycle(daemon, prev, DaemonState::Idle, Some("manual resume".into()));
2692
2693            s.audit_log.record(
2694                &client, AuditAction::ConfigUpdate, &name,
2695                serde_json::json!({"action": "daemon_resume"}),
2696                true,
2697            );
2698
2699            Ok(Json(serde_json::json!({
2700                "success": true,
2701                "daemon": name,
2702                "state": "idle",
2703            })))
2704        }
2705        None => Ok(Json(serde_json::json!({
2706            "error": format!("daemon '{}' not found", name),
2707        }))),
2708    }
2709}
2710
2711/// POST /v1/daemons/:name/run — execute a daemon's flow with full lifecycle management.
2712///
2713/// Transitions daemon state: Idle/Waiting → Running → (Waiting on success, crash handling on failure).
2714/// Auto-records trace, updates supervisor, emits events, and records audit trail.
2715async fn daemon_run_handler(
2716    State(state): State<SharedState>,
2717    headers: HeaderMap,
2718    Path(name): Path<String>,
2719) -> Result<Json<serde_json::Value>, StatusCode> {
2720    let req_start = Instant::now();
2721    let client = client_key_from_headers(&headers);
2722    {
2723        let mut s = state.lock().unwrap();
2724        check_auth(&mut s, &headers, AccessLevel::Write)?;
2725        check_rate_limit(&mut s, &headers)?;
2726    }
2727
2728    // Look up daemon and its source
2729    let (source, source_file, flow_name, backend) = {
2730        let s = state.lock().unwrap();
2731        let daemon = match s.daemons.get(&name) {
2732            Some(d) => d,
2733            None => {
2734                return Ok(Json(serde_json::json!({
2735                    "success": false,
2736                    "error": format!("daemon '{}' not found", name),
2737                })));
2738            }
2739        };
2740
2741        let flow = daemon.flow_name.clone();
2742        let src_file = daemon.source_file.clone();
2743
2744        // Get source from version registry
2745        let history = s.versions.get_history(&flow);
2746        match history.and_then(|h| h.active()) {
2747            Some(active) => (
2748                active.source.clone(),
2749                src_file,
2750                flow,
2751                active.backend.clone(),
2752            ),
2753            None => {
2754                return Ok(Json(serde_json::json!({
2755                    "success": false,
2756                    "error": format!("no deployed source for daemon '{}'", name),
2757                })));
2758            }
2759        }
2760    };
2761
2762    // Transition to Running
2763    {
2764        let mut s = state.lock().unwrap();
2765        s.supervisor.mark_started(&name);
2766        if let Some(daemon) = s.daemons.get_mut(&name) {
2767            let prev = daemon.state;
2768            daemon.state = DaemonState::Running;
2769            record_lifecycle(daemon, prev, DaemonState::Running, None);
2770        }
2771    }
2772
2773    // Execute flow (outside lock — CPU-bound, full backend stack)
2774    let (exec_result, _) = server_execute_full(&state, &source, &source_file, &flow_name, &backend);
2775
2776    match exec_result {
2777        Ok(mut result) => {
2778            // Build and record trace
2779            let trace_entry = crate::trace_store::build_trace(
2780                &result.flow_name,
2781                &result.source_file,
2782                &result.backend,
2783                &client,
2784                if result.success {
2785                    crate::trace_store::TraceStatus::Success
2786                } else {
2787                    crate::trace_store::TraceStatus::Partial
2788                },
2789                result.steps_executed,
2790                result.latency_ms,
2791            );
2792
2793            let (trace_id, supervisor_state) = {
2794                let mut s = state.lock().unwrap();
2795
2796                // Record trace
2797                let mut entry = trace_entry;
2798                entry.tokens_input = result.tokens_input;
2799                entry.tokens_output = result.tokens_output;
2800                entry.anchor_checks = result.anchor_checks;
2801                entry.anchor_breaches = result.anchor_breaches;
2802                entry.errors = result.errors;
2803                let trace_id = s.trace_store.record(entry);
2804
2805                // Update daemon
2806                if let Some(daemon) = s.daemons.get_mut(&name) {
2807                    daemon.event_count += 1;
2808                    let prev = daemon.state;
2809                    daemon.state = DaemonState::Hibernating;
2810                    record_lifecycle(daemon, prev, DaemonState::Hibernating, Some("trigger execution complete".into()));
2811                }
2812
2813                // Supervisor: Running → Waiting (success)
2814                s.supervisor.heartbeat(&name);
2815                s.supervisor.mark_waiting(&name);
2816                let sup_state = s.supervisor.get(&name)
2817                    .map(|d| format!("{:?}", d.state))
2818                    .unwrap_or_default();
2819
2820                // Audit trail
2821                s.audit_log.record(
2822                    &client,
2823                    AuditAction::Execute,
2824                    &name,
2825                    serde_json::json!({
2826                        "daemon": &name,
2827                        "flow": &result.flow_name,
2828                        "success": result.success,
2829                        "trace_id": trace_id,
2830                    }),
2831                    result.success,
2832                );
2833
2834                s.request_logger.record("POST", &format!("/v1/daemons/{}/run", name), 200, req_start.elapsed(), &client);
2835
2836                (trace_id, sup_state)
2837            };
2838
2839            result.trace_id = trace_id;
2840
2841            // Emit events
2842            {
2843                let s = state.lock().unwrap();
2844                s.event_bus.publish(
2845                    "daemon.executed",
2846                    serde_json::json!({
2847                        "daemon": &name,
2848                        "flow": &result.flow_name,
2849                        "success": result.success,
2850                        "trace_id": trace_id,
2851                        "latency_ms": result.latency_ms,
2852                    }),
2853                    "daemon-executor",
2854                );
2855            }
2856
2857            trigger_webhook_delivery(
2858                &state,
2859                "daemon.executed",
2860                serde_json::json!({
2861                    "daemon": &name,
2862                    "flow": &result.flow_name,
2863                    "success": result.success,
2864                    "trace_id": trace_id,
2865                }),
2866                "daemon-executor",
2867            );
2868
2869            Ok(Json(serde_json::json!({
2870                "success": result.success,
2871                "daemon": name,
2872                "flow": result.flow_name,
2873                "trace_id": trace_id,
2874                "steps_executed": result.steps_executed,
2875                "latency_ms": result.latency_ms,
2876                "supervisor_state": supervisor_state,
2877                "daemon_state": "hibernating",
2878            })))
2879        }
2880        Err(e) => {
2881            // Record failed trace
2882            let mut entry = crate::trace_store::build_trace(
2883                &flow_name,
2884                &source_file,
2885                &backend,
2886                &client,
2887                crate::trace_store::TraceStatus::Failed,
2888                0,
2889                req_start.elapsed().as_millis() as u64,
2890            );
2891            entry.errors = 1;
2892
2893            let (trace_id, will_restart) = {
2894                let mut s = state.lock().unwrap();
2895                let tid = s.trace_store.record(entry);
2896                s.metrics.total_errors += 1;
2897
2898                // Report crash to supervisor
2899                let will_restart = s.supervisor.report_crash(&name, &e);
2900
2901                // Update daemon state based on supervisor decision
2902                if let Some(daemon) = s.daemons.get_mut(&name) {
2903                    daemon.event_count += 1;
2904                    daemon.restart_count += 1;
2905                    let prev = daemon.state;
2906                    let new_state = if will_restart { DaemonState::Idle } else { DaemonState::Crashed };
2907                    daemon.state = new_state;
2908                    record_lifecycle(daemon, prev, new_state, Some(e.clone()));
2909                }
2910
2911                s.audit_log.record(
2912                    &client,
2913                    AuditAction::Execute,
2914                    &name,
2915                    serde_json::json!({
2916                        "daemon": &name,
2917                        "flow": &flow_name,
2918                        "error": &e,
2919                        "trace_id": tid,
2920                        "will_restart": will_restart,
2921                    }),
2922                    false,
2923                );
2924
2925                s.request_logger.record("POST", &format!("/v1/daemons/{}/run", name), 500, req_start.elapsed(), &client);
2926
2927                (tid, will_restart)
2928            };
2929
2930            Ok(Json(serde_json::json!({
2931                "success": false,
2932                "daemon": name,
2933                "flow": flow_name,
2934                "error": e,
2935                "trace_id": trace_id,
2936                "will_restart": will_restart,
2937                "daemon_state": if will_restart { "idle" } else { "crashed" },
2938            })))
2939        }
2940    }
2941}
2942
2943// ── Daemon trigger management ─────────────────────────────────────────────
2944
2945/// Subscribe request payload.
2946#[derive(Debug, Deserialize)]
2947pub struct DaemonSubscribeRequest {
2948    /// Topic pattern to listen for (e.g., "deploy", "data.*", "*").
2949    pub topic: String,
2950}
2951
2952/// PUT /v1/daemons/:name/trigger — bind daemon to a topic trigger.
2953async fn daemon_trigger_set_handler(
2954    State(state): State<SharedState>,
2955    headers: HeaderMap,
2956    Path(name): Path<String>,
2957    Json(payload): Json<DaemonSubscribeRequest>,
2958) -> Result<Json<serde_json::Value>, StatusCode> {
2959    let mut s = state.lock().unwrap();
2960    check_auth(&mut s, &headers, AccessLevel::Write)?;
2961
2962    let client = client_key_from_headers(&headers);
2963
2964    match s.daemons.get_mut(&name) {
2965        Some(daemon) => {
2966            let old_topic = daemon.trigger_topic.clone();
2967            daemon.trigger_topic = Some(payload.topic.clone());
2968
2969            s.audit_log.record(
2970                &client,
2971                AuditAction::ConfigUpdate,
2972                &name,
2973                serde_json::json!({
2974                    "action": "trigger_set",
2975                    "daemon": &name,
2976                    "topic": &payload.topic,
2977                    "previous": old_topic,
2978                }),
2979                true,
2980            );
2981
2982            s.event_bus.publish(
2983                "daemon.trigger.set",
2984                serde_json::json!({
2985                    "daemon": &name,
2986                    "topic": &payload.topic,
2987                }),
2988                "server",
2989            );
2990
2991            Ok(Json(serde_json::json!({
2992                "daemon": name,
2993                "trigger_topic": payload.topic,
2994                "status": "bound",
2995            })))
2996        }
2997        None => Err(StatusCode::NOT_FOUND),
2998    }
2999}
3000
3001/// DELETE /v1/daemons/:name/trigger — unbind daemon from topic trigger.
3002async fn daemon_trigger_clear_handler(
3003    State(state): State<SharedState>,
3004    headers: HeaderMap,
3005    Path(name): Path<String>,
3006) -> Result<Json<serde_json::Value>, StatusCode> {
3007    let mut s = state.lock().unwrap();
3008    check_auth(&mut s, &headers, AccessLevel::Write)?;
3009
3010    let client = client_key_from_headers(&headers);
3011
3012    match s.daemons.get_mut(&name) {
3013        Some(daemon) => {
3014            let old_topic = daemon.trigger_topic.take();
3015
3016            s.audit_log.record(
3017                &client,
3018                AuditAction::ConfigUpdate,
3019                &name,
3020                serde_json::json!({
3021                    "action": "trigger_clear",
3022                    "daemon": &name,
3023                    "previous": old_topic,
3024                }),
3025                true,
3026            );
3027
3028            Ok(Json(serde_json::json!({
3029                "daemon": name,
3030                "trigger_topic": serde_json::Value::Null,
3031                "status": "unbound",
3032            })))
3033        }
3034        None => Err(StatusCode::NOT_FOUND),
3035    }
3036}
3037
3038/// GET /v1/daemons/:name/trigger — view daemon trigger config.
3039async fn daemon_trigger_get_handler(
3040    State(state): State<SharedState>,
3041    headers: HeaderMap,
3042    Path(name): Path<String>,
3043) -> Result<Json<serde_json::Value>, StatusCode> {
3044    let s = state.lock().unwrap();
3045    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3046
3047    match s.daemons.get(&name) {
3048        Some(daemon) => Ok(Json(serde_json::json!({
3049            "daemon": name,
3050            "trigger_topic": daemon.trigger_topic,
3051            "state": daemon.state,
3052            "flow_name": daemon.flow_name,
3053        }))),
3054        None => Err(StatusCode::NOT_FOUND),
3055    }
3056}
3057
3058/// GET /v1/triggers — list all daemon trigger bindings.
3059async fn triggers_list_handler(
3060    State(state): State<SharedState>,
3061    headers: HeaderMap,
3062) -> Result<Json<serde_json::Value>, StatusCode> {
3063    let s = state.lock().unwrap();
3064    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3065
3066    let triggers: Vec<serde_json::Value> = s.daemons.values()
3067        .filter(|d| d.trigger_topic.is_some())
3068        .map(|d| serde_json::json!({
3069            "daemon": d.name,
3070            "flow_name": d.flow_name,
3071            "trigger_topic": d.trigger_topic,
3072            "state": d.state,
3073            "event_count": d.event_count,
3074        }))
3075        .collect();
3076
3077    Ok(Json(serde_json::json!({
3078        "triggers": triggers,
3079        "total": triggers.len(),
3080        "total_daemons": s.daemons.len(),
3081    })))
3082}
3083
3084// ── Daemon chain management ──────────────────────────────────────────────
3085
3086/// Request to set a daemon's output chain topic.
3087#[derive(Debug, Deserialize)]
3088pub struct DaemonChainRequest {
3089    /// Topic to publish execution result to.
3090    pub topic: String,
3091}
3092
3093/// PUT /v1/daemons/:name/chain — set the output topic for daemon chaining.
3094async fn daemon_chain_set_handler(
3095    State(state): State<SharedState>,
3096    headers: HeaderMap,
3097    Path(name): Path<String>,
3098    Json(payload): Json<DaemonChainRequest>,
3099) -> Result<Json<serde_json::Value>, StatusCode> {
3100    let mut s = state.lock().unwrap();
3101    check_auth(&mut s, &headers, AccessLevel::Write)?;
3102
3103    match s.daemons.get_mut(&name) {
3104        Some(daemon) => {
3105            daemon.output_topic = Some(payload.topic.clone());
3106            Ok(Json(serde_json::json!({
3107                "daemon": name,
3108                "output_topic": payload.topic,
3109                "status": "chained",
3110            })))
3111        }
3112        None => Ok(Json(serde_json::json!({
3113            "error": format!("daemon '{}' not found", name),
3114        }))),
3115    }
3116}
3117
3118/// DELETE /v1/daemons/:name/chain — remove the output topic.
3119async fn daemon_chain_clear_handler(
3120    State(state): State<SharedState>,
3121    headers: HeaderMap,
3122    Path(name): Path<String>,
3123) -> Result<Json<serde_json::Value>, StatusCode> {
3124    let mut s = state.lock().unwrap();
3125    check_auth(&mut s, &headers, AccessLevel::Write)?;
3126
3127    match s.daemons.get_mut(&name) {
3128        Some(daemon) => {
3129            daemon.output_topic = None;
3130            Ok(Json(serde_json::json!({
3131                "daemon": name,
3132                "output_topic": serde_json::Value::Null,
3133                "status": "unchained",
3134            })))
3135        }
3136        None => Ok(Json(serde_json::json!({
3137            "error": format!("daemon '{}' not found", name),
3138        }))),
3139    }
3140}
3141
3142/// GET /v1/daemons/:name/chain — get the output topic for a daemon.
3143async fn daemon_chain_get_handler(
3144    State(state): State<SharedState>,
3145    headers: HeaderMap,
3146    Path(name): Path<String>,
3147) -> Result<Json<serde_json::Value>, StatusCode> {
3148    let s = state.lock().unwrap();
3149    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3150
3151    match s.daemons.get(&name) {
3152        Some(daemon) => Ok(Json(serde_json::json!({
3153            "daemon": name,
3154            "output_topic": daemon.output_topic,
3155        }))),
3156        None => Ok(Json(serde_json::json!({
3157            "error": format!("daemon '{}' not found", name),
3158        }))),
3159    }
3160}
3161
3162/// Request body for event replay.
3163#[derive(Debug, Deserialize)]
3164pub struct ReplayEventsRequest {
3165    /// Topic filter (exact, prefix with .*, or * for all).
3166    pub topic: String,
3167    /// Max events to replay (default 10).
3168    #[serde(default = "default_replay_limit")]
3169    pub limit: usize,
3170}
3171
3172fn default_replay_limit() -> usize { 10 }
3173
3174/// POST /v1/triggers/replay — replay historical events to re-trigger daemons.
3175async fn triggers_replay_handler(
3176    State(state): State<SharedState>,
3177    headers: HeaderMap,
3178    Json(payload): Json<ReplayEventsRequest>,
3179) -> Result<Json<serde_json::Value>, StatusCode> {
3180    let client = client_key_from_headers(&headers);
3181    {
3182        let mut s = state.lock().unwrap();
3183        check_auth(&mut s, &headers, AccessLevel::Write)?;
3184    }
3185
3186    let limit = if payload.limit == 0 { 10 } else { payload.limit.min(50) };
3187
3188    // Get matching events from history
3189    let events = {
3190        let s = state.lock().unwrap();
3191        s.event_bus.recent_events(limit, Some(&payload.topic))
3192    };
3193
3194    if events.is_empty() {
3195        return Ok(Json(serde_json::json!({
3196            "replayed": 0,
3197            "topic_filter": payload.topic,
3198            "message": "no matching events in history",
3199        })));
3200    }
3201
3202    // Re-publish each event
3203    let mut replayed = Vec::new();
3204    for ev in &events {
3205        let s = state.lock().unwrap();
3206        s.event_bus.publish(&ev.topic, ev.payload.clone(), &format!("replay:{}", ev.source));
3207        replayed.push(serde_json::json!({
3208            "topic": ev.topic,
3209            "source": ev.source,
3210            "original_timestamp": ev.timestamp_secs,
3211        }));
3212    }
3213
3214    // Audit
3215    {
3216        let mut s = state.lock().unwrap();
3217        s.audit_log.record(
3218            &client, AuditAction::Execute, "triggers_replay",
3219            serde_json::json!({"topic_filter": payload.topic, "replayed": replayed.len()}),
3220            true,
3221        );
3222    }
3223
3224    Ok(Json(serde_json::json!({
3225        "replayed": replayed.len(),
3226        "topic_filter": payload.topic,
3227        "events": replayed,
3228    })))
3229}
3230
3231/// Query parameters for event history.
3232#[derive(Debug, Deserialize)]
3233pub struct EventHistoryQuery {
3234    /// Max events to return (default 50).
3235    #[serde(default = "default_event_history_limit")]
3236    pub limit: usize,
3237    /// Optional topic filter.
3238    pub topic: Option<String>,
3239}
3240
3241fn default_event_history_limit() -> usize { 50 }
3242
3243/// GET /v1/events/history — view recent event bus history.
3244async fn events_history_handler(
3245    State(state): State<SharedState>,
3246    headers: HeaderMap,
3247    Query(params): Query<EventHistoryQuery>,
3248) -> Result<Json<serde_json::Value>, StatusCode> {
3249    let s = state.lock().unwrap();
3250    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3251
3252    let events = s.event_bus.recent_events(params.limit, params.topic.as_deref());
3253
3254    let entries: Vec<serde_json::Value> = events.iter().map(|ev| {
3255        serde_json::json!({
3256            "topic": ev.topic,
3257            "source": ev.source,
3258            "timestamp": ev.timestamp_secs,
3259            "payload": ev.payload,
3260        })
3261    }).collect();
3262
3263    Ok(Json(serde_json::json!({
3264        "count": entries.len(),
3265        "topic_filter": params.topic,
3266        "events": entries,
3267    })))
3268}
3269
3270/// GET /v1/daemons/:name/events — lifecycle events for a daemon.
3271async fn daemon_events_handler(
3272    State(state): State<SharedState>,
3273    headers: HeaderMap,
3274    Path(name): Path<String>,
3275    Query(params): Query<std::collections::HashMap<String, String>>,
3276) -> Result<Json<serde_json::Value>, StatusCode> {
3277    let s = state.lock().unwrap();
3278    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3279
3280    match s.daemons.get(&name) {
3281        Some(daemon) => {
3282            let limit: usize = params.get("limit")
3283                .and_then(|v| v.parse().ok())
3284                .unwrap_or(100);
3285
3286            let events: Vec<&DaemonLifecycleEvent> = daemon.lifecycle_events.iter().rev().take(limit).collect();
3287            Ok(Json(serde_json::json!({
3288                "daemon": name,
3289                "state": daemon.state,
3290                "total_events": daemon.lifecycle_events.len(),
3291                "events": events,
3292            })))
3293        }
3294        None => Ok(Json(serde_json::json!({
3295            "error": format!("daemon '{}' not found", name),
3296        }))),
3297    }
3298}
3299
3300/// GET /v1/chains — list all daemon chains (trigger → daemon → output).
3301async fn chains_list_handler(
3302    State(state): State<SharedState>,
3303    headers: HeaderMap,
3304) -> Result<Json<serde_json::Value>, StatusCode> {
3305    let s = state.lock().unwrap();
3306    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3307
3308    let chains: Vec<serde_json::Value> = s.daemons.values()
3309        .filter(|d| d.trigger_topic.is_some() || d.output_topic.is_some())
3310        .map(|d| serde_json::json!({
3311            "daemon": d.name,
3312            "flow": d.flow_name,
3313            "trigger_topic": d.trigger_topic,
3314            "output_topic": d.output_topic,
3315            "state": d.state,
3316        }))
3317        .collect();
3318
3319    Ok(Json(serde_json::json!({
3320        "chains": chains,
3321        "total": chains.len(),
3322    })))
3323}
3324
3325/// Query parameters for chain graph export.
3326#[derive(Debug, Deserialize)]
3327pub struct ChainGraphQuery {
3328    /// Graph format: "dot" (default) or "mermaid".
3329    #[serde(default = "default_chain_graph_format")]
3330    pub format: String,
3331}
3332
3333fn default_chain_graph_format() -> String { "dot".to_string() }
3334
3335/// GET /v1/chains/graph — export daemon chain topology as DOT or Mermaid.
3336///
3337/// Builds a directed graph where:
3338/// - Topic nodes are ellipses (DOT) or circles (Mermaid)
3339/// - Daemon nodes are boxes (DOT) or rectangles (Mermaid)
3340/// - Edges: topic → daemon (trigger) and daemon → topic (output)
3341/// - Daemon state shown as label suffix
3342async fn chains_graph_handler(
3343    State(state): State<SharedState>,
3344    headers: HeaderMap,
3345    Query(params): Query<ChainGraphQuery>,
3346) -> Result<(StatusCode, HeaderMap, String), StatusCode> {
3347    let s = state.lock().unwrap();
3348    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3349
3350    // Collect all topics and daemons involved in chains
3351    let mut topics: std::collections::HashSet<String> = std::collections::HashSet::new();
3352    let mut edges: Vec<(String, String, &str)> = Vec::new(); // (from, to, label)
3353
3354    for d in s.daemons.values() {
3355        if let Some(ref trigger) = d.trigger_topic {
3356            topics.insert(trigger.clone());
3357            edges.push((
3358                format!("topic:{}", trigger),
3359                format!("daemon:{}", d.name),
3360                "triggers",
3361            ));
3362        }
3363        if let Some(ref output) = d.output_topic {
3364            topics.insert(output.clone());
3365            edges.push((
3366                format!("daemon:{}", d.name),
3367                format!("topic:{}", output),
3368                "outputs",
3369            ));
3370        }
3371    }
3372
3373    // Collect daemons that participate in chains
3374    let chain_daemons: Vec<&DaemonInfo> = s.daemons.values()
3375        .filter(|d| d.trigger_topic.is_some() || d.output_topic.is_some())
3376        .collect();
3377
3378    let is_mermaid = params.format.to_lowercase() == "mermaid";
3379
3380    let body = if is_mermaid {
3381        let mut lines = vec!["graph LR".to_string()];
3382
3383        // Topic nodes (circles)
3384        for topic in &topics {
3385            let safe_id = topic.replace('.', "_").replace('*', "star");
3386            lines.push(format!("    t_{}(({}))", safe_id, topic));
3387        }
3388
3389        // Daemon nodes (rectangles with state)
3390        for d in &chain_daemons {
3391            let state_str = serde_json::to_value(&d.state)
3392                .ok()
3393                .and_then(|v| v.as_str().map(String::from))
3394                .unwrap_or_else(|| "unknown".into());
3395            lines.push(format!("    d_{}[{} <br/> {}]", d.name, d.name, state_str));
3396        }
3397
3398        // Edges
3399        for (from, to, label) in &edges {
3400            let from_id = if from.starts_with("topic:") {
3401                format!("t_{}", from[6..].replace('.', "_").replace('*', "star"))
3402            } else {
3403                format!("d_{}", &from[7..])
3404            };
3405            let to_id = if to.starts_with("topic:") {
3406                format!("t_{}", to[6..].replace('.', "_").replace('*', "star"))
3407            } else {
3408                format!("d_{}", &to[7..])
3409            };
3410            lines.push(format!("    {} -->|{}| {}", from_id, label, to_id));
3411        }
3412
3413        lines.join("\n")
3414    } else {
3415        // DOT format
3416        let mut lines = vec![
3417            "digraph chains {".to_string(),
3418            "    rankdir=LR;".to_string(),
3419            "    node [fontname=\"Helvetica\"];".to_string(),
3420        ];
3421
3422        // Topic nodes (ellipse)
3423        for topic in &topics {
3424            let safe_id = topic.replace('.', "_").replace('*', "star");
3425            lines.push(format!("    \"t_{}\" [label=\"{}\" shape=ellipse style=filled fillcolor=\"#e8f4fd\"];",
3426                safe_id, topic));
3427        }
3428
3429        // Daemon nodes (box with state)
3430        for d in &chain_daemons {
3431            let state_str = serde_json::to_value(&d.state)
3432                .ok()
3433                .and_then(|v| v.as_str().map(String::from))
3434                .unwrap_or_else(|| "unknown".into());
3435            let color = match d.state {
3436                DaemonState::Idle => "#d4edda",
3437                DaemonState::Running => "#fff3cd",
3438                DaemonState::Hibernating => "#cce5ff",
3439                DaemonState::Paused => "#fce4ec",
3440                DaemonState::Stopped => "#e2e3e5",
3441                DaemonState::Crashed => "#f8d7da",
3442            };
3443            lines.push(format!("    \"d_{}\" [label=\"{}\\n[{}]\" shape=box style=filled fillcolor=\"{}\"];",
3444                d.name, d.name, state_str, color));
3445        }
3446
3447        // Edges
3448        for (from, to, label) in &edges {
3449            let from_id = if from.starts_with("topic:") {
3450                format!("t_{}", from[6..].replace('.', "_").replace('*', "star"))
3451            } else {
3452                format!("d_{}", &from[7..])
3453            };
3454            let to_id = if to.starts_with("topic:") {
3455                format!("t_{}", to[6..].replace('.', "_").replace('*', "star"))
3456            } else {
3457                format!("d_{}", &to[7..])
3458            };
3459            lines.push(format!("    \"{}\" -> \"{}\" [label=\"{}\"];", from_id, to_id, label));
3460        }
3461
3462        lines.push("}".to_string());
3463        lines.join("\n")
3464    };
3465
3466    let mut response_headers = HeaderMap::new();
3467    let ct = if is_mermaid { "text/plain" } else { "text/vnd.graphviz" };
3468    if let Ok(val) = ct.parse() {
3469        response_headers.insert("content-type", val);
3470    }
3471
3472    Ok((StatusCode::OK, response_headers, body))
3473}
3474
3475/// POST /v1/triggers/dispatch — check all triggered daemons and execute matching ones.
3476///
3477/// Accepts an event topic+payload. Any daemon whose trigger_topic matches
3478/// the event topic (using TopicFilter pattern matching) will be executed.
3479/// Returns the list of dispatched daemons with their execution results.
3480#[derive(Debug, Deserialize)]
3481pub struct DispatchRequest {
3482    /// Event topic to match against daemon triggers.
3483    pub topic: String,
3484    /// Event payload (forwarded to daemon context).
3485    #[serde(default)]
3486    pub payload: serde_json::Value,
3487}
3488
3489async fn triggers_dispatch_handler(
3490    State(state): State<SharedState>,
3491    headers: HeaderMap,
3492    Json(payload): Json<DispatchRequest>,
3493) -> Result<Json<serde_json::Value>, StatusCode> {
3494    let req_start = Instant::now();
3495    let client = client_key_from_headers(&headers);
3496    {
3497        let mut s = state.lock().unwrap();
3498        check_auth(&mut s, &headers, AccessLevel::Write)?;
3499        check_rate_limit(&mut s, &headers)?;
3500    }
3501
3502    // Find matching daemons (name, flow_name, source_file, output_topic)
3503    let matched_daemons: Vec<(String, String, String, Option<String>)> = {
3504        let s = state.lock().unwrap();
3505        let filter_topic = &payload.topic;
3506
3507        s.daemons.values()
3508            .filter(|d| {
3509                if let Some(ref trigger) = d.trigger_topic {
3510                    let filter = crate::event_bus::TopicFilter::new(trigger);
3511                    filter.matches(filter_topic)
3512                } else {
3513                    false
3514                }
3515            })
3516            .filter(|d| d.state != DaemonState::Crashed && d.state != DaemonState::Stopped && d.state != DaemonState::Paused)
3517            .map(|d| (d.name.clone(), d.flow_name.clone(), d.source_file.clone(), d.output_topic.clone()))
3518            .collect()
3519    };
3520
3521    if matched_daemons.is_empty() {
3522        return Ok(Json(serde_json::json!({
3523            "topic": payload.topic,
3524            "dispatched": 0,
3525            "results": [],
3526        })));
3527    }
3528
3529    // Publish the triggering event on the bus
3530    {
3531        let s = state.lock().unwrap();
3532        s.event_bus.publish(
3533            &payload.topic,
3534            payload.payload.clone(),
3535            "trigger-dispatch",
3536        );
3537    }
3538
3539    // Execute each matched daemon
3540    let mut results = Vec::new();
3541
3542    for (daemon_name, flow_name, _source_file, output_topic) in &matched_daemons {
3543        // Look up source
3544        let (source, source_file, backend) = {
3545            let s = state.lock().unwrap();
3546            let history = s.versions.get_history(flow_name);
3547            match history.and_then(|h| h.active()) {
3548                Some(active) => (active.source.clone(), active.source_file.clone(), active.backend.clone()),
3549                None => {
3550                    results.push(serde_json::json!({
3551                        "daemon": daemon_name,
3552                        "success": false,
3553                        "error": "no deployed source",
3554                    }));
3555                    continue;
3556                }
3557            }
3558        };
3559
3560        // Transition to Running
3561        {
3562            let mut s = state.lock().unwrap();
3563            s.supervisor.mark_started(daemon_name);
3564            if let Some(daemon) = s.daemons.get_mut(daemon_name) {
3565                let prev = daemon.state;
3566                daemon.state = DaemonState::Running;
3567                record_lifecycle(daemon, prev, DaemonState::Running, Some("trigger dispatch".into()));
3568            }
3569        }
3570
3571        // Execute (outside lock — full backend stack)
3572        let (exec_result, _) = server_execute_full(&state, &source, &source_file, flow_name, &backend);
3573
3574        match exec_result {
3575            Ok(result) => {
3576                let trace_entry = crate::trace_store::build_trace(
3577                    &result.flow_name,
3578                    &result.source_file,
3579                    &result.backend,
3580                    &client,
3581                    if result.success {
3582                        crate::trace_store::TraceStatus::Success
3583                    } else {
3584                        crate::trace_store::TraceStatus::Partial
3585                    },
3586                    result.steps_executed,
3587                    result.latency_ms,
3588                );
3589
3590                let trace_id = {
3591                    let mut s = state.lock().unwrap();
3592                    let mut entry = trace_entry;
3593                    entry.tokens_input = result.tokens_input;
3594                    entry.tokens_output = result.tokens_output;
3595                    entry.anchor_checks = result.anchor_checks;
3596                    entry.anchor_breaches = result.anchor_breaches;
3597                    entry.errors = result.errors;
3598                    let tid = s.trace_store.record(entry);
3599
3600                    if let Some(daemon) = s.daemons.get_mut(daemon_name) {
3601                        daemon.event_count += 1;
3602                        let prev = daemon.state;
3603                        daemon.state = DaemonState::Hibernating;
3604                        record_lifecycle(daemon, prev, DaemonState::Hibernating, Some("dispatch execution complete".into()));
3605                    }
3606                    s.supervisor.heartbeat(daemon_name);
3607                    s.supervisor.mark_waiting(daemon_name);
3608
3609                    tid
3610                };
3611
3612                // Publish to output_topic for daemon chaining
3613                if let Some(ref out_topic) = output_topic {
3614                    let s = state.lock().unwrap();
3615                    s.event_bus.publish(
3616                        out_topic,
3617                        serde_json::json!({
3618                            "source_daemon": daemon_name,
3619                            "flow": flow_name,
3620                            "success": result.success,
3621                            "trace_id": trace_id,
3622                            "steps_executed": result.steps_executed,
3623                            "latency_ms": result.latency_ms,
3624                        }),
3625                        "daemon-chain",
3626                    );
3627                }
3628
3629                results.push(serde_json::json!({
3630                    "daemon": daemon_name,
3631                    "flow": flow_name,
3632                    "success": result.success,
3633                    "trace_id": trace_id,
3634                    "steps_executed": result.steps_executed,
3635                    "latency_ms": result.latency_ms,
3636                    "chained_to": output_topic,
3637                }));
3638            }
3639            Err(e) => {
3640                let mut err_entry = crate::trace_store::build_trace(
3641                    flow_name,
3642                    &source_file,
3643                    &backend,
3644                    &client,
3645                    crate::trace_store::TraceStatus::Failed,
3646                    0,
3647                    req_start.elapsed().as_millis() as u64,
3648                );
3649                err_entry.errors = 1;
3650
3651                let (trace_id, will_restart) = {
3652                    let mut s = state.lock().unwrap();
3653                    let tid = s.trace_store.record(err_entry);
3654                    let will_restart = s.supervisor.report_crash(daemon_name, &e);
3655                    if let Some(daemon) = s.daemons.get_mut(daemon_name) {
3656                        daemon.event_count += 1;
3657                        daemon.restart_count += 1;
3658                        let prev = daemon.state;
3659                        let new_state = if will_restart { DaemonState::Idle } else { DaemonState::Crashed };
3660                        daemon.state = new_state;
3661                        record_lifecycle(daemon, prev, new_state, Some(e.clone()));
3662                    }
3663                    (tid, will_restart)
3664                };
3665
3666                results.push(serde_json::json!({
3667                    "daemon": daemon_name,
3668                    "flow": flow_name,
3669                    "success": false,
3670                    "error": e,
3671                    "trace_id": trace_id,
3672                    "will_restart": will_restart,
3673                }));
3674            }
3675        }
3676    }
3677
3678    // Audit trail
3679    {
3680        let mut s = state.lock().unwrap();
3681        s.audit_log.record(
3682            &client,
3683            AuditAction::Execute,
3684            &payload.topic,
3685            serde_json::json!({
3686                "topic": &payload.topic,
3687                "dispatched": results.len(),
3688                "daemons": matched_daemons.iter().map(|(n, _, _, _)| n.as_str()).collect::<Vec<_>>(),
3689            }),
3690            true,
3691        );
3692        s.request_logger.record("POST", "/v1/triggers/dispatch", 200, req_start.elapsed(), &client);
3693    }
3694
3695    Ok(Json(serde_json::json!({
3696        "topic": payload.topic,
3697        "dispatched": results.len(),
3698        "results": results,
3699    })))
3700}
3701
3702// ── Event bus + supervisor handlers ───────────────────────────────────────
3703
3704/// POST /v1/events — publish an event to the bus.
3705#[derive(Debug, Deserialize)]
3706pub struct PublishEventRequest {
3707    pub topic: String,
3708    #[serde(default)]
3709    pub payload: serde_json::Value,
3710    #[serde(default = "default_source")]
3711    pub source: String,
3712}
3713
3714fn default_source() -> String {
3715    "api".to_string()
3716}
3717
3718async fn publish_event_handler(
3719    State(state): State<SharedState>,
3720    headers: HeaderMap,
3721    Json(payload): Json<PublishEventRequest>,
3722) -> Result<Json<serde_json::Value>, StatusCode> {
3723    let topic = payload.topic.clone();
3724    let event_payload = payload.payload.clone();
3725    let source = payload.source.clone();
3726
3727    {
3728        let mut s = state.lock().unwrap();
3729        check_auth(&mut s, &headers, AccessLevel::Write)?;
3730        s.event_bus.publish(&payload.topic, payload.payload, &payload.source);
3731    }
3732
3733    // Trigger async webhook delivery
3734    trigger_webhook_delivery(&state, &topic, event_payload, &source);
3735
3736    Ok(Json(serde_json::json!({
3737        "published": true,
3738        "topic": topic,
3739        "source": source,
3740    })))
3741}
3742
3743/// GET /v1/events/stats — event bus statistics.
3744async fn event_stats_handler(
3745    State(state): State<SharedState>,
3746    headers: HeaderMap,
3747) -> Result<Json<serde_json::Value>, StatusCode> {
3748    let s = state.lock().unwrap();
3749    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3750
3751    let stats = s.event_bus.stats();
3752    Ok(Json(serde_json::json!({
3753        "events_published": stats.events_published,
3754        "events_delivered": stats.events_delivered,
3755        "events_dropped": stats.events_dropped,
3756        "active_subscribers": stats.active_subscribers,
3757        "topics_seen": stats.topics_seen,
3758    })))
3759}
3760
3761/// GET /v1/supervisor — supervisor overview.
3762async fn supervisor_handler(
3763    State(state): State<SharedState>,
3764    headers: HeaderMap,
3765) -> Result<Json<serde_json::Value>, StatusCode> {
3766    let s = state.lock().unwrap();
3767    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3768
3769    let daemons: Vec<serde_json::Value> = s.supervisor.list().iter().map(|d| {
3770        serde_json::json!({
3771            "name": d.name,
3772            "state": d.state,
3773            "restart_policy": d.restart_policy,
3774            "restart_count": d.restart_count,
3775            "crash_reason": d.crash_reason,
3776        })
3777    }).collect();
3778
3779    Ok(Json(serde_json::json!({
3780        "summary": s.supervisor.summary(),
3781        "state_counts": s.supervisor.state_counts(),
3782        "daemons": daemons,
3783    })))
3784}
3785
3786/// POST /v1/supervisor/:name/start — mark daemon as started.
3787async fn supervisor_start_handler(
3788    State(state): State<SharedState>,
3789    headers: HeaderMap,
3790    Path(name): Path<String>,
3791) -> Result<Json<serde_json::Value>, StatusCode> {
3792    let mut s = state.lock().unwrap();
3793    check_auth(&mut s, &headers, AccessLevel::Write)?;
3794
3795    if s.supervisor.mark_started(&name) {
3796        Ok(Json(serde_json::json!({ "started": name })))
3797    } else {
3798        Err(StatusCode::NOT_FOUND)
3799    }
3800}
3801
3802/// POST /v1/supervisor/:name/stop — stop a daemon.
3803async fn supervisor_stop_handler(
3804    State(state): State<SharedState>,
3805    headers: HeaderMap,
3806    Path(name): Path<String>,
3807) -> Result<Json<serde_json::Value>, StatusCode> {
3808    let mut s = state.lock().unwrap();
3809    check_auth(&mut s, &headers, AccessLevel::Write)?;
3810
3811    if s.supervisor.stop(&name) {
3812        Ok(Json(serde_json::json!({ "stopped": name })))
3813    } else {
3814        Err(StatusCode::NOT_FOUND)
3815    }
3816}
3817
3818// ── Version endpoints ─────────────────────────────────────────────────────
3819
3820/// GET /v1/versions — list all flows with version info.
3821async fn versions_handler(
3822    State(state): State<SharedState>,
3823    headers: HeaderMap,
3824) -> Result<Json<serde_json::Value>, StatusCode> {
3825    let s = state.lock().unwrap();
3826    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3827
3828    let flows = s.versions.list_flows();
3829    Ok(Json(serde_json::json!({
3830        "flows": flows,
3831        "total_flows": s.versions.flow_count(),
3832        "total_versions": s.versions.total_versions(),
3833    })))
3834}
3835
3836/// GET /v1/versions/:name ��� version history for a specific flow.
3837async fn version_history_handler(
3838    State(state): State<SharedState>,
3839    headers: HeaderMap,
3840    Path(name): Path<String>,
3841) -> Result<Json<serde_json::Value>, StatusCode> {
3842    let s = state.lock().unwrap();
3843    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3844
3845    match s.versions.get_history(&name) {
3846        Some(history) => {
3847            let versions: Vec<serde_json::Value> = history.versions.iter().map(|v| {
3848                serde_json::json!({
3849                    "version": v.version,
3850                    "source_hash": v.source_hash,
3851                    "source_file": v.source_file,
3852                    "backend": v.backend,
3853                    "flow_names": v.flow_names,
3854                    "active": v.active,
3855                })
3856            }).collect();
3857
3858            Ok(Json(serde_json::json!({
3859                "flow_name": history.flow_name,
3860                "active_version": history.active_version,
3861                "deploy_count": history.deploy_count,
3862                "versions": versions,
3863            })))
3864        }
3865        None => Err(StatusCode::NOT_FOUND),
3866    }
3867}
3868
3869/// Version diff query parameters.
3870#[derive(Debug, Deserialize)]
3871pub struct VersionDiffQuery {
3872    pub from: u32,
3873    pub to: u32,
3874}
3875
3876/// GET /v1/versions/:name/diff?from=1&to=2 — diff source between two versions.
3877async fn version_diff_handler(
3878    State(state): State<SharedState>,
3879    headers: HeaderMap,
3880    Path(name): Path<String>,
3881    axum::extract::Query(query): axum::extract::Query<VersionDiffQuery>,
3882) -> Result<Json<serde_json::Value>, StatusCode> {
3883    let s = state.lock().unwrap();
3884    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3885
3886    match crate::version_diff::diff_versions(&s.versions, &name, query.from, query.to) {
3887        Ok(diff) => Ok(Json(serde_json::to_value(&diff).unwrap())),
3888        Err(e) => Ok(Json(serde_json::json!({
3889            "success": false,
3890            "error": e,
3891        }))),
3892    }
3893}
3894
3895/// Rollback request payload.
3896#[derive(Debug, Deserialize)]
3897pub struct RollbackRequest {
3898    pub version: u32,
3899}
3900
3901/// POST /v1/versions/:name/rollback — rollback to a specific version.
3902async fn rollback_handler(
3903    State(state): State<SharedState>,
3904    headers: HeaderMap,
3905    Path(name): Path<String>,
3906    Json(payload): Json<RollbackRequest>,
3907) -> Result<Json<serde_json::Value>, StatusCode> {
3908    let mut s = state.lock().unwrap();
3909    check_auth(&mut s, &headers, AccessLevel::Write)?;
3910
3911    let client = client_key_from_headers(&headers);
3912    match s.versions.rollback(&name, payload.version) {
3913        Ok(_source) => {
3914            s.event_bus.publish(
3915                "version.rollback",
3916                serde_json::json!({
3917                    "flow": &name,
3918                    "version": payload.version,
3919                }),
3920                "server",
3921            );
3922            s.audit_log.record(&client, AuditAction::Rollback, &name, serde_json::json!({"version": payload.version}), true);
3923
3924            Ok(Json(serde_json::json!({
3925                "success": true,
3926                "flow": name,
3927                "rolled_back_to": payload.version,
3928            })))
3929        }
3930        Err(e) => {
3931            s.audit_log.record(&client, AuditAction::Rollback, &name, serde_json::json!({"error": &e}), false);
3932            Ok(Json(serde_json::json!({
3933                "success": false,
3934                "error": e,
3935            })))
3936        }
3937    }
3938}
3939
3940// ── Session endpoints ────────────────────────────────────────────────────
3941
3942/// Request payload for session write operations.
3943#[derive(Debug, Deserialize)]
3944pub struct SessionWriteRequest {
3945    pub key: String,
3946    pub value: String,
3947    #[serde(default = "default_source_step")]
3948    pub source_step: String,
3949    #[serde(default = "default_scope")]
3950    pub scope: String,
3951}
3952
3953fn default_source_step() -> String {
3954    "api".to_string()
3955}
3956
3957/// Request payload for session purge.
3958#[derive(Debug, Deserialize)]
3959pub struct SessionPurgeRequest {
3960    pub key: String,
3961    #[serde(default = "default_scope")]
3962    pub scope: String,
3963}
3964
3965/// Request payload for session query operations.
3966#[derive(Debug, Deserialize)]
3967pub struct SessionQueryRequest {
3968    pub query: String,
3969    #[serde(default = "default_scope")]
3970    pub scope: String,
3971}
3972
3973fn default_scope() -> String {
3974    crate::session_scope::DEFAULT_SCOPE.to_string()
3975}
3976
3977/// Query parameter for optional scope on GET session endpoints.
3978#[derive(Debug, Deserialize)]
3979pub struct ScopeQuery {
3980    #[serde(default = "default_scope")]
3981    pub scope: String,
3982}
3983
3984/// POST /v1/session/remember — store ephemeral memory entry.
3985async fn session_remember_handler(
3986    State(state): State<SharedState>,
3987    headers: HeaderMap,
3988    Json(payload): Json<SessionWriteRequest>,
3989) -> Result<Json<serde_json::Value>, StatusCode> {
3990    let mut s = state.lock().unwrap();
3991    check_auth(&mut s, &headers, AccessLevel::Write)?;
3992
3993    let client = client_key_from_headers(&headers);
3994    s.scoped_sessions.remember(&payload.scope, &payload.key, &payload.value, &payload.source_step);
3995    s.event_bus.publish(
3996        "session.remember",
3997        serde_json::json!({ "key": &payload.key, "scope": &payload.scope }),
3998        "server",
3999    );
4000    s.audit_log.record(&client, AuditAction::SessionWrite, &payload.key, serde_json::json!({"scope": &payload.scope}), true);
4001
4002    Ok(Json(serde_json::json!({
4003        "success": true,
4004        "key": payload.key,
4005        "scope": payload.scope,
4006        "store": "memory",
4007    })))
4008}
4009
4010/// GET /v1/session/recall/:key?scope= — recall ephemeral memory entry.
4011async fn session_recall_handler(
4012    State(state): State<SharedState>,
4013    headers: HeaderMap,
4014    Path(key): Path<String>,
4015    Query(params): Query<ScopeQuery>,
4016) -> Result<Json<serde_json::Value>, StatusCode> {
4017    let mut s = state.lock().unwrap();
4018    check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
4019
4020    match s.scoped_sessions.recall(&params.scope, &key) {
4021        Some(entry) => Ok(Json(serde_json::json!({
4022            "found": true,
4023            "key": entry.key,
4024            "value": entry.value,
4025            "timestamp": entry.timestamp,
4026            "source_step": entry.source_step,
4027            "scope": params.scope,
4028        }))),
4029        None => Ok(Json(serde_json::json!({
4030            "found": false,
4031            "key": key,
4032            "scope": params.scope,
4033        }))),
4034    }
4035}
4036
4037/// POST /v1/session/persist — store persistent entry (file-backed).
4038async fn session_persist_handler(
4039    State(state): State<SharedState>,
4040    headers: HeaderMap,
4041    Json(payload): Json<SessionWriteRequest>,
4042) -> Result<Json<serde_json::Value>, StatusCode> {
4043    let mut s = state.lock().unwrap();
4044    check_auth(&mut s, &headers, AccessLevel::Write)?;
4045
4046    s.scoped_sessions.persist(&payload.scope, &payload.key, &payload.value, &payload.source_step);
4047    let flush_result = s.scoped_sessions.flush(&payload.scope);
4048
4049    s.event_bus.publish(
4050        "session.persist",
4051        serde_json::json!({ "key": &payload.key, "scope": &payload.scope }),
4052        "server",
4053    );
4054
4055    Ok(Json(serde_json::json!({
4056        "success": true,
4057        "key": payload.key,
4058        "scope": payload.scope,
4059        "store": "persistent",
4060        "flushed": flush_result.is_ok(),
4061    })))
4062}
4063
4064/// GET /v1/session/retrieve/:key — retrieve persistent entry.
4065async fn session_retrieve_handler(
4066    State(state): State<SharedState>,
4067    headers: HeaderMap,
4068    Path(key): Path<String>,
4069    Query(params): Query<ScopeQuery>,
4070) -> Result<Json<serde_json::Value>, StatusCode> {
4071    let mut s = state.lock().unwrap();
4072    check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
4073
4074    match s.scoped_sessions.retrieve(&params.scope, &key) {
4075        Some(entry) => Ok(Json(serde_json::json!({
4076            "found": true,
4077            "key": entry.key,
4078            "value": entry.value,
4079            "timestamp": entry.timestamp,
4080            "source_step": entry.source_step,
4081            "scope": params.scope,
4082        }))),
4083        None => Ok(Json(serde_json::json!({
4084            "found": false,
4085            "key": key,
4086            "scope": params.scope,
4087        }))),
4088    }
4089}
4090
4091/// POST /v1/session/query — retrieve entries matching a query string.
4092async fn session_query_handler(
4093    State(state): State<SharedState>,
4094    headers: HeaderMap,
4095    Json(payload): Json<SessionQueryRequest>,
4096) -> Result<Json<serde_json::Value>, StatusCode> {
4097    let mut s = state.lock().unwrap();
4098    check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
4099
4100    let results = s.scoped_sessions.query(&payload.scope, &payload.query);
4101    let entries: Vec<serde_json::Value> = results.iter().map(|e| {
4102        serde_json::json!({
4103            "key": e.key,
4104            "value": e.value,
4105            "timestamp": e.timestamp,
4106            "source_step": e.source_step,
4107        })
4108    }).collect();
4109
4110    Ok(Json(serde_json::json!({
4111        "query": payload.query,
4112        "scope": payload.scope,
4113        "count": entries.len(),
4114        "entries": entries,
4115    })))
4116}
4117
4118/// POST /v1/session/mutate — update an existing persistent entry.
4119async fn session_mutate_handler(
4120    State(state): State<SharedState>,
4121    headers: HeaderMap,
4122    Json(payload): Json<SessionWriteRequest>,
4123) -> Result<Json<serde_json::Value>, StatusCode> {
4124    let mut s = state.lock().unwrap();
4125    check_auth(&mut s, &headers, AccessLevel::Write)?;
4126
4127    let updated = s.scoped_sessions.mutate(&payload.scope, &payload.key, &payload.value, &payload.source_step);
4128    if updated {
4129        let _ = s.scoped_sessions.flush(&payload.scope);
4130        s.event_bus.publish(
4131            "session.mutate",
4132            serde_json::json!({ "key": &payload.key, "scope": &payload.scope }),
4133            "server",
4134        );
4135    }
4136
4137    Ok(Json(serde_json::json!({
4138        "success": updated,
4139        "key": payload.key,
4140        "scope": payload.scope,
4141    })))
4142}
4143
4144/// POST /v1/session/purge — delete a persistent entry.
4145async fn session_purge_handler(
4146    State(state): State<SharedState>,
4147    headers: HeaderMap,
4148    Json(payload): Json<SessionPurgeRequest>,
4149) -> Result<Json<serde_json::Value>, StatusCode> {
4150    let mut s = state.lock().unwrap();
4151    check_auth(&mut s, &headers, AccessLevel::Write)?;
4152
4153    let client = client_key_from_headers(&headers);
4154    let removed = s.scoped_sessions.purge(&payload.scope, &payload.key);
4155    if removed {
4156        let _ = s.scoped_sessions.flush(&payload.scope);
4157        s.event_bus.publish(
4158            "session.purge",
4159            serde_json::json!({ "key": &payload.key, "scope": &payload.scope }),
4160            "server",
4161        );
4162    }
4163    s.audit_log.record(&client, AuditAction::SessionPurge, &payload.key, serde_json::json!({"scope": &payload.scope, "removed": removed}), removed);
4164
4165    Ok(Json(serde_json::json!({
4166        "success": removed,
4167        "key": payload.key,
4168        "scope": payload.scope,
4169    })))
4170}
4171
4172/// Query parameters for session scoped export.
4173#[derive(Debug, Deserialize)]
4174pub struct SessionExportQuery {
4175    /// Export format: "json" (default) or "csv".
4176    #[serde(default = "default_session_export_format")]
4177    pub format: String,
4178}
4179
4180fn default_session_export_format() -> String { "json".into() }
4181
4182/// GET /v1/session/:scope/export — export all entries in a scope as JSON or CSV.
4183async fn session_scope_export_handler(
4184    State(state): State<SharedState>,
4185    headers: HeaderMap,
4186    Path(scope): Path<String>,
4187    Query(params): Query<SessionExportQuery>,
4188) -> Result<(StatusCode, [(String, String); 1], String), StatusCode> {
4189    let mut s = state.lock().unwrap();
4190    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
4191
4192    let entries = s.scoped_sessions.list_entries(&scope);
4193
4194    let format = params.format.to_lowercase();
4195    match format.as_str() {
4196        "csv" => {
4197            let mut csv = String::from("scope,layer,key,value,timestamp,source_step\n");
4198            for e in &entries {
4199                let val = e.value.replace('"', "\"\"");
4200                csv.push_str(&format!(
4201                    "{},{},{},\"{}\",{},{}\n",
4202                    e.scope, e.layer, e.key, val, e.timestamp, e.source_step
4203                ));
4204            }
4205            Ok((
4206                StatusCode::OK,
4207                [("content-type".into(), "text/csv".into())],
4208                csv,
4209            ))
4210        }
4211        _ => {
4212            // JSON
4213            let json = serde_json::json!({
4214                "scope": scope,
4215                "count": entries.len(),
4216                "entries": entries,
4217            });
4218            Ok((
4219                StatusCode::OK,
4220                [("content-type".into(), "application/json".into())],
4221                serde_json::to_string_pretty(&json).unwrap_or_default(),
4222            ))
4223        }
4224    }
4225}
4226
4227/// GET /v1/session — list session stats with scoped summary.
4228async fn session_list_handler(
4229    State(state): State<SharedState>,
4230    headers: HeaderMap,
4231) -> Result<Json<serde_json::Value>, StatusCode> {
4232    let s = state.lock().unwrap();
4233    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
4234
4235    let summary = s.scoped_sessions.summary();
4236
4237    Ok(Json(serde_json::json!({
4238        "scope_count": s.scoped_sessions.scope_count(),
4239        "total_memory_count": s.scoped_sessions.total_memory_count(),
4240        "total_store_count": s.scoped_sessions.total_store_count(),
4241        "scopes": summary,
4242    })))
4243}
4244
4245// ── AxonStore endpoints — cognitive durable persistence (primitive #18) ──────
4246
4247/// POST /v1/axonstore — create a named AxonStore instance.
4248/// Body: { "name": "my_store", "ontology": "knowledge_base" }
4249async fn axonstore_create_handler(
4250    State(state): State<SharedState>,
4251    headers: HeaderMap,
4252    Json(payload): Json<serde_json::Value>,
4253) -> Result<Json<serde_json::Value>, StatusCode> {
4254    let mut s = state.lock().unwrap();
4255    let client = client_key_from_headers(&headers);
4256    check_auth(&mut s, &headers, AccessLevel::Write)?;
4257
4258    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
4259    let ontology = payload.get("ontology").and_then(|v| v.as_str()).unwrap_or("general").to_string();
4260
4261    if name.is_empty() {
4262        return Ok(Json(serde_json::json!({"error": "name is required"})));
4263    }
4264    if s.axon_stores.contains_key(&name) {
4265        return Ok(Json(serde_json::json!({"error": format!("axonstore '{}' already exists", name)})));
4266    }
4267
4268    let now = std::time::SystemTime::now()
4269        .duration_since(std::time::UNIX_EPOCH)
4270        .unwrap_or_default()
4271        .as_secs();
4272
4273    let store = AxonStoreInstance {
4274        name: name.clone(),
4275        ontology: ontology.clone(),
4276        entries: HashMap::new(),
4277        created_at: now,
4278        total_ops: 0,
4279    };
4280    s.axon_stores.insert(name.clone(), store);
4281
4282    s.audit_log.record(&client, AuditAction::ConfigUpdate, "axonstore",
4283        serde_json::json!({"action": "create", "store": &name, "ontology": &ontology}), true);
4284
4285    Ok(Json(serde_json::json!({
4286        "success": true,
4287        "store": name,
4288        "ontology": ontology,
4289        "created_at": now,
4290    })))
4291}
4292
4293/// GET /v1/axonstore — list all named AxonStore instances.
4294async fn axonstore_list_handler(
4295    State(state): State<SharedState>,
4296    headers: HeaderMap,
4297) -> Result<Json<serde_json::Value>, StatusCode> {
4298    let s = state.lock().unwrap();
4299    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
4300
4301    let stores: Vec<serde_json::Value> = s.axon_stores.values().map(|st| {
4302        serde_json::json!({
4303            "name": st.name,
4304            "ontology": st.ontology,
4305            "entry_count": st.entries.len(),
4306            "total_ops": st.total_ops,
4307            "created_at": st.created_at,
4308        })
4309    }).collect();
4310
4311    Ok(Json(serde_json::json!({
4312        "stores": stores,
4313        "total": stores.len(),
4314    })))
4315}
4316
4317/// GET /v1/axonstore/{name} — introspect a named AxonStore (keys, metadata, epistemic state).
4318async fn axonstore_get_handler(
4319    State(state): State<SharedState>,
4320    headers: HeaderMap,
4321    Path(name): Path<String>,
4322) -> Result<Json<serde_json::Value>, StatusCode> {
4323    let s = state.lock().unwrap();
4324    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
4325
4326    match s.axon_stores.get(&name) {
4327        Some(store) => {
4328            let entries: Vec<serde_json::Value> = store.entries.values().map(|e| {
4329                serde_json::json!({
4330                    "key": e.key,
4331                    "value": e.value,
4332                    "version": e.version,
4333                    "created_at": e.created_at,
4334                    "updated_at": e.updated_at,
4335                    "envelope": {
4336                        "ontology": e.envelope.ontology,
4337                        "certainty": e.envelope.certainty,
4338                        "provenance": e.envelope.provenance,
4339                        "derivation": e.envelope.derivation,
4340                        "temporal_start": e.envelope.temporal_start,
4341                        "temporal_end": e.envelope.temporal_end,
4342                    }
4343                })
4344            }).collect();
4345
4346            Ok(Json(serde_json::json!({
4347                "name": store.name,
4348                "ontology": store.ontology,
4349                "entry_count": store.entries.len(),
4350                "total_ops": store.total_ops,
4351                "created_at": store.created_at,
4352                "entries": entries,
4353            })))
4354        }
4355        None => Ok(Json(serde_json::json!({"error": format!("axonstore '{}' not found", name)}))),
4356    }
4357}
4358
4359/// DELETE /v1/axonstore/{name} — delete a named AxonStore and all its entries.
4360async fn axonstore_delete_handler(
4361    State(state): State<SharedState>,
4362    headers: HeaderMap,
4363    Path(name): Path<String>,
4364) -> Result<Json<serde_json::Value>, StatusCode> {
4365    let mut s = state.lock().unwrap();
4366    let client = client_key_from_headers(&headers);
4367    check_auth(&mut s, &headers, AccessLevel::Admin)?;
4368
4369    match s.axon_stores.remove(&name) {
4370        Some(removed) => {
4371            s.audit_log.record(&client, AuditAction::ConfigUpdate, "axonstore",
4372                serde_json::json!({"action": "delete", "store": &name, "entries_purged": removed.entries.len()}), true);
4373            Ok(Json(serde_json::json!({
4374                "success": true,
4375                "store": name,
4376                "entries_purged": removed.entries.len(),
4377            })))
4378        }
4379        None => Ok(Json(serde_json::json!({"error": format!("axonstore '{}' not found", name)}))),
4380    }
4381}
4382
4383/// POST /v1/axonstore/{name}/persist — store a key-value entry with ΛD envelope (c=1.0, δ=raw).
4384/// Body: { "key": "fact_1", "value": <any JSON> }
4385async fn axonstore_persist_handler(
4386    State(state): State<SharedState>,
4387    headers: HeaderMap,
4388    Path(store_name): Path<String>,
4389    Json(payload): Json<serde_json::Value>,
4390) -> Result<Json<serde_json::Value>, StatusCode> {
4391    let mut s = state.lock().unwrap();
4392    let client = client_key_from_headers(&headers);
4393    check_auth(&mut s, &headers, AccessLevel::Write)?;
4394
4395    let key = payload.get("key").and_then(|v| v.as_str()).unwrap_or("").to_string();
4396    let value = payload.get("value").cloned().unwrap_or(serde_json::json!(null));
4397
4398    if key.is_empty() {
4399        return Ok(Json(serde_json::json!({"error": "key is required"})));
4400    }
4401
4402    let store = match s.axon_stores.get_mut(&store_name) {
4403        Some(st) => st,
4404        None => return Ok(Json(serde_json::json!({"error": format!("axonstore '{}' not found", store_name)}))),
4405    };
4406
4407    let now = std::time::SystemTime::now()
4408        .duration_since(std::time::UNIX_EPOCH)
4409        .unwrap_or_default()
4410        .as_secs();
4411
4412    // ΛD: persist = raw write → c=1.0, δ=raw
4413    let envelope = EpistemicEnvelope::raw_config(&store.ontology, &client);
4414
4415    let entry = AxonStoreEntry {
4416        key: key.clone(),
4417        value: value.clone(),
4418        envelope,
4419        created_at: now,
4420        updated_at: now,
4421        version: 1,
4422    };
4423
4424    store.entries.insert(key.clone(), entry);
4425    store.total_ops += 1;
4426
4427    Ok(Json(serde_json::json!({
4428        "success": true,
4429        "store": store_name,
4430        "key": key,
4431        "version": 1,
4432        "envelope": { "certainty": 1.0, "derivation": "raw" },
4433    })))
4434}
4435
4436/// GET /v1/axonstore/{name}/retrieve/{key} — retrieve an entry with its ΛD envelope.
4437async fn axonstore_retrieve_handler(
4438    State(state): State<SharedState>,
4439    headers: HeaderMap,
4440    Path((store_name, key)): Path<(String, String)>,
4441) -> Result<Json<serde_json::Value>, StatusCode> {
4442    let s = state.lock().unwrap();
4443    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
4444
4445    let store = match s.axon_stores.get(&store_name) {
4446        Some(st) => st,
4447        None => return Ok(Json(serde_json::json!({"error": format!("axonstore '{}' not found", store_name)}))),
4448    };
4449
4450    match store.entries.get(&key) {
4451        Some(entry) => Ok(Json(serde_json::json!({
4452            "store": store_name,
4453            "key": entry.key,
4454            "value": entry.value,
4455            "version": entry.version,
4456            "created_at": entry.created_at,
4457            "updated_at": entry.updated_at,
4458            "envelope": {
4459                "ontology": entry.envelope.ontology,
4460                "certainty": entry.envelope.certainty,
4461                "provenance": entry.envelope.provenance,
4462                "derivation": entry.envelope.derivation,
4463                "temporal_start": entry.envelope.temporal_start,
4464                "temporal_end": entry.envelope.temporal_end,
4465            }
4466        }))),
4467        None => Ok(Json(serde_json::json!({
4468            "store": store_name,
4469            "key": key,
4470            "found": false,
4471        }))),
4472    }
4473}
4474
4475/// POST /v1/axonstore/{name}/mutate — update an existing entry.
4476/// ΛD: mutate → c clamped ≤0.99, δ=derived (Theorem 5.1: only raw may carry c=1.0).
4477/// Body: { "key": "fact_1", "value": <new JSON> }
4478async fn axonstore_mutate_handler(
4479    State(state): State<SharedState>,
4480    headers: HeaderMap,
4481    Path(store_name): Path<String>,
4482    Json(payload): Json<serde_json::Value>,
4483) -> Result<Json<serde_json::Value>, StatusCode> {
4484    let mut s = state.lock().unwrap();
4485    let client = client_key_from_headers(&headers);
4486    check_auth(&mut s, &headers, AccessLevel::Write)?;
4487
4488    let key = payload.get("key").and_then(|v| v.as_str()).unwrap_or("").to_string();
4489    let value = payload.get("value").cloned().unwrap_or(serde_json::json!(null));
4490
4491    if key.is_empty() {
4492        return Ok(Json(serde_json::json!({"error": "key is required"})));
4493    }
4494
4495    let store = match s.axon_stores.get_mut(&store_name) {
4496        Some(st) => st,
4497        None => return Ok(Json(serde_json::json!({"error": format!("axonstore '{}' not found", store_name)}))),
4498    };
4499
4500    match store.entries.get_mut(&key) {
4501        Some(entry) => {
4502            let now = std::time::SystemTime::now()
4503                .duration_since(std::time::UNIX_EPOCH)
4504                .unwrap_or_default()
4505                .as_secs();
4506
4507            entry.value = value;
4508            entry.version += 1;
4509            entry.updated_at = now;
4510            // ΛD Theorem 5.1: mutation degrades certainty — derived, c ≤ 0.99
4511            entry.envelope = EpistemicEnvelope::derived(&store.ontology, 0.99, &client);
4512
4513            store.total_ops += 1;
4514            let version = entry.version;
4515
4516            Ok(Json(serde_json::json!({
4517                "success": true,
4518                "store": store_name,
4519                "key": key,
4520                "version": version,
4521                "envelope": { "certainty": 0.99, "derivation": "derived" },
4522            })))
4523        }
4524        None => Ok(Json(serde_json::json!({
4525            "error": format!("key '{}' not found in axonstore '{}'", key, store_name),
4526        }))),
4527    }
4528}
4529
4530/// POST /v1/axonstore/{name}/purge — delete an entry from the store.
4531/// Body: { "key": "fact_1" }
4532async fn axonstore_purge_handler(
4533    State(state): State<SharedState>,
4534    headers: HeaderMap,
4535    Path(store_name): Path<String>,
4536    Json(payload): Json<serde_json::Value>,
4537) -> Result<Json<serde_json::Value>, StatusCode> {
4538    let mut s = state.lock().unwrap();
4539    let client = client_key_from_headers(&headers);
4540    check_auth(&mut s, &headers, AccessLevel::Write)?;
4541
4542    let key = payload.get("key").and_then(|v| v.as_str()).unwrap_or("").to_string();
4543
4544    if key.is_empty() {
4545        return Ok(Json(serde_json::json!({"error": "key is required"})));
4546    }
4547
4548    let store = match s.axon_stores.get_mut(&store_name) {
4549        Some(st) => st,
4550        None => return Ok(Json(serde_json::json!({"error": format!("axonstore '{}' not found", store_name)}))),
4551    };
4552
4553    match store.entries.remove(&key) {
4554        Some(_) => {
4555            store.total_ops += 1;
4556
4557            s.audit_log.record(&client, AuditAction::ConfigUpdate, "axonstore",
4558                serde_json::json!({"action": "purge", "store": &store_name, "key": &key}), true);
4559
4560            Ok(Json(serde_json::json!({
4561                "success": true,
4562                "store": store_name,
4563                "key": key,
4564                "purged": true,
4565            })))
4566        }
4567        None => Ok(Json(serde_json::json!({
4568            "error": format!("key '{}' not found in axonstore '{}'", key, store_name),
4569        }))),
4570    }
4571}
4572
4573/// POST /v1/axonstore/{name}/transact — atomic batch of persist/mutate/purge operations.
4574/// Body: { "ops": [ { "op": "persist", "key": "k1", "value": "v1" }, { "op": "purge", "key": "k2" } ] }
4575/// All-or-nothing: if any op fails validation, the entire batch is rejected.
4576async fn axonstore_transact_handler(
4577    State(state): State<SharedState>,
4578    headers: HeaderMap,
4579    Path(store_name): Path<String>,
4580    Json(payload): Json<serde_json::Value>,
4581) -> Result<Json<serde_json::Value>, StatusCode> {
4582    let mut s = state.lock().unwrap();
4583    let client = client_key_from_headers(&headers);
4584    check_auth(&mut s, &headers, AccessLevel::Write)?;
4585
4586    let ops: Vec<AxonStoreTransactOp> = match payload.get("ops") {
4587        Some(ops_val) => serde_json::from_value(ops_val.clone()).unwrap_or_default(),
4588        None => return Ok(Json(serde_json::json!({"error": "ops array is required"}))),
4589    };
4590
4591    if ops.is_empty() {
4592        return Ok(Json(serde_json::json!({"error": "ops array must not be empty"})));
4593    }
4594
4595    let store = match s.axon_stores.get_mut(&store_name) {
4596        Some(st) => st,
4597        None => return Ok(Json(serde_json::json!({"error": format!("axonstore '{}' not found", store_name)}))),
4598    };
4599
4600    let now = std::time::SystemTime::now()
4601        .duration_since(std::time::UNIX_EPOCH)
4602        .unwrap_or_default()
4603        .as_secs();
4604
4605    // Validate all ops first (all-or-nothing)
4606    for op in &ops {
4607        match op.op.as_str() {
4608            "persist" => {
4609                if op.key.is_empty() {
4610                    return Ok(Json(serde_json::json!({"error": "persist op requires non-empty key"})));
4611                }
4612            }
4613            "mutate" => {
4614                if op.key.is_empty() {
4615                    return Ok(Json(serde_json::json!({"error": "mutate op requires non-empty key"})));
4616                }
4617                if !store.entries.contains_key(&op.key) {
4618                    return Ok(Json(serde_json::json!({
4619                        "error": format!("mutate op: key '{}' not found (transact is all-or-nothing)", op.key)
4620                    })));
4621                }
4622            }
4623            "purge" => {
4624                if op.key.is_empty() {
4625                    return Ok(Json(serde_json::json!({"error": "purge op requires non-empty key"})));
4626                }
4627                if !store.entries.contains_key(&op.key) {
4628                    return Ok(Json(serde_json::json!({
4629                        "error": format!("purge op: key '{}' not found (transact is all-or-nothing)", op.key)
4630                    })));
4631                }
4632            }
4633            other => {
4634                return Ok(Json(serde_json::json!({
4635                    "error": format!("unknown op '{}', expected persist|mutate|purge", other)
4636                })));
4637            }
4638        }
4639    }
4640
4641    // Apply all ops (validation passed)
4642    let mut results: Vec<serde_json::Value> = Vec::new();
4643    let ontology = store.ontology.clone();
4644
4645    for op in &ops {
4646        match op.op.as_str() {
4647            "persist" => {
4648                let envelope = EpistemicEnvelope::raw_config(&ontology, &client);
4649                let entry = AxonStoreEntry {
4650                    key: op.key.clone(),
4651                    value: op.value.clone(),
4652                    envelope,
4653                    created_at: now,
4654                    updated_at: now,
4655                    version: 1,
4656                };
4657                store.entries.insert(op.key.clone(), entry);
4658                store.total_ops += 1;
4659                results.push(serde_json::json!({"op": "persist", "key": &op.key, "version": 1}));
4660            }
4661            "mutate" => {
4662                if let Some(entry) = store.entries.get_mut(&op.key) {
4663                    entry.value = op.value.clone();
4664                    entry.version += 1;
4665                    entry.updated_at = now;
4666                    entry.envelope = EpistemicEnvelope::derived(&ontology, 0.99, &client);
4667                    store.total_ops += 1;
4668                    results.push(serde_json::json!({"op": "mutate", "key": &op.key, "version": entry.version}));
4669                }
4670            }
4671            "purge" => {
4672                store.entries.remove(&op.key);
4673                store.total_ops += 1;
4674                results.push(serde_json::json!({"op": "purge", "key": &op.key}));
4675            }
4676            _ => {}
4677        }
4678    }
4679
4680    Ok(Json(serde_json::json!({
4681        "success": true,
4682        "store": store_name,
4683        "ops_applied": results.len(),
4684        "results": results,
4685    })))
4686}
4687
4688// ── Dataspace endpoints — cognitive data navigation (primitive #13) ──────────
4689
4690/// POST /v1/dataspace — create a named Dataspace instance.
4691/// Body: { "name": "my_space", "ontology": "research_domain" }
4692async fn dataspace_create_handler(
4693    State(state): State<SharedState>,
4694    headers: HeaderMap,
4695    Json(payload): Json<serde_json::Value>,
4696) -> Result<Json<serde_json::Value>, StatusCode> {
4697    let mut s = state.lock().unwrap();
4698    let client = client_key_from_headers(&headers);
4699    check_auth(&mut s, &headers, AccessLevel::Write)?;
4700
4701    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
4702    let ontology = payload.get("ontology").and_then(|v| v.as_str()).unwrap_or("general").to_string();
4703
4704    if name.is_empty() {
4705        return Ok(Json(serde_json::json!({"error": "name is required"})));
4706    }
4707    if s.dataspaces.contains_key(&name) {
4708        return Ok(Json(serde_json::json!({"error": format!("dataspace '{}' already exists", name)})));
4709    }
4710
4711    let now = std::time::SystemTime::now()
4712        .duration_since(std::time::UNIX_EPOCH)
4713        .unwrap_or_default()
4714        .as_secs();
4715
4716    let ds = DataspaceInstance {
4717        name: name.clone(),
4718        ontology: ontology.clone(),
4719        entries: HashMap::new(),
4720        associations: Vec::new(),
4721        created_at: now,
4722        total_ops: 0,
4723        next_id: 1,
4724    };
4725    s.dataspaces.insert(name.clone(), ds);
4726
4727    s.audit_log.record(&client, AuditAction::ConfigUpdate, "dataspace",
4728        serde_json::json!({"action": "create", "dataspace": &name, "ontology": &ontology}), true);
4729
4730    Ok(Json(serde_json::json!({
4731        "success": true,
4732        "dataspace": name,
4733        "ontology": ontology,
4734        "created_at": now,
4735    })))
4736}
4737
4738/// GET /v1/dataspace — list all Dataspace instances.
4739async fn dataspace_list_handler(
4740    State(state): State<SharedState>,
4741    headers: HeaderMap,
4742) -> Result<Json<serde_json::Value>, StatusCode> {
4743    let s = state.lock().unwrap();
4744    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
4745
4746    let spaces: Vec<serde_json::Value> = s.dataspaces.values().map(|ds| {
4747        serde_json::json!({
4748            "name": ds.name,
4749            "ontology": ds.ontology,
4750            "entry_count": ds.entries.len(),
4751            "association_count": ds.associations.len(),
4752            "total_ops": ds.total_ops,
4753            "created_at": ds.created_at,
4754        })
4755    }).collect();
4756
4757    Ok(Json(serde_json::json!({
4758        "dataspaces": spaces,
4759        "total": spaces.len(),
4760    })))
4761}
4762
4763/// DELETE /v1/dataspace/{name} — delete a Dataspace and all its entries.
4764async fn dataspace_delete_handler(
4765    State(state): State<SharedState>,
4766    headers: HeaderMap,
4767    Path(name): Path<String>,
4768) -> Result<Json<serde_json::Value>, StatusCode> {
4769    let mut s = state.lock().unwrap();
4770    let client = client_key_from_headers(&headers);
4771    check_auth(&mut s, &headers, AccessLevel::Admin)?;
4772
4773    match s.dataspaces.remove(&name) {
4774        Some(removed) => {
4775            s.audit_log.record(&client, AuditAction::ConfigUpdate, "dataspace",
4776                serde_json::json!({"action": "delete", "dataspace": &name,
4777                    "entries_removed": removed.entries.len(),
4778                    "associations_removed": removed.associations.len()}), true);
4779            Ok(Json(serde_json::json!({
4780                "success": true,
4781                "dataspace": name,
4782                "entries_removed": removed.entries.len(),
4783                "associations_removed": removed.associations.len(),
4784            })))
4785        }
4786        None => Ok(Json(serde_json::json!({"error": format!("dataspace '{}' not found", name)}))),
4787    }
4788}
4789
4790/// POST /v1/dataspace/{name}/ingest — add a data entry to the dataspace.
4791/// ΛD: ingest = raw data ingestion → c=1.0, δ=raw.
4792/// Body: { "ontology": "observation", "data": <any JSON>, "tags": ["tag1", "tag2"] }
4793async fn dataspace_ingest_handler(
4794    State(state): State<SharedState>,
4795    headers: HeaderMap,
4796    Path(ds_name): Path<String>,
4797    Json(payload): Json<serde_json::Value>,
4798) -> Result<Json<serde_json::Value>, StatusCode> {
4799    let mut s = state.lock().unwrap();
4800    let client = client_key_from_headers(&headers);
4801    check_auth(&mut s, &headers, AccessLevel::Write)?;
4802
4803    let ds = match s.dataspaces.get_mut(&ds_name) {
4804        Some(d) => d,
4805        None => return Ok(Json(serde_json::json!({"error": format!("dataspace '{}' not found", ds_name)}))),
4806    };
4807
4808    let entry_ontology = payload.get("ontology").and_then(|v| v.as_str())
4809        .unwrap_or(&ds.ontology).to_string();
4810    let data = payload.get("data").cloned().unwrap_or(serde_json::json!(null));
4811    let tags: Vec<String> = payload.get("tags")
4812        .and_then(|v| serde_json::from_value(v.clone()).ok())
4813        .unwrap_or_default();
4814
4815    let now = std::time::SystemTime::now()
4816        .duration_since(std::time::UNIX_EPOCH)
4817        .unwrap_or_default()
4818        .as_secs();
4819
4820    let id = format!("ds_{}_{}", ds_name, ds.next_id);
4821    ds.next_id += 1;
4822
4823    // ΛD: ingest = raw → c=1.0, δ=raw
4824    let envelope = EpistemicEnvelope::raw_config(&entry_ontology, &client);
4825
4826    let entry = DataspaceEntry {
4827        id: id.clone(),
4828        ontology: entry_ontology,
4829        data,
4830        envelope,
4831        ingested_at: now,
4832        tags,
4833    };
4834
4835    ds.entries.insert(id.clone(), entry);
4836    ds.total_ops += 1;
4837
4838    Ok(Json(serde_json::json!({
4839        "success": true,
4840        "dataspace": ds_name,
4841        "entry_id": id,
4842        "envelope": { "certainty": 1.0, "derivation": "raw" },
4843    })))
4844}
4845
4846/// POST /v1/dataspace/{name}/focus — filter entries by predicate.
4847/// ΛD: focus = derived computation → c≤0.99, δ=derived (Theorem 5.1).
4848/// Body: { "ontology": "observation", "tags": ["tag1"], "limit": 100 }
4849/// All filter fields are optional; omitted fields match everything.
4850async fn dataspace_focus_handler(
4851    State(state): State<SharedState>,
4852    headers: HeaderMap,
4853    Path(ds_name): Path<String>,
4854    Json(payload): Json<serde_json::Value>,
4855) -> Result<Json<serde_json::Value>, StatusCode> {
4856    let s = state.lock().unwrap();
4857    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
4858
4859    let ds = match s.dataspaces.get(&ds_name) {
4860        Some(d) => d,
4861        None => return Ok(Json(serde_json::json!({"error": format!("dataspace '{}' not found", ds_name)}))),
4862    };
4863
4864    let filter_ontology = payload.get("ontology").and_then(|v| v.as_str());
4865    let filter_tags: Option<Vec<String>> = payload.get("tags")
4866        .and_then(|v| serde_json::from_value(v.clone()).ok());
4867    let limit = payload.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
4868
4869    let results: Vec<serde_json::Value> = ds.entries.values()
4870        .filter(|e| {
4871            if let Some(ont) = filter_ontology {
4872                if e.ontology != ont { return false; }
4873            }
4874            if let Some(ref tags) = filter_tags {
4875                if !tags.iter().all(|t| e.tags.contains(t)) { return false; }
4876            }
4877            true
4878        })
4879        .take(limit)
4880        .map(|e| {
4881            serde_json::json!({
4882                "id": e.id,
4883                "ontology": e.ontology,
4884                "data": e.data,
4885                "tags": e.tags,
4886                "ingested_at": e.ingested_at,
4887                "envelope": {
4888                    "certainty": e.envelope.certainty,
4889                    "derivation": e.envelope.derivation,
4890                    "provenance": e.envelope.provenance,
4891                }
4892            })
4893        })
4894        .collect();
4895
4896    // ΛD: focus result is derived (filtered subset of raw data)
4897    Ok(Json(serde_json::json!({
4898        "dataspace": ds_name,
4899        "matched": results.len(),
4900        "total_entries": ds.entries.len(),
4901        "results": results,
4902        "result_envelope": {
4903            "certainty": 0.99,
4904            "derivation": "derived",
4905            "reason": "Theorem 5.1: focus is a derived computation over raw data"
4906        },
4907    })))
4908}
4909
4910/// POST /v1/dataspace/{name}/associate — link two entries by named relation.
4911/// Body: { "from": "ds_x_1", "to": "ds_x_2", "relation": "supports", "certainty": 0.85 }
4912async fn dataspace_associate_handler(
4913    State(state): State<SharedState>,
4914    headers: HeaderMap,
4915    Path(ds_name): Path<String>,
4916    Json(payload): Json<serde_json::Value>,
4917) -> Result<Json<serde_json::Value>, StatusCode> {
4918    let mut s = state.lock().unwrap();
4919    let client = client_key_from_headers(&headers);
4920    check_auth(&mut s, &headers, AccessLevel::Write)?;
4921
4922    let ds = match s.dataspaces.get_mut(&ds_name) {
4923        Some(d) => d,
4924        None => return Ok(Json(serde_json::json!({"error": format!("dataspace '{}' not found", ds_name)}))),
4925    };
4926
4927    let from = payload.get("from").and_then(|v| v.as_str()).unwrap_or("").to_string();
4928    let to = payload.get("to").and_then(|v| v.as_str()).unwrap_or("").to_string();
4929    let relation = payload.get("relation").and_then(|v| v.as_str()).unwrap_or("related").to_string();
4930    let certainty = payload.get("certainty").and_then(|v| v.as_f64()).unwrap_or(0.9);
4931
4932    if from.is_empty() || to.is_empty() {
4933        return Ok(Json(serde_json::json!({"error": "from and to are required"})));
4934    }
4935    if !ds.entries.contains_key(&from) {
4936        return Ok(Json(serde_json::json!({"error": format!("entry '{}' not found", from)})));
4937    }
4938    if !ds.entries.contains_key(&to) {
4939        return Ok(Json(serde_json::json!({"error": format!("entry '{}' not found", to)})));
4940    }
4941
4942    let now = std::time::SystemTime::now()
4943        .duration_since(std::time::UNIX_EPOCH)
4944        .unwrap_or_default()
4945        .as_secs();
4946
4947    // ΛD: association certainty is clamped to [0, 0.99] — associations are derived knowledge
4948    let clamped_certainty = certainty.clamp(0.0, 0.99);
4949
4950    let assoc = DataspaceAssociation {
4951        from: from.clone(),
4952        to: to.clone(),
4953        relation: relation.clone(),
4954        certainty: clamped_certainty,
4955        created_at: now,
4956    };
4957
4958    ds.associations.push(assoc);
4959    ds.total_ops += 1;
4960
4961    Ok(Json(serde_json::json!({
4962        "success": true,
4963        "dataspace": ds_name,
4964        "from": from,
4965        "to": to,
4966        "relation": relation,
4967        "certainty": clamped_certainty,
4968    })))
4969}
4970
4971/// POST /v1/dataspace/{name}/aggregate — reduce entries to a single value.
4972/// Body: { "op": "count|sum|avg|min|max", "field": "data.score", "ontology": "observation" }
4973/// For count, field is optional. For sum/avg/min/max, field must point to a numeric JSON path.
4974async fn dataspace_aggregate_handler(
4975    State(state): State<SharedState>,
4976    headers: HeaderMap,
4977    Path(ds_name): Path<String>,
4978    Json(payload): Json<serde_json::Value>,
4979) -> Result<Json<serde_json::Value>, StatusCode> {
4980    let s = state.lock().unwrap();
4981    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
4982
4983    let ds = match s.dataspaces.get(&ds_name) {
4984        Some(d) => d,
4985        None => return Ok(Json(serde_json::json!({"error": format!("dataspace '{}' not found", ds_name)}))),
4986    };
4987
4988    let op = payload.get("op").and_then(|v| v.as_str()).unwrap_or("count");
4989    let field = payload.get("field").and_then(|v| v.as_str()).unwrap_or("");
4990    let filter_ontology = payload.get("ontology").and_then(|v| v.as_str());
4991
4992    // Filter entries by ontology if specified
4993    let filtered: Vec<&DataspaceEntry> = ds.entries.values()
4994        .filter(|e| {
4995            if let Some(ont) = filter_ontology {
4996                e.ontology == ont
4997            } else {
4998                true
4999            }
5000        })
5001        .collect();
5002
5003    // Extract numeric values from the specified field path
5004    let extract_number = |entry: &DataspaceEntry| -> Option<f64> {
5005        let parts: Vec<&str> = field.split('.').collect();
5006        let mut current = &entry.data;
5007        for part in &parts[..] {
5008            // Skip "data" prefix if present
5009            if *part == "data" { continue; }
5010            current = current.get(part)?;
5011        }
5012        current.as_f64()
5013    };
5014
5015    let result: serde_json::Value = match op {
5016        "count" => serde_json::json!(filtered.len()),
5017        "sum" => {
5018            let sum: f64 = filtered.iter().filter_map(|e| extract_number(e)).sum();
5019            serde_json::json!(sum)
5020        }
5021        "avg" => {
5022            let values: Vec<f64> = filtered.iter().filter_map(|e| extract_number(e)).collect();
5023            if values.is_empty() {
5024                serde_json::json!(0.0)
5025            } else {
5026                let avg = values.iter().sum::<f64>() / values.len() as f64;
5027                serde_json::json!((avg * 10000.0).round() / 10000.0)
5028            }
5029        }
5030        "min" => {
5031            let min = filtered.iter().filter_map(|e| extract_number(e))
5032                .fold(f64::INFINITY, f64::min);
5033            if min.is_infinite() { serde_json::json!(null) } else { serde_json::json!(min) }
5034        }
5035        "max" => {
5036            let max = filtered.iter().filter_map(|e| extract_number(e))
5037                .fold(f64::NEG_INFINITY, f64::max);
5038            if max.is_infinite() { serde_json::json!(null) } else { serde_json::json!(max) }
5039        }
5040        other => return Ok(Json(serde_json::json!({
5041            "error": format!("unknown aggregate op '{}', expected count|sum|avg|min|max", other)
5042        }))),
5043    };
5044
5045    // ΛD: aggregation is a derived computation → c≤0.99
5046    Ok(Json(serde_json::json!({
5047        "dataspace": ds_name,
5048        "op": op,
5049        "field": field,
5050        "entries_considered": filtered.len(),
5051        "result": result,
5052        "result_envelope": {
5053            "certainty": 0.99,
5054            "derivation": "aggregated",
5055            "reason": "Theorem 5.1: aggregation is a derived reduction over raw data"
5056        },
5057    })))
5058}
5059
5060/// GET /v1/dataspace/{name}/explore — discover structure of the dataspace.
5061/// Returns entry count, ontology distribution, tag frequency, association graph summary.
5062async fn dataspace_explore_handler(
5063    State(state): State<SharedState>,
5064    headers: HeaderMap,
5065    Path(ds_name): Path<String>,
5066) -> Result<Json<serde_json::Value>, StatusCode> {
5067    let s = state.lock().unwrap();
5068    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5069
5070    let ds = match s.dataspaces.get(&ds_name) {
5071        Some(d) => d,
5072        None => return Ok(Json(serde_json::json!({"error": format!("dataspace '{}' not found", ds_name)}))),
5073    };
5074
5075    // Ontology distribution
5076    let mut ontology_counts: HashMap<&str, u64> = HashMap::new();
5077    for entry in ds.entries.values() {
5078        *ontology_counts.entry(&entry.ontology).or_insert(0) += 1;
5079    }
5080
5081    // Tag frequency
5082    let mut tag_counts: HashMap<&str, u64> = HashMap::new();
5083    for entry in ds.entries.values() {
5084        for tag in &entry.tags {
5085            *tag_counts.entry(tag).or_insert(0) += 1;
5086        }
5087    }
5088
5089    // Association summary
5090    let mut relation_counts: HashMap<&str, u64> = HashMap::new();
5091    for assoc in &ds.associations {
5092        *relation_counts.entry(&assoc.relation).or_insert(0) += 1;
5093    }
5094
5095    // Certainty distribution
5096    let certainties: Vec<f64> = ds.entries.values().map(|e| e.envelope.certainty).collect();
5097    let avg_certainty = if certainties.is_empty() {
5098        0.0
5099    } else {
5100        certainties.iter().sum::<f64>() / certainties.len() as f64
5101    };
5102    let min_certainty = certainties.iter().cloned().fold(f64::INFINITY, f64::min);
5103    let max_certainty = certainties.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
5104
5105    Ok(Json(serde_json::json!({
5106        "dataspace": ds_name,
5107        "ontology": ds.ontology,
5108        "entry_count": ds.entries.len(),
5109        "association_count": ds.associations.len(),
5110        "total_ops": ds.total_ops,
5111        "ontology_distribution": ontology_counts,
5112        "tag_frequency": tag_counts,
5113        "relation_types": relation_counts,
5114        "epistemic_summary": {
5115            "avg_certainty": (avg_certainty * 10000.0).round() / 10000.0,
5116            "min_certainty": if min_certainty.is_infinite() { serde_json::json!(null) } else { serde_json::json!(min_certainty) },
5117            "max_certainty": if max_certainty.is_infinite() { serde_json::json!(null) } else { serde_json::json!(max_certainty) },
5118        },
5119        "result_envelope": {
5120            "certainty": 0.99,
5121            "derivation": "derived",
5122            "reason": "Theorem 5.1: exploration is a derived introspection"
5123        },
5124    })))
5125}
5126
5127// ── Shield endpoints ────────────────────────────────────────────────────────
5128
5129/// POST /v1/shields — create a named Shield instance.
5130/// Body: { "name": "toxicity", "mode": "output", "rules": [...] }
5131async fn shield_create_handler(
5132    State(state): State<SharedState>,
5133    headers: HeaderMap,
5134    Json(payload): Json<serde_json::Value>,
5135) -> Result<Json<serde_json::Value>, StatusCode> {
5136    let mut s = state.lock().unwrap();
5137    let client = client_key_from_headers(&headers);
5138    check_auth(&mut s, &headers, AccessLevel::Write)?;
5139
5140    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
5141    let mode = payload.get("mode").and_then(|v| v.as_str()).unwrap_or("both").to_string();
5142
5143    if name.is_empty() {
5144        return Ok(Json(serde_json::json!({"error": "name is required"})));
5145    }
5146
5147    if !["input", "output", "both"].contains(&mode.as_str()) {
5148        return Ok(Json(serde_json::json!({"error": "mode must be 'input', 'output', or 'both'"})));
5149    }
5150
5151    if s.shields.contains_key(&name) {
5152        return Ok(Json(serde_json::json!({"error": format!("shield '{}' already exists", name)})));
5153    }
5154
5155    let rules: Vec<ShieldRule> = payload.get("rules")
5156        .and_then(|v| serde_json::from_value(v.clone()).ok())
5157        .unwrap_or_default();
5158
5159    let now = std::time::SystemTime::now()
5160        .duration_since(std::time::UNIX_EPOCH)
5161        .unwrap_or_default()
5162        .as_secs();
5163
5164    let shield = ShieldInstance {
5165        name: name.clone(),
5166        mode,
5167        rules,
5168        created_at: now,
5169        total_evaluations: 0,
5170        total_blocks: 0,
5171    };
5172
5173    s.shields.insert(name.clone(), shield);
5174
5175    s.audit_log.record(&client, AuditAction::ConfigUpdate, "shield",
5176        serde_json::json!({"action": "create", "name": &name}), true);
5177
5178    Ok(Json(serde_json::json!({
5179        "success": true,
5180        "name": name,
5181    })))
5182}
5183
5184/// GET /v1/shields — list all Shield instances.
5185async fn shield_list_handler(
5186    State(state): State<SharedState>,
5187    headers: HeaderMap,
5188) -> Result<Json<serde_json::Value>, StatusCode> {
5189    let s = state.lock().unwrap();
5190    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5191
5192    let shields: Vec<serde_json::Value> = s.shields.values().map(|sh| {
5193        serde_json::json!({
5194            "name": sh.name,
5195            "mode": sh.mode,
5196            "rule_count": sh.rules.len(),
5197            "total_evaluations": sh.total_evaluations,
5198            "total_blocks": sh.total_blocks,
5199            "created_at": sh.created_at,
5200        })
5201    }).collect();
5202
5203    Ok(Json(serde_json::json!({
5204        "shields": shields,
5205        "count": shields.len(),
5206    })))
5207}
5208
5209/// GET /v1/shields/{name} — introspect a Shield (rules, stats).
5210async fn shield_get_handler(
5211    State(state): State<SharedState>,
5212    headers: HeaderMap,
5213    Path(name): Path<String>,
5214) -> Result<Json<serde_json::Value>, StatusCode> {
5215    let s = state.lock().unwrap();
5216    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5217
5218    match s.shields.get(&name) {
5219        Some(sh) => Ok(Json(serde_json::json!({
5220            "name": sh.name,
5221            "mode": sh.mode,
5222            "rules": sh.rules,
5223            "total_evaluations": sh.total_evaluations,
5224            "total_blocks": sh.total_blocks,
5225            "created_at": sh.created_at,
5226        }))),
5227        None => Ok(Json(serde_json::json!({"error": format!("shield '{}' not found", name)}))),
5228    }
5229}
5230
5231/// DELETE /v1/shields/{name} — remove a Shield instance.
5232async fn shield_delete_handler(
5233    State(state): State<SharedState>,
5234    headers: HeaderMap,
5235    Path(name): Path<String>,
5236) -> Result<Json<serde_json::Value>, StatusCode> {
5237    let mut s = state.lock().unwrap();
5238    let client = client_key_from_headers(&headers);
5239    check_auth(&mut s, &headers, AccessLevel::Admin)?;
5240
5241    match s.shields.remove(&name) {
5242        Some(_) => {
5243            s.audit_log.record(&client, AuditAction::ConfigUpdate, "shield",
5244                serde_json::json!({"action": "delete", "name": &name}), true);
5245            Ok(Json(serde_json::json!({"success": true, "deleted": name})))
5246        }
5247        None => Ok(Json(serde_json::json!({"error": format!("shield '{}' not found", name)}))),
5248    }
5249}
5250
5251/// POST /v1/shields/{name}/evaluate — evaluate content against a Shield.
5252/// Body: { "content": "text to check", "direction": "input"|"output" }
5253/// Returns ShieldResult with block/warn/redact actions and ΛD envelope.
5254async fn shield_evaluate_handler(
5255    State(state): State<SharedState>,
5256    headers: HeaderMap,
5257    Path(name): Path<String>,
5258    Json(payload): Json<serde_json::Value>,
5259) -> Result<Json<serde_json::Value>, StatusCode> {
5260    let mut s = state.lock().unwrap();
5261    check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
5262
5263    let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
5264    let direction = payload.get("direction").and_then(|v| v.as_str()).unwrap_or("input");
5265
5266    let shield = match s.shields.get_mut(&name) {
5267        Some(sh) => sh,
5268        None => return Ok(Json(serde_json::json!({"error": format!("shield '{}' not found", name)}))),
5269    };
5270
5271    // Check mode compatibility
5272    let mode_ok = match shield.mode.as_str() {
5273        "both" => true,
5274        m => m == direction,
5275    };
5276
5277    if !mode_ok {
5278        return Ok(Json(serde_json::json!({
5279            "error": format!("shield '{}' is configured for '{}' only, got '{}'", name, shield.mode, direction),
5280        })));
5281    }
5282
5283    let result = shield.evaluate(&content);
5284    shield.total_evaluations += 1;
5285    if result.blocked {
5286        shield.total_blocks += 1;
5287    }
5288
5289    // ΛD: shield evaluation is derived (c≤0.99) — pattern matching is speculative
5290    let certainty = if result.rules_triggered == 0 { 0.95 } else { 0.85 };
5291
5292    Ok(Json(serde_json::json!({
5293        "shield": name,
5294        "direction": direction,
5295        "blocked": result.blocked,
5296        "warnings": result.warnings,
5297        "redactions": result.redactions,
5298        "content": result.content,
5299        "rules_evaluated": result.rules_evaluated,
5300        "rules_triggered": result.rules_triggered,
5301        "envelope": {
5302            "certainty": certainty,
5303            "derivation": "derived",
5304            "reason": "Theorem 5.1: shield evaluation is approximate pattern matching (δ=derived, c≤0.99)",
5305        },
5306        "lattice_position": if result.blocked { "doubt" } else { "speculate" },
5307        "effect_row": ["io", "epistemic:speculate"],
5308    })))
5309}
5310
5311/// POST /v1/shields/{name}/rules — add a rule to a Shield.
5312/// Body: { "id": "rule_1", "kind": "deny_list", "value": "password", "action": "redact", "description": "..." }
5313async fn shield_add_rule_handler(
5314    State(state): State<SharedState>,
5315    headers: HeaderMap,
5316    Path(name): Path<String>,
5317    Json(payload): Json<serde_json::Value>,
5318) -> Result<Json<serde_json::Value>, StatusCode> {
5319    let mut s = state.lock().unwrap();
5320    let client = client_key_from_headers(&headers);
5321    check_auth(&mut s, &headers, AccessLevel::Write)?;
5322
5323    let shield = match s.shields.get_mut(&name) {
5324        Some(sh) => sh,
5325        None => return Ok(Json(serde_json::json!({"error": format!("shield '{}' not found", name)}))),
5326    };
5327
5328    let rule: ShieldRule = match serde_json::from_value(payload) {
5329        Ok(r) => r,
5330        Err(e) => return Ok(Json(serde_json::json!({"error": format!("invalid rule: {}", e)}))),
5331    };
5332
5333    // Check for duplicate rule ID
5334    if shield.rules.iter().any(|r| r.id == rule.id) {
5335        return Ok(Json(serde_json::json!({"error": format!("rule '{}' already exists in shield '{}'", rule.id, name)})));
5336    }
5337
5338    let rule_id = rule.id.clone();
5339    shield.rules.push(rule);
5340    let total_rules = shield.rules.len();
5341
5342    s.audit_log.record(&client, AuditAction::ConfigUpdate, "shield",
5343        serde_json::json!({"action": "add_rule", "shield": &name, "rule": &rule_id}), true);
5344
5345    Ok(Json(serde_json::json!({
5346        "success": true,
5347        "shield": name,
5348        "rule_added": rule_id,
5349        "total_rules": total_rules,
5350    })))
5351}
5352
5353// ── Corpus endpoints ────────────────────────────────────────────────────────
5354
5355/// POST /v1/corpus — create a named Corpus instance.
5356/// Body: { "name": "research_papers", "ontology": "academic" }
5357async fn corpus_create_handler(
5358    State(state): State<SharedState>,
5359    headers: HeaderMap,
5360    Json(payload): Json<serde_json::Value>,
5361) -> Result<Json<serde_json::Value>, StatusCode> {
5362    let mut s = state.lock().unwrap();
5363    let client = client_key_from_headers(&headers);
5364    check_auth(&mut s, &headers, AccessLevel::Write)?;
5365
5366    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
5367    let ontology = payload.get("ontology").and_then(|v| v.as_str()).unwrap_or("general").to_string();
5368
5369    if name.is_empty() {
5370        return Ok(Json(serde_json::json!({"error": "name is required"})));
5371    }
5372
5373    if s.corpora.contains_key(&name) {
5374        return Ok(Json(serde_json::json!({"error": format!("corpus '{}' already exists", name)})));
5375    }
5376
5377    let now = std::time::SystemTime::now()
5378        .duration_since(std::time::UNIX_EPOCH)
5379        .unwrap_or_default()
5380        .as_secs();
5381
5382    let corpus = CorpusInstance {
5383        name: name.clone(),
5384        ontology,
5385        documents: HashMap::new(),
5386        created_at: now,
5387        total_ops: 0,
5388        next_id: 1,
5389    };
5390
5391    s.corpora.insert(name.clone(), corpus);
5392
5393    s.audit_log.record(&client, AuditAction::ConfigUpdate, "corpus",
5394        serde_json::json!({"action": "create", "name": &name}), true);
5395
5396    Ok(Json(serde_json::json!({"success": true, "name": name})))
5397}
5398
5399/// GET /v1/corpus — list all Corpus instances.
5400async fn corpus_list_handler(
5401    State(state): State<SharedState>,
5402    headers: HeaderMap,
5403) -> Result<Json<serde_json::Value>, StatusCode> {
5404    let s = state.lock().unwrap();
5405    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5406
5407    let corpora: Vec<serde_json::Value> = s.corpora.values().map(|c| {
5408        serde_json::json!({
5409            "name": c.name,
5410            "ontology": c.ontology,
5411            "document_count": c.documents.len(),
5412            "total_ops": c.total_ops,
5413            "created_at": c.created_at,
5414        })
5415    }).collect();
5416
5417    Ok(Json(serde_json::json!({"corpora": corpora, "count": corpora.len()})))
5418}
5419
5420/// DELETE /v1/corpus/{name} — delete a Corpus instance and all its documents.
5421async fn corpus_delete_handler(
5422    State(state): State<SharedState>,
5423    headers: HeaderMap,
5424    Path(name): Path<String>,
5425) -> Result<Json<serde_json::Value>, StatusCode> {
5426    let mut s = state.lock().unwrap();
5427    let client = client_key_from_headers(&headers);
5428    check_auth(&mut s, &headers, AccessLevel::Admin)?;
5429
5430    match s.corpora.remove(&name) {
5431        Some(_) => {
5432            s.audit_log.record(&client, AuditAction::ConfigUpdate, "corpus",
5433                serde_json::json!({"action": "delete", "name": &name}), true);
5434            Ok(Json(serde_json::json!({"success": true, "deleted": name})))
5435        }
5436        None => Ok(Json(serde_json::json!({"error": format!("corpus '{}' not found", name)}))),
5437    }
5438}
5439
5440/// POST /v1/corpus/{name}/ingest — add a document to the corpus.
5441/// Body: { "title": "Paper Title", "content": "Full text...", "tags": ["ml", "nlp"], "source": "arxiv:2301.00001" }
5442/// ΛD: ingest = raw write → c=1.0, δ=raw (the document itself is ground truth).
5443async fn corpus_ingest_handler(
5444    State(state): State<SharedState>,
5445    headers: HeaderMap,
5446    Path(name): Path<String>,
5447    Json(payload): Json<serde_json::Value>,
5448) -> Result<Json<serde_json::Value>, StatusCode> {
5449    let mut s = state.lock().unwrap();
5450    let client = client_key_from_headers(&headers);
5451    check_auth(&mut s, &headers, AccessLevel::Write)?;
5452
5453    let corpus = match s.corpora.get_mut(&name) {
5454        Some(c) => c,
5455        None => return Ok(Json(serde_json::json!({"error": format!("corpus '{}' not found", name)}))),
5456    };
5457
5458    let title = payload.get("title").and_then(|v| v.as_str()).unwrap_or("Untitled").to_string();
5459    let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
5460    let tags: Vec<String> = payload.get("tags")
5461        .and_then(|v| serde_json::from_value(v.clone()).ok())
5462        .unwrap_or_default();
5463    let source = payload.get("source").and_then(|v| v.as_str()).unwrap_or("manual").to_string();
5464
5465    if content.is_empty() {
5466        return Ok(Json(serde_json::json!({"error": "content is required"})));
5467    }
5468
5469    let now = std::time::SystemTime::now()
5470        .duration_since(std::time::UNIX_EPOCH)
5471        .unwrap_or_default()
5472        .as_secs();
5473
5474    let doc_id = format!("doc_{}_{}", name, corpus.next_id);
5475    corpus.next_id += 1;
5476
5477    let word_count = content.split_whitespace().count() as u64;
5478    let envelope = EpistemicEnvelope::raw_config(&corpus.ontology, &client);
5479
5480    let doc = CorpusDocument {
5481        id: doc_id.clone(),
5482        title: title.clone(),
5483        content,
5484        tags,
5485        source,
5486        envelope,
5487        ingested_at: now,
5488        word_count,
5489    };
5490
5491    corpus.documents.insert(doc_id.clone(), doc);
5492    corpus.total_ops += 1;
5493
5494    Ok(Json(serde_json::json!({
5495        "success": true,
5496        "corpus": name,
5497        "document_id": doc_id,
5498        "title": title,
5499        "word_count": word_count,
5500        "envelope": { "certainty": 1.0, "derivation": "raw" },
5501    })))
5502}
5503
5504/// POST /v1/corpus/{name}/search — search documents by keyword with relevance scoring.
5505/// Body: { "query": "neural networks", "tags": ["ml"], "limit": 10 }
5506/// ΛD: search = derived → c≤0.99, δ=derived (relevance scoring is approximate).
5507async fn corpus_search_handler(
5508    State(state): State<SharedState>,
5509    headers: HeaderMap,
5510    Path(name): Path<String>,
5511    Json(payload): Json<serde_json::Value>,
5512) -> Result<Json<serde_json::Value>, StatusCode> {
5513    let mut s = state.lock().unwrap();
5514    check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
5515
5516    let corpus = match s.corpora.get_mut(&name) {
5517        Some(c) => c,
5518        None => return Ok(Json(serde_json::json!({"error": format!("corpus '{}' not found", name)}))),
5519    };
5520
5521    let query = payload.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string();
5522    let filter_tags: Option<Vec<String>> = payload.get("tags")
5523        .and_then(|v| serde_json::from_value(v.clone()).ok());
5524    let limit = payload.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
5525
5526    if query.is_empty() {
5527        return Ok(Json(serde_json::json!({"error": "query is required"})));
5528    }
5529
5530    let query_lower = query.to_lowercase();
5531    let query_terms: Vec<&str> = query_lower.split_whitespace().collect();
5532
5533    // Score documents by keyword relevance (term frequency)
5534    let mut scored: Vec<(String, String, f64, u64)> = Vec::new();
5535    for doc in corpus.documents.values() {
5536        // Tag filter
5537        if let Some(ref tags) = filter_tags {
5538            if !tags.iter().all(|t| doc.tags.contains(t)) {
5539                continue;
5540            }
5541        }
5542
5543        let content_lower = doc.content.to_lowercase();
5544        let title_lower = doc.title.to_lowercase();
5545
5546        // Simple TF-based relevance: count term hits in content + title (title weighted 3x)
5547        let mut hits = 0.0f64;
5548        for term in &query_terms {
5549            hits += content_lower.matches(term).count() as f64;
5550            hits += title_lower.matches(term).count() as f64 * 3.0;
5551        }
5552
5553        if hits > 0.0 {
5554            // Normalize: relevance = hits / (word_count + title_words), capped at 1.0
5555            let total_words = doc.word_count.max(1) as f64 + doc.title.split_whitespace().count() as f64;
5556            let relevance = (hits / total_words).min(1.0);
5557            scored.push((doc.id.clone(), doc.title.clone(), relevance, doc.word_count));
5558        }
5559    }
5560
5561    // Sort by relevance descending
5562    scored.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
5563    scored.truncate(limit);
5564
5565    corpus.total_ops += 1;
5566
5567    let results: Vec<serde_json::Value> = scored.iter().map(|(id, title, rel, wc)| {
5568        serde_json::json!({
5569            "document_id": id,
5570            "title": title,
5571            "relevance": (rel * 10000.0).round() / 10000.0,
5572            "word_count": wc,
5573        })
5574    }).collect();
5575
5576    Ok(Json(serde_json::json!({
5577        "corpus": name,
5578        "query": query,
5579        "results": results,
5580        "total_matches": results.len(),
5581        "envelope": {
5582            "certainty": 0.99,
5583            "derivation": "derived",
5584            "reason": "Theorem 5.1: search relevance is approximate (δ=derived, c≤0.99)",
5585        },
5586        "lattice_position": "speculate",
5587    })))
5588}
5589
5590/// POST /v1/corpus/{name}/cite — generate citations for a query from matching documents.
5591/// Body: { "query": "attention mechanisms", "max_citations": 5, "excerpt_length": 200 }
5592/// ΛD: citation = derived → c≤0.99, δ=derived (excerpt extraction is interpretation).
5593async fn corpus_cite_handler(
5594    State(state): State<SharedState>,
5595    headers: HeaderMap,
5596    Path(name): Path<String>,
5597    Json(payload): Json<serde_json::Value>,
5598) -> Result<Json<serde_json::Value>, StatusCode> {
5599    let mut s = state.lock().unwrap();
5600    let client = client_key_from_headers(&headers);
5601    check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
5602
5603    let corpus = match s.corpora.get_mut(&name) {
5604        Some(c) => c,
5605        None => return Ok(Json(serde_json::json!({"error": format!("corpus '{}' not found", name)}))),
5606    };
5607
5608    let query = payload.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string();
5609    let max_citations = payload.get("max_citations").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
5610    let excerpt_length = payload.get("excerpt_length").and_then(|v| v.as_u64()).unwrap_or(200) as usize;
5611
5612    if query.is_empty() {
5613        return Ok(Json(serde_json::json!({"error": "query is required"})));
5614    }
5615
5616    let query_lower = query.to_lowercase();
5617    let ontology = corpus.ontology.clone();
5618
5619    // Find relevant passages and build citations
5620    let mut citations: Vec<serde_json::Value> = Vec::new();
5621
5622    for doc in corpus.documents.values() {
5623        let content_lower = doc.content.to_lowercase();
5624
5625        // Find best matching position
5626        if let Some(pos) = content_lower.find(&query_lower) {
5627            // Extract excerpt around match
5628            let start = pos.saturating_sub(excerpt_length / 4);
5629            let end = (pos + query.len() + excerpt_length * 3 / 4).min(doc.content.len());
5630            // Ensure we don't split in the middle of a UTF-8 char
5631            let safe_start = doc.content[..start].char_indices().map(|(i, _)| i).last().unwrap_or(0);
5632            let safe_end = doc.content[end..].char_indices().next().map(|(i, _)| end + i).unwrap_or(doc.content.len()).min(doc.content.len());
5633            let excerpt = doc.content[safe_start..safe_end].to_string();
5634
5635            let relevance = 1.0 - (pos as f64 / doc.content.len().max(1) as f64 * 0.1);
5636
5637            let envelope = EpistemicEnvelope::derived(&ontology, 0.99, &client);
5638
5639            citations.push(serde_json::json!({
5640                "document_id": doc.id,
5641                "title": doc.title,
5642                "excerpt": excerpt,
5643                "relevance": (relevance.min(1.0) * 10000.0).round() / 10000.0,
5644                "envelope": {
5645                    "certainty": envelope.certainty,
5646                    "derivation": envelope.derivation,
5647                },
5648            }));
5649        } else {
5650            // Partial term matching
5651            let terms: Vec<&str> = query_lower.split_whitespace().collect();
5652            let hit_count = terms.iter().filter(|t| content_lower.contains(*t)).count();
5653            if hit_count > 0 {
5654                let best_term = terms.iter().find(|t| content_lower.contains(*t)).unwrap();
5655                if let Some(pos) = content_lower.find(*best_term) {
5656                    let start = pos.saturating_sub(excerpt_length / 4);
5657                    let end = (pos + best_term.len() + excerpt_length * 3 / 4).min(doc.content.len());
5658                    let excerpt = doc.content[start..end].to_string();
5659
5660                    let relevance = hit_count as f64 / terms.len().max(1) as f64 * 0.8;
5661                    let envelope = EpistemicEnvelope::derived(&ontology, 0.99, &client);
5662
5663                    citations.push(serde_json::json!({
5664                        "document_id": doc.id,
5665                        "title": doc.title,
5666                        "excerpt": excerpt,
5667                        "relevance": (relevance.min(1.0) * 10000.0).round() / 10000.0,
5668                        "envelope": {
5669                            "certainty": envelope.certainty,
5670                            "derivation": envelope.derivation,
5671                        },
5672                    }));
5673                }
5674            }
5675        }
5676    }
5677
5678    // Sort by relevance, take top N
5679    citations.sort_by(|a, b| {
5680        b["relevance"].as_f64().unwrap_or(0.0)
5681            .partial_cmp(&a["relevance"].as_f64().unwrap_or(0.0))
5682            .unwrap_or(std::cmp::Ordering::Equal)
5683    });
5684    citations.truncate(max_citations);
5685
5686    corpus.total_ops += 1;
5687
5688    Ok(Json(serde_json::json!({
5689        "corpus": name,
5690        "query": query,
5691        "citations": citations,
5692        "total_citations": citations.len(),
5693        "envelope": {
5694            "certainty": 0.99,
5695            "derivation": "derived",
5696            "reason": "Theorem 5.1: citation extraction is interpretive (δ=derived, c≤0.99)",
5697        },
5698        "lattice_position": "speculate",
5699    })))
5700}
5701
5702// ── Compute endpoints ───────────────────────────────────────────────────────
5703
5704/// POST /v1/compute/evaluate — evaluate a numeric/symbolic expression.
5705/// Body: { "expression": "2 * (3 + 4) ^ 2", "variables": { "x": 10 } }
5706/// ΛD: exact integer arithmetic → c=1.0, δ=raw; floating/symbolic → c=0.99, δ=derived.
5707async fn compute_evaluate_handler(
5708    State(state): State<SharedState>,
5709    headers: HeaderMap,
5710    Json(payload): Json<serde_json::Value>,
5711) -> Result<Json<serde_json::Value>, StatusCode> {
5712    let s = state.lock().unwrap();
5713    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5714    drop(s);
5715
5716    let expression = payload.get("expression").and_then(|v| v.as_str()).unwrap_or("").to_string();
5717    let variables: HashMap<String, f64> = payload.get("variables")
5718        .and_then(|v| serde_json::from_value(v.clone()).ok())
5719        .unwrap_or_default();
5720
5721    if expression.is_empty() {
5722        return Ok(Json(serde_json::json!({"error": "expression is required"})));
5723    }
5724
5725    match compute_evaluate(&expression, &variables) {
5726        Ok(result) => {
5727            Ok(Json(serde_json::json!({
5728                "expression": result.expression,
5729                "value": result.value,
5730                "exact": result.exact,
5731                "variables": result.variables,
5732                "envelope": {
5733                    "certainty": result.certainty,
5734                    "derivation": result.derivation,
5735                },
5736                "lattice_position": if result.exact { "know" } else { "speculate" },
5737                "effect_row": ["compute", if result.exact { "epistemic:know" } else { "epistemic:speculate" }],
5738            })))
5739        }
5740        Err(e) => {
5741            Ok(Json(serde_json::json!({
5742                "error": e,
5743                "expression": expression,
5744                "_axon_blame": { "blame": "caller", "reason": "CT-2: invalid expression" },
5745            })))
5746        }
5747    }
5748}
5749
5750/// POST /v1/compute/batch — evaluate multiple expressions in one call.
5751/// Body: { "expressions": ["2+3", "x*y"], "variables": { "x": 10, "y": 5 } }
5752async fn compute_batch_handler(
5753    State(state): State<SharedState>,
5754    headers: HeaderMap,
5755    Json(payload): Json<serde_json::Value>,
5756) -> Result<Json<serde_json::Value>, StatusCode> {
5757    let s = state.lock().unwrap();
5758    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5759    drop(s);
5760
5761    let expressions: Vec<String> = payload.get("expressions")
5762        .and_then(|v| serde_json::from_value(v.clone()).ok())
5763        .unwrap_or_default();
5764    let variables: HashMap<String, f64> = payload.get("variables")
5765        .and_then(|v| serde_json::from_value(v.clone()).ok())
5766        .unwrap_or_default();
5767
5768    if expressions.is_empty() {
5769        return Ok(Json(serde_json::json!({"error": "expressions array is required"})));
5770    }
5771
5772    let mut results: Vec<serde_json::Value> = Vec::new();
5773    let mut all_exact = true;
5774
5775    for expr in &expressions {
5776        match compute_evaluate(expr, &variables) {
5777            Ok(result) => {
5778                if !result.exact { all_exact = false; }
5779                results.push(serde_json::json!({
5780                    "expression": result.expression,
5781                    "value": result.value,
5782                    "exact": result.exact,
5783                    "certainty": result.certainty,
5784                }));
5785            }
5786            Err(e) => {
5787                all_exact = false;
5788                results.push(serde_json::json!({
5789                    "expression": expr,
5790                    "error": e,
5791                }));
5792            }
5793        }
5794    }
5795
5796    Ok(Json(serde_json::json!({
5797        "results": results,
5798        "count": results.len(),
5799        "all_exact": all_exact,
5800        "envelope": {
5801            "certainty": if all_exact { 1.0 } else { 0.99 },
5802            "derivation": if all_exact { "raw" } else { "derived" },
5803        },
5804    })))
5805}
5806
5807/// GET /v1/compute/functions — list available functions and constants.
5808async fn compute_functions_handler(
5809    State(state): State<SharedState>,
5810    headers: HeaderMap,
5811) -> Result<Json<serde_json::Value>, StatusCode> {
5812    let s = state.lock().unwrap();
5813    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5814
5815    Ok(Json(serde_json::json!({
5816        "operators": ["+", "-", "*", "/", "%", "^"],
5817        "functions": {
5818            "sqrt": { "args": 1, "description": "Square root", "exact": false },
5819            "abs": { "args": 1, "description": "Absolute value", "exact": true },
5820            "sin": { "args": 1, "description": "Sine (radians)", "exact": false },
5821            "cos": { "args": 1, "description": "Cosine (radians)", "exact": false },
5822            "log": { "args": 1, "description": "Natural logarithm", "exact": false },
5823            "exp": { "args": 1, "description": "Exponential (e^x)", "exact": false },
5824            "ceil": { "args": 1, "description": "Ceiling", "exact": true },
5825            "floor": { "args": 1, "description": "Floor", "exact": true },
5826            "round": { "args": 1, "description": "Round to nearest integer", "exact": true },
5827        },
5828        "constants": {
5829            "pi": std::f64::consts::PI,
5830            "e": std::f64::consts::E,
5831            "tau": std::f64::consts::TAU,
5832        },
5833        "epistemic_rules": {
5834            "exact_arithmetic": "c=1.0, δ=raw (integer arithmetic only)",
5835            "approximate": "c=0.99, δ=derived (float division, transcendentals, constants)",
5836            "theorem": "Theorem 5.1: only exact computations may carry c=1.0",
5837        },
5838    })))
5839}
5840
5841// ── Mandate endpoints ───────────────────────────────────────────────────────
5842
5843/// POST /v1/mandates — create a named Mandate policy.
5844/// Body: { "name": "flow_access", "description": "Controls flow execution permissions", "rules": [...] }
5845async fn mandate_create_handler(
5846    State(state): State<SharedState>,
5847    headers: HeaderMap,
5848    Json(payload): Json<serde_json::Value>,
5849) -> Result<Json<serde_json::Value>, StatusCode> {
5850    let mut s = state.lock().unwrap();
5851    let client = client_key_from_headers(&headers);
5852    check_auth(&mut s, &headers, AccessLevel::Admin)?;
5853
5854    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
5855    let description = payload.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string();
5856
5857    if name.is_empty() {
5858        return Ok(Json(serde_json::json!({"error": "name is required"})));
5859    }
5860
5861    if s.mandates.contains_key(&name) {
5862        return Ok(Json(serde_json::json!({"error": format!("mandate '{}' already exists", name)})));
5863    }
5864
5865    let rules: Vec<MandateRule> = payload.get("rules")
5866        .and_then(|v| serde_json::from_value(v.clone()).ok())
5867        .unwrap_or_default();
5868
5869    let now = std::time::SystemTime::now()
5870        .duration_since(std::time::UNIX_EPOCH)
5871        .unwrap_or_default()
5872        .as_secs();
5873
5874    let policy = MandatePolicy {
5875        name: name.clone(),
5876        description,
5877        rules,
5878        created_at: now,
5879        total_evaluations: 0,
5880        total_denials: 0,
5881    };
5882
5883    s.mandates.insert(name.clone(), policy);
5884
5885    s.audit_log.record(&client, AuditAction::ConfigUpdate, "mandate",
5886        serde_json::json!({"action": "create", "name": &name}), true);
5887
5888    Ok(Json(serde_json::json!({"success": true, "name": name})))
5889}
5890
5891/// GET /v1/mandates — list all Mandate policies.
5892async fn mandate_list_handler(
5893    State(state): State<SharedState>,
5894    headers: HeaderMap,
5895) -> Result<Json<serde_json::Value>, StatusCode> {
5896    let s = state.lock().unwrap();
5897    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5898
5899    let mandates: Vec<serde_json::Value> = s.mandates.values().map(|m| {
5900        serde_json::json!({
5901            "name": m.name,
5902            "description": m.description,
5903            "rule_count": m.rules.len(),
5904            "total_evaluations": m.total_evaluations,
5905            "total_denials": m.total_denials,
5906            "created_at": m.created_at,
5907        })
5908    }).collect();
5909
5910    Ok(Json(serde_json::json!({"mandates": mandates, "count": mandates.len()})))
5911}
5912
5913/// GET /v1/mandates/{name} — introspect a Mandate policy (rules, stats).
5914async fn mandate_get_handler(
5915    State(state): State<SharedState>,
5916    headers: HeaderMap,
5917    Path(name): Path<String>,
5918) -> Result<Json<serde_json::Value>, StatusCode> {
5919    let s = state.lock().unwrap();
5920    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5921
5922    match s.mandates.get(&name) {
5923        Some(m) => Ok(Json(serde_json::json!({
5924            "name": m.name,
5925            "description": m.description,
5926            "rules": m.rules,
5927            "total_evaluations": m.total_evaluations,
5928            "total_denials": m.total_denials,
5929            "created_at": m.created_at,
5930        }))),
5931        None => Ok(Json(serde_json::json!({"error": format!("mandate '{}' not found", name)}))),
5932    }
5933}
5934
5935/// DELETE /v1/mandates/{name} — delete a Mandate policy.
5936async fn mandate_delete_handler(
5937    State(state): State<SharedState>,
5938    headers: HeaderMap,
5939    Path(name): Path<String>,
5940) -> Result<Json<serde_json::Value>, StatusCode> {
5941    let mut s = state.lock().unwrap();
5942    let client = client_key_from_headers(&headers);
5943    check_auth(&mut s, &headers, AccessLevel::Admin)?;
5944
5945    match s.mandates.remove(&name) {
5946        Some(_) => {
5947            s.audit_log.record(&client, AuditAction::ConfigUpdate, "mandate",
5948                serde_json::json!({"action": "delete", "name": &name}), true);
5949            Ok(Json(serde_json::json!({"success": true, "deleted": name})))
5950        }
5951        None => Ok(Json(serde_json::json!({"error": format!("mandate '{}' not found", name)}))),
5952    }
5953}
5954
5955/// POST /v1/mandates/{name}/evaluate — evaluate a request against a Mandate policy.
5956/// Body: { "subject": "admin", "action": "execute", "resource": "/v1/flows/analyze" }
5957/// ΛD: explicit match → c=1.0/raw, default deny → c=0.99/derived.
5958async fn mandate_evaluate_handler(
5959    State(state): State<SharedState>,
5960    headers: HeaderMap,
5961    Path(name): Path<String>,
5962    Json(payload): Json<serde_json::Value>,
5963) -> Result<Json<serde_json::Value>, StatusCode> {
5964    let mut s = state.lock().unwrap();
5965    check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
5966
5967    let subject = payload.get("subject").and_then(|v| v.as_str()).unwrap_or("anonymous");
5968    let action = payload.get("action").and_then(|v| v.as_str()).unwrap_or("");
5969    let resource = payload.get("resource").and_then(|v| v.as_str()).unwrap_or("");
5970
5971    if action.is_empty() || resource.is_empty() {
5972        return Ok(Json(serde_json::json!({"error": "action and resource are required"})));
5973    }
5974
5975    let policy = match s.mandates.get_mut(&name) {
5976        Some(m) => m,
5977        None => return Ok(Json(serde_json::json!({"error": format!("mandate '{}' not found", name)}))),
5978    };
5979
5980    let result = policy.evaluate(subject, action, resource);
5981    policy.total_evaluations += 1;
5982    if !result.allowed {
5983        policy.total_denials += 1;
5984    }
5985
5986    Ok(Json(serde_json::json!({
5987        "mandate": name,
5988        "subject": subject,
5989        "action": action,
5990        "resource": resource,
5991        "allowed": result.allowed,
5992        "effect": result.effect,
5993        "matched_rule": result.matched_rule,
5994        "rules_evaluated": result.rules_evaluated,
5995        "envelope": {
5996            "certainty": result.certainty,
5997            "derivation": result.derivation,
5998        },
5999        "lattice_position": if result.certainty == 1.0 { "know" } else { "speculate" },
6000        "effect_row": ["io", if result.certainty == 1.0 { "epistemic:know" } else { "epistemic:speculate" }],
6001    })))
6002}
6003
6004/// POST /v1/mandates/{name}/rules — add a rule to a Mandate policy.
6005/// Body: { "id": "r1", "subject": "admin", "action": "*", "resource": "*", "effect": "allow", "priority": 100, "enabled": true }
6006async fn mandate_add_rule_handler(
6007    State(state): State<SharedState>,
6008    headers: HeaderMap,
6009    Path(name): Path<String>,
6010    Json(payload): Json<serde_json::Value>,
6011) -> Result<Json<serde_json::Value>, StatusCode> {
6012    let mut s = state.lock().unwrap();
6013    let client = client_key_from_headers(&headers);
6014    check_auth(&mut s, &headers, AccessLevel::Admin)?;
6015
6016    let policy = match s.mandates.get_mut(&name) {
6017        Some(m) => m,
6018        None => return Ok(Json(serde_json::json!({"error": format!("mandate '{}' not found", name)}))),
6019    };
6020
6021    let rule: MandateRule = match serde_json::from_value(payload) {
6022        Ok(r) => r,
6023        Err(e) => return Ok(Json(serde_json::json!({"error": format!("invalid rule: {}", e)}))),
6024    };
6025
6026    if policy.rules.iter().any(|r| r.id == rule.id) {
6027        return Ok(Json(serde_json::json!({"error": format!("rule '{}' already exists in mandate '{}'", rule.id, name)})));
6028    }
6029
6030    let rule_id = rule.id.clone();
6031    policy.rules.push(rule);
6032    let total_rules = policy.rules.len();
6033
6034    s.audit_log.record(&client, AuditAction::ConfigUpdate, "mandate",
6035        serde_json::json!({"action": "add_rule", "mandate": &name, "rule": &rule_id}), true);
6036
6037    Ok(Json(serde_json::json!({
6038        "success": true,
6039        "mandate": name,
6040        "rule_added": rule_id,
6041        "total_rules": total_rules,
6042    })))
6043}
6044
6045// ── Refine endpoints ────────────────────────────────────────────────────────
6046
6047/// POST /v1/refine — start a new Refine session.
6048/// Body: { "name": "improve_summary", "initial_content": "...", "initial_quality": 0.3, "target_quality": 0.9, "max_iterations": 10 }
6049async fn refine_start_handler(
6050    State(state): State<SharedState>,
6051    headers: HeaderMap,
6052    Json(payload): Json<serde_json::Value>,
6053) -> Result<Json<serde_json::Value>, StatusCode> {
6054    let mut s = state.lock().unwrap();
6055    let client = client_key_from_headers(&headers);
6056    check_auth(&mut s, &headers, AccessLevel::Write)?;
6057
6058    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
6059    let initial_content = payload.get("initial_content").and_then(|v| v.as_str()).unwrap_or("").to_string();
6060    let initial_quality = payload.get("initial_quality").and_then(|v| v.as_f64()).unwrap_or(0.0);
6061    let target_quality = payload.get("target_quality").and_then(|v| v.as_f64()).unwrap_or(0.9);
6062    let convergence_threshold = payload.get("convergence_threshold").and_then(|v| v.as_f64()).unwrap_or(0.01);
6063    let max_iterations = payload.get("max_iterations").and_then(|v| v.as_u64()).unwrap_or(10) as u32;
6064
6065    if name.is_empty() || initial_content.is_empty() {
6066        return Ok(Json(serde_json::json!({"error": "name and initial_content are required"})));
6067    }
6068
6069    let now = std::time::SystemTime::now()
6070        .duration_since(std::time::UNIX_EPOCH)
6071        .unwrap_or_default()
6072        .as_secs();
6073
6074    let session_id = format!("refine_{}_{}", name, now);
6075
6076    let mut session = RefineSession {
6077        id: session_id.clone(),
6078        name: name.clone(),
6079        target_quality,
6080        convergence_threshold,
6081        max_iterations,
6082        converged: false,
6083        iterations: Vec::new(),
6084        created_at: now,
6085    };
6086
6087    // Record initial state as iteration 0
6088    let _ = session.add_iteration(initial_content, initial_quality, "initial".into());
6089
6090    s.refine_sessions.insert(session_id.clone(), session);
6091
6092    s.audit_log.record(&client, AuditAction::ConfigUpdate, "refine",
6093        serde_json::json!({"action": "start", "session": &session_id}), true);
6094
6095    Ok(Json(serde_json::json!({
6096        "success": true,
6097        "session_id": session_id,
6098        "name": name,
6099        "initial_quality": initial_quality,
6100        "target_quality": target_quality,
6101        "max_iterations": max_iterations,
6102        "envelope": { "certainty": 0.99, "derivation": "derived" },
6103    })))
6104}
6105
6106/// POST /v1/refine/{id}/iterate — submit the next iteration of a Refine session.
6107/// Body: { "content": "improved text...", "quality": 0.7, "feedback": "improve clarity" }
6108/// ΛD: all refinements are derived (c≤0.99, δ=derived per Theorem 5.1).
6109async fn refine_iterate_handler(
6110    State(state): State<SharedState>,
6111    headers: HeaderMap,
6112    Path(session_id): Path<String>,
6113    Json(payload): Json<serde_json::Value>,
6114) -> Result<Json<serde_json::Value>, StatusCode> {
6115    let mut s = state.lock().unwrap();
6116    check_auth(&mut s, &headers, AccessLevel::Write)?;
6117
6118    let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
6119    let quality = payload.get("quality").and_then(|v| v.as_f64()).unwrap_or(0.0);
6120    let feedback = payload.get("feedback").and_then(|v| v.as_str()).unwrap_or("").to_string();
6121
6122    if content.is_empty() {
6123        return Ok(Json(serde_json::json!({"error": "content is required"})));
6124    }
6125
6126    let session = match s.refine_sessions.get_mut(&session_id) {
6127        Some(sess) => sess,
6128        None => return Ok(Json(serde_json::json!({"error": format!("refine session '{}' not found", session_id)}))),
6129    };
6130
6131    match session.add_iteration(content, quality, feedback) {
6132        Ok(iteration) => {
6133            let iter_num = iteration.iteration;
6134            let delta = iteration.delta;
6135            let converged = session.converged;
6136            let remaining = session.max_iterations.saturating_sub(session.iteration_count());
6137
6138            // ΛD: certainty increases with quality but capped at 0.99 (derived)
6139            let certainty = (0.5 + quality * 0.49).min(0.99);
6140
6141            Ok(Json(serde_json::json!({
6142                "session_id": session_id,
6143                "iteration": iter_num,
6144                "quality": quality,
6145                "delta": (delta * 10000.0).round() / 10000.0,
6146                "converged": converged,
6147                "remaining_iterations": remaining,
6148                "envelope": {
6149                    "certainty": (certainty * 10000.0).round() / 10000.0,
6150                    "derivation": "derived",
6151                    "reason": "Theorem 5.1: refinement is transformation (δ=derived, c≤0.99)",
6152                },
6153                "lattice_position": if converged { "believe" } else { "speculate" },
6154                "effect_row": ["io", "epistemic:speculate"],
6155            })))
6156        }
6157        Err(e) => Ok(Json(serde_json::json!({
6158            "error": e,
6159            "session_id": session_id,
6160        }))),
6161    }
6162}
6163
6164/// GET /v1/refine/{id} — get status and history of a Refine session.
6165async fn refine_status_handler(
6166    State(state): State<SharedState>,
6167    headers: HeaderMap,
6168    Path(session_id): Path<String>,
6169) -> Result<Json<serde_json::Value>, StatusCode> {
6170    let s = state.lock().unwrap();
6171    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6172
6173    let session = match s.refine_sessions.get(&session_id) {
6174        Some(sess) => sess,
6175        None => return Ok(Json(serde_json::json!({"error": format!("refine session '{}' not found", session_id)}))),
6176    };
6177
6178    let quality_trend: Vec<f64> = session.iterations.iter().map(|i| i.quality).collect();
6179    let delta_trend: Vec<f64> = session.iterations.iter().map(|i| (i.delta * 10000.0).round() / 10000.0).collect();
6180
6181    Ok(Json(serde_json::json!({
6182        "session_id": session.id,
6183        "name": session.name,
6184        "converged": session.converged,
6185        "current_quality": session.current_quality(),
6186        "target_quality": session.target_quality,
6187        "iteration_count": session.iteration_count(),
6188        "max_iterations": session.max_iterations,
6189        "quality_trend": quality_trend,
6190        "delta_trend": delta_trend,
6191        "iterations": session.iterations,
6192    })))
6193}
6194
6195/// GET /v1/refine — list all Refine sessions.
6196async fn refine_list_handler(
6197    State(state): State<SharedState>,
6198    headers: HeaderMap,
6199) -> Result<Json<serde_json::Value>, StatusCode> {
6200    let s = state.lock().unwrap();
6201    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6202
6203    let sessions: Vec<serde_json::Value> = s.refine_sessions.values().map(|sess| {
6204        serde_json::json!({
6205            "session_id": sess.id,
6206            "name": sess.name,
6207            "converged": sess.converged,
6208            "current_quality": sess.current_quality(),
6209            "target_quality": sess.target_quality,
6210            "iteration_count": sess.iteration_count(),
6211            "max_iterations": sess.max_iterations,
6212        })
6213    }).collect();
6214
6215    Ok(Json(serde_json::json!({"sessions": sessions, "count": sessions.len()})))
6216}
6217
6218// ── Trail endpoints ─────────────────────────────────────────────────────────
6219
6220/// POST /v1/trails — start a new Trail record.
6221/// Body: { "name": "analyze_flow_trace", "target": "flow:analyze" }
6222async fn trail_start_handler(
6223    State(state): State<SharedState>,
6224    headers: HeaderMap,
6225    Json(payload): Json<serde_json::Value>,
6226) -> Result<Json<serde_json::Value>, StatusCode> {
6227    let mut s = state.lock().unwrap();
6228    let client = client_key_from_headers(&headers);
6229    check_auth(&mut s, &headers, AccessLevel::Write)?;
6230
6231    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
6232    let target = payload.get("target").and_then(|v| v.as_str()).unwrap_or("").to_string();
6233
6234    if name.is_empty() {
6235        return Ok(Json(serde_json::json!({"error": "name is required"})));
6236    }
6237
6238    let now = std::time::SystemTime::now()
6239        .duration_since(std::time::UNIX_EPOCH)
6240        .unwrap_or_default()
6241        .as_secs();
6242
6243    let trail_id = format!("trail_{}_{}", name, now);
6244
6245    let trail = TrailRecord {
6246        id: trail_id.clone(),
6247        name: name.clone(),
6248        target,
6249        completed: false,
6250        outcome: "in_progress".into(),
6251        steps: Vec::new(),
6252        created_at: now,
6253        completed_at: 0,
6254        total_duration_ms: 0,
6255    };
6256
6257    s.trails.insert(trail_id.clone(), trail);
6258
6259    s.audit_log.record(&client, AuditAction::ConfigUpdate, "trail",
6260        serde_json::json!({"action": "start", "trail": &trail_id}), true);
6261
6262    Ok(Json(serde_json::json!({
6263        "success": true,
6264        "trail_id": trail_id,
6265        "name": name,
6266        "envelope": { "certainty": 0.95, "derivation": "raw" },
6267    })))
6268}
6269
6270/// POST /v1/trails/{id}/step — record a step in the trail.
6271/// Body: { "operation": "validate", "input": "flow source", "output": "valid", "duration_ms": 12, "outcome": "success", "metadata": {} }
6272async fn trail_step_handler(
6273    State(state): State<SharedState>,
6274    headers: HeaderMap,
6275    Path(trail_id): Path<String>,
6276    Json(payload): Json<serde_json::Value>,
6277) -> Result<Json<serde_json::Value>, StatusCode> {
6278    let mut s = state.lock().unwrap();
6279    check_auth(&mut s, &headers, AccessLevel::Write)?;
6280
6281    let operation = payload.get("operation").and_then(|v| v.as_str()).unwrap_or("").to_string();
6282    let input = payload.get("input").and_then(|v| v.as_str()).unwrap_or("").to_string();
6283    let output = payload.get("output").and_then(|v| v.as_str()).unwrap_or("").to_string();
6284    let duration_ms = payload.get("duration_ms").and_then(|v| v.as_u64()).unwrap_or(0);
6285    let outcome = payload.get("outcome").and_then(|v| v.as_str()).unwrap_or("success").to_string();
6286    let metadata: HashMap<String, serde_json::Value> = payload.get("metadata")
6287        .and_then(|v| serde_json::from_value(v.clone()).ok())
6288        .unwrap_or_default();
6289
6290    if operation.is_empty() {
6291        return Ok(Json(serde_json::json!({"error": "operation is required"})));
6292    }
6293
6294    let trail = match s.trails.get_mut(&trail_id) {
6295        Some(t) => t,
6296        None => return Ok(Json(serde_json::json!({"error": format!("trail '{}' not found", trail_id)}))),
6297    };
6298
6299    match trail.add_step(operation, input, output, duration_ms, outcome, metadata) {
6300        Ok(step_num) => {
6301            Ok(Json(serde_json::json!({
6302                "trail_id": trail_id,
6303                "step": step_num,
6304                "total_steps": trail.step_count(),
6305                "total_duration_ms": trail.total_duration_ms,
6306                "envelope": { "certainty": 1.0, "derivation": "raw" },
6307            })))
6308        }
6309        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
6310    }
6311}
6312
6313/// POST /v1/trails/{id}/complete — mark a trail as complete.
6314/// Body: { "outcome": "success" }
6315async fn trail_complete_handler(
6316    State(state): State<SharedState>,
6317    headers: HeaderMap,
6318    Path(trail_id): Path<String>,
6319    Json(payload): Json<serde_json::Value>,
6320) -> Result<Json<serde_json::Value>, StatusCode> {
6321    let mut s = state.lock().unwrap();
6322    let client = client_key_from_headers(&headers);
6323    check_auth(&mut s, &headers, AccessLevel::Write)?;
6324
6325    let outcome = payload.get("outcome").and_then(|v| v.as_str()).unwrap_or("success").to_string();
6326
6327    let trail = match s.trails.get_mut(&trail_id) {
6328        Some(t) => t,
6329        None => return Ok(Json(serde_json::json!({"error": format!("trail '{}' not found", trail_id)}))),
6330    };
6331
6332    match trail.complete(outcome.clone()) {
6333        Ok(()) => {
6334            let step_count = trail.step_count();
6335            let success_count = trail.success_count();
6336            let failure_count = trail.failure_count();
6337            let total_duration = trail.total_duration_ms;
6338
6339            s.audit_log.record(&client, AuditAction::ConfigUpdate, "trail",
6340                serde_json::json!({"action": "complete", "trail": &trail_id, "outcome": &outcome}), true);
6341
6342            Ok(Json(serde_json::json!({
6343                "trail_id": trail_id,
6344                "outcome": outcome,
6345                "completed": true,
6346                "step_count": step_count,
6347                "success_count": success_count,
6348                "failure_count": failure_count,
6349                "total_duration_ms": total_duration,
6350                "envelope": { "certainty": 1.0, "derivation": "raw" },
6351                "lattice_position": "know",
6352            })))
6353        }
6354        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
6355    }
6356}
6357
6358/// GET /v1/trails/{id} — get a trail with full step history.
6359async fn trail_get_handler(
6360    State(state): State<SharedState>,
6361    headers: HeaderMap,
6362    Path(trail_id): Path<String>,
6363) -> Result<Json<serde_json::Value>, StatusCode> {
6364    let s = state.lock().unwrap();
6365    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6366
6367    match s.trails.get(&trail_id) {
6368        Some(trail) => Ok(Json(serde_json::json!({
6369            "trail_id": trail.id,
6370            "name": trail.name,
6371            "target": trail.target,
6372            "completed": trail.completed,
6373            "outcome": trail.outcome,
6374            "step_count": trail.step_count(),
6375            "success_count": trail.success_count(),
6376            "failure_count": trail.failure_count(),
6377            "total_duration_ms": trail.total_duration_ms,
6378            "steps": trail.steps,
6379            "created_at": trail.created_at,
6380            "completed_at": trail.completed_at,
6381            "envelope": {
6382                "certainty": if trail.completed { 1.0 } else { 0.95 },
6383                "derivation": "raw",
6384            },
6385        }))),
6386        None => Ok(Json(serde_json::json!({"error": format!("trail '{}' not found", trail_id)}))),
6387    }
6388}
6389
6390/// GET /v1/trails — list all trails.
6391async fn trail_list_handler(
6392    State(state): State<SharedState>,
6393    headers: HeaderMap,
6394) -> Result<Json<serde_json::Value>, StatusCode> {
6395    let s = state.lock().unwrap();
6396    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6397
6398    let trails: Vec<serde_json::Value> = s.trails.values().map(|t| {
6399        serde_json::json!({
6400            "trail_id": t.id,
6401            "name": t.name,
6402            "target": t.target,
6403            "completed": t.completed,
6404            "outcome": t.outcome,
6405            "step_count": t.step_count(),
6406            "total_duration_ms": t.total_duration_ms,
6407        })
6408    }).collect();
6409
6410    Ok(Json(serde_json::json!({"trails": trails, "count": trails.len()})))
6411}
6412
6413// ── Probe endpoints ─────────────────────────────────────────────────────────
6414
6415/// POST /v1/probes — start a new Probe session.
6416/// Body: { "name": "investigate_topic", "question": "What is attention in transformers?", "sources": ["corpus:papers", "axonstore:facts"] }
6417async fn probe_create_handler(
6418    State(state): State<SharedState>,
6419    headers: HeaderMap,
6420    Json(payload): Json<serde_json::Value>,
6421) -> Result<Json<serde_json::Value>, StatusCode> {
6422    let mut s = state.lock().unwrap();
6423    let client = client_key_from_headers(&headers);
6424    check_auth(&mut s, &headers, AccessLevel::Write)?;
6425
6426    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
6427    let question = payload.get("question").and_then(|v| v.as_str()).unwrap_or("").to_string();
6428    let sources: Vec<String> = payload.get("sources")
6429        .and_then(|v| serde_json::from_value(v.clone()).ok())
6430        .unwrap_or_default();
6431
6432    if name.is_empty() || question.is_empty() {
6433        return Ok(Json(serde_json::json!({"error": "name and question are required"})));
6434    }
6435
6436    let now = std::time::SystemTime::now()
6437        .duration_since(std::time::UNIX_EPOCH)
6438        .unwrap_or_default()
6439        .as_secs();
6440
6441    let probe_id = format!("probe_{}_{}", name, now);
6442
6443    let probe = ProbeSession {
6444        id: probe_id.clone(),
6445        name: name.clone(),
6446        question: question.clone(),
6447        sources,
6448        findings: Vec::new(),
6449        completed: false,
6450        created_at: now,
6451        total_queries: 0,
6452    };
6453
6454    s.probes.insert(probe_id.clone(), probe);
6455
6456    s.audit_log.record(&client, AuditAction::ConfigUpdate, "probe",
6457        serde_json::json!({"action": "create", "probe": &probe_id}), true);
6458
6459    Ok(Json(serde_json::json!({
6460        "success": true,
6461        "probe_id": probe_id,
6462        "name": name,
6463        "question": question,
6464        "envelope": { "certainty": 0.5, "derivation": "derived" },
6465        "lattice_position": "speculate",
6466    })))
6467}
6468
6469/// POST /v1/probes/{id}/query — execute an exploratory query within a probe.
6470/// Body: { "source": "corpus:papers", "query": "attention mechanism", "results": [...] }
6471/// Accepts pre-gathered results (since probing is orchestrated by the caller).
6472async fn probe_query_handler(
6473    State(state): State<SharedState>,
6474    headers: HeaderMap,
6475    Path(probe_id): Path<String>,
6476    Json(payload): Json<serde_json::Value>,
6477) -> Result<Json<serde_json::Value>, StatusCode> {
6478    let mut s = state.lock().unwrap();
6479    check_auth(&mut s, &headers, AccessLevel::Write)?;
6480
6481    let source = payload.get("source").and_then(|v| v.as_str()).unwrap_or("").to_string();
6482    let query = payload.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string();
6483
6484    if source.is_empty() || query.is_empty() {
6485        return Ok(Json(serde_json::json!({"error": "source and query are required"})));
6486    }
6487
6488    let probe = match s.probes.get_mut(&probe_id) {
6489        Some(p) => p,
6490        None => return Ok(Json(serde_json::json!({"error": format!("probe '{}' not found", probe_id)}))),
6491    };
6492
6493    if probe.completed {
6494        return Ok(Json(serde_json::json!({"error": "probe already completed"})));
6495    }
6496
6497    // Accept findings from the payload
6498    let results: Vec<serde_json::Value> = payload.get("results")
6499        .and_then(|v| v.as_array().cloned())
6500        .unwrap_or_default();
6501
6502    let mut added = 0u32;
6503    for result in &results {
6504        let content = result.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
6505        let relevance = result.get("relevance").and_then(|v| v.as_f64()).unwrap_or(0.5);
6506
6507        if !content.is_empty() {
6508            probe.add_finding(source.clone(), query.clone(), content, relevance);
6509            added += 1;
6510        }
6511    }
6512
6513    probe.total_queries += 1;
6514    let total_findings = probe.findings.len();
6515    let agg_certainty = probe.aggregate_certainty();
6516
6517    Ok(Json(serde_json::json!({
6518        "probe_id": probe_id,
6519        "source": source,
6520        "query": query,
6521        "findings_added": added,
6522        "total_findings": total_findings,
6523        "aggregate_certainty": agg_certainty,
6524        "envelope": {
6525            "certainty": agg_certainty,
6526            "derivation": "derived",
6527            "reason": "Theorem 5.1: probe findings are exploratory (δ=derived, c≤0.99)",
6528        },
6529        "lattice_position": "speculate",
6530    })))
6531}
6532
6533/// POST /v1/probes/{id}/complete — mark probe as complete, get summary.
6534/// Body: {} (optional)
6535async fn probe_complete_handler(
6536    State(state): State<SharedState>,
6537    headers: HeaderMap,
6538    Path(probe_id): Path<String>,
6539) -> Result<Json<serde_json::Value>, StatusCode> {
6540    let mut s = state.lock().unwrap();
6541    let client = client_key_from_headers(&headers);
6542    check_auth(&mut s, &headers, AccessLevel::Write)?;
6543
6544    let probe = match s.probes.get_mut(&probe_id) {
6545        Some(p) => p,
6546        None => return Ok(Json(serde_json::json!({"error": format!("probe '{}' not found", probe_id)}))),
6547    };
6548
6549    if probe.completed {
6550        return Ok(Json(serde_json::json!({"error": "probe already completed"})));
6551    }
6552
6553    probe.completed = true;
6554
6555    let top = probe.top_findings(5);
6556    let top_json: Vec<serde_json::Value> = top.iter().map(|f| {
6557        serde_json::json!({
6558            "source": f.source, "content": f.content,
6559            "relevance": (f.relevance * 10000.0).round() / 10000.0,
6560            "certainty": (f.certainty * 10000.0).round() / 10000.0,
6561        })
6562    }).collect();
6563
6564    let per_source = probe.findings_per_source();
6565    let agg_certainty = probe.aggregate_certainty();
6566    let question = probe.question.clone();
6567    let total_findings = probe.findings.len();
6568    let total_queries = probe.total_queries;
6569
6570    s.audit_log.record(&client, AuditAction::ConfigUpdate, "probe",
6571        serde_json::json!({"action": "complete", "probe": &probe_id}), true);
6572
6573    Ok(Json(serde_json::json!({
6574        "probe_id": probe_id,
6575        "question": question,
6576        "completed": true,
6577        "total_findings": total_findings,
6578        "total_queries": total_queries,
6579        "top_findings": top_json,
6580        "findings_per_source": per_source,
6581        "aggregate_certainty": agg_certainty,
6582        "envelope": {
6583            "certainty": agg_certainty,
6584            "derivation": "derived",
6585        },
6586        "lattice_position": if agg_certainty > 0.8 { "believe" } else { "speculate" },
6587    })))
6588}
6589
6590/// GET /v1/probes/{id} — get probe status and findings.
6591async fn probe_get_handler(
6592    State(state): State<SharedState>,
6593    headers: HeaderMap,
6594    Path(probe_id): Path<String>,
6595) -> Result<Json<serde_json::Value>, StatusCode> {
6596    let s = state.lock().unwrap();
6597    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6598
6599    match s.probes.get(&probe_id) {
6600        Some(probe) => Ok(Json(serde_json::json!({
6601            "probe_id": probe.id,
6602            "name": probe.name,
6603            "question": probe.question,
6604            "sources": probe.sources,
6605            "completed": probe.completed,
6606            "total_findings": probe.findings.len(),
6607            "total_queries": probe.total_queries,
6608            "aggregate_certainty": probe.aggregate_certainty(),
6609            "findings_per_source": probe.findings_per_source(),
6610            "findings": probe.findings,
6611        }))),
6612        None => Ok(Json(serde_json::json!({"error": format!("probe '{}' not found", probe_id)}))),
6613    }
6614}
6615
6616/// GET /v1/probes — list all probe sessions.
6617async fn probe_list_handler(
6618    State(state): State<SharedState>,
6619    headers: HeaderMap,
6620) -> Result<Json<serde_json::Value>, StatusCode> {
6621    let s = state.lock().unwrap();
6622    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6623
6624    let probes_list: Vec<serde_json::Value> = s.probes.values().map(|p| {
6625        serde_json::json!({
6626            "probe_id": p.id,
6627            "name": p.name,
6628            "question": p.question,
6629            "completed": p.completed,
6630            "total_findings": p.findings.len(),
6631            "total_queries": p.total_queries,
6632            "aggregate_certainty": p.aggregate_certainty(),
6633        })
6634    }).collect();
6635
6636    Ok(Json(serde_json::json!({"probes": probes_list, "count": probes_list.len()})))
6637}
6638
6639// ── Weave endpoints ─────────────────────────────────────────────────────────
6640
6641/// POST /v1/weaves — start a new Weave session.
6642/// Body: { "name": "research_synthesis", "goal": "Combine findings on attention mechanisms" }
6643async fn weave_create_handler(
6644    State(state): State<SharedState>,
6645    headers: HeaderMap,
6646    Json(payload): Json<serde_json::Value>,
6647) -> Result<Json<serde_json::Value>, StatusCode> {
6648    let mut s = state.lock().unwrap();
6649    let client = client_key_from_headers(&headers);
6650    check_auth(&mut s, &headers, AccessLevel::Write)?;
6651
6652    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
6653    let goal = payload.get("goal").and_then(|v| v.as_str()).unwrap_or("").to_string();
6654
6655    if name.is_empty() {
6656        return Ok(Json(serde_json::json!({"error": "name is required"})));
6657    }
6658
6659    let now = std::time::SystemTime::now()
6660        .duration_since(std::time::UNIX_EPOCH)
6661        .unwrap_or_default()
6662        .as_secs();
6663
6664    let weave_id = format!("weave_{}_{}", name, now);
6665
6666    let weave = WeaveSession {
6667        id: weave_id.clone(),
6668        name: name.clone(),
6669        goal,
6670        strands: Vec::new(),
6671        synthesis: String::new(),
6672        synthesized: false,
6673        created_at: now,
6674        next_strand_id: 1,
6675    };
6676
6677    s.weaves.insert(weave_id.clone(), weave);
6678
6679    s.audit_log.record(&client, AuditAction::ConfigUpdate, "weave",
6680        serde_json::json!({"action": "create", "weave": &weave_id}), true);
6681
6682    Ok(Json(serde_json::json!({
6683        "success": true,
6684        "weave_id": weave_id,
6685        "name": name,
6686    })))
6687}
6688
6689/// POST /v1/weaves/{id}/strand — add a source strand to the weave.
6690/// Body: { "source": "corpus:papers/doc_1", "content": "...", "weight": 0.8, "source_certainty": 1.0 }
6691async fn weave_strand_handler(
6692    State(state): State<SharedState>,
6693    headers: HeaderMap,
6694    Path(weave_id): Path<String>,
6695    Json(payload): Json<serde_json::Value>,
6696) -> Result<Json<serde_json::Value>, StatusCode> {
6697    let mut s = state.lock().unwrap();
6698    check_auth(&mut s, &headers, AccessLevel::Write)?;
6699
6700    let source = payload.get("source").and_then(|v| v.as_str()).unwrap_or("").to_string();
6701    let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
6702    let weight = payload.get("weight").and_then(|v| v.as_f64()).unwrap_or(1.0);
6703    let source_certainty = payload.get("source_certainty").and_then(|v| v.as_f64()).unwrap_or(0.99);
6704
6705    if source.is_empty() || content.is_empty() {
6706        return Ok(Json(serde_json::json!({"error": "source and content are required"})));
6707    }
6708
6709    let weave = match s.weaves.get_mut(&weave_id) {
6710        Some(w) => w,
6711        None => return Ok(Json(serde_json::json!({"error": format!("weave '{}' not found", weave_id)}))),
6712    };
6713
6714    if weave.synthesized {
6715        return Ok(Json(serde_json::json!({"error": "weave already synthesized, cannot add strands"})));
6716    }
6717
6718    let strand_id = weave.add_strand(source, content, weight, source_certainty);
6719    let total_strands = weave.strands.len();
6720
6721    Ok(Json(serde_json::json!({
6722        "weave_id": weave_id,
6723        "strand_id": strand_id,
6724        "total_strands": total_strands,
6725        "synthesis_certainty": weave.synthesis_certainty(),
6726    })))
6727}
6728
6729/// POST /v1/weaves/{id}/synthesize — synthesize strands into unified output.
6730/// Returns the synthesis with attribution and ΛD envelope.
6731async fn weave_synthesize_handler(
6732    State(state): State<SharedState>,
6733    headers: HeaderMap,
6734    Path(weave_id): Path<String>,
6735) -> Result<Json<serde_json::Value>, StatusCode> {
6736    let mut s = state.lock().unwrap();
6737    let client = client_key_from_headers(&headers);
6738    check_auth(&mut s, &headers, AccessLevel::Write)?;
6739
6740    let weave = match s.weaves.get_mut(&weave_id) {
6741        Some(w) => w,
6742        None => return Ok(Json(serde_json::json!({"error": format!("weave '{}' not found", weave_id)}))),
6743    };
6744
6745    match weave.synthesize() {
6746        Ok(synthesis) => {
6747            let certainty = weave.synthesis_certainty().min(0.99);
6748            let attributions = weave.attributions();
6749            let strand_count = weave.strands.len();
6750
6751            let attr_json: Vec<serde_json::Value> = attributions.iter().map(|(src, w)| {
6752                serde_json::json!({"source": src, "weight": w})
6753            }).collect();
6754
6755            s.audit_log.record(&client, AuditAction::ConfigUpdate, "weave",
6756                serde_json::json!({"action": "synthesize", "weave": &weave_id}), true);
6757
6758            Ok(Json(serde_json::json!({
6759                "weave_id": weave_id,
6760                "synthesized": true,
6761                "synthesis": synthesis,
6762                "strand_count": strand_count,
6763                "attributions": attr_json,
6764                "envelope": {
6765                    "certainty": certainty,
6766                    "derivation": "derived",
6767                    "reason": "Theorem 5.1: synthesis is always derived (δ=derived, c≤0.99)",
6768                },
6769                "lattice_position": if certainty > 0.8 { "believe" } else { "speculate" },
6770                "effect_row": ["io", "epistemic:speculate"],
6771            })))
6772        }
6773        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
6774    }
6775}
6776
6777/// GET /v1/weaves/{id} — get weave session with strands and synthesis.
6778async fn weave_get_handler(
6779    State(state): State<SharedState>,
6780    headers: HeaderMap,
6781    Path(weave_id): Path<String>,
6782) -> Result<Json<serde_json::Value>, StatusCode> {
6783    let s = state.lock().unwrap();
6784    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6785
6786    match s.weaves.get(&weave_id) {
6787        Some(weave) => Ok(Json(serde_json::json!({
6788            "weave_id": weave.id,
6789            "name": weave.name,
6790            "goal": weave.goal,
6791            "synthesized": weave.synthesized,
6792            "synthesis": weave.synthesis,
6793            "strand_count": weave.strands.len(),
6794            "strands": weave.strands,
6795            "synthesis_certainty": weave.synthesis_certainty(),
6796            "attributions": weave.attributions().iter().map(|(s, w)| serde_json::json!({"source": s, "weight": w})).collect::<Vec<_>>(),
6797        }))),
6798        None => Ok(Json(serde_json::json!({"error": format!("weave '{}' not found", weave_id)}))),
6799    }
6800}
6801
6802/// GET /v1/weaves — list all weave sessions.
6803async fn weave_list_handler(
6804    State(state): State<SharedState>,
6805    headers: HeaderMap,
6806) -> Result<Json<serde_json::Value>, StatusCode> {
6807    let s = state.lock().unwrap();
6808    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6809
6810    let weaves_list: Vec<serde_json::Value> = s.weaves.values().map(|w| {
6811        serde_json::json!({
6812            "weave_id": w.id,
6813            "name": w.name,
6814            "goal": w.goal,
6815            "synthesized": w.synthesized,
6816            "strand_count": w.strands.len(),
6817            "synthesis_certainty": w.synthesis_certainty(),
6818        })
6819    }).collect();
6820
6821    Ok(Json(serde_json::json!({"weaves": weaves_list, "count": weaves_list.len()})))
6822}
6823
6824// ── Corroborate endpoints ────────────────────────────────────────────────────
6825
6826/// POST /v1/corroborate — start a new Corroborate session.
6827/// Body: { "name": "verify_claim", "claim": "Transformers outperform RNNs on long sequences" }
6828async fn corroborate_create_handler(
6829    State(state): State<SharedState>,
6830    headers: HeaderMap,
6831    Json(payload): Json<serde_json::Value>,
6832) -> Result<Json<serde_json::Value>, StatusCode> {
6833    let mut s = state.lock().unwrap();
6834    let client = client_key_from_headers(&headers);
6835    check_auth(&mut s, &headers, AccessLevel::Write)?;
6836
6837    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
6838    let claim = payload.get("claim").and_then(|v| v.as_str()).unwrap_or("").to_string();
6839
6840    if name.is_empty() || claim.is_empty() {
6841        return Ok(Json(serde_json::json!({"error": "name and claim are required"})));
6842    }
6843
6844    let now = std::time::SystemTime::now()
6845        .duration_since(std::time::UNIX_EPOCH)
6846        .unwrap_or_default()
6847        .as_secs();
6848
6849    let session_id = format!("corr_{}_{}", name, now);
6850
6851    let session = CorroborateSession {
6852        id: session_id.clone(),
6853        name: name.clone(),
6854        claim: claim.clone(),
6855        evidence: Vec::new(),
6856        verified: false,
6857        verdict: "pending".into(),
6858        created_at: now,
6859        next_evidence_id: 1,
6860    };
6861
6862    s.corroborations.insert(session_id.clone(), session);
6863
6864    s.audit_log.record(&client, AuditAction::ConfigUpdate, "corroborate",
6865        serde_json::json!({"action": "create", "session": &session_id}), true);
6866
6867    Ok(Json(serde_json::json!({
6868        "success": true,
6869        "session_id": session_id,
6870        "claim": claim,
6871    })))
6872}
6873
6874/// POST /v1/corroborate/{id}/evidence — submit evidence for or against the claim.
6875/// Body: { "source": "corpus:papers/doc_1", "content": "Study confirms...", "stance": "supports", "confidence": 0.9 }
6876async fn corroborate_evidence_handler(
6877    State(state): State<SharedState>,
6878    headers: HeaderMap,
6879    Path(session_id): Path<String>,
6880    Json(payload): Json<serde_json::Value>,
6881) -> Result<Json<serde_json::Value>, StatusCode> {
6882    let mut s = state.lock().unwrap();
6883    check_auth(&mut s, &headers, AccessLevel::Write)?;
6884
6885    let source = payload.get("source").and_then(|v| v.as_str()).unwrap_or("").to_string();
6886    let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
6887    let stance = payload.get("stance").and_then(|v| v.as_str()).unwrap_or("").to_string();
6888    let confidence = payload.get("confidence").and_then(|v| v.as_f64()).unwrap_or(0.5);
6889
6890    if source.is_empty() || content.is_empty() || stance.is_empty() {
6891        return Ok(Json(serde_json::json!({"error": "source, content, and stance are required"})));
6892    }
6893
6894    let session = match s.corroborations.get_mut(&session_id) {
6895        Some(sess) => sess,
6896        None => return Ok(Json(serde_json::json!({"error": format!("corroborate session '{}' not found", session_id)}))),
6897    };
6898
6899    match session.add_evidence(source, content, stance, confidence) {
6900        Ok(evidence_id) => {
6901            let (agreement, certainty, verdict_preview) = session.compute_agreement();
6902            let (sup, con, neu) = session.stance_counts();
6903
6904            Ok(Json(serde_json::json!({
6905                "session_id": session_id,
6906                "evidence_id": evidence_id,
6907                "total_evidence": session.evidence.len(),
6908                "stance_counts": { "supports": sup, "contradicts": con, "neutral": neu },
6909                "current_agreement": agreement,
6910                "current_certainty": certainty,
6911                "verdict_preview": verdict_preview,
6912            })))
6913        }
6914        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
6915    }
6916}
6917
6918/// POST /v1/corroborate/{id}/verify — finalize verification with computed verdict.
6919async fn corroborate_verify_handler(
6920    State(state): State<SharedState>,
6921    headers: HeaderMap,
6922    Path(session_id): Path<String>,
6923) -> Result<Json<serde_json::Value>, StatusCode> {
6924    let mut s = state.lock().unwrap();
6925    let client = client_key_from_headers(&headers);
6926    check_auth(&mut s, &headers, AccessLevel::Write)?;
6927
6928    let session = match s.corroborations.get_mut(&session_id) {
6929        Some(sess) => sess,
6930        None => return Ok(Json(serde_json::json!({"error": format!("corroborate session '{}' not found", session_id)}))),
6931    };
6932
6933    match session.verify() {
6934        Ok((agreement, certainty, verdict)) => {
6935            let (sup, con, neu) = session.stance_counts();
6936            let claim = session.claim.clone();
6937
6938            s.audit_log.record(&client, AuditAction::ConfigUpdate, "corroborate",
6939                serde_json::json!({"action": "verify", "session": &session_id, "verdict": &verdict}), true);
6940
6941            // ΛD lattice based on verdict
6942            let lattice = match verdict.as_str() {
6943                "corroborated" => "believe",
6944                "disputed" => "doubt",
6945                _ => "speculate",
6946            };
6947
6948            Ok(Json(serde_json::json!({
6949                "session_id": session_id,
6950                "claim": claim,
6951                "verified": true,
6952                "verdict": verdict,
6953                "agreement": agreement,
6954                "stance_counts": { "supports": sup, "contradicts": con, "neutral": neu },
6955                "envelope": {
6956                    "certainty": certainty,
6957                    "derivation": "derived",
6958                    "reason": "Theorem 5.1: cross-source verification is inferential (δ=derived, c≤0.99)",
6959                },
6960                "lattice_position": lattice,
6961                "effect_row": ["io", format!("epistemic:{}", lattice)],
6962            })))
6963        }
6964        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
6965    }
6966}
6967
6968/// GET /v1/corroborate/{id} — get session with evidence and verdict.
6969async fn corroborate_get_handler(
6970    State(state): State<SharedState>,
6971    headers: HeaderMap,
6972    Path(session_id): Path<String>,
6973) -> Result<Json<serde_json::Value>, StatusCode> {
6974    let s = state.lock().unwrap();
6975    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6976
6977    match s.corroborations.get(&session_id) {
6978        Some(sess) => {
6979            let (agreement, certainty, _) = sess.compute_agreement();
6980            let (sup, con, neu) = sess.stance_counts();
6981            Ok(Json(serde_json::json!({
6982                "session_id": sess.id,
6983                "name": sess.name,
6984                "claim": sess.claim,
6985                "verified": sess.verified,
6986                "verdict": sess.verdict,
6987                "agreement": agreement,
6988                "certainty": certainty,
6989                "stance_counts": { "supports": sup, "contradicts": con, "neutral": neu },
6990                "evidence": sess.evidence,
6991            })))
6992        }
6993        None => Ok(Json(serde_json::json!({"error": format!("corroborate session '{}' not found", session_id)}))),
6994    }
6995}
6996
6997/// GET /v1/corroborate — list all corroborate sessions.
6998async fn corroborate_list_handler(
6999    State(state): State<SharedState>,
7000    headers: HeaderMap,
7001) -> Result<Json<serde_json::Value>, StatusCode> {
7002    let s = state.lock().unwrap();
7003    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7004
7005    let sessions: Vec<serde_json::Value> = s.corroborations.values().map(|sess| {
7006        let (sup, con, neu) = sess.stance_counts();
7007        serde_json::json!({
7008            "session_id": sess.id,
7009            "name": sess.name,
7010            "claim": sess.claim,
7011            "verified": sess.verified,
7012            "verdict": sess.verdict,
7013            "evidence_count": sess.evidence.len(),
7014            "stance_counts": { "supports": sup, "contradicts": con, "neutral": neu },
7015        })
7016    }).collect();
7017
7018    Ok(Json(serde_json::json!({"sessions": sessions, "count": sessions.len()})))
7019}
7020
7021// ── Drill endpoints ─────────────────────────────────────────────────────────
7022
7023/// POST /v1/drills — start a new Drill session.
7024/// Body: { "name": "explore_attention", "root_question": "How does attention work?", "root_answer": "...", "max_depth": 5 }
7025async fn drill_create_handler(
7026    State(state): State<SharedState>,
7027    headers: HeaderMap,
7028    Json(payload): Json<serde_json::Value>,
7029) -> Result<Json<serde_json::Value>, StatusCode> {
7030    let mut s = state.lock().unwrap();
7031    let client = client_key_from_headers(&headers);
7032    check_auth(&mut s, &headers, AccessLevel::Write)?;
7033
7034    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
7035    let root_question = payload.get("root_question").and_then(|v| v.as_str()).unwrap_or("").to_string();
7036    let root_answer = payload.get("root_answer").and_then(|v| v.as_str()).unwrap_or("").to_string();
7037    let max_depth = payload.get("max_depth").and_then(|v| v.as_u64()).unwrap_or(5) as u32;
7038
7039    if name.is_empty() || root_question.is_empty() {
7040        return Ok(Json(serde_json::json!({"error": "name and root_question are required"})));
7041    }
7042
7043    let now = std::time::SystemTime::now()
7044        .duration_since(std::time::UNIX_EPOCH)
7045        .unwrap_or_default()
7046        .as_secs();
7047
7048    let drill_id = format!("drill_{}_{}", name, now);
7049
7050    let mut drill = DrillSession {
7051        id: drill_id.clone(),
7052        name: name.clone(),
7053        root_question: root_question.clone(),
7054        max_depth,
7055        nodes: HashMap::new(),
7056        completed: false,
7057        created_at: now,
7058    };
7059
7060    let _ = drill.add_root(root_answer);
7061
7062    s.drills.insert(drill_id.clone(), drill);
7063
7064    s.audit_log.record(&client, AuditAction::ConfigUpdate, "drill",
7065        serde_json::json!({"action": "create", "drill": &drill_id}), true);
7066
7067    Ok(Json(serde_json::json!({
7068        "success": true,
7069        "drill_id": drill_id,
7070        "name": name,
7071        "max_depth": max_depth,
7072        "root_certainty": DrillSession::certainty_at_depth(0),
7073        "envelope": { "certainty": 0.99, "derivation": "derived" },
7074    })))
7075}
7076
7077/// POST /v1/drills/{id}/expand — expand a node by adding a child exploration.
7078/// Body: { "parent_id": "root", "question": "What is self-attention?", "answer": "..." }
7079async fn drill_expand_handler(
7080    State(state): State<SharedState>,
7081    headers: HeaderMap,
7082    Path(drill_id): Path<String>,
7083    Json(payload): Json<serde_json::Value>,
7084) -> Result<Json<serde_json::Value>, StatusCode> {
7085    let mut s = state.lock().unwrap();
7086    check_auth(&mut s, &headers, AccessLevel::Write)?;
7087
7088    let parent_id = payload.get("parent_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
7089    let question = payload.get("question").and_then(|v| v.as_str()).unwrap_or("").to_string();
7090    let answer = payload.get("answer").and_then(|v| v.as_str()).unwrap_or("").to_string();
7091
7092    if parent_id.is_empty() || question.is_empty() {
7093        return Ok(Json(serde_json::json!({"error": "parent_id and question are required"})));
7094    }
7095
7096    let drill = match s.drills.get_mut(&drill_id) {
7097        Some(d) => d,
7098        None => return Ok(Json(serde_json::json!({"error": format!("drill '{}' not found", drill_id)}))),
7099    };
7100
7101    match drill.expand(&parent_id, question, answer) {
7102        Ok(child_id) => {
7103            let depth = drill.nodes.get(&child_id).unwrap().depth;
7104            let certainty = drill.nodes.get(&child_id).unwrap().certainty;
7105            let is_leaf = drill.nodes.get(&child_id).unwrap().is_leaf;
7106
7107            Ok(Json(serde_json::json!({
7108                "drill_id": drill_id,
7109                "node_id": child_id,
7110                "depth": depth,
7111                "is_leaf": is_leaf,
7112                "node_count": drill.node_count(),
7113                "envelope": {
7114                    "certainty": certainty,
7115                    "derivation": "derived",
7116                },
7117                "lattice_position": if certainty > 0.8 { "believe" } else { "speculate" },
7118            })))
7119        }
7120        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7121    }
7122}
7123
7124/// POST /v1/drills/{id}/complete — mark drill as complete.
7125async fn drill_complete_handler(
7126    State(state): State<SharedState>,
7127    headers: HeaderMap,
7128    Path(drill_id): Path<String>,
7129) -> Result<Json<serde_json::Value>, StatusCode> {
7130    let mut s = state.lock().unwrap();
7131    let client = client_key_from_headers(&headers);
7132    check_auth(&mut s, &headers, AccessLevel::Write)?;
7133
7134    let drill = match s.drills.get_mut(&drill_id) {
7135        Some(d) => d,
7136        None => return Ok(Json(serde_json::json!({"error": format!("drill '{}' not found", drill_id)}))),
7137    };
7138
7139    if drill.completed {
7140        return Ok(Json(serde_json::json!({"error": "drill already completed"})));
7141    }
7142
7143    drill.completed = true;
7144
7145    let node_count = drill.node_count();
7146    let max_depth_reached = drill.max_depth_reached();
7147    let leaf_count = drill.leaf_count();
7148    let avg_certainty = drill.avg_certainty();
7149
7150    s.audit_log.record(&client, AuditAction::ConfigUpdate, "drill",
7151        serde_json::json!({"action": "complete", "drill": &drill_id}), true);
7152
7153    Ok(Json(serde_json::json!({
7154        "drill_id": drill_id,
7155        "completed": true,
7156        "node_count": node_count,
7157        "max_depth_reached": max_depth_reached,
7158        "leaf_count": leaf_count,
7159        "avg_certainty": avg_certainty,
7160        "envelope": {
7161            "certainty": avg_certainty.min(0.99),
7162            "derivation": "derived",
7163        },
7164    })))
7165}
7166
7167/// GET /v1/drills/{id} — get drill with full exploration tree.
7168async fn drill_get_handler(
7169    State(state): State<SharedState>,
7170    headers: HeaderMap,
7171    Path(drill_id): Path<String>,
7172) -> Result<Json<serde_json::Value>, StatusCode> {
7173    let s = state.lock().unwrap();
7174    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7175
7176    match s.drills.get(&drill_id) {
7177        Some(drill) => Ok(Json(serde_json::json!({
7178            "drill_id": drill.id,
7179            "name": drill.name,
7180            "root_question": drill.root_question,
7181            "max_depth": drill.max_depth,
7182            "completed": drill.completed,
7183            "node_count": drill.node_count(),
7184            "max_depth_reached": drill.max_depth_reached(),
7185            "leaf_count": drill.leaf_count(),
7186            "avg_certainty": drill.avg_certainty(),
7187            "nodes": drill.nodes,
7188        }))),
7189        None => Ok(Json(serde_json::json!({"error": format!("drill '{}' not found", drill_id)}))),
7190    }
7191}
7192
7193/// GET /v1/drills — list all drill sessions.
7194async fn drill_list_handler(
7195    State(state): State<SharedState>,
7196    headers: HeaderMap,
7197) -> Result<Json<serde_json::Value>, StatusCode> {
7198    let s = state.lock().unwrap();
7199    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7200
7201    let drills_list: Vec<serde_json::Value> = s.drills.values().map(|d| {
7202        serde_json::json!({
7203            "drill_id": d.id,
7204            "name": d.name,
7205            "root_question": d.root_question,
7206            "completed": d.completed,
7207            "node_count": d.node_count(),
7208            "max_depth_reached": d.max_depth_reached(),
7209        })
7210    }).collect();
7211
7212    Ok(Json(serde_json::json!({"drills": drills_list, "count": drills_list.len()})))
7213}
7214
7215// ── Forge endpoints ─────────────────────────────────────────────────────────
7216
7217/// POST /v1/forges — create a new Forge session.
7218/// Body: { "name": "report_generator" }
7219async fn forge_create_handler(
7220    State(state): State<SharedState>,
7221    headers: HeaderMap,
7222    Json(payload): Json<serde_json::Value>,
7223) -> Result<Json<serde_json::Value>, StatusCode> {
7224    let mut s = state.lock().unwrap();
7225    let client = client_key_from_headers(&headers);
7226    check_auth(&mut s, &headers, AccessLevel::Write)?;
7227
7228    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
7229
7230    if name.is_empty() {
7231        return Ok(Json(serde_json::json!({"error": "name is required"})));
7232    }
7233
7234    let now = std::time::SystemTime::now()
7235        .duration_since(std::time::UNIX_EPOCH)
7236        .unwrap_or_default()
7237        .as_secs();
7238
7239    let forge_id = format!("forge_{}_{}", name, now);
7240
7241    let forge = ForgeSession {
7242        id: forge_id.clone(),
7243        name: name.clone(),
7244        templates: HashMap::new(),
7245        artifacts: Vec::new(),
7246        created_at: now,
7247        next_artifact_id: 1,
7248    };
7249
7250    s.forges.insert(forge_id.clone(), forge);
7251
7252    s.audit_log.record(&client, AuditAction::ConfigUpdate, "forge",
7253        serde_json::json!({"action": "create", "forge": &forge_id}), true);
7254
7255    Ok(Json(serde_json::json!({"success": true, "forge_id": forge_id, "name": name})))
7256}
7257
7258/// POST /v1/forges/{id}/template — register a template in the forge.
7259/// Body: { "name": "summary", "content": "# {{title}}\n\n{{body}}", "format": "markdown" }
7260async fn forge_template_handler(
7261    State(state): State<SharedState>,
7262    headers: HeaderMap,
7263    Path(forge_id): Path<String>,
7264    Json(payload): Json<serde_json::Value>,
7265) -> Result<Json<serde_json::Value>, StatusCode> {
7266    let mut s = state.lock().unwrap();
7267    check_auth(&mut s, &headers, AccessLevel::Write)?;
7268
7269    let template_name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
7270    let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
7271    let format = payload.get("format").and_then(|v| v.as_str()).unwrap_or("text").to_string();
7272
7273    if template_name.is_empty() || content.is_empty() {
7274        return Ok(Json(serde_json::json!({"error": "name and content are required"})));
7275    }
7276
7277    let forge = match s.forges.get_mut(&forge_id) {
7278        Some(f) => f,
7279        None => return Ok(Json(serde_json::json!({"error": format!("forge '{}' not found", forge_id)}))),
7280    };
7281
7282    let variables = ForgeSession::extract_variables(&content);
7283
7284    match forge.add_template(template_name.clone(), content, format) {
7285        Ok(()) => Ok(Json(serde_json::json!({
7286            "forge_id": forge_id,
7287            "template": template_name,
7288            "variables": variables,
7289            "total_templates": forge.templates.len(),
7290        }))),
7291        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7292    }
7293}
7294
7295/// POST /v1/forges/{id}/render — render a template with variables to produce an artifact.
7296/// Body: { "template": "summary", "variables": { "title": "Report", "body": "Content..." } }
7297async fn forge_render_handler(
7298    State(state): State<SharedState>,
7299    headers: HeaderMap,
7300    Path(forge_id): Path<String>,
7301    Json(payload): Json<serde_json::Value>,
7302) -> Result<Json<serde_json::Value>, StatusCode> {
7303    let mut s = state.lock().unwrap();
7304    check_auth(&mut s, &headers, AccessLevel::Write)?;
7305
7306    let template_name = payload.get("template").and_then(|v| v.as_str()).unwrap_or("").to_string();
7307    let variables: HashMap<String, String> = payload.get("variables")
7308        .and_then(|v| serde_json::from_value(v.clone()).ok())
7309        .unwrap_or_default();
7310
7311    if template_name.is_empty() {
7312        return Ok(Json(serde_json::json!({"error": "template name is required"})));
7313    }
7314
7315    let forge = match s.forges.get_mut(&forge_id) {
7316        Some(f) => f,
7317        None => return Ok(Json(serde_json::json!({"error": format!("forge '{}' not found", forge_id)}))),
7318    };
7319
7320    match forge.render(&template_name, &variables) {
7321        Ok(artifact) => {
7322            Ok(Json(serde_json::json!({
7323                "forge_id": forge_id,
7324                "artifact_id": artifact.id,
7325                "template": artifact.template_name,
7326                "content": artifact.content,
7327                "format": artifact.format,
7328                "variables_used": artifact.variables_used,
7329                "total_artifacts": forge.artifacts.len(),
7330                "envelope": {
7331                    "certainty": artifact.certainty,
7332                    "derivation": "derived",
7333                    "reason": "Theorem 5.1: template rendering is transformation (δ=derived, c=0.99)",
7334                },
7335                "lattice_position": "believe",
7336                "effect_row": ["io", "epistemic:believe"],
7337            })))
7338        }
7339        Err(e) => Ok(Json(serde_json::json!({
7340            "error": e,
7341            "_axon_blame": { "blame": "caller", "reason": "CT-2" },
7342        }))),
7343    }
7344}
7345
7346/// GET /v1/forges/{id} — get forge session with templates and artifacts.
7347async fn forge_get_handler(
7348    State(state): State<SharedState>,
7349    headers: HeaderMap,
7350    Path(forge_id): Path<String>,
7351) -> Result<Json<serde_json::Value>, StatusCode> {
7352    let s = state.lock().unwrap();
7353    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7354
7355    match s.forges.get(&forge_id) {
7356        Some(forge) => {
7357            let templates: Vec<serde_json::Value> = forge.templates.values().map(|t| {
7358                serde_json::json!({
7359                    "name": t.name, "format": t.format, "variables": t.variables,
7360                })
7361            }).collect();
7362
7363            Ok(Json(serde_json::json!({
7364                "forge_id": forge.id,
7365                "name": forge.name,
7366                "templates": templates,
7367                "artifact_count": forge.artifacts.len(),
7368                "artifacts": forge.artifacts,
7369            })))
7370        }
7371        None => Ok(Json(serde_json::json!({"error": format!("forge '{}' not found", forge_id)}))),
7372    }
7373}
7374
7375/// GET /v1/forges — list all forge sessions.
7376async fn forge_list_handler(
7377    State(state): State<SharedState>,
7378    headers: HeaderMap,
7379) -> Result<Json<serde_json::Value>, StatusCode> {
7380    let s = state.lock().unwrap();
7381    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7382
7383    let forges_list: Vec<serde_json::Value> = s.forges.values().map(|f| {
7384        serde_json::json!({
7385            "forge_id": f.id,
7386            "name": f.name,
7387            "template_count": f.templates.len(),
7388            "artifact_count": f.artifacts.len(),
7389        })
7390    }).collect();
7391
7392    Ok(Json(serde_json::json!({"forges": forges_list, "count": forges_list.len()})))
7393}
7394
7395// ── Deliberate endpoints ────────────────────────────────────────────────────
7396
7397/// POST /v1/deliberate — start a new Deliberate session.
7398/// Body: { "name": "choose_backend", "question": "Which backend should we use for production?" }
7399async fn deliberate_create_handler(
7400    State(state): State<SharedState>,
7401    headers: HeaderMap,
7402    Json(payload): Json<serde_json::Value>,
7403) -> Result<Json<serde_json::Value>, StatusCode> {
7404    let mut s = state.lock().unwrap();
7405    let client = client_key_from_headers(&headers);
7406    check_auth(&mut s, &headers, AccessLevel::Write)?;
7407
7408    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
7409    let question = payload.get("question").and_then(|v| v.as_str()).unwrap_or("").to_string();
7410
7411    if name.is_empty() || question.is_empty() {
7412        return Ok(Json(serde_json::json!({"error": "name and question are required"})));
7413    }
7414
7415    let now = std::time::SystemTime::now()
7416        .duration_since(std::time::UNIX_EPOCH)
7417        .unwrap_or_default()
7418        .as_secs();
7419
7420    let session_id = format!("delib_{}_{}", name, now);
7421
7422    let session = DeliberateSession {
7423        id: session_id.clone(),
7424        name: name.clone(),
7425        question: question.clone(),
7426        options: Vec::new(),
7427        decided: false,
7428        chosen_option: None,
7429        created_at: now,
7430        next_option_id: 1,
7431    };
7432
7433    s.deliberations.insert(session_id.clone(), session);
7434
7435    s.audit_log.record(&client, AuditAction::ConfigUpdate, "deliberate",
7436        serde_json::json!({"action": "create", "session": &session_id}), true);
7437
7438    Ok(Json(serde_json::json!({"success": true, "session_id": session_id, "question": question})))
7439}
7440
7441/// POST /v1/deliberate/{id}/option — add an option to consider.
7442/// Body: { "label": "Anthropic", "description": "Claude API with high reliability" }
7443async fn deliberate_option_handler(
7444    State(state): State<SharedState>,
7445    headers: HeaderMap,
7446    Path(session_id): Path<String>,
7447    Json(payload): Json<serde_json::Value>,
7448) -> Result<Json<serde_json::Value>, StatusCode> {
7449    let mut s = state.lock().unwrap();
7450    check_auth(&mut s, &headers, AccessLevel::Write)?;
7451
7452    let label = payload.get("label").and_then(|v| v.as_str()).unwrap_or("").to_string();
7453    let description = payload.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string();
7454
7455    if label.is_empty() {
7456        return Ok(Json(serde_json::json!({"error": "label is required"})));
7457    }
7458
7459    let session = match s.deliberations.get_mut(&session_id) {
7460        Some(sess) => sess,
7461        None => return Ok(Json(serde_json::json!({"error": format!("deliberate session '{}' not found", session_id)}))),
7462    };
7463
7464    match session.add_option(label, description) {
7465        Ok(option_id) => Ok(Json(serde_json::json!({
7466            "session_id": session_id,
7467            "option_id": option_id,
7468            "total_options": session.options.len(),
7469        }))),
7470        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7471    }
7472}
7473
7474/// POST /v1/deliberate/{id}/evaluate — add pros/cons to an option.
7475/// Body: { "option_id": 1, "pro": "High reliability", "con": "Expensive" }
7476async fn deliberate_evaluate_handler(
7477    State(state): State<SharedState>,
7478    headers: HeaderMap,
7479    Path(session_id): Path<String>,
7480    Json(payload): Json<serde_json::Value>,
7481) -> Result<Json<serde_json::Value>, StatusCode> {
7482    let mut s = state.lock().unwrap();
7483    check_auth(&mut s, &headers, AccessLevel::Write)?;
7484
7485    let option_id = payload.get("option_id").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
7486    let pro = payload.get("pro").and_then(|v| v.as_str()).map(String::from);
7487    let con = payload.get("con").and_then(|v| v.as_str()).map(String::from);
7488
7489    let session = match s.deliberations.get_mut(&session_id) {
7490        Some(sess) => sess,
7491        None => return Ok(Json(serde_json::json!({"error": format!("deliberate session '{}' not found", session_id)}))),
7492    };
7493
7494    match session.evaluate(option_id, pro, con) {
7495        Ok(score) => Ok(Json(serde_json::json!({
7496            "session_id": session_id,
7497            "option_id": option_id,
7498            "score": score,
7499            "envelope": { "certainty": (score * 0.99 * 10000.0).round() / 10000.0, "derivation": "derived" },
7500        }))),
7501        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7502    }
7503}
7504
7505/// POST /v1/deliberate/{id}/eliminate — backtrack: eliminate an option.
7506/// Body: { "option_id": 2, "reason": "Too expensive for our budget" }
7507async fn deliberate_eliminate_handler(
7508    State(state): State<SharedState>,
7509    headers: HeaderMap,
7510    Path(session_id): Path<String>,
7511    Json(payload): Json<serde_json::Value>,
7512) -> Result<Json<serde_json::Value>, StatusCode> {
7513    let mut s = state.lock().unwrap();
7514    check_auth(&mut s, &headers, AccessLevel::Write)?;
7515
7516    let option_id = payload.get("option_id").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
7517    let reason = payload.get("reason").and_then(|v| v.as_str()).unwrap_or("").to_string();
7518
7519    let session = match s.deliberations.get_mut(&session_id) {
7520        Some(sess) => sess,
7521        None => return Ok(Json(serde_json::json!({"error": format!("deliberate session '{}' not found", session_id)}))),
7522    };
7523
7524    match session.eliminate(option_id, reason) {
7525        Ok(()) => Ok(Json(serde_json::json!({
7526            "session_id": session_id,
7527            "option_id": option_id,
7528            "eliminated": true,
7529            "viable_remaining": session.viable_count(),
7530        }))),
7531        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7532    }
7533}
7534
7535/// POST /v1/deliberate/{id}/decide — make the final decision.
7536async fn deliberate_decide_handler(
7537    State(state): State<SharedState>,
7538    headers: HeaderMap,
7539    Path(session_id): Path<String>,
7540) -> Result<Json<serde_json::Value>, StatusCode> {
7541    let mut s = state.lock().unwrap();
7542    let client = client_key_from_headers(&headers);
7543    check_auth(&mut s, &headers, AccessLevel::Write)?;
7544
7545    let session = match s.deliberations.get_mut(&session_id) {
7546        Some(sess) => sess,
7547        None => return Ok(Json(serde_json::json!({"error": format!("deliberate session '{}' not found", session_id)}))),
7548    };
7549
7550    match session.decide() {
7551        Ok((chosen_id, score, certainty)) => {
7552            let chosen_label = session.options.iter().find(|o| o.id == chosen_id)
7553                .map(|o| o.label.clone()).unwrap_or_default();
7554            let question = session.question.clone();
7555
7556            s.audit_log.record(&client, AuditAction::ConfigUpdate, "deliberate",
7557                serde_json::json!({"action": "decide", "session": &session_id, "chosen": chosen_id}), true);
7558
7559            let lattice = if certainty > 0.5 { "believe" } else { "speculate" };
7560
7561            Ok(Json(serde_json::json!({
7562                "session_id": session_id,
7563                "question": question,
7564                "decided": true,
7565                "chosen_option": chosen_id,
7566                "chosen_label": chosen_label,
7567                "chosen_score": score,
7568                "envelope": {
7569                    "certainty": certainty,
7570                    "derivation": "derived",
7571                    "reason": "Theorem 5.1: deliberation is inferential reasoning (δ=derived)",
7572                },
7573                "lattice_position": lattice,
7574            })))
7575        }
7576        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7577    }
7578}
7579
7580/// GET /v1/deliberate/{id} — get session with all options and decision.
7581async fn deliberate_get_handler(
7582    State(state): State<SharedState>,
7583    headers: HeaderMap,
7584    Path(session_id): Path<String>,
7585) -> Result<Json<serde_json::Value>, StatusCode> {
7586    let s = state.lock().unwrap();
7587    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7588
7589    match s.deliberations.get(&session_id) {
7590        Some(sess) => Ok(Json(serde_json::json!({
7591            "session_id": sess.id,
7592            "name": sess.name,
7593            "question": sess.question,
7594            "decided": sess.decided,
7595            "chosen_option": sess.chosen_option,
7596            "viable_count": sess.viable_count(),
7597            "options": sess.options,
7598        }))),
7599        None => Ok(Json(serde_json::json!({"error": format!("deliberate session '{}' not found", session_id)}))),
7600    }
7601}
7602
7603/// GET /v1/deliberate — list all deliberate sessions.
7604async fn deliberate_list_handler(
7605    State(state): State<SharedState>,
7606    headers: HeaderMap,
7607) -> Result<Json<serde_json::Value>, StatusCode> {
7608    let s = state.lock().unwrap();
7609    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7610
7611    let sessions: Vec<serde_json::Value> = s.deliberations.values().map(|sess| {
7612        serde_json::json!({
7613            "session_id": sess.id,
7614            "name": sess.name,
7615            "question": sess.question,
7616            "decided": sess.decided,
7617            "option_count": sess.options.len(),
7618            "viable_count": sess.viable_count(),
7619        })
7620    }).collect();
7621
7622    Ok(Json(serde_json::json!({"sessions": sessions, "count": sessions.len()})))
7623}
7624
7625// ── Consensus endpoints ─────────────────────────────────────────────────────
7626
7627/// POST /v1/consensus — start a new Consensus session.
7628/// Body: { "name": "model_selection", "proposal": "Which model for production?", "choices": ["claude", "gpt4", "gemini"], "quorum": 3 }
7629async fn consensus_create_handler(
7630    State(state): State<SharedState>,
7631    headers: HeaderMap,
7632    Json(payload): Json<serde_json::Value>,
7633) -> Result<Json<serde_json::Value>, StatusCode> {
7634    let mut s = state.lock().unwrap();
7635    let client = client_key_from_headers(&headers);
7636    check_auth(&mut s, &headers, AccessLevel::Write)?;
7637
7638    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
7639    let proposal = payload.get("proposal").and_then(|v| v.as_str()).unwrap_or("").to_string();
7640    let choices: Vec<String> = payload.get("choices")
7641        .and_then(|v| serde_json::from_value(v.clone()).ok())
7642        .unwrap_or_default();
7643    let quorum = payload.get("quorum").and_then(|v| v.as_u64()).unwrap_or(3) as u32;
7644
7645    if name.is_empty() || proposal.is_empty() || choices.len() < 2 {
7646        return Ok(Json(serde_json::json!({"error": "name, proposal, and at least 2 choices are required"})));
7647    }
7648
7649    let now = std::time::SystemTime::now()
7650        .duration_since(std::time::UNIX_EPOCH)
7651        .unwrap_or_default()
7652        .as_secs();
7653
7654    let session_id = format!("cons_{}_{}", name, now);
7655
7656    let session = ConsensusSession {
7657        id: session_id.clone(),
7658        name: name.clone(),
7659        proposal: proposal.clone(),
7660        choices: choices.clone(),
7661        quorum,
7662        votes: Vec::new(),
7663        resolved: false,
7664        winner: String::new(),
7665        created_at: now,
7666    };
7667
7668    s.consensus_sessions.insert(session_id.clone(), session);
7669
7670    s.audit_log.record(&client, AuditAction::ConfigUpdate, "consensus",
7671        serde_json::json!({"action": "create", "session": &session_id}), true);
7672
7673    Ok(Json(serde_json::json!({
7674        "success": true, "session_id": session_id,
7675        "proposal": proposal, "choices": choices, "quorum": quorum,
7676    })))
7677}
7678
7679/// POST /v1/consensus/{id}/vote — cast a vote.
7680/// Body: { "voter": "agent_1", "choice": "claude", "confidence": 0.9, "rationale": "Best reasoning" }
7681async fn consensus_vote_handler(
7682    State(state): State<SharedState>,
7683    headers: HeaderMap,
7684    Path(session_id): Path<String>,
7685    Json(payload): Json<serde_json::Value>,
7686) -> Result<Json<serde_json::Value>, StatusCode> {
7687    let mut s = state.lock().unwrap();
7688    check_auth(&mut s, &headers, AccessLevel::Write)?;
7689
7690    let voter = payload.get("voter").and_then(|v| v.as_str()).unwrap_or("").to_string();
7691    let choice = payload.get("choice").and_then(|v| v.as_str()).unwrap_or("").to_string();
7692    let confidence = payload.get("confidence").and_then(|v| v.as_f64()).unwrap_or(1.0);
7693    let rationale = payload.get("rationale").and_then(|v| v.as_str()).unwrap_or("").to_string();
7694
7695    if voter.is_empty() || choice.is_empty() {
7696        return Ok(Json(serde_json::json!({"error": "voter and choice are required"})));
7697    }
7698
7699    let session = match s.consensus_sessions.get_mut(&session_id) {
7700        Some(sess) => sess,
7701        None => return Ok(Json(serde_json::json!({"error": format!("consensus session '{}' not found", session_id)}))),
7702    };
7703
7704    match session.vote(voter, choice, confidence, rationale) {
7705        Ok(()) => {
7706            let vote_count = session.vote_count();
7707            let has_quorum = session.has_quorum();
7708            let tally = session.tally();
7709
7710            Ok(Json(serde_json::json!({
7711                "session_id": session_id,
7712                "vote_count": vote_count,
7713                "quorum": session.quorum,
7714                "has_quorum": has_quorum,
7715                "tally": tally.iter().map(|(c, s, n)| serde_json::json!({"choice": c, "score": s, "votes": n})).collect::<Vec<_>>(),
7716            })))
7717        }
7718        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7719    }
7720}
7721
7722/// POST /v1/consensus/{id}/resolve — resolve consensus if quorum is met.
7723async fn consensus_resolve_handler(
7724    State(state): State<SharedState>,
7725    headers: HeaderMap,
7726    Path(session_id): Path<String>,
7727) -> Result<Json<serde_json::Value>, StatusCode> {
7728    let mut s = state.lock().unwrap();
7729    let client = client_key_from_headers(&headers);
7730    check_auth(&mut s, &headers, AccessLevel::Write)?;
7731
7732    let session = match s.consensus_sessions.get_mut(&session_id) {
7733        Some(sess) => sess,
7734        None => return Ok(Json(serde_json::json!({"error": format!("consensus session '{}' not found", session_id)}))),
7735    };
7736
7737    match session.resolve() {
7738        Ok((winner, agreement, certainty)) => {
7739            let proposal = session.proposal.clone();
7740            let tally = session.tally();
7741
7742            s.audit_log.record(&client, AuditAction::ConfigUpdate, "consensus",
7743                serde_json::json!({"action": "resolve", "session": &session_id, "winner": &winner}), true);
7744
7745            let lattice = if agreement > 0.8 { "believe" } else if agreement > 0.5 { "speculate" } else { "doubt" };
7746
7747            Ok(Json(serde_json::json!({
7748                "session_id": session_id,
7749                "proposal": proposal,
7750                "resolved": true,
7751                "winner": winner,
7752                "agreement": agreement,
7753                "tally": tally.iter().map(|(c, s, n)| serde_json::json!({"choice": c, "score": s, "votes": n})).collect::<Vec<_>>(),
7754                "envelope": {
7755                    "certainty": certainty,
7756                    "derivation": "derived",
7757                    "reason": "Theorem 5.1: consensus is aggregated opinion (δ=derived, c≤0.99)",
7758                },
7759                "lattice_position": lattice,
7760            })))
7761        }
7762        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7763    }
7764}
7765
7766/// GET /v1/consensus/{id} — get session with votes and tally.
7767async fn consensus_get_handler(
7768    State(state): State<SharedState>,
7769    headers: HeaderMap,
7770    Path(session_id): Path<String>,
7771) -> Result<Json<serde_json::Value>, StatusCode> {
7772    let s = state.lock().unwrap();
7773    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7774
7775    match s.consensus_sessions.get(&session_id) {
7776        Some(sess) => {
7777            let tally = sess.tally();
7778            Ok(Json(serde_json::json!({
7779                "session_id": sess.id, "name": sess.name,
7780                "proposal": sess.proposal, "choices": sess.choices,
7781                "quorum": sess.quorum, "vote_count": sess.vote_count(),
7782                "has_quorum": sess.has_quorum(), "resolved": sess.resolved,
7783                "winner": sess.winner, "votes": sess.votes,
7784                "tally": tally.iter().map(|(c, s, n)| serde_json::json!({"choice": c, "score": s, "votes": n})).collect::<Vec<_>>(),
7785            })))
7786        }
7787        None => Ok(Json(serde_json::json!({"error": format!("consensus session '{}' not found", session_id)}))),
7788    }
7789}
7790
7791/// GET /v1/consensus — list all consensus sessions.
7792async fn consensus_list_handler(
7793    State(state): State<SharedState>,
7794    headers: HeaderMap,
7795) -> Result<Json<serde_json::Value>, StatusCode> {
7796    let s = state.lock().unwrap();
7797    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7798
7799    let sessions: Vec<serde_json::Value> = s.consensus_sessions.values().map(|sess| {
7800        serde_json::json!({
7801            "session_id": sess.id, "name": sess.name,
7802            "proposal": sess.proposal, "resolved": sess.resolved,
7803            "vote_count": sess.vote_count(), "quorum": sess.quorum,
7804            "winner": sess.winner,
7805        })
7806    }).collect();
7807
7808    Ok(Json(serde_json::json!({"sessions": sessions, "count": sessions.len()})))
7809}
7810
7811// ── Hibernate endpoints ─────────────────────────────────────────────────────
7812
7813/// POST /v1/hibernate — create a new Hibernate session.
7814/// Body: { "name": "long_analysis", "operation": "flow:deep_analysis" }
7815async fn hibernate_create_handler(
7816    State(state): State<SharedState>,
7817    headers: HeaderMap,
7818    Json(payload): Json<serde_json::Value>,
7819) -> Result<Json<serde_json::Value>, StatusCode> {
7820    let mut s = state.lock().unwrap();
7821    let client = client_key_from_headers(&headers);
7822    check_auth(&mut s, &headers, AccessLevel::Write)?;
7823
7824    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
7825    let operation = payload.get("operation").and_then(|v| v.as_str()).unwrap_or("").to_string();
7826
7827    if name.is_empty() {
7828        return Ok(Json(serde_json::json!({"error": "name is required"})));
7829    }
7830
7831    let now = std::time::SystemTime::now()
7832        .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
7833
7834    let session_id = format!("hib_{}_{}", name, now);
7835
7836    let session = HibernateSession {
7837        id: session_id.clone(),
7838        name: name.clone(),
7839        operation,
7840        status: "active".into(),
7841        checkpoints: Vec::new(),
7842        resumed_from: None,
7843        created_at: now,
7844        last_status_change: now,
7845        next_checkpoint_id: 1,
7846    };
7847
7848    s.hibernations.insert(session_id.clone(), session);
7849
7850    s.audit_log.record(&client, AuditAction::ConfigUpdate, "hibernate",
7851        serde_json::json!({"action": "create", "session": &session_id}), true);
7852
7853    Ok(Json(serde_json::json!({"success": true, "session_id": session_id, "status": "active"})))
7854}
7855
7856/// POST /v1/hibernate/{id}/checkpoint — save a state checkpoint.
7857/// Body: { "label": "after_phase_1", "state": {...}, "phase": "phase_1" }
7858async fn hibernate_checkpoint_handler(
7859    State(state): State<SharedState>,
7860    headers: HeaderMap,
7861    Path(session_id): Path<String>,
7862    Json(payload): Json<serde_json::Value>,
7863) -> Result<Json<serde_json::Value>, StatusCode> {
7864    let mut s = state.lock().unwrap();
7865    check_auth(&mut s, &headers, AccessLevel::Write)?;
7866
7867    let label = payload.get("label").and_then(|v| v.as_str()).unwrap_or("").to_string();
7868    let state_data = payload.get("state").cloned().unwrap_or(serde_json::json!({}));
7869    let phase = payload.get("phase").and_then(|v| v.as_str()).unwrap_or("").to_string();
7870
7871    let session = match s.hibernations.get_mut(&session_id) {
7872        Some(sess) => sess,
7873        None => return Ok(Json(serde_json::json!({"error": format!("hibernate session '{}' not found", session_id)}))),
7874    };
7875
7876    match session.checkpoint(label, state_data, phase) {
7877        Ok(cp_id) => Ok(Json(serde_json::json!({
7878            "session_id": session_id,
7879            "checkpoint_id": cp_id,
7880            "total_checkpoints": session.checkpoints.len(),
7881            "envelope": { "certainty": 1.0, "derivation": "raw" },
7882        }))),
7883        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7884    }
7885}
7886
7887/// POST /v1/hibernate/{id}/suspend — suspend (hibernate) the session.
7888async fn hibernate_suspend_handler(
7889    State(state): State<SharedState>,
7890    headers: HeaderMap,
7891    Path(session_id): Path<String>,
7892) -> Result<Json<serde_json::Value>, StatusCode> {
7893    let mut s = state.lock().unwrap();
7894    let client = client_key_from_headers(&headers);
7895    check_auth(&mut s, &headers, AccessLevel::Write)?;
7896
7897    let session = match s.hibernations.get_mut(&session_id) {
7898        Some(sess) => sess,
7899        None => return Ok(Json(serde_json::json!({"error": format!("hibernate session '{}' not found", session_id)}))),
7900    };
7901
7902    match session.suspend() {
7903        Ok(()) => {
7904            let cp_count = session.checkpoints.len();
7905            s.audit_log.record(&client, AuditAction::ConfigUpdate, "hibernate",
7906                serde_json::json!({"action": "suspend", "session": &session_id}), true);
7907            Ok(Json(serde_json::json!({
7908                "session_id": session_id, "status": "suspended",
7909                "checkpoints": cp_count,
7910            })))
7911        }
7912        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7913    }
7914}
7915
7916/// POST /v1/hibernate/{id}/resume — resume from a checkpoint.
7917/// Body: { "checkpoint_id": 1 }
7918async fn hibernate_resume_handler(
7919    State(state): State<SharedState>,
7920    headers: HeaderMap,
7921    Path(session_id): Path<String>,
7922    Json(payload): Json<serde_json::Value>,
7923) -> Result<Json<serde_json::Value>, StatusCode> {
7924    let mut s = state.lock().unwrap();
7925    let client = client_key_from_headers(&headers);
7926    check_auth(&mut s, &headers, AccessLevel::Write)?;
7927
7928    let checkpoint_id = payload.get("checkpoint_id").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
7929
7930    let session = match s.hibernations.get_mut(&session_id) {
7931        Some(sess) => sess,
7932        None => return Ok(Json(serde_json::json!({"error": format!("hibernate session '{}' not found", session_id)}))),
7933    };
7934
7935    match session.resume(checkpoint_id) {
7936        Ok(cp) => {
7937            let cp_label = cp.label.clone();
7938            let cp_phase = cp.phase.clone();
7939            let cp_state = cp.state.clone();
7940
7941            s.audit_log.record(&client, AuditAction::ConfigUpdate, "hibernate",
7942                serde_json::json!({"action": "resume", "session": &session_id, "checkpoint": checkpoint_id}), true);
7943
7944            Ok(Json(serde_json::json!({
7945                "session_id": session_id,
7946                "status": "resumed",
7947                "resumed_from": checkpoint_id,
7948                "checkpoint_label": cp_label,
7949                "checkpoint_phase": cp_phase,
7950                "restored_state": cp_state,
7951                "envelope": { "certainty": 0.99, "derivation": "derived" },
7952                "lattice_position": "believe",
7953            })))
7954        }
7955        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7956    }
7957}
7958
7959/// GET /v1/hibernate/{id} — get session status and checkpoints.
7960async fn hibernate_get_handler(
7961    State(state): State<SharedState>,
7962    headers: HeaderMap,
7963    Path(session_id): Path<String>,
7964) -> Result<Json<serde_json::Value>, StatusCode> {
7965    let s = state.lock().unwrap();
7966    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7967
7968    match s.hibernations.get(&session_id) {
7969        Some(sess) => Ok(Json(serde_json::json!({
7970            "session_id": sess.id, "name": sess.name,
7971            "operation": sess.operation, "status": sess.status,
7972            "checkpoints": sess.checkpoints, "resumed_from": sess.resumed_from,
7973            "checkpoint_count": sess.checkpoints.len(),
7974        }))),
7975        None => Ok(Json(serde_json::json!({"error": format!("hibernate session '{}' not found", session_id)}))),
7976    }
7977}
7978
7979/// GET /v1/hibernate — list all hibernate sessions.
7980async fn hibernate_list_handler(
7981    State(state): State<SharedState>,
7982    headers: HeaderMap,
7983) -> Result<Json<serde_json::Value>, StatusCode> {
7984    let s = state.lock().unwrap();
7985    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7986
7987    let sessions: Vec<serde_json::Value> = s.hibernations.values().map(|sess| {
7988        serde_json::json!({
7989            "session_id": sess.id, "name": sess.name,
7990            "status": sess.status, "checkpoint_count": sess.checkpoints.len(),
7991        })
7992    }).collect();
7993
7994    Ok(Json(serde_json::json!({"sessions": sessions, "count": sessions.len()})))
7995}
7996
7997// ── OTS endpoints ───────────────────────────────────────────────────────────
7998
7999/// POST /v1/ots — create a one-time secret.
8000/// Body: { "value": "supersecret123", "ttl_secs": 3600, "label": "db_password" }
8001async fn ots_create_handler(
8002    State(state): State<SharedState>,
8003    headers: HeaderMap,
8004    Json(payload): Json<serde_json::Value>,
8005) -> Result<Json<serde_json::Value>, StatusCode> {
8006    let mut s = state.lock().unwrap();
8007    let client = client_key_from_headers(&headers);
8008    check_auth(&mut s, &headers, AccessLevel::Write)?;
8009
8010    let value = payload.get("value").and_then(|v| v.as_str()).unwrap_or("").to_string();
8011    let ttl_secs = payload.get("ttl_secs").and_then(|v| v.as_u64()).unwrap_or(3600);
8012    let label = payload.get("label").and_then(|v| v.as_str()).unwrap_or("").to_string();
8013
8014    if value.is_empty() {
8015        return Ok(Json(serde_json::json!({"error": "value is required"})));
8016    }
8017
8018    let now = std::time::SystemTime::now()
8019        .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
8020
8021    let token = generate_ots_token(&label);
8022
8023    let secret = OtsSecret {
8024        token: token.clone(),
8025        value,
8026        consumed: false,
8027        created_at: now,
8028        ttl_secs,
8029        created_by: client.clone(),
8030        label: label.clone(),
8031    };
8032
8033    s.ots_secrets.insert(token.clone(), secret);
8034
8035    s.audit_log.record(&client, AuditAction::ConfigUpdate, "ots",
8036        serde_json::json!({"action": "create", "token": &token, "label": &label, "ttl_secs": ttl_secs}), true);
8037
8038    Ok(Json(serde_json::json!({
8039        "success": true,
8040        "token": token,
8041        "label": label,
8042        "ttl_secs": ttl_secs,
8043        "expires_at": now + ttl_secs,
8044        "envelope": { "certainty": 1.0, "derivation": "raw" },
8045    })))
8046}
8047
8048/// GET /v1/ots/{token} — retrieve and consume a one-time secret.
8049/// The secret is destroyed after retrieval (one-time use).
8050async fn ots_retrieve_handler(
8051    State(state): State<SharedState>,
8052    headers: HeaderMap,
8053    Path(token): Path<String>,
8054) -> Result<Json<serde_json::Value>, StatusCode> {
8055    let mut s = state.lock().unwrap();
8056    let client = client_key_from_headers(&headers);
8057    check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
8058
8059    let now = std::time::SystemTime::now()
8060        .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
8061
8062    let secret = match s.ots_secrets.get_mut(&token) {
8063        Some(sec) => sec,
8064        None => return Ok(Json(serde_json::json!({
8065            "error": "secret not found or already consumed",
8066            "token": token,
8067            "envelope": { "certainty": 0.0, "derivation": "void" },
8068        }))),
8069    };
8070
8071    match secret.consume(now) {
8072        Ok(value) => {
8073            let label = secret.label.clone();
8074
8075            s.audit_log.record(&client, AuditAction::ConfigUpdate, "ots",
8076                serde_json::json!({"action": "consume", "token": &token}), true);
8077
8078            Ok(Json(serde_json::json!({
8079                "token": token,
8080                "value": value,
8081                "label": label,
8082                "consumed": true,
8083                "envelope": { "certainty": 1.0, "derivation": "raw" },
8084                "lattice_position": "know",
8085                "warning": "This secret has been consumed and is no longer available.",
8086            })))
8087        }
8088        Err(e) => Ok(Json(serde_json::json!({
8089            "error": e,
8090            "token": token,
8091            "envelope": { "certainty": 0.0, "derivation": "void" },
8092        }))),
8093    }
8094}
8095
8096/// GET /v1/ots — list all OTS tokens (metadata only, never values).
8097async fn ots_list_handler(
8098    State(state): State<SharedState>,
8099    headers: HeaderMap,
8100) -> Result<Json<serde_json::Value>, StatusCode> {
8101    let s = state.lock().unwrap();
8102    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8103
8104    let now = std::time::SystemTime::now()
8105        .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
8106
8107    let secrets: Vec<serde_json::Value> = s.ots_secrets.values().map(|sec| {
8108        serde_json::json!({
8109            "token": sec.token,
8110            "label": sec.label,
8111            "consumed": sec.consumed,
8112            "expired": sec.is_expired(now),
8113            "created_at": sec.created_at,
8114            "ttl_secs": sec.ttl_secs,
8115            // NEVER expose the value in listings
8116        })
8117    }).collect();
8118
8119    Ok(Json(serde_json::json!({"secrets": secrets, "count": secrets.len()})))
8120}
8121
8122// ── Psyche endpoints ────────────────────────────────────────────────────────
8123
8124/// POST /v1/psyche — start a new Psyche introspection session.
8125/// Body: { "name": "analysis_review", "context": "After analyzing 50 documents on attention mechanisms" }
8126async fn psyche_create_handler(
8127    State(state): State<SharedState>,
8128    headers: HeaderMap,
8129    Json(payload): Json<serde_json::Value>,
8130) -> Result<Json<serde_json::Value>, StatusCode> {
8131    let mut s = state.lock().unwrap();
8132    let client = client_key_from_headers(&headers);
8133    check_auth(&mut s, &headers, AccessLevel::Write)?;
8134
8135    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
8136    let context = payload.get("context").and_then(|v| v.as_str()).unwrap_or("").to_string();
8137
8138    if name.is_empty() {
8139        return Ok(Json(serde_json::json!({"error": "name is required"})));
8140    }
8141
8142    let now = std::time::SystemTime::now()
8143        .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
8144
8145    let session_id = format!("psyche_{}_{}", name, now);
8146
8147    let session = PsycheSession {
8148        id: session_id.clone(),
8149        name: name.clone(),
8150        context,
8151        insights: Vec::new(),
8152        completed: false,
8153        created_at: now,
8154        next_insight_id: 1,
8155    };
8156
8157    s.psyche_sessions.insert(session_id.clone(), session);
8158
8159    s.audit_log.record(&client, AuditAction::ConfigUpdate, "psyche",
8160        serde_json::json!({"action": "create", "session": &session_id}), true);
8161
8162    Ok(Json(serde_json::json!({"success": true, "session_id": session_id})))
8163}
8164
8165/// POST /v1/psyche/{id}/insight — add a metacognitive insight.
8166/// Body: { "category": "knowledge_gap", "content": "Unclear on cross-attention variants", "confidence": 0.7, "severity": "warning" }
8167async fn psyche_insight_handler(
8168    State(state): State<SharedState>,
8169    headers: HeaderMap,
8170    Path(session_id): Path<String>,
8171    Json(payload): Json<serde_json::Value>,
8172) -> Result<Json<serde_json::Value>, StatusCode> {
8173    let mut s = state.lock().unwrap();
8174    check_auth(&mut s, &headers, AccessLevel::Write)?;
8175
8176    let category = payload.get("category").and_then(|v| v.as_str()).unwrap_or("").to_string();
8177    let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
8178    let confidence = payload.get("confidence").and_then(|v| v.as_f64()).unwrap_or(0.5);
8179    let severity = payload.get("severity").and_then(|v| v.as_str()).unwrap_or("info").to_string();
8180
8181    if category.is_empty() || content.is_empty() {
8182        return Ok(Json(serde_json::json!({"error": "category and content are required"})));
8183    }
8184
8185    let session = match s.psyche_sessions.get_mut(&session_id) {
8186        Some(sess) => sess,
8187        None => return Ok(Json(serde_json::json!({"error": format!("psyche session '{}' not found", session_id)}))),
8188    };
8189
8190    match session.add_insight(category, content, confidence, severity) {
8191        Ok(insight_id) => Ok(Json(serde_json::json!({
8192            "session_id": session_id,
8193            "insight_id": insight_id,
8194            "total_insights": session.insights.len(),
8195            "envelope": { "certainty": 0.99, "derivation": "derived" },
8196        }))),
8197        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
8198    }
8199}
8200
8201/// POST /v1/psyche/{id}/complete — complete introspection and generate report.
8202async fn psyche_complete_handler(
8203    State(state): State<SharedState>,
8204    headers: HeaderMap,
8205    Path(session_id): Path<String>,
8206) -> Result<Json<serde_json::Value>, StatusCode> {
8207    let mut s = state.lock().unwrap();
8208    let client = client_key_from_headers(&headers);
8209    check_auth(&mut s, &headers, AccessLevel::Write)?;
8210
8211    let session = match s.psyche_sessions.get_mut(&session_id) {
8212        Some(sess) => sess,
8213        None => return Ok(Json(serde_json::json!({"error": format!("psyche session '{}' not found", session_id)}))),
8214    };
8215
8216    if session.completed {
8217        return Ok(Json(serde_json::json!({"error": "session already completed"})));
8218    }
8219
8220    let report = session.report();
8221    session.completed = true;
8222    let context = session.context.clone();
8223
8224    s.audit_log.record(&client, AuditAction::ConfigUpdate, "psyche",
8225        serde_json::json!({"action": "complete", "session": &session_id}), true);
8226
8227    let awareness = report["self_awareness_score"].as_f64().unwrap_or(0.0);
8228    let certainty = (awareness * 0.99).min(0.99);
8229    let lattice = if awareness > 0.7 { "believe" } else { "speculate" };
8230
8231    Ok(Json(serde_json::json!({
8232        "session_id": session_id,
8233        "context": context,
8234        "completed": true,
8235        "report": report,
8236        "envelope": {
8237            "certainty": (certainty * 10000.0).round() / 10000.0,
8238            "derivation": "derived",
8239            "reason": "Theorem 5.1: self-reflection is meta-reasoning (δ=derived, c≤0.99)",
8240        },
8241        "lattice_position": lattice,
8242    })))
8243}
8244
8245/// GET /v1/psyche/{id} — get session with insights and report.
8246async fn psyche_get_handler(
8247    State(state): State<SharedState>,
8248    headers: HeaderMap,
8249    Path(session_id): Path<String>,
8250) -> Result<Json<serde_json::Value>, StatusCode> {
8251    let s = state.lock().unwrap();
8252    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8253
8254    match s.psyche_sessions.get(&session_id) {
8255        Some(sess) => {
8256            let report = sess.report();
8257            Ok(Json(serde_json::json!({
8258                "session_id": sess.id, "name": sess.name,
8259                "context": sess.context, "completed": sess.completed,
8260                "insights": sess.insights, "report": report,
8261            })))
8262        }
8263        None => Ok(Json(serde_json::json!({"error": format!("psyche session '{}' not found", session_id)}))),
8264    }
8265}
8266
8267/// GET /v1/psyche — list all psyche sessions.
8268async fn psyche_list_handler(
8269    State(state): State<SharedState>,
8270    headers: HeaderMap,
8271) -> Result<Json<serde_json::Value>, StatusCode> {
8272    let s = state.lock().unwrap();
8273    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8274
8275    let sessions: Vec<serde_json::Value> = s.psyche_sessions.values().map(|sess| {
8276        serde_json::json!({
8277            "session_id": sess.id, "name": sess.name,
8278            "completed": sess.completed, "insight_count": sess.insights.len(),
8279        })
8280    }).collect();
8281
8282    Ok(Json(serde_json::json!({"sessions": sessions, "count": sessions.len()})))
8283}
8284
8285// ── AxonEndpoint endpoints ───────────────────────────────────────────────────
8286
8287/// POST /v1/endpoints — register an external API endpoint binding.
8288/// Body: { "name": "weather_api", "method": "GET", "url_template": "https://api.weather.com/v1/{city}", "auth_type": "api_key", "auth_ref": "WEATHER_KEY", "timeout_ms": 5000, "description": "Weather API" }
8289async fn endpoint_create_handler(
8290    State(state): State<SharedState>,
8291    headers: HeaderMap,
8292    Json(payload): Json<serde_json::Value>,
8293) -> Result<Json<serde_json::Value>, StatusCode> {
8294    let mut s = state.lock().unwrap();
8295    let client = client_key_from_headers(&headers);
8296    check_auth(&mut s, &headers, AccessLevel::Write)?;
8297
8298    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
8299    let method = payload.get("method").and_then(|v| v.as_str()).unwrap_or("GET").to_string().to_uppercase();
8300    let url_template = payload.get("url_template").and_then(|v| v.as_str()).unwrap_or("").to_string();
8301    let auth_type = payload.get("auth_type").and_then(|v| v.as_str()).unwrap_or("none").to_string();
8302    let auth_ref = payload.get("auth_ref").and_then(|v| v.as_str()).unwrap_or("").to_string();
8303    let timeout_ms = payload.get("timeout_ms").and_then(|v| v.as_u64()).unwrap_or(10000);
8304    let description = payload.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string();
8305    let hdrs: HashMap<String, String> = payload.get("headers")
8306        .and_then(|v| serde_json::from_value(v.clone()).ok())
8307        .unwrap_or_default();
8308
8309    if name.is_empty() || url_template.is_empty() {
8310        return Ok(Json(serde_json::json!({"error": "name and url_template are required"})));
8311    }
8312
8313    if !["GET", "POST", "PUT", "DELETE"].contains(&method.as_str()) {
8314        return Ok(Json(serde_json::json!({"error": "method must be GET, POST, PUT, or DELETE"})));
8315    }
8316
8317    if s.axon_endpoints.contains_key(&name) {
8318        return Ok(Json(serde_json::json!({"error": format!("endpoint '{}' already exists", name)})));
8319    }
8320
8321    let now = std::time::SystemTime::now()
8322        .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
8323
8324    let binding = EndpointBinding {
8325        name: name.clone(),
8326        method: method.clone(),
8327        url_template: url_template.clone(),
8328        headers: hdrs,
8329        auth_type,
8330        auth_ref,
8331        timeout_ms,
8332        enabled: true,
8333        description,
8334        created_at: now,
8335        total_calls: 0,
8336        total_errors: 0,
8337    };
8338
8339    s.axon_endpoints.insert(name.clone(), binding);
8340
8341    s.audit_log.record(&client, AuditAction::ConfigUpdate, "axonendpoint",
8342        serde_json::json!({"action": "create", "name": &name}), true);
8343
8344    Ok(Json(serde_json::json!({"success": true, "name": name, "method": method, "url_template": url_template})))
8345}
8346
8347/// POST /v1/endpoints/{name}/call — record an endpoint call (intent-based, no actual HTTP).
8348/// Body: { "params": {"city": "London"}, "body": {"query": "forecast"} }
8349/// Returns the resolved URL and call record. Actual HTTP is delegated to external orchestration.
8350async fn endpoint_call_handler(
8351    State(state): State<SharedState>,
8352    headers: HeaderMap,
8353    Path(name): Path<String>,
8354    Json(payload): Json<serde_json::Value>,
8355) -> Result<Json<serde_json::Value>, StatusCode> {
8356    let mut s = state.lock().unwrap();
8357    check_auth(&mut s, &headers, AccessLevel::Write)?;
8358
8359    let params: HashMap<String, String> = payload.get("params")
8360        .and_then(|v| serde_json::from_value(v.clone()).ok())
8361        .unwrap_or_default();
8362    let body = payload.get("body").cloned().unwrap_or(serde_json::json!(null));
8363
8364    let binding = match s.axon_endpoints.get_mut(&name) {
8365        Some(b) => b,
8366        None => return Ok(Json(serde_json::json!({"error": format!("endpoint '{}' not found", name)}))),
8367    };
8368
8369    if !binding.enabled {
8370        return Ok(Json(serde_json::json!({"error": format!("endpoint '{}' is disabled", name)})));
8371    }
8372
8373    // Resolve URL template
8374    let mut resolved_url = binding.url_template.clone();
8375    for (key, value) in &params {
8376        resolved_url = resolved_url.replace(&format!("{{{}}}", key), value);
8377    }
8378
8379    let now = std::time::SystemTime::now()
8380        .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
8381
8382    let call_id = format!("call_{}_{}_{}", name, binding.total_calls + 1, now);
8383    binding.total_calls += 1;
8384
8385    let method = binding.method.clone();
8386    let timeout_ms = binding.timeout_ms;
8387
8388    let record = EndpointCallRecord {
8389        id: call_id.clone(),
8390        binding: name.clone(),
8391        resolved_url: resolved_url.clone(),
8392        method: method.clone(),
8393        body: body.clone(),
8394        params: params.clone(),
8395        called_at: now,
8396    };
8397
8398    s.endpoint_calls.push(record);
8399    // Cap call history at 500
8400    if s.endpoint_calls.len() > 500 {
8401        s.endpoint_calls.remove(0);
8402    }
8403
8404    Ok(Json(serde_json::json!({
8405        "call_id": call_id,
8406        "binding": name,
8407        "method": method,
8408        "resolved_url": resolved_url,
8409        "params": params,
8410        "body": body,
8411        "timeout_ms": timeout_ms,
8412        "envelope": {
8413            "certainty": 0.99,
8414            "derivation": "derived",
8415            "reason": "Theorem 5.1: external API call result is derived (δ=derived, c≤0.99)",
8416        },
8417        "lattice_position": "speculate",
8418        "effect_row": ["io", "network", "epistemic:speculate"],
8419        "note": "Intent recorded. Actual HTTP execution delegated to external orchestration.",
8420    })))
8421}
8422
8423/// GET /v1/endpoints/{name} — get binding details and call stats.
8424async fn endpoint_get_handler(
8425    State(state): State<SharedState>,
8426    headers: HeaderMap,
8427    Path(name): Path<String>,
8428) -> Result<Json<serde_json::Value>, StatusCode> {
8429    let s = state.lock().unwrap();
8430    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8431
8432    match s.axon_endpoints.get(&name) {
8433        Some(b) => Ok(Json(serde_json::json!({
8434            "name": b.name, "method": b.method, "url_template": b.url_template,
8435            "headers": b.headers, "auth_type": b.auth_type,
8436            "timeout_ms": b.timeout_ms, "enabled": b.enabled,
8437            "description": b.description,
8438            "total_calls": b.total_calls, "total_errors": b.total_errors,
8439        }))),
8440        None => Ok(Json(serde_json::json!({"error": format!("endpoint '{}' not found", name)}))),
8441    }
8442}
8443
8444/// DELETE /v1/endpoints/{name} — remove an endpoint binding.
8445async fn endpoint_delete_handler(
8446    State(state): State<SharedState>,
8447    headers: HeaderMap,
8448    Path(name): Path<String>,
8449) -> Result<Json<serde_json::Value>, StatusCode> {
8450    let mut s = state.lock().unwrap();
8451    let client = client_key_from_headers(&headers);
8452    check_auth(&mut s, &headers, AccessLevel::Admin)?;
8453
8454    match s.axon_endpoints.remove(&name) {
8455        Some(_) => {
8456            s.audit_log.record(&client, AuditAction::ConfigUpdate, "axonendpoint",
8457                serde_json::json!({"action": "delete", "name": &name}), true);
8458            Ok(Json(serde_json::json!({"success": true, "deleted": name})))
8459        }
8460        None => Ok(Json(serde_json::json!({"error": format!("endpoint '{}' not found", name)}))),
8461    }
8462}
8463
8464/// GET /v1/endpoints — list all endpoint bindings.
8465async fn endpoint_list_handler(
8466    State(state): State<SharedState>,
8467    headers: HeaderMap,
8468) -> Result<Json<serde_json::Value>, StatusCode> {
8469    let s = state.lock().unwrap();
8470    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8471
8472    let endpoints: Vec<serde_json::Value> = s.axon_endpoints.values().map(|b| {
8473        serde_json::json!({
8474            "name": b.name, "method": b.method, "url_template": b.url_template,
8475            "enabled": b.enabled, "total_calls": b.total_calls,
8476        })
8477    }).collect();
8478
8479    Ok(Json(serde_json::json!({"endpoints": endpoints, "count": endpoints.len()})))
8480}
8481
8482// ── Pix endpoints ───────────────────────────────────────────────────────────
8483
8484/// POST /v1/pix — create a new Pix session.
8485/// Body: { "name": "visual_analysis" }
8486async fn pix_create_handler(
8487    State(state): State<SharedState>,
8488    headers: HeaderMap,
8489    Json(payload): Json<serde_json::Value>,
8490) -> Result<Json<serde_json::Value>, StatusCode> {
8491    let mut s = state.lock().unwrap();
8492    let client = client_key_from_headers(&headers);
8493    check_auth(&mut s, &headers, AccessLevel::Write)?;
8494
8495    let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
8496
8497    if name.is_empty() {
8498        return Ok(Json(serde_json::json!({"error": "name is required"})));
8499    }
8500
8501    let now = std::time::SystemTime::now()
8502        .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
8503
8504    let session_id = format!("pix_{}_{}", name, now);
8505
8506    let session = PixSession {
8507        id: session_id.clone(),
8508        name: name.clone(),
8509        images: HashMap::new(),
8510        created_at: now,
8511        next_image_id: 1,
8512    };
8513
8514    s.pix_sessions.insert(session_id.clone(), session);
8515
8516    s.audit_log.record(&client, AuditAction::ConfigUpdate, "pix",
8517        serde_json::json!({"action": "create", "session": &session_id}), true);
8518
8519    Ok(Json(serde_json::json!({"success": true, "session_id": session_id})))
8520}
8521
8522/// POST /v1/pix/{id}/image — register an image in the session.
8523/// Body: { "source": "https://example.com/image.png", "width": 1920, "height": 1080, "format": "png" }
8524async fn pix_image_handler(
8525    State(state): State<SharedState>,
8526    headers: HeaderMap,
8527    Path(session_id): Path<String>,
8528    Json(payload): Json<serde_json::Value>,
8529) -> Result<Json<serde_json::Value>, StatusCode> {
8530    let mut s = state.lock().unwrap();
8531    let client = client_key_from_headers(&headers);
8532    check_auth(&mut s, &headers, AccessLevel::Write)?;
8533
8534    let source = payload.get("source").and_then(|v| v.as_str()).unwrap_or("").to_string();
8535    let width = payload.get("width").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
8536    let height = payload.get("height").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
8537    let format = payload.get("format").and_then(|v| v.as_str()).unwrap_or("png").to_string();
8538
8539    if source.is_empty() {
8540        return Ok(Json(serde_json::json!({"error": "source is required"})));
8541    }
8542
8543    let session = match s.pix_sessions.get_mut(&session_id) {
8544        Some(sess) => sess,
8545        None => return Ok(Json(serde_json::json!({"error": format!("pix session '{}' not found", session_id)}))),
8546    };
8547
8548    let image_id = session.register_image(source, width, height, format, &client);
8549
8550    Ok(Json(serde_json::json!({
8551        "session_id": session_id,
8552        "image_id": image_id,
8553        "total_images": session.image_count(),
8554        "envelope": { "certainty": 1.0, "derivation": "raw" },
8555    })))
8556}
8557
8558/// POST /v1/pix/{id}/annotate — annotate a region on an image.
8559/// Body: { "image_id": "img_...", "label": "cat", "bbox": [0.1, 0.2, 0.3, 0.4], "confidence": 0.95, "category": "object", "description": "A cat sitting" }
8560async fn pix_annotate_handler(
8561    State(state): State<SharedState>,
8562    headers: HeaderMap,
8563    Path(session_id): Path<String>,
8564    Json(payload): Json<serde_json::Value>,
8565) -> Result<Json<serde_json::Value>, StatusCode> {
8566    let mut s = state.lock().unwrap();
8567    check_auth(&mut s, &headers, AccessLevel::Write)?;
8568
8569    let image_id = payload.get("image_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
8570    let label = payload.get("label").and_then(|v| v.as_str()).unwrap_or("").to_string();
8571    let confidence = payload.get("confidence").and_then(|v| v.as_f64()).unwrap_or(0.5);
8572    let category = payload.get("category").and_then(|v| v.as_str()).unwrap_or("region").to_string();
8573    let description = payload.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string();
8574
8575    let bbox_arr = payload.get("bbox").and_then(|v| v.as_array()).cloned().unwrap_or_default();
8576    let bbox: [f64; 4] = if bbox_arr.len() == 4 {
8577        [
8578            bbox_arr[0].as_f64().unwrap_or(0.0),
8579            bbox_arr[1].as_f64().unwrap_or(0.0),
8580            bbox_arr[2].as_f64().unwrap_or(0.0),
8581            bbox_arr[3].as_f64().unwrap_or(0.0),
8582        ]
8583    } else {
8584        return Ok(Json(serde_json::json!({"error": "bbox must be [x, y, width, height] with 4 values"})));
8585    };
8586
8587    if image_id.is_empty() || label.is_empty() {
8588        return Ok(Json(serde_json::json!({"error": "image_id and label are required"})));
8589    }
8590
8591    let session = match s.pix_sessions.get_mut(&session_id) {
8592        Some(sess) => sess,
8593        None => return Ok(Json(serde_json::json!({"error": format!("pix session '{}' not found", session_id)}))),
8594    };
8595
8596    match session.annotate(&image_id, label, bbox, confidence, category, description) {
8597        Ok(ann_id) => {
8598            let total_ann = session.total_annotations();
8599            Ok(Json(serde_json::json!({
8600                "session_id": session_id,
8601                "image_id": image_id,
8602                "annotation_id": ann_id,
8603                "total_annotations": total_ann,
8604                "envelope": {
8605                    "certainty": 0.99,
8606                    "derivation": "derived",
8607                    "reason": "Theorem 5.1: visual annotation is interpretation (δ=derived, c≤0.99)",
8608                },
8609                "lattice_position": "speculate",
8610            })))
8611        }
8612        Err(e) => Ok(Json(serde_json::json!({"error": e}))),
8613    }
8614}
8615
8616/// GET /v1/pix/{id} — get session with images and annotations.
8617async fn pix_get_handler(
8618    State(state): State<SharedState>,
8619    headers: HeaderMap,
8620    Path(session_id): Path<String>,
8621) -> Result<Json<serde_json::Value>, StatusCode> {
8622    let s = state.lock().unwrap();
8623    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8624
8625    match s.pix_sessions.get(&session_id) {
8626        Some(sess) => Ok(Json(serde_json::json!({
8627            "session_id": sess.id, "name": sess.name,
8628            "image_count": sess.image_count(),
8629            "total_annotations": sess.total_annotations(),
8630            "images": sess.images,
8631        }))),
8632        None => Ok(Json(serde_json::json!({"error": format!("pix session '{}' not found", session_id)}))),
8633    }
8634}
8635
8636/// GET /v1/pix — list all pix sessions.
8637async fn pix_list_handler(
8638    State(state): State<SharedState>,
8639    headers: HeaderMap,
8640) -> Result<Json<serde_json::Value>, StatusCode> {
8641    let s = state.lock().unwrap();
8642    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8643
8644    let sessions: Vec<serde_json::Value> = s.pix_sessions.values().map(|sess| {
8645        serde_json::json!({
8646            "session_id": sess.id, "name": sess.name,
8647            "image_count": sess.image_count(),
8648            "total_annotations": sess.total_annotations(),
8649        })
8650    }).collect();
8651
8652    Ok(Json(serde_json::json!({"sessions": sessions, "count": sessions.len()})))
8653}
8654
8655// ── API key management endpoints ──────────────────────────────────────────
8656
8657/// Request payload for creating an API key.
8658#[derive(Debug, Deserialize)]
8659pub struct CreateKeyRequest {
8660    pub name: String,
8661    pub token: String,
8662    #[serde(default = "default_key_role")]
8663    pub role: String,
8664    pub rate_limit: Option<u32>,
8665}
8666
8667fn default_key_role() -> String { "operator".to_string() }
8668
8669/// Request payload for revoking an API key.
8670#[derive(Debug, Deserialize)]
8671pub struct RevokeKeyRequest {
8672    pub name: String,
8673}
8674
8675/// Request payload for rotating an API key.
8676#[derive(Debug, Deserialize)]
8677pub struct RotateKeyRequest {
8678    pub old_token: String,
8679    pub new_token: String,
8680}
8681
8682/// GET /v1/keys — list all API keys (tokens masked).
8683async fn keys_list_handler(
8684    State(state): State<SharedState>,
8685    headers: HeaderMap,
8686) -> Result<Json<serde_json::Value>, StatusCode> {
8687    let s = state.lock().unwrap();
8688    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8689
8690    let list = s.api_keys.list();
8691    Ok(Json(serde_json::json!({
8692        "enabled": s.api_keys.is_enabled(),
8693        "active_count": s.api_keys.active_count(),
8694        "total_count": s.api_keys.total_count(),
8695        "keys": list,
8696    })))
8697}
8698
8699/// POST /v1/keys — create a new API key (admin only).
8700async fn keys_create_handler(
8701    State(state): State<SharedState>,
8702    headers: HeaderMap,
8703    Json(payload): Json<CreateKeyRequest>,
8704) -> Result<Json<serde_json::Value>, StatusCode> {
8705    let mut s = state.lock().unwrap();
8706    check_auth(&mut s, &headers, AccessLevel::Admin)?;
8707
8708    let role = match payload.role.as_str() {
8709        "admin" => crate::api_keys::KeyRole::Admin,
8710        "readonly" => crate::api_keys::KeyRole::ReadOnly,
8711        _ => crate::api_keys::KeyRole::Operator,
8712    };
8713
8714    let client = client_key_from_headers(&headers);
8715    let created = s.api_keys.create_key(&payload.name, &payload.token, role, payload.rate_limit);
8716    s.audit_log.record(&client, AuditAction::KeyCreate, &payload.name, serde_json::json!({"role": role.as_str()}), created);
8717
8718    Ok(Json(serde_json::json!({
8719        "success": created,
8720        "name": payload.name,
8721        "role": role.as_str(),
8722    })))
8723}
8724
8725/// POST /v1/keys/revoke — revoke an API key by name.
8726async fn keys_revoke_handler(
8727    State(state): State<SharedState>,
8728    headers: HeaderMap,
8729    Json(payload): Json<RevokeKeyRequest>,
8730) -> Result<Json<serde_json::Value>, StatusCode> {
8731    let mut s = state.lock().unwrap();
8732    check_auth(&mut s, &headers, AccessLevel::Admin)?;
8733
8734    let client = client_key_from_headers(&headers);
8735    let revoked = s.api_keys.revoke_by_name(&payload.name);
8736    s.audit_log.record(&client, AuditAction::KeyRevoke, &payload.name, serde_json::json!(null), revoked);
8737
8738    Ok(Json(serde_json::json!({
8739        "success": revoked,
8740        "name": payload.name,
8741    })))
8742}
8743
8744/// POST /v1/keys/rotate — rotate an API key (old→new).
8745async fn keys_rotate_handler(
8746    State(state): State<SharedState>,
8747    headers: HeaderMap,
8748    Json(payload): Json<RotateKeyRequest>,
8749) -> Result<Json<serde_json::Value>, StatusCode> {
8750    let mut s = state.lock().unwrap();
8751    check_auth(&mut s, &headers, AccessLevel::Admin)?;
8752
8753    let client = client_key_from_headers(&headers);
8754    match s.api_keys.rotate(&payload.old_token, &payload.new_token) {
8755        Some(name) => {
8756            s.audit_log.record(&client, AuditAction::KeyRotate, &name, serde_json::json!(null), true);
8757            Ok(Json(serde_json::json!({
8758                "success": true,
8759                "name": name,
8760            })))
8761        }
8762        None => {
8763            s.audit_log.record(&client, AuditAction::KeyRotate, "unknown", serde_json::json!(null), false);
8764            Ok(Json(serde_json::json!({
8765                "success": false,
8766                "error": "old token not found or already revoked",
8767            })))
8768        }
8769    }
8770}
8771
8772/// Query parameters for log retrieval.
8773#[derive(Debug, Deserialize)]
8774pub struct LogQuery {
8775    #[serde(default = "default_log_limit")]
8776    pub limit: usize,
8777    pub path: Option<String>,
8778    pub min_status: Option<u16>,
8779    pub max_status: Option<u16>,
8780    pub client: Option<String>,
8781}
8782
8783fn default_log_limit() -> usize { 50 }
8784
8785/// GET /v1/logs — query recent request logs.
8786async fn logs_handler(
8787    State(state): State<SharedState>,
8788    headers: HeaderMap,
8789    Query(params): Query<LogQuery>,
8790) -> Result<Json<serde_json::Value>, StatusCode> {
8791    let s = state.lock().unwrap();
8792    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8793
8794    let filter = if params.path.is_some() || params.min_status.is_some()
8795        || params.max_status.is_some() || params.client.is_some()
8796    {
8797        Some(LogFilter {
8798            path_prefix: params.path,
8799            min_status: params.min_status,
8800            max_status: params.max_status,
8801            client_key: params.client,
8802        })
8803    } else {
8804        None
8805    };
8806
8807    let entries = s.request_logger.recent(params.limit, filter.as_ref());
8808    let json_entries: Vec<serde_json::Value> = entries.iter().map(|e| {
8809        serde_json::to_value(e).unwrap_or_default()
8810    }).collect();
8811
8812    Ok(Json(serde_json::json!({
8813        "count": json_entries.len(),
8814        "entries": json_entries,
8815    })))
8816}
8817
8818/// GET /v1/logs/stats — aggregate request statistics.
8819async fn logs_stats_handler(
8820    State(state): State<SharedState>,
8821    headers: HeaderMap,
8822) -> Result<Json<serde_json::Value>, StatusCode> {
8823    let s = state.lock().unwrap();
8824    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8825
8826    let stats = s.request_logger.stats();
8827    Ok(Json(serde_json::to_value(&stats).unwrap_or_default()))
8828}
8829
8830/// Query parameters for request log export.
8831#[derive(Debug, Deserialize)]
8832pub struct LogExportQuery {
8833    /// Export format: "jsonl" (default) or "csv".
8834    #[serde(default = "default_log_export_format")]
8835    pub format: String,
8836    /// Filter by HTTP method.
8837    pub method: Option<String>,
8838    /// Filter by path prefix.
8839    pub path_prefix: Option<String>,
8840    /// Filter by minimum status code.
8841    pub min_status: Option<u16>,
8842    /// Max entries (default 1000).
8843    #[serde(default = "default_log_export_limit")]
8844    pub limit: usize,
8845}
8846
8847fn default_log_export_format() -> String { "jsonl".into() }
8848fn default_log_export_limit() -> usize { 1000 }
8849
8850/// GET /v1/logs/export — export request logs as JSONL or CSV with filtering.
8851async fn logs_export_handler(
8852    State(state): State<SharedState>,
8853    headers: HeaderMap,
8854    Query(params): Query<LogExportQuery>,
8855) -> Result<(StatusCode, [(String, String); 1], String), StatusCode> {
8856    let s = state.lock().unwrap();
8857    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8858
8859    let entries = s.request_logger.recent(params.limit, None);
8860
8861    let filtered: Vec<&&crate::request_log::RequestLogEntry> = entries.iter()
8862        .filter(|e| {
8863            let method_ok = params.method.as_ref().map_or(true, |m| e.method.eq_ignore_ascii_case(m));
8864            let path_ok = params.path_prefix.as_ref().map_or(true, |p| e.path.starts_with(p.as_str()));
8865            let status_ok = params.min_status.map_or(true, |ms| e.status >= ms);
8866            method_ok && path_ok && status_ok
8867        })
8868        .collect();
8869
8870    let format = params.format.to_lowercase();
8871    match format.as_str() {
8872        "csv" => {
8873            let mut csv = String::from("timestamp,method,path,status,latency_us,client_key\n");
8874            for e in &filtered {
8875                csv.push_str(&format!(
8876                    "{},{},{},{},{},{}\n",
8877                    e.timestamp, e.method, e.path, e.status, e.latency_us, e.client_key
8878                ));
8879            }
8880            Ok((StatusCode::OK, [("content-type".into(), "text/csv".into())], csv))
8881        }
8882        _ => {
8883            let mut jsonl = String::new();
8884            for e in &filtered {
8885                let line = serde_json::json!({
8886                    "timestamp": e.timestamp, "method": e.method, "path": e.path,
8887                    "status": e.status, "latency_us": e.latency_us, "client_key": e.client_key,
8888                });
8889                jsonl.push_str(&serde_json::to_string(&line).unwrap_or_default());
8890                jsonl.push('\n');
8891            }
8892            Ok((StatusCode::OK, [("content-type".into(), "application/x-ndjson".into())], jsonl))
8893        }
8894    }
8895}
8896
8897// ── Webhook endpoints ────────────────────────────────────────────────────
8898
8899/// Request payload for registering a webhook.
8900#[derive(Debug, Deserialize)]
8901pub struct RegisterWebhookRequest {
8902    pub name: String,
8903    pub url: String,
8904    pub events: Vec<String>,
8905    pub secret: Option<String>,
8906    /// Optional payload template with {{topic}}, {{timestamp}}, {{source}}, {{payload}}, {{webhook_name}}, {{webhook_id}}.
8907    pub template: Option<String>,
8908}
8909
8910/// Query parameters for webhook deliveries.
8911#[derive(Debug, Deserialize)]
8912pub struct DeliveryQuery {
8913    #[serde(default = "default_delivery_limit")]
8914    pub limit: usize,
8915    pub webhook_id: Option<String>,
8916}
8917
8918fn default_delivery_limit() -> usize { 50 }
8919
8920/// GET /v1/webhooks — list registered webhooks.
8921async fn webhooks_list_handler(
8922    State(state): State<SharedState>,
8923    headers: HeaderMap,
8924) -> Result<Json<serde_json::Value>, StatusCode> {
8925    let s = state.lock().unwrap();
8926    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8927
8928    let list = s.webhooks.list();
8929    let stats = s.webhooks.stats();
8930    Ok(Json(serde_json::json!({
8931        "total": list.len(),
8932        "active": stats.active_webhooks,
8933        "webhooks": list,
8934    })))
8935}
8936
8937/// POST /v1/webhooks — register a new webhook.
8938async fn webhooks_register_handler(
8939    State(state): State<SharedState>,
8940    headers: HeaderMap,
8941    Json(payload): Json<RegisterWebhookRequest>,
8942) -> Result<Json<serde_json::Value>, StatusCode> {
8943    let mut s = state.lock().unwrap();
8944    check_auth(&mut s, &headers, AccessLevel::Admin)?;
8945
8946    let client = client_key_from_headers(&headers);
8947    let id = s.webhooks.register_with_template(&payload.name, &payload.url, payload.events, payload.secret, payload.template);
8948
8949    s.event_bus.publish(
8950        "webhook.registered",
8951        serde_json::json!({ "id": &id, "name": &payload.name }),
8952        "server",
8953    );
8954    s.audit_log.record(&client, AuditAction::WebhookRegister, &id, serde_json::json!({"name": &payload.name, "url": &payload.url}), true);
8955
8956    Ok(Json(serde_json::json!({
8957        "success": true,
8958        "id": id,
8959        "name": payload.name,
8960    })))
8961}
8962
8963/// DELETE /v1/webhooks/:id — unregister a webhook.
8964async fn webhooks_delete_handler(
8965    State(state): State<SharedState>,
8966    headers: HeaderMap,
8967    Path(id): Path<String>,
8968) -> Result<Json<serde_json::Value>, StatusCode> {
8969    let mut s = state.lock().unwrap();
8970    check_auth(&mut s, &headers, AccessLevel::Admin)?;
8971
8972    let client = client_key_from_headers(&headers);
8973    let removed = s.webhooks.unregister(&id);
8974    if removed {
8975        s.event_bus.publish(
8976            "webhook.removed",
8977            serde_json::json!({ "id": &id }),
8978            "server",
8979        );
8980    }
8981    s.audit_log.record(&client, AuditAction::WebhookRemove, &id, serde_json::json!(null), removed);
8982
8983    Ok(Json(serde_json::json!({
8984        "success": removed,
8985        "id": id,
8986    })))
8987}
8988
8989/// POST /v1/webhooks/:id/toggle — toggle webhook active state.
8990async fn webhooks_toggle_handler(
8991    State(state): State<SharedState>,
8992    headers: HeaderMap,
8993    Path(id): Path<String>,
8994) -> Result<Json<serde_json::Value>, StatusCode> {
8995    let mut s = state.lock().unwrap();
8996    check_auth(&mut s, &headers, AccessLevel::Admin)?;
8997
8998    let client = client_key_from_headers(&headers);
8999    match s.webhooks.toggle(&id) {
9000        Some(active) => {
9001            s.audit_log.record(&client, AuditAction::WebhookToggle, &id, serde_json::json!({"active": active}), true);
9002            Ok(Json(serde_json::json!({
9003                "success": true,
9004                "id": id,
9005                "active": active,
9006            })))
9007        }
9008        None => Ok(Json(serde_json::json!({
9009            "success": false,
9010            "error": "webhook not found",
9011        }))),
9012    }
9013}
9014
9015/// GET /v1/webhooks/deliveries — recent delivery log.
9016async fn webhooks_deliveries_handler(
9017    State(state): State<SharedState>,
9018    headers: HeaderMap,
9019    Query(params): Query<DeliveryQuery>,
9020) -> Result<Json<serde_json::Value>, StatusCode> {
9021    let s = state.lock().unwrap();
9022    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9023
9024    let deliveries = s.webhooks.recent_deliveries(params.limit, params.webhook_id.as_deref());
9025    let json_entries: Vec<serde_json::Value> = deliveries.iter().map(|d| {
9026        serde_json::to_value(d).unwrap_or_default()
9027    }).collect();
9028
9029    Ok(Json(serde_json::json!({
9030        "count": json_entries.len(),
9031        "deliveries": json_entries,
9032    })))
9033}
9034
9035/// GET /v1/webhooks/stats — webhook statistics.
9036async fn webhooks_stats_handler(
9037    State(state): State<SharedState>,
9038    headers: HeaderMap,
9039) -> Result<Json<serde_json::Value>, StatusCode> {
9040    let s = state.lock().unwrap();
9041    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9042
9043    let stats = s.webhooks.stats();
9044    Ok(Json(serde_json::to_value(&stats).unwrap_or_default()))
9045}
9046
9047/// GET /v1/webhooks/retry-queue — view pending retries.
9048async fn webhooks_retry_queue_handler(
9049    State(state): State<SharedState>,
9050    headers: HeaderMap,
9051) -> Result<Json<serde_json::Value>, StatusCode> {
9052    let s = state.lock().unwrap();
9053    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9054
9055    let queue = s.webhooks.retry_queue();
9056    Ok(Json(serde_json::json!({
9057        "count": queue.len(),
9058        "entries": serde_json::to_value(queue).unwrap_or_default(),
9059    })))
9060}
9061
9062/// GET /v1/webhooks/dead-letters — view permanently failed deliveries.
9063async fn webhooks_dead_letters_handler(
9064    State(state): State<SharedState>,
9065    headers: HeaderMap,
9066) -> Result<Json<serde_json::Value>, StatusCode> {
9067    let s = state.lock().unwrap();
9068    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9069
9070    let dead = s.webhooks.dead_letters();
9071    Ok(Json(serde_json::json!({
9072        "count": dead.len(),
9073        "entries": serde_json::to_value(dead).unwrap_or_default(),
9074    })))
9075}
9076
9077/// GET /v1/webhooks/:id/template — get the payload template for a webhook.
9078async fn webhook_template_get_handler(
9079    State(state): State<SharedState>,
9080    headers: HeaderMap,
9081    Path(id): Path<String>,
9082) -> Result<Json<serde_json::Value>, StatusCode> {
9083    let s = state.lock().unwrap();
9084    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9085
9086    match s.webhooks.get_template(&id) {
9087        Some(template) => Ok(Json(serde_json::json!({
9088            "webhook_id": id,
9089            "template": template,
9090            "has_template": template.is_some(),
9091        }))),
9092        None => Ok(Json(serde_json::json!({
9093            "error": format!("webhook '{}' not found", id),
9094        }))),
9095    }
9096}
9097
9098/// Request to set a webhook template.
9099#[derive(Debug, Deserialize)]
9100pub struct SetTemplateRequest {
9101    /// Template string (null to remove).
9102    pub template: Option<String>,
9103}
9104
9105/// PUT /v1/webhooks/:id/template — set or remove payload template.
9106async fn webhook_template_set_handler(
9107    State(state): State<SharedState>,
9108    headers: HeaderMap,
9109    Path(id): Path<String>,
9110    Json(payload): Json<SetTemplateRequest>,
9111) -> Result<Json<serde_json::Value>, StatusCode> {
9112    let mut s = state.lock().unwrap();
9113    check_auth(&mut s, &headers, AccessLevel::Write)?;
9114
9115    if s.webhooks.set_template(&id, payload.template.clone()) {
9116        Ok(Json(serde_json::json!({
9117            "success": true,
9118            "webhook_id": id,
9119            "template": payload.template,
9120        })))
9121    } else {
9122        Ok(Json(serde_json::json!({
9123            "error": format!("webhook '{}' not found", id),
9124        })))
9125    }
9126}
9127
9128/// Request to render a webhook template preview.
9129#[derive(Debug, Deserialize)]
9130pub struct RenderPreviewRequest {
9131    pub topic: String,
9132    pub payload: serde_json::Value,
9133    #[serde(default = "default_render_source")]
9134    pub source: String,
9135}
9136
9137fn default_render_source() -> String { "preview".into() }
9138
9139/// POST /v1/webhooks/:id/render — preview rendered payload with template.
9140async fn webhook_render_handler(
9141    State(state): State<SharedState>,
9142    headers: HeaderMap,
9143    Path(id): Path<String>,
9144    Json(payload): Json<RenderPreviewRequest>,
9145) -> Result<Json<serde_json::Value>, StatusCode> {
9146    let s = state.lock().unwrap();
9147    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9148
9149    let rendered = s.webhooks.render_payload(&id, &payload.topic, &payload.payload, &payload.source);
9150    Ok(Json(serde_json::json!({
9151        "webhook_id": id,
9152        "rendered": rendered,
9153    })))
9154}
9155
9156/// Request for webhook delivery simulation.
9157#[derive(Debug, Deserialize)]
9158pub struct SimulateDeliveryRequest {
9159    /// Event topic.
9160    pub topic: String,
9161    /// Event payload.
9162    pub payload: serde_json::Value,
9163    /// Event source.
9164    #[serde(default = "default_simulate_source")]
9165    pub source: String,
9166}
9167
9168fn default_simulate_source() -> String { "simulate".into() }
9169
9170/// POST /v1/webhooks/:id/simulate — dry-run webhook delivery.
9171///
9172/// Renders the payload (template or default), computes HMAC signature if secret
9173/// is set, returns the full delivery preview without actually sending.
9174async fn webhook_simulate_handler(
9175    State(state): State<SharedState>,
9176    headers: HeaderMap,
9177    Path(id): Path<String>,
9178    Json(payload): Json<SimulateDeliveryRequest>,
9179) -> Result<Json<serde_json::Value>, StatusCode> {
9180    let s = state.lock().unwrap();
9181    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9182
9183    let wh = match s.webhooks.get(&id) {
9184        Some(w) => w,
9185        None => {
9186            return Ok(Json(serde_json::json!({
9187                "error": format!("webhook '{}' not found", id),
9188            })));
9189        }
9190    };
9191
9192    // Render payload
9193    let rendered = s.webhooks.render_payload(&id, &payload.topic, &payload.payload, &payload.source);
9194    let rendered_bytes = serde_json::to_vec(&rendered).unwrap_or_default();
9195
9196    // Compute signature if secret exists
9197    let signature = wh.secret.as_ref().map(|secret| {
9198        crate::webhooks::WebhookRegistry::compute_signature(secret, &rendered_bytes)
9199    });
9200
9201    // Check if topic matches webhook filters
9202    let topic_matches = wh.events.iter().any(|f| {
9203        f == "*" || f == &payload.topic
9204            || (f.ends_with(".*") && payload.topic.starts_with(&f[..f.len()-2]))
9205    });
9206
9207    Ok(Json(serde_json::json!({
9208        "webhook_id": id,
9209        "webhook_name": wh.name,
9210        "url": wh.url,
9211        "active": wh.active,
9212        "topic": payload.topic,
9213        "topic_matches": topic_matches,
9214        "has_template": wh.template.is_some(),
9215        "has_secret": wh.secret.is_some(),
9216        "rendered_payload": rendered,
9217        "signature": signature,
9218        "content_type": "application/json",
9219        "method": "POST",
9220        "dry_run": true,
9221    })))
9222}
9223
9224// ── Server config endpoints ──────────────────────────────────────────────
9225
9226/// GET /v1/config — get current server configuration.
9227async fn config_get_handler(
9228    State(state): State<SharedState>,
9229    headers: HeaderMap,
9230) -> Result<Json<serde_json::Value>, StatusCode> {
9231    let s = state.lock().unwrap();
9232    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9233
9234    let snap = crate::server_config::snapshot(&s.rate_limiter, &s.request_logger, &s.api_keys);
9235    Ok(Json(serde_json::to_value(&snap).unwrap_or_default()))
9236}
9237
9238/// PUT /v1/config — update server configuration at runtime.
9239async fn config_put_handler(
9240    State(state): State<SharedState>,
9241    headers: HeaderMap,
9242    Json(update): Json<crate::server_config::ConfigUpdate>,
9243) -> Result<Json<serde_json::Value>, StatusCode> {
9244    let mut s = state.lock().unwrap();
9245    check_auth(&mut s, &headers, AccessLevel::Admin)?;
9246
9247    // Apply changes per-section to satisfy borrow checker (each borrows one field)
9248    let mut changes = Vec::new();
9249    if let Some(ref rl) = update.rate_limit {
9250        changes.extend(crate::server_config::apply_rate_limit(rl, &mut s.rate_limiter));
9251    }
9252    if let Some(ref log) = update.request_log {
9253        changes.extend(crate::server_config::apply_request_log(log, &mut s.request_logger));
9254    }
9255    let snap = crate::server_config::snapshot(&s.rate_limiter, &s.request_logger, &s.api_keys);
9256    let result = crate::server_config::ConfigUpdateResult {
9257        applied: !changes.is_empty(),
9258        changes,
9259        snapshot: snap,
9260    };
9261
9262    let client = client_key_from_headers(&headers);
9263    if result.applied {
9264        s.event_bus.publish(
9265            "config.updated",
9266            serde_json::json!({
9267                "changes": result.changes.len(),
9268                "sections": result.changes.iter().map(|c| c.section.clone()).collect::<Vec<_>>(),
9269            }),
9270            "server",
9271        );
9272        s.audit_log.record(&client, AuditAction::ConfigUpdate, "config", serde_json::json!({"changes": result.changes.len()}), true);
9273    }
9274
9275    Ok(Json(serde_json::to_value(&result).unwrap_or_default()))
9276}
9277
9278/// POST /v1/config/save — persist current config to disk.
9279async fn config_save_handler(
9280    State(state): State<SharedState>,
9281    headers: HeaderMap,
9282) -> Result<Json<serde_json::Value>, StatusCode> {
9283    let mut s = state.lock().unwrap();
9284    check_auth(&mut s, &headers, AccessLevel::Admin)?;
9285
9286    let snap = crate::server_config::snapshot(&s.rate_limiter, &s.request_logger, &s.api_keys);
9287    let path = crate::config_persistence::resolve_path(s.config.config_path.as_deref());
9288    let result = crate::config_persistence::save(&snap, &path, crate::runner::AXON_VERSION);
9289
9290    let client = client_key_from_headers(&headers);
9291    if result.success {
9292        s.event_bus.publish(
9293            "config.saved",
9294            serde_json::json!({ "path": &result.path, "save_count": result.save_count }),
9295            "server",
9296        );
9297    }
9298    s.audit_log.record(&client, AuditAction::ConfigSave, "config", serde_json::json!({"path": &result.path}), result.success);
9299
9300    Ok(Json(serde_json::to_value(&result).unwrap_or_default()))
9301}
9302
9303/// POST /v1/config/load — reload config from disk.
9304async fn config_load_handler(
9305    State(state): State<SharedState>,
9306    headers: HeaderMap,
9307) -> Result<Json<serde_json::Value>, StatusCode> {
9308    let mut s = state.lock().unwrap();
9309    check_auth(&mut s, &headers, AccessLevel::Admin)?;
9310
9311    let client = client_key_from_headers(&headers);
9312    let path = crate::config_persistence::resolve_path(s.config.config_path.as_deref());
9313
9314    match crate::config_persistence::load(&path) {
9315        Ok(persisted) => {
9316            let update = crate::config_persistence::snapshot_to_update(&persisted.config);
9317            let mut changes = Vec::new();
9318            if let Some(ref rl) = update.rate_limit {
9319                changes.extend(crate::server_config::apply_rate_limit(rl, &mut s.rate_limiter));
9320            }
9321            if let Some(ref log) = update.request_log {
9322                changes.extend(crate::server_config::apply_request_log(log, &mut s.request_logger));
9323            }
9324
9325            s.event_bus.publish(
9326                "config.loaded",
9327                serde_json::json!({
9328                    "path": path.display().to_string(),
9329                    "changes": changes.len(),
9330                    "save_count": persisted.save_count,
9331                }),
9332                "server",
9333            );
9334            s.audit_log.record(&client, AuditAction::ConfigLoad, "config", serde_json::json!({"path": path.display().to_string(), "changes": changes.len()}), true);
9335
9336            Ok(Json(serde_json::json!({
9337                "success": true,
9338                "path": path.display().to_string(),
9339                "saved_at": persisted.saved_at,
9340                "save_count": persisted.save_count,
9341                "changes_applied": changes.len(),
9342            })))
9343        }
9344        Err(e) => {
9345            s.audit_log.record(&client, AuditAction::ConfigLoad, "config", serde_json::json!({"error": &e}), false);
9346            Ok(Json(serde_json::json!({
9347                "success": false,
9348                "path": path.display().to_string(),
9349                "error": e,
9350            })))
9351        }
9352    }
9353}
9354
9355/// DELETE /v1/config/saved — remove persisted config file.
9356async fn config_delete_handler(
9357    State(state): State<SharedState>,
9358    headers: HeaderMap,
9359) -> Result<Json<serde_json::Value>, StatusCode> {
9360    let mut s = state.lock().unwrap();
9361    check_auth(&mut s, &headers, AccessLevel::Admin)?;
9362
9363    let client = client_key_from_headers(&headers);
9364    let path = crate::config_persistence::resolve_path(s.config.config_path.as_deref());
9365    let removed = crate::config_persistence::remove(&path);
9366    s.audit_log.record(&client, AuditAction::ConfigDelete, "config", serde_json::json!({"path": path.display().to_string()}), removed);
9367
9368    Ok(Json(serde_json::json!({
9369        "success": removed,
9370        "path": path.display().to_string(),
9371    })))
9372}
9373
9374// ── Config snapshots endpoints ────────────────────────────────────────────
9375
9376/// GET /v1/config/snapshots — list all saved configuration snapshots.
9377async fn config_snapshots_list_handler(
9378    State(state): State<SharedState>,
9379    headers: HeaderMap,
9380) -> Result<Json<serde_json::Value>, StatusCode> {
9381    let s = state.lock().unwrap();
9382    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9383
9384    let summaries: Vec<serde_json::Value> = s.config_snapshots.iter().map(|snap| {
9385        serde_json::json!({
9386            "name": snap.name,
9387            "created_at": snap.created_at,
9388        })
9389    }).collect();
9390
9391    Ok(Json(serde_json::json!({
9392        "count": summaries.len(),
9393        "snapshots": summaries,
9394    })))
9395}
9396
9397/// Request to save a config snapshot.
9398#[derive(Debug, Deserialize)]
9399pub struct SnapshotSaveRequest {
9400    pub name: String,
9401}
9402
9403/// POST /v1/config/snapshots — save current configuration as a named snapshot.
9404async fn config_snapshots_save_handler(
9405    State(state): State<SharedState>,
9406    headers: HeaderMap,
9407    Json(payload): Json<SnapshotSaveRequest>,
9408) -> Result<Json<serde_json::Value>, StatusCode> {
9409    let client = client_key_from_headers(&headers);
9410    let mut s = state.lock().unwrap();
9411    check_auth(&mut s, &headers, AccessLevel::Admin)?;
9412
9413    if payload.name.is_empty() {
9414        return Ok(Json(serde_json::json!({
9415            "success": false,
9416            "error": "snapshot name must not be empty",
9417        })));
9418    }
9419
9420    // Check for duplicate name
9421    if s.config_snapshots.iter().any(|snap| snap.name == payload.name) {
9422        return Ok(Json(serde_json::json!({
9423            "success": false,
9424            "error": format!("snapshot '{}' already exists", payload.name),
9425        })));
9426    }
9427
9428    let snap = crate::server_config::snapshot(&s.rate_limiter, &s.request_logger, &s.api_keys);
9429    let now = std::time::SystemTime::now()
9430        .duration_since(std::time::UNIX_EPOCH)
9431        .unwrap_or_default()
9432        .as_secs();
9433
9434    s.config_snapshots.push(NamedConfigSnapshot {
9435        name: payload.name.clone(),
9436        created_at: now,
9437        snapshot: snap,
9438    });
9439
9440    // Cap at 50 snapshots
9441    if s.config_snapshots.len() > 50 {
9442        s.config_snapshots.remove(0);
9443    }
9444
9445    s.audit_log.record(
9446        &client, AuditAction::ConfigUpdate, "config_snapshot",
9447        serde_json::json!({"action": "save", "name": payload.name}),
9448        true,
9449    );
9450
9451    Ok(Json(serde_json::json!({
9452        "success": true,
9453        "name": payload.name,
9454        "total_snapshots": s.config_snapshots.len(),
9455    })))
9456}
9457
9458/// Request to restore a config snapshot.
9459#[derive(Debug, Deserialize)]
9460pub struct SnapshotRestoreRequest {
9461    pub name: String,
9462}
9463
9464/// POST /v1/config/snapshots/restore — restore configuration from a named snapshot.
9465async fn config_snapshots_restore_handler(
9466    State(state): State<SharedState>,
9467    headers: HeaderMap,
9468    Json(payload): Json<SnapshotRestoreRequest>,
9469) -> Result<Json<serde_json::Value>, StatusCode> {
9470    let client = client_key_from_headers(&headers);
9471    let mut s = state.lock().unwrap();
9472    check_auth(&mut s, &headers, AccessLevel::Admin)?;
9473
9474    let snap = match s.config_snapshots.iter().find(|snap| snap.name == payload.name) {
9475        Some(snap) => snap.snapshot.clone(),
9476        None => {
9477            return Ok(Json(serde_json::json!({
9478                "success": false,
9479                "error": format!("snapshot '{}' not found", payload.name),
9480            })));
9481        }
9482    };
9483
9484    // Apply rate limiter settings
9485    s.rate_limiter.update_config(
9486        Some(snap.rate_limit.max_requests),
9487        Some(snap.rate_limit.window_secs),
9488        Some(snap.rate_limit.enabled),
9489    );
9490
9491    // Apply request log settings
9492    s.request_logger.update_config(
9493        Some(snap.request_log.capacity),
9494        Some(snap.request_log.enabled),
9495    );
9496
9497    s.audit_log.record(
9498        &client, AuditAction::ConfigUpdate, "config_snapshot",
9499        serde_json::json!({"action": "restore", "name": payload.name}),
9500        true,
9501    );
9502
9503    Ok(Json(serde_json::json!({
9504        "success": true,
9505        "restored_from": payload.name,
9506        "applied": {
9507            "rate_limit": snap.rate_limit,
9508            "request_log": snap.request_log,
9509        },
9510    })))
9511}
9512
9513// ── Audit trail endpoints ────────────────────────────────────────────────
9514
9515/// Query parameters for audit log retrieval.
9516#[derive(Debug, Deserialize)]
9517pub struct AuditQuery {
9518    #[serde(default = "default_audit_limit")]
9519    pub limit: usize,
9520    pub action: Option<String>,
9521    pub actor: Option<String>,
9522    pub target: Option<String>,
9523    pub success: Option<bool>,
9524}
9525
9526fn default_audit_limit() -> usize { 50 }
9527
9528/// GET /v1/audit — query recent audit entries with optional filters.
9529async fn audit_handler(
9530    State(state): State<SharedState>,
9531    headers: HeaderMap,
9532    Query(params): Query<AuditQuery>,
9533) -> Result<Json<serde_json::Value>, StatusCode> {
9534    let s = state.lock().unwrap();
9535    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9536
9537    let filter = if params.action.is_some() || params.actor.is_some()
9538        || params.target.is_some() || params.success.is_some()
9539    {
9540        Some(AuditFilter {
9541            action: params.action.as_deref().and_then(crate::audit_trail::parse_action),
9542            actor: params.actor,
9543            target_prefix: params.target,
9544            success: params.success,
9545            ..Default::default()
9546        })
9547    } else {
9548        None
9549    };
9550
9551    let entries = s.audit_log.query(params.limit, filter.as_ref());
9552    let json_entries: Vec<serde_json::Value> = entries.iter().map(|e| {
9553        serde_json::to_value(e).unwrap_or_default()
9554    }).collect();
9555
9556    Ok(Json(serde_json::json!({
9557        "count": json_entries.len(),
9558        "total": s.audit_log.total_recorded(),
9559        "entries": json_entries,
9560    })))
9561}
9562
9563/// GET /v1/audit/stats — aggregated audit statistics.
9564async fn audit_stats_handler(
9565    State(state): State<SharedState>,
9566    headers: HeaderMap,
9567) -> Result<Json<serde_json::Value>, StatusCode> {
9568    let s = state.lock().unwrap();
9569    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9570
9571    let stats = s.audit_log.stats();
9572    Ok(Json(serde_json::to_value(&stats).unwrap_or_default()))
9573}
9574
9575/// Query parameters for audit trail export.
9576#[derive(Debug, Deserialize)]
9577pub struct AuditExportQuery {
9578    /// Export format: "jsonl" (default) or "csv".
9579    #[serde(default = "default_audit_export_format")]
9580    pub format: String,
9581    /// Only entries after this Unix timestamp (0 = no filter).
9582    #[serde(default)]
9583    pub from: u64,
9584    /// Only entries before this Unix timestamp (0 = no filter).
9585    #[serde(default)]
9586    pub to: u64,
9587    /// Max entries (default 1000).
9588    #[serde(default = "default_audit_export_limit")]
9589    pub limit: usize,
9590}
9591
9592fn default_audit_export_format() -> String { "jsonl".into() }
9593fn default_audit_export_limit() -> usize { 1000 }
9594
9595/// §Fase 32.h — `GET /v1/replay/<trace_id>` — return the recorded
9596/// replay binding for an axonendpoint POST/PUT.
9597///
9598/// Auth: `AccessLevel::ReadOnly` (same as `/v1/audit` — auditors
9599/// AND adopter ops with a read-only API key). Missing trace_id → 404.
9600///
9601/// Response shape (status 200):
9602/// ```json
9603/// {
9604///   "trace_id": "uuid",
9605///   "timestamp_ms": 1715459123000,
9606///   "endpoint_name": "LoanDecision",
9607///   "flow_name": "ApproveOrDeny",
9608///   "method": "POST",
9609///   "path": "/loan/decision",
9610///   "client_id": "tenant-X",
9611///   "capabilities_used": ["bank.officer"],
9612///   "request_body_hash_hex": "...",
9613///   "request_body_base64": "...",   // bytes ↦ base64
9614///   "response_status": 200,
9615///   "response_body_hash_hex": "...",
9616///   "response_body_base64": "...",
9617///   "response_content_type": "application/json",
9618///   "model_version": "axon.runtime.dynamic_route.v1",
9619///   "deterministic": true
9620/// }
9621/// ```
9622///
9623/// Response header `Replay-Status: deterministic | non_deterministic`
9624/// per plan vivo §9.2.
9625async fn replay_get_handler(
9626    State(state): State<SharedState>,
9627    headers: HeaderMap,
9628    axum::extract::Path(trace_id): axum::extract::Path<String>,
9629) -> axum::response::Response {
9630    use axum::response::IntoResponse;
9631    {
9632        let s = state.lock().unwrap();
9633        if check_auth_peek(&s, &headers, AccessLevel::ReadOnly).is_err() {
9634            return (
9635                StatusCode::UNAUTHORIZED,
9636                Json(serde_json::json!({
9637                    "error": "unauthorized",
9638                    "hint": "GET /v1/replay/<trace_id> requires read-only auth (same as /v1/audit).",
9639                })),
9640            )
9641                .into_response();
9642        }
9643    }
9644
9645    let entry_opt = {
9646        let s = state.lock().unwrap();
9647        s.axonendpoint_replay.get(&trace_id).cloned()
9648    };
9649    let entry = match entry_opt {
9650        Some(e) => e,
9651        None => {
9652            return (
9653                StatusCode::NOT_FOUND,
9654                Json(serde_json::json!({
9655                    "error": "replay_trace_not_found",
9656                    "trace_id": trace_id,
9657                    "hint": "No replay binding exists for this trace_id. Either the trace_id is wrong, the entry expired past retention (default 30 days), or the original endpoint had `replay: false` declared.",
9658                    "d_letter": "D9",
9659                })),
9660            )
9661                .into_response();
9662        }
9663    };
9664
9665    use base64::engine::general_purpose::STANDARD as B64;
9666    use base64::Engine;
9667    // §Fase 33.x.f — `step_audit` field surfaces the per-step
9668    // sequence (D6). Empty for legacy JSON 2xx capture entries
9669    // (Fase 32.h shape); populated for SSE routes whose
9670    // `replay: true` declaration fired the streaming per-step
9671    // recording. The wire field is elided when empty per the
9672    // serde `skip_serializing_if` on the entry struct, but here
9673    // we always include it (possibly as `[]`) for adopter
9674    // diagnostic clarity — adopters expect a stable wire field
9675    // shape from the replay-GET surface.
9676    let step_audit_json = serde_json::to_value(&entry.step_audit).unwrap_or_else(|_| {
9677        serde_json::Value::Array(Vec::new())
9678    });
9679    // §Fase 33.x.g — Always include `runtime_warnings` on the GET
9680    // payload (possibly `[]`) so adopter dashboards have a stable
9681    // wire field shape even for legacy JSON-mode entries that
9682    // never carry warnings.
9683    let runtime_warnings_json =
9684        serde_json::to_value(&entry.runtime_warnings).unwrap_or_else(|_| {
9685            serde_json::Value::Array(Vec::new())
9686        });
9687    let payload = serde_json::json!({
9688        "trace_id": entry.trace_id,
9689        "timestamp_ms": entry.timestamp_ms,
9690        "endpoint_name": entry.endpoint_name,
9691        "flow_name": entry.flow_name,
9692        "method": entry.method,
9693        "path": entry.path,
9694        "client_id": entry.client_id,
9695        "capabilities_used": entry.capabilities_used,
9696        "request_body_hash_hex": entry.request_body_hash_hex,
9697        "request_body_base64": B64.encode(&entry.request_body),
9698        "response_status": entry.response_status,
9699        "response_body_hash_hex": entry.response_body_hash_hex,
9700        "response_body_base64": B64.encode(&entry.response_body),
9701        "response_content_type": entry.response_content_type,
9702        "model_version": entry.model_version,
9703        "deterministic": entry.deterministic,
9704        "step_audit": step_audit_json,
9705        "runtime_warnings": runtime_warnings_json,
9706    });
9707    let replay_status = if entry.deterministic {
9708        "deterministic"
9709    } else {
9710        "non_deterministic"
9711    };
9712    let mut resp = (StatusCode::OK, Json(payload)).into_response();
9713    if let Ok(val) = axum::http::HeaderValue::from_str(replay_status) {
9714        resp.headers_mut().insert("replay-status", val);
9715    }
9716    resp
9717}
9718
9719/// GET /v1/audit/export — export audit trail as JSONL or CSV.
9720async fn audit_export_handler(
9721    State(state): State<SharedState>,
9722    headers: HeaderMap,
9723    Query(params): Query<AuditExportQuery>,
9724) -> Result<(StatusCode, [(String, String); 1], String), StatusCode> {
9725    let s = state.lock().unwrap();
9726    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9727
9728    let entries = s.audit_log.query(params.limit, None);
9729
9730    // Apply date range filter
9731    let filtered: Vec<&&crate::audit_trail::AuditEntry> = entries.iter()
9732        .filter(|e| {
9733            let after = params.from == 0 || e.timestamp >= params.from;
9734            let before = params.to == 0 || e.timestamp <= params.to;
9735            after && before
9736        })
9737        .collect();
9738
9739    let format = params.format.to_lowercase();
9740    match format.as_str() {
9741        "csv" => {
9742            let mut csv = String::from("id,timestamp,actor,action,target,success,detail\n");
9743            for e in &filtered {
9744                let detail_str = serde_json::to_string(&e.detail).unwrap_or_default().replace('"', "\"\"");
9745                csv.push_str(&format!(
9746                    "{},{},{},{},{},{},\"{}\"\n",
9747                    e.id, e.timestamp, e.actor, e.action.as_str(), e.target, e.success, detail_str
9748                ));
9749            }
9750            Ok((
9751                StatusCode::OK,
9752                [("content-type".into(), "text/csv".into())],
9753                csv,
9754            ))
9755        }
9756        _ => {
9757            // JSONL
9758            let mut jsonl = String::new();
9759            for e in &filtered {
9760                let line = serde_json::json!({
9761                    "id": e.id,
9762                    "timestamp": e.timestamp,
9763                    "actor": e.actor,
9764                    "action": e.action.as_str(),
9765                    "target": e.target,
9766                    "success": e.success,
9767                    "detail": e.detail,
9768                });
9769                jsonl.push_str(&serde_json::to_string(&line).unwrap_or_default());
9770                jsonl.push('\n');
9771            }
9772            Ok((
9773                StatusCode::OK,
9774                [("content-type".into(), "application/x-ndjson".into())],
9775                jsonl,
9776            ))
9777        }
9778    }
9779}
9780
9781// ── CORS config endpoints ────────────────────────────────────────────────
9782
9783/// GET /v1/cors — view current CORS configuration.
9784async fn cors_config_handler(
9785    State(state): State<SharedState>,
9786    headers: HeaderMap,
9787) -> Result<Json<serde_json::Value>, StatusCode> {
9788    let s = state.lock().unwrap();
9789    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9790
9791    Ok(Json(serde_json::to_value(&s.cors_config).unwrap_or_default()))
9792}
9793
9794/// PUT /v1/cors — update CORS configuration.
9795/// Note: changes take effect on next server restart (CORS layer is built at startup).
9796async fn cors_config_put_handler(
9797    State(state): State<SharedState>,
9798    headers: HeaderMap,
9799    Json(update): Json<crate::cors::CorsUpdate>,
9800) -> Result<Json<serde_json::Value>, StatusCode> {
9801    let mut s = state.lock().unwrap();
9802    check_auth(&mut s, &headers, AccessLevel::Admin)?;
9803
9804    let changes = crate::cors::apply_update(&mut s.cors_config, &update);
9805
9806    Ok(Json(serde_json::json!({
9807        "updated": !changes.is_empty(),
9808        "changes": changes,
9809        "note": "CORS changes take effect on next server restart",
9810        "config": serde_json::to_value(&s.cors_config).unwrap_or_default(),
9811    })))
9812}
9813
9814// ── Request middleware endpoints ─────────────────────────────────────────
9815
9816/// GET /v1/middleware — view current middleware configuration and stats.
9817async fn middleware_config_handler(
9818    State(state): State<SharedState>,
9819    headers: HeaderMap,
9820) -> Result<Json<serde_json::Value>, StatusCode> {
9821    let s = state.lock().unwrap();
9822    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9823
9824    let stats = crate::request_middleware::MiddlewareStats {
9825        total_requests: s.request_id_gen.count(),
9826        config: s.middleware_config.clone(),
9827    };
9828
9829    Ok(Json(serde_json::to_value(&stats).unwrap_or_default()))
9830}
9831
9832/// PUT /v1/middleware — update middleware configuration at runtime.
9833async fn middleware_config_put_handler(
9834    State(state): State<SharedState>,
9835    headers: HeaderMap,
9836    Json(update): Json<crate::request_middleware::MiddlewareUpdate>,
9837) -> Result<Json<serde_json::Value>, StatusCode> {
9838    let mut s = state.lock().unwrap();
9839    check_auth(&mut s, &headers, AccessLevel::Admin)?;
9840
9841    let changes = crate::request_middleware::apply_update(&mut s.middleware_config, &update);
9842
9843    Ok(Json(serde_json::json!({
9844        "updated": !changes.is_empty(),
9845        "changes": changes,
9846        "config": serde_json::to_value(&s.middleware_config).unwrap_or_default(),
9847    })))
9848}
9849
9850// ── Webhook delivery config endpoints ────────────────────────────────────
9851
9852/// GET /v1/webhooks/delivery-config — get current delivery configuration.
9853async fn delivery_config_handler(
9854    State(state): State<SharedState>,
9855    headers: HeaderMap,
9856) -> Result<Json<serde_json::Value>, StatusCode> {
9857    let s = state.lock().unwrap();
9858    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9859
9860    let c = &s.delivery_config;
9861    Ok(Json(serde_json::json!({
9862        "timeout_secs": c.timeout.as_secs(),
9863        "max_retries": c.max_retries,
9864        "base_delay_ms": c.base_delay.as_millis() as u64,
9865        "max_delay_secs": c.max_delay.as_secs(),
9866    })))
9867}
9868
9869/// Update payload for delivery config.
9870#[derive(Debug, Deserialize)]
9871pub struct DeliveryConfigUpdate {
9872    pub timeout_secs: Option<u64>,
9873    pub max_retries: Option<u32>,
9874    pub base_delay_ms: Option<u64>,
9875    pub max_delay_secs: Option<u64>,
9876}
9877
9878/// PUT /v1/webhooks/delivery-config — update delivery configuration.
9879async fn delivery_config_put_handler(
9880    State(state): State<SharedState>,
9881    headers: HeaderMap,
9882    Json(update): Json<DeliveryConfigUpdate>,
9883) -> Result<Json<serde_json::Value>, StatusCode> {
9884    let mut s = state.lock().unwrap();
9885    check_auth(&mut s, &headers, AccessLevel::Admin)?;
9886
9887    if let Some(t) = update.timeout_secs {
9888        s.delivery_config.timeout = std::time::Duration::from_secs(t);
9889    }
9890    if let Some(r) = update.max_retries {
9891        s.delivery_config.max_retries = r;
9892    }
9893    if let Some(d) = update.base_delay_ms {
9894        s.delivery_config.base_delay = std::time::Duration::from_millis(d);
9895    }
9896    if let Some(m) = update.max_delay_secs {
9897        s.delivery_config.max_delay = std::time::Duration::from_secs(m);
9898    }
9899
9900    let c = &s.delivery_config;
9901    Ok(Json(serde_json::json!({
9902        "updated": true,
9903        "timeout_secs": c.timeout.as_secs(),
9904        "max_retries": c.max_retries,
9905        "base_delay_ms": c.base_delay.as_millis() as u64,
9906        "max_delay_secs": c.max_delay.as_secs(),
9907    })))
9908}
9909
9910// ── Shutdown endpoint ────────────────────────────────────────────────────
9911
9912/// POST /v1/shutdown — initiate graceful server shutdown (admin only).
9913async fn shutdown_handler(
9914    State(state): State<SharedState>,
9915    headers: HeaderMap,
9916) -> Result<Json<serde_json::Value>, StatusCode> {
9917    let mut s = state.lock().unwrap();
9918    check_auth(&mut s, &headers, AccessLevel::Admin)?;
9919
9920    let client = client_key_from_headers(&headers);
9921
9922    if let Some(ref coordinator) = s.shutdown {
9923        let triggered = coordinator.trigger();
9924        let uptime = coordinator.uptime_secs();
9925
9926        if triggered {
9927            // Auto-persist state before shutdown
9928            let auto_persist = s.auto_persist_on_shutdown;
9929            let persist_result = if auto_persist {
9930                match persist_state_to_disk(&s, &format!("shutdown:{}", client)) {
9931                    Ok(path) => Some(serde_json::json!({"success": true, "path": path})),
9932                    Err(e) => Some(serde_json::json!({"success": false, "error": e})),
9933                }
9934            } else {
9935                None
9936            };
9937
9938            s.audit_log.record(
9939                &client,
9940                AuditAction::ServerShutdown,
9941                "server",
9942                serde_json::json!({"reason": "api", "initiated_by": &client, "auto_persisted": auto_persist}),
9943                true,
9944            );
9945            Ok(Json(serde_json::json!({
9946                "initiated": true,
9947                "reason": "api",
9948                "uptime_secs": uptime,
9949                "message": "graceful shutdown initiated",
9950                "auto_persist": persist_result,
9951            })))
9952        } else {
9953            Ok(Json(serde_json::json!({
9954                "initiated": false,
9955                "reason": "api",
9956                "uptime_secs": uptime,
9957                "message": "shutdown already in progress",
9958            })))
9959        }
9960    } else {
9961        Ok(Json(serde_json::json!({
9962            "initiated": false,
9963            "message": "shutdown coordinator not available",
9964        })))
9965    }
9966}
9967
9968// ── Flow inspect endpoints ───────────────────────────────────────────────
9969
9970/// GET /v1/inspect/:name — introspect a deployed flow.
9971///
9972/// Re-compiles the flow's stored source and returns structured metadata:
9973/// signature, steps, edges, execution levels, anchors, tools, personas.
9974async fn inspect_flow_handler(
9975    State(state): State<SharedState>,
9976    headers: HeaderMap,
9977    Path(name): Path<String>,
9978) -> Result<Json<serde_json::Value>, StatusCode> {
9979    let s = state.lock().unwrap();
9980    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9981
9982    // Check if daemon exists
9983    if !s.daemons.contains_key(&name) {
9984        return Ok(Json(serde_json::json!({
9985            "error": format!("flow '{}' not deployed", name),
9986            "available": s.daemons.keys().collect::<Vec<_>>(),
9987        })));
9988    }
9989
9990    // Get the latest version source
9991    let history = match s.versions.get_history(&name) {
9992        Some(h) => h,
9993        None => {
9994            return Ok(Json(serde_json::json!({
9995                "error": format!("no version history for flow '{}'", name),
9996            })));
9997        }
9998    };
9999
10000    let active = match history.active() {
10001        Some(v) => v,
10002        None => {
10003            return Ok(Json(serde_json::json!({
10004                "error": format!("no active version for flow '{}'", name),
10005            })));
10006        }
10007    };
10008
10009    let source = active.source.clone();
10010    let source_file = active.source_file.clone();
10011    let source_hash = active.source_hash.clone();
10012    drop(s); // Release lock before compilation
10013
10014    match crate::flow_inspect::inspect_flow(&name, &source, &source_file, &source_hash) {
10015        Ok(inspection) => Ok(Json(serde_json::to_value(&inspection).unwrap_or_default())),
10016        Err(e) => Ok(Json(serde_json::json!({
10017            "error": e,
10018            "flow": name,
10019        }))),
10020    }
10021}
10022
10023/// Query parameters for graph export.
10024#[derive(Debug, Deserialize)]
10025pub struct GraphQuery {
10026    /// Output format: "dot" (default) or "mermaid".
10027    #[serde(default = "default_graph_format")]
10028    pub format: String,
10029}
10030
10031fn default_graph_format() -> String {
10032    "dot".to_string()
10033}
10034
10035/// GET /v1/inspect/:name/graph — export flow dependency graph as DOT or Mermaid.
10036async fn inspect_graph_handler(
10037    State(state): State<SharedState>,
10038    headers: HeaderMap,
10039    Path(name): Path<String>,
10040    Query(query): Query<GraphQuery>,
10041) -> Result<Json<serde_json::Value>, StatusCode> {
10042    let s = state.lock().unwrap();
10043    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10044
10045    if !s.daemons.contains_key(&name) {
10046        return Ok(Json(serde_json::json!({
10047            "error": format!("flow '{}' not deployed", name),
10048            "available": s.daemons.keys().collect::<Vec<_>>(),
10049        })));
10050    }
10051
10052    let history = match s.versions.get_history(&name) {
10053        Some(h) => h,
10054        None => {
10055            return Ok(Json(serde_json::json!({
10056                "error": format!("no version history for flow '{}'", name),
10057            })));
10058        }
10059    };
10060
10061    let active = match history.active() {
10062        Some(v) => v,
10063        None => {
10064            return Ok(Json(serde_json::json!({
10065                "error": format!("no active version for flow '{}'", name),
10066            })));
10067        }
10068    };
10069
10070    let source = active.source.clone();
10071    let source_file = active.source_file.clone();
10072    drop(s);
10073
10074    let format = crate::flow_inspect::GraphFormat::from_str(&query.format);
10075
10076    match crate::flow_inspect::export_flow_graph(&name, &source, &source_file, format) {
10077        Ok(export) => Ok(Json(serde_json::to_value(&export).unwrap_or_default())),
10078        Err(e) => Ok(Json(serde_json::json!({
10079            "error": e,
10080            "flow": name,
10081        }))),
10082    }
10083}
10084
10085/// GET /v1/inspect/:name/dependencies — step dependency analysis for a flow.
10086async fn inspect_dependencies_handler(
10087    State(state): State<SharedState>,
10088    headers: HeaderMap,
10089    Path(name): Path<String>,
10090) -> Result<Json<serde_json::Value>, StatusCode> {
10091    let s = state.lock().unwrap();
10092    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10093
10094    let history = match s.versions.get_history(&name) {
10095        Some(h) => h,
10096        None => {
10097            return Ok(Json(serde_json::json!({
10098                "error": format!("no version history for flow '{}'", name),
10099            })));
10100        }
10101    };
10102
10103    let active = match history.active() {
10104        Some(v) => v,
10105        None => {
10106            return Ok(Json(serde_json::json!({
10107                "error": format!("no active version for flow '{}'", name),
10108            })));
10109        }
10110    };
10111
10112    let source = active.source.clone();
10113    let source_file = active.source_file.clone();
10114    drop(s);
10115
10116    // Lex → Parse → IR
10117    let tokens = match crate::lexer::Lexer::new(&source, &source_file).tokenize() {
10118        Ok(t) => t,
10119        Err(e) => return Ok(Json(serde_json::json!({"error": format!("lex error: {e:?}")}))),
10120    };
10121    let mut parser = crate::parser::Parser::new(tokens);
10122    let program = match parser.parse() {
10123        Ok(p) => p,
10124        Err(e) => return Ok(Json(serde_json::json!({"error": format!("parse error: {e:?}")}))),
10125    };
10126    let ir = crate::ir_generator::IRGenerator::new().generate(&program);
10127
10128    let ir_flow = match ir.flows.iter().find(|f| f.name == name) {
10129        Some(f) => f,
10130        None => return Ok(Json(serde_json::json!({"error": format!("flow '{}' not found in IR", name)}))),
10131    };
10132
10133    // Extract StepInfo for step_deps analysis
10134    let step_infos: Vec<crate::step_deps::StepInfo> = ir_flow.steps.iter().filter_map(|node| {
10135        if let crate::ir_nodes::IRFlowNode::Step(step) = node {
10136            Some(crate::step_deps::StepInfo {
10137                name: step.name.clone(),
10138                step_type: step.node_type.to_string(),
10139                user_prompt: step.ask.clone(),
10140                argument: step.use_tool.as_ref()
10141                    .and_then(|t| t.get("argument").and_then(|a| a.as_str()).map(String::from))
10142                    .unwrap_or_default(),
10143            })
10144        } else {
10145            None
10146        }
10147    }).collect();
10148
10149    let graph = crate::step_deps::analyze(&step_infos);
10150
10151    // Serialize
10152    let steps_json: Vec<serde_json::Value> = graph.steps.iter().map(|s| {
10153        serde_json::json!({
10154            "name": s.name,
10155            "step_type": s.step_type,
10156            "depends_on": s.depends_on,
10157            "all_refs": s.all_refs,
10158            "step_refs": s.step_refs,
10159            "is_root": s.is_root,
10160        })
10161    }).collect();
10162
10163    Ok(Json(serde_json::json!({
10164        "flow": name,
10165        "total_steps": step_infos.len(),
10166        "max_depth": graph.max_depth,
10167        "parallel_groups": graph.parallel_groups,
10168        "unresolved_refs": graph.unresolved_refs,
10169        "steps": steps_json,
10170    })))
10171}
10172
10173/// GET /v1/inspect — list all deployed flows with summary info.
10174async fn inspect_list_handler(
10175    State(state): State<SharedState>,
10176    headers: HeaderMap,
10177) -> Result<Json<serde_json::Value>, StatusCode> {
10178    let s = state.lock().unwrap();
10179    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10180
10181    let mut summaries = Vec::new();
10182
10183    for (name, daemon) in &s.daemons {
10184        if let Some(history) = s.versions.get_history(name) {
10185            if let Some(active) = history.active() {
10186                // Quick summary without full recompilation
10187                summaries.push(serde_json::json!({
10188                    "name": name,
10189                    "source_file": daemon.source_file,
10190                    "source_hash": active.source_hash,
10191                    "version": active.version,
10192                    "state": daemon.state,
10193                    "event_count": daemon.event_count,
10194                }));
10195            }
10196        }
10197    }
10198
10199    Ok(Json(serde_json::json!({
10200        "flows": summaries,
10201        "total": summaries.len(),
10202    })))
10203}
10204
10205// ── Trace store endpoints ─────────────────────────────────────────────────
10206
10207/// Query parameters for trace listing.
10208#[derive(Debug, Deserialize)]
10209pub struct TraceQuery {
10210    /// Max entries to return (default 50).
10211    #[serde(default = "default_trace_limit")]
10212    pub limit: usize,
10213    /// Filter by flow name.
10214    pub flow_name: Option<String>,
10215    /// Filter by status (success/failed/partial/timeout).
10216    pub status: Option<String>,
10217    /// Filter by client key.
10218    pub client_key: Option<String>,
10219    /// Only traces with latency >= this (ms).
10220    pub min_latency_ms: Option<u64>,
10221    /// Only traces with errors.
10222    pub has_errors: Option<bool>,
10223}
10224
10225fn default_trace_limit() -> usize { 50 }
10226
10227/// GET /v1/traces — list recent execution traces with optional filters.
10228async fn traces_list_handler(
10229    State(state): State<SharedState>,
10230    headers: HeaderMap,
10231    Query(params): Query<TraceQuery>,
10232) -> Result<Json<serde_json::Value>, StatusCode> {
10233    let s = state.lock().unwrap();
10234    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10235
10236    let filter = if params.flow_name.is_some() || params.status.is_some()
10237        || params.client_key.is_some() || params.min_latency_ms.is_some()
10238        || params.has_errors.is_some()
10239    {
10240        Some(TraceFilter {
10241            flow_name: params.flow_name,
10242            status: params.status,
10243            client_key: params.client_key,
10244            min_latency_ms: params.min_latency_ms,
10245            has_errors: params.has_errors,
10246            tag: None,
10247        })
10248    } else {
10249        None
10250    };
10251
10252    let entries = s.trace_store.recent(params.limit, filter.as_ref());
10253    let json_entries: Vec<serde_json::Value> = entries.iter().map(|e| {
10254        serde_json::to_value(e).unwrap_or_default()
10255    }).collect();
10256
10257    Ok(Json(serde_json::json!({
10258        "count": json_entries.len(),
10259        "total_recorded": s.trace_store.total_recorded(),
10260        "entries": json_entries,
10261    })))
10262}
10263
10264/// GET /v1/traces/stats — aggregate analytics across buffered traces.
10265async fn traces_stats_handler(
10266    State(state): State<SharedState>,
10267    headers: HeaderMap,
10268) -> Result<Json<serde_json::Value>, StatusCode> {
10269    let s = state.lock().unwrap();
10270    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10271
10272    let stats = s.trace_store.stats();
10273    Ok(Json(serde_json::to_value(&stats).unwrap_or_default()))
10274}
10275
10276/// GET /v1/traces/:id — get a specific trace by ID.
10277async fn traces_get_handler(
10278    State(state): State<SharedState>,
10279    headers: HeaderMap,
10280    Path(id): Path<u64>,
10281) -> Result<Json<serde_json::Value>, StatusCode> {
10282    let s = state.lock().unwrap();
10283    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10284
10285    match s.trace_store.get(id) {
10286        Some(entry) => Ok(Json(serde_json::to_value(entry).unwrap_or_default())),
10287        None => Ok(Json(serde_json::json!({
10288            "error": "trace not found",
10289            "id": id,
10290        }))),
10291    }
10292}
10293
10294/// Request to annotate a trace.
10295#[derive(Debug, Deserialize)]
10296pub struct AnnotateRequest {
10297    /// Free-form note text.
10298    pub text: String,
10299    /// Tags for categorization/filtering.
10300    #[serde(default)]
10301    pub tags: Vec<String>,
10302    /// Author of the annotation (default: client key).
10303    pub author: Option<String>,
10304}
10305
10306/// POST /v1/traces/:id/annotate — add an annotation to a trace.
10307async fn traces_annotate_handler(
10308    State(state): State<SharedState>,
10309    headers: HeaderMap,
10310    Path(id): Path<u64>,
10311    Json(payload): Json<AnnotateRequest>,
10312) -> Result<Json<serde_json::Value>, StatusCode> {
10313    let client = client_key_from_headers(&headers);
10314    let mut s = state.lock().unwrap();
10315    check_auth(&mut s, &headers, AccessLevel::Write)?;
10316
10317    let author = payload.author.unwrap_or_else(|| client.clone());
10318
10319    let now = std::time::SystemTime::now()
10320        .duration_since(std::time::UNIX_EPOCH)
10321        .unwrap_or_default()
10322        .as_secs();
10323
10324    let annotation = crate::trace_store::TraceAnnotation {
10325        author: author.clone(),
10326        text: payload.text.clone(),
10327        tags: payload.tags.clone(),
10328        timestamp: now,
10329    };
10330
10331    if s.trace_store.annotate(id, annotation) {
10332        let annotation_count = s.trace_store.get(id)
10333            .map(|e| e.annotations.len())
10334            .unwrap_or(0);
10335
10336        Ok(Json(serde_json::json!({
10337            "success": true,
10338            "trace_id": id,
10339            "author": author,
10340            "text": payload.text,
10341            "tags": payload.tags,
10342            "annotation_count": annotation_count,
10343        })))
10344    } else {
10345        Ok(Json(serde_json::json!({
10346            "success": false,
10347            "error": format!("trace {} not found", id),
10348        })))
10349    }
10350}
10351
10352/// GET /v1/traces/:id/annotations — get all annotations for a trace.
10353async fn traces_annotations_handler(
10354    State(state): State<SharedState>,
10355    headers: HeaderMap,
10356    Path(id): Path<u64>,
10357) -> Result<Json<serde_json::Value>, StatusCode> {
10358    let s = state.lock().unwrap();
10359    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10360
10361    match s.trace_store.get(id) {
10362        Some(entry) => Ok(Json(serde_json::json!({
10363            "trace_id": id,
10364            "annotations": entry.annotations,
10365            "count": entry.annotations.len(),
10366        }))),
10367        None => Ok(Json(serde_json::json!({
10368            "error": format!("trace {} not found", id),
10369        }))),
10370    }
10371}
10372
10373/// Query parameters for trace diff.
10374#[derive(Debug, Deserialize)]
10375pub struct TraceDiffQuery {
10376    /// First trace ID.
10377    pub a: u64,
10378    /// Second trace ID.
10379    pub b: u64,
10380}
10381
10382/// GET /v1/traces/diff — compare two traces side-by-side.
10383async fn traces_diff_handler(
10384    State(state): State<SharedState>,
10385    headers: HeaderMap,
10386    Query(params): Query<TraceDiffQuery>,
10387) -> Result<Json<serde_json::Value>, StatusCode> {
10388    let s = state.lock().unwrap();
10389    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10390
10391    let trace_a = match s.trace_store.get(params.a) {
10392        Some(e) => e,
10393        None => {
10394            return Ok(Json(serde_json::json!({
10395                "error": format!("trace {} not found", params.a),
10396            })));
10397        }
10398    };
10399
10400    let trace_b = match s.trace_store.get(params.b) {
10401        Some(e) => e,
10402        None => {
10403            return Ok(Json(serde_json::json!({
10404                "error": format!("trace {} not found", params.b),
10405            })));
10406        }
10407    };
10408
10409    // Build field-level diffs
10410    let status_a = trace_a.status.as_str();
10411    let status_b = trace_b.status.as_str();
10412
10413    let mut field_diffs = Vec::new();
10414
10415    if trace_a.flow_name != trace_b.flow_name {
10416        field_diffs.push(serde_json::json!({
10417            "field": "flow_name", "a": trace_a.flow_name, "b": trace_b.flow_name,
10418        }));
10419    }
10420    if status_a != status_b {
10421        field_diffs.push(serde_json::json!({
10422            "field": "status", "a": status_a, "b": status_b,
10423        }));
10424    }
10425    if trace_a.backend != trace_b.backend {
10426        field_diffs.push(serde_json::json!({
10427            "field": "backend", "a": trace_a.backend, "b": trace_b.backend,
10428        }));
10429    }
10430    if trace_a.steps_executed != trace_b.steps_executed {
10431        field_diffs.push(serde_json::json!({
10432            "field": "steps_executed",
10433            "a": trace_a.steps_executed,
10434            "b": trace_b.steps_executed,
10435            "delta": trace_b.steps_executed as i64 - trace_a.steps_executed as i64,
10436        }));
10437    }
10438    if trace_a.latency_ms != trace_b.latency_ms {
10439        field_diffs.push(serde_json::json!({
10440            "field": "latency_ms",
10441            "a": trace_a.latency_ms,
10442            "b": trace_b.latency_ms,
10443            "delta": trace_b.latency_ms as i64 - trace_a.latency_ms as i64,
10444        }));
10445    }
10446    if trace_a.tokens_input != trace_b.tokens_input {
10447        field_diffs.push(serde_json::json!({
10448            "field": "tokens_input",
10449            "a": trace_a.tokens_input,
10450            "b": trace_b.tokens_input,
10451            "delta": trace_b.tokens_input as i64 - trace_a.tokens_input as i64,
10452        }));
10453    }
10454    if trace_a.tokens_output != trace_b.tokens_output {
10455        field_diffs.push(serde_json::json!({
10456            "field": "tokens_output",
10457            "a": trace_a.tokens_output,
10458            "b": trace_b.tokens_output,
10459            "delta": trace_b.tokens_output as i64 - trace_a.tokens_output as i64,
10460        }));
10461    }
10462    if trace_a.anchor_checks != trace_b.anchor_checks {
10463        field_diffs.push(serde_json::json!({
10464            "field": "anchor_checks",
10465            "a": trace_a.anchor_checks,
10466            "b": trace_b.anchor_checks,
10467            "delta": trace_b.anchor_checks as i64 - trace_a.anchor_checks as i64,
10468        }));
10469    }
10470    if trace_a.anchor_breaches != trace_b.anchor_breaches {
10471        field_diffs.push(serde_json::json!({
10472            "field": "anchor_breaches",
10473            "a": trace_a.anchor_breaches,
10474            "b": trace_b.anchor_breaches,
10475            "delta": trace_b.anchor_breaches as i64 - trace_a.anchor_breaches as i64,
10476        }));
10477    }
10478    if trace_a.errors != trace_b.errors {
10479        field_diffs.push(serde_json::json!({
10480            "field": "errors",
10481            "a": trace_a.errors,
10482            "b": trace_b.errors,
10483            "delta": trace_b.errors as i64 - trace_a.errors as i64,
10484        }));
10485    }
10486    if trace_a.retries != trace_b.retries {
10487        field_diffs.push(serde_json::json!({
10488            "field": "retries",
10489            "a": trace_a.retries,
10490            "b": trace_b.retries,
10491            "delta": trace_b.retries as i64 - trace_a.retries as i64,
10492        }));
10493    }
10494    if trace_a.source_file != trace_b.source_file {
10495        field_diffs.push(serde_json::json!({
10496            "field": "source_file", "a": trace_a.source_file, "b": trace_b.source_file,
10497        }));
10498    }
10499    if trace_a.client_key != trace_b.client_key {
10500        field_diffs.push(serde_json::json!({
10501            "field": "client_key", "a": trace_a.client_key, "b": trace_b.client_key,
10502        }));
10503    }
10504
10505    let identical = field_diffs.is_empty();
10506
10507    Ok(Json(serde_json::json!({
10508        "trace_a": params.a,
10509        "trace_b": params.b,
10510        "identical": identical,
10511        "differences": field_diffs.len(),
10512        "diffs": field_diffs,
10513        "summary": {
10514            "a": {
10515                "flow": trace_a.flow_name,
10516                "status": status_a,
10517                "steps": trace_a.steps_executed,
10518                "latency_ms": trace_a.latency_ms,
10519                "errors": trace_a.errors,
10520                "timestamp": trace_a.timestamp,
10521            },
10522            "b": {
10523                "flow": trace_b.flow_name,
10524                "status": status_b,
10525                "steps": trace_b.steps_executed,
10526                "latency_ms": trace_b.latency_ms,
10527                "errors": trace_b.errors,
10528                "timestamp": trace_b.timestamp,
10529            },
10530        },
10531    })))
10532}
10533
10534/// Query parameters for full-text trace search.
10535#[derive(Debug, Deserialize)]
10536pub struct TraceSearchQuery {
10537    /// Search query string (case-insensitive substring match).
10538    pub q: String,
10539    /// Max results to return (default 50).
10540    #[serde(default = "default_search_limit")]
10541    pub limit: usize,
10542}
10543
10544fn default_search_limit() -> usize {
10545    50
10546}
10547
10548/// Query parameters for trace aggregation.
10549#[derive(Debug, Deserialize)]
10550pub struct TraceAggregateQuery {
10551    /// Time window in seconds (0 = all buffered traces).
10552    #[serde(default)]
10553    pub window: u64,
10554}
10555
10556/// GET /v1/traces/aggregate — compute aggregated metrics across traces.
10557async fn traces_aggregate_handler(
10558    State(state): State<SharedState>,
10559    headers: HeaderMap,
10560    Query(params): Query<TraceAggregateQuery>,
10561) -> Result<Json<serde_json::Value>, StatusCode> {
10562    let s = state.lock().unwrap();
10563    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10564
10565    let agg = s.trace_store.aggregate(params.window);
10566    Ok(Json(serde_json::to_value(&agg).unwrap_or_default()))
10567}
10568
10569/// GET /v1/traces/search — full-text search across buffered traces.
10570///
10571/// Matches query against flow_name, source_file, backend, client_key,
10572/// event step_name, event detail, annotation text, and annotation tags.
10573async fn traces_search_handler(
10574    State(state): State<SharedState>,
10575    headers: HeaderMap,
10576    Query(params): Query<TraceSearchQuery>,
10577) -> Result<Json<serde_json::Value>, StatusCode> {
10578    let s = state.lock().unwrap();
10579    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10580
10581    if params.q.is_empty() {
10582        return Ok(Json(serde_json::json!({
10583            "error": "query parameter 'q' must not be empty",
10584        })));
10585    }
10586
10587    let results = s.trace_store.search(&params.q, params.limit);
10588
10589    let hits: Vec<serde_json::Value> = results.iter().map(|e| {
10590        serde_json::json!({
10591            "id": e.id,
10592            "flow_name": e.flow_name,
10593            "status": e.status.as_str(),
10594            "timestamp": e.timestamp,
10595            "latency_ms": e.latency_ms,
10596            "steps_executed": e.steps_executed,
10597            "errors": e.errors,
10598            "source_file": e.source_file,
10599            "backend": e.backend,
10600            "client_key": e.client_key,
10601            "events_count": e.events.len(),
10602            "annotations_count": e.annotations.len(),
10603        })
10604    }).collect();
10605
10606    Ok(Json(serde_json::json!({
10607        "query": params.q,
10608        "hits": hits.len(),
10609        "total_buffered": s.trace_store.len(),
10610        "results": hits,
10611    })))
10612}
10613
10614/// GET /v1/traces/retention — get current retention policy.
10615async fn traces_retention_get_handler(
10616    State(state): State<SharedState>,
10617    headers: HeaderMap,
10618) -> Result<Json<serde_json::Value>, StatusCode> {
10619    let s = state.lock().unwrap();
10620    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10621
10622    let cfg = s.trace_store.config();
10623    Ok(Json(serde_json::json!({
10624        "max_age_secs": cfg.max_age_secs,
10625        "capacity": cfg.capacity,
10626        "enabled": cfg.enabled,
10627    })))
10628}
10629
10630/// Request to update retention policy.
10631#[derive(Debug, Deserialize)]
10632pub struct RetentionUpdateRequest {
10633    /// Maximum age of traces in seconds (0 = no TTL).
10634    pub max_age_secs: u64,
10635}
10636
10637/// PUT /v1/traces/retention — update retention policy and run immediate eviction.
10638async fn traces_retention_put_handler(
10639    State(state): State<SharedState>,
10640    headers: HeaderMap,
10641    Json(payload): Json<RetentionUpdateRequest>,
10642) -> Result<Json<serde_json::Value>, StatusCode> {
10643    let client = client_key_from_headers(&headers);
10644    let mut s = state.lock().unwrap();
10645    check_auth(&mut s, &headers, AccessLevel::Write)?;
10646
10647    let previous = s.trace_store.set_max_age_secs(payload.max_age_secs);
10648    let evicted = s.trace_store.evict_expired();
10649
10650    s.audit_log.record(
10651        &client,
10652        AuditAction::ConfigUpdate,
10653        "trace_retention",
10654        serde_json::json!({
10655            "previous_max_age_secs": previous,
10656            "new_max_age_secs": payload.max_age_secs,
10657            "evicted": evicted,
10658        }),
10659        true,
10660    );
10661
10662    Ok(Json(serde_json::json!({
10663        "success": true,
10664        "previous_max_age_secs": previous,
10665        "new_max_age_secs": payload.max_age_secs,
10666        "evicted": evicted,
10667        "buffered": s.trace_store.len(),
10668    })))
10669}
10670
10671/// POST /v1/traces/evict — manually trigger TTL-based eviction.
10672async fn traces_evict_handler(
10673    State(state): State<SharedState>,
10674    headers: HeaderMap,
10675) -> Result<Json<serde_json::Value>, StatusCode> {
10676    let mut s = state.lock().unwrap();
10677    check_auth(&mut s, &headers, AccessLevel::Write)?;
10678
10679    let evicted = s.trace_store.evict_expired();
10680
10681    Ok(Json(serde_json::json!({
10682        "evicted": evicted,
10683        "buffered": s.trace_store.len(),
10684        "max_age_secs": s.trace_store.config().max_age_secs,
10685    })))
10686}
10687
10688/// Request for bulk trace deletion.
10689#[derive(Debug, Deserialize)]
10690pub struct BulkDeleteRequest {
10691    /// Trace IDs to delete.
10692    pub ids: Vec<u64>,
10693}
10694
10695/// DELETE /v1/traces/bulk — delete multiple traces by ID.
10696async fn traces_bulk_delete_handler(
10697    State(state): State<SharedState>,
10698    headers: HeaderMap,
10699    Json(payload): Json<BulkDeleteRequest>,
10700) -> Result<Json<serde_json::Value>, StatusCode> {
10701    let client = client_key_from_headers(&headers);
10702    let mut s = state.lock().unwrap();
10703    check_auth(&mut s, &headers, AccessLevel::Write)?;
10704
10705    let requested = payload.ids.len();
10706    let deleted = s.trace_store.bulk_delete(&payload.ids);
10707
10708    s.audit_log.record(
10709        &client,
10710        AuditAction::ConfigUpdate,
10711        "traces_bulk_delete",
10712        serde_json::json!({
10713            "requested": requested,
10714            "deleted": deleted,
10715            "ids": payload.ids,
10716        }),
10717        true,
10718    );
10719
10720    Ok(Json(serde_json::json!({
10721        "success": true,
10722        "requested": requested,
10723        "deleted": deleted,
10724        "buffered": s.trace_store.len(),
10725    })))
10726}
10727
10728/// Request for bulk trace annotation.
10729#[derive(Debug, Deserialize)]
10730pub struct BulkAnnotateRequest {
10731    /// Trace IDs to annotate.
10732    pub ids: Vec<u64>,
10733    /// Annotation author.
10734    pub author: String,
10735    /// Annotation text.
10736    pub text: String,
10737    /// Annotation tags.
10738    #[serde(default)]
10739    pub tags: Vec<String>,
10740}
10741
10742/// POST /v1/traces/bulk/annotate — annotate multiple traces at once.
10743async fn traces_bulk_annotate_handler(
10744    State(state): State<SharedState>,
10745    headers: HeaderMap,
10746    Json(payload): Json<BulkAnnotateRequest>,
10747) -> Result<Json<serde_json::Value>, StatusCode> {
10748    let mut s = state.lock().unwrap();
10749    check_auth(&mut s, &headers, AccessLevel::Write)?;
10750
10751    let annotation = crate::trace_store::TraceAnnotation {
10752        author: payload.author.clone(),
10753        text: payload.text.clone(),
10754        tags: payload.tags.clone(),
10755        timestamp: std::time::SystemTime::now()
10756            .duration_since(std::time::UNIX_EPOCH)
10757            .unwrap_or_default()
10758            .as_secs(),
10759    };
10760
10761    let requested = payload.ids.len();
10762    let annotated = s.trace_store.bulk_annotate(&payload.ids, annotation);
10763
10764    Ok(Json(serde_json::json!({
10765        "success": true,
10766        "requested": requested,
10767        "annotated": annotated,
10768        "author": payload.author,
10769        "text": payload.text,
10770        "tags": payload.tags,
10771    })))
10772}
10773
10774/// Query parameters for trace export.
10775#[derive(Debug, Deserialize)]
10776pub struct TraceExportQuery {
10777    /// Export format: "jsonl" (default), "csv", "prometheus".
10778    #[serde(default = "default_export_format")]
10779    pub format: String,
10780    /// Max traces to export (default 100).
10781    #[serde(default = "default_export_limit")]
10782    pub limit: usize,
10783    /// Filter by flow name.
10784    pub flow_name: Option<String>,
10785    /// Filter by status.
10786    pub status: Option<String>,
10787    /// Filter by client key.
10788    pub client_key: Option<String>,
10789}
10790
10791fn default_export_format() -> String { "jsonl".to_string() }
10792fn default_export_limit() -> usize { 100 }
10793
10794/// GET /v1/traces/export — export buffered traces in various formats.
10795async fn traces_export_handler(
10796    State(state): State<SharedState>,
10797    headers: HeaderMap,
10798    Query(params): Query<TraceExportQuery>,
10799) -> Result<(StatusCode, HeaderMap, String), StatusCode> {
10800    let s = state.lock().unwrap();
10801    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10802
10803    let format = crate::trace_store::ExportFormat::from_str(&params.format);
10804
10805    let filter = if params.flow_name.is_some() || params.status.is_some() || params.client_key.is_some() {
10806        Some(TraceFilter {
10807            flow_name: params.flow_name,
10808            status: params.status,
10809            client_key: params.client_key,
10810            min_latency_ms: None,
10811            has_errors: None,
10812            tag: None,
10813        })
10814    } else {
10815        None
10816    };
10817
10818    let entries = s.trace_store.recent(params.limit, filter.as_ref());
10819
10820    let body = match format {
10821        crate::trace_store::ExportFormat::JsonLines => crate::trace_store::export_jsonl(&entries),
10822        crate::trace_store::ExportFormat::Csv => crate::trace_store::export_csv(&entries),
10823        crate::trace_store::ExportFormat::Prometheus => crate::trace_store::export_prometheus(&entries),
10824    };
10825
10826    let mut response_headers = HeaderMap::new();
10827    if let Ok(ct) = format.content_type().parse() {
10828        response_headers.insert("content-type", ct);
10829    }
10830
10831    Ok((StatusCode::OK, response_headers, body))
10832}
10833
10834// ── Flow scheduler endpoints ──────────────────────────────────────────────
10835
10836/// Request to create a new schedule.
10837#[derive(Debug, Deserialize)]
10838pub struct CreateScheduleRequest {
10839    /// Flow name to schedule (must be deployed).
10840    pub flow_name: String,
10841    /// Interval in seconds between executions (min 1).
10842    pub interval_secs: u64,
10843    /// Backend for execution (default: "stub").
10844    #[serde(default = "default_execute_backend")]
10845    pub backend: String,
10846}
10847
10848/// POST /v1/schedules — create a new scheduled flow execution.
10849async fn schedules_create_handler(
10850    State(state): State<SharedState>,
10851    headers: HeaderMap,
10852    Json(payload): Json<CreateScheduleRequest>,
10853) -> Result<Json<serde_json::Value>, StatusCode> {
10854    let client = client_key_from_headers(&headers);
10855    let mut s = state.lock().unwrap();
10856    check_auth(&mut s, &headers, AccessLevel::Write)?;
10857
10858    if payload.interval_secs == 0 {
10859        return Ok(Json(serde_json::json!({
10860            "success": false,
10861            "error": "interval_secs must be >= 1",
10862        })));
10863    }
10864
10865    // Verify flow is deployed
10866    let history = s.versions.get_history(&payload.flow_name);
10867    if history.and_then(|h| h.active()).is_none() {
10868        return Ok(Json(serde_json::json!({
10869            "success": false,
10870            "error": format!("flow '{}' not deployed", payload.flow_name),
10871        })));
10872    }
10873
10874    if s.schedules.contains_key(&payload.flow_name) {
10875        return Ok(Json(serde_json::json!({
10876            "success": false,
10877            "error": format!("schedule for '{}' already exists", payload.flow_name),
10878        })));
10879    }
10880
10881    let now = std::time::SystemTime::now()
10882        .duration_since(std::time::UNIX_EPOCH)
10883        .unwrap_or_default()
10884        .as_secs();
10885
10886    let entry = ScheduleEntry {
10887        flow_name: payload.flow_name.clone(),
10888        interval_secs: payload.interval_secs,
10889        enabled: true,
10890        backend: payload.backend.clone(),
10891        last_run: 0,
10892        next_run: now + payload.interval_secs,
10893        run_count: 0,
10894        error_count: 0,
10895        history: Vec::new(),
10896    };
10897
10898    s.schedules.insert(payload.flow_name.clone(), entry);
10899
10900    s.audit_log.record(
10901        &client,
10902        AuditAction::ConfigUpdate,
10903        &payload.flow_name,
10904        serde_json::json!({
10905            "action": "schedule_create",
10906            "flow": &payload.flow_name,
10907            "interval_secs": payload.interval_secs,
10908            "backend": &payload.backend,
10909        }),
10910        true,
10911    );
10912
10913    Ok(Json(serde_json::json!({
10914        "success": true,
10915        "flow_name": payload.flow_name,
10916        "interval_secs": payload.interval_secs,
10917        "next_run": now + payload.interval_secs,
10918    })))
10919}
10920
10921/// GET /v1/schedules — list all scheduled flow executions.
10922async fn schedules_list_handler(
10923    State(state): State<SharedState>,
10924    headers: HeaderMap,
10925) -> Result<Json<serde_json::Value>, StatusCode> {
10926    let s = state.lock().unwrap();
10927    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10928
10929    let entries: Vec<serde_json::Value> = s.schedules.values()
10930        .map(|e| serde_json::to_value(e).unwrap_or_default())
10931        .collect();
10932
10933    Ok(Json(serde_json::json!({
10934        "schedules": entries,
10935        "total": entries.len(),
10936    })))
10937}
10938
10939/// GET /v1/schedules/{name} — get a specific schedule.
10940async fn schedules_get_handler(
10941    State(state): State<SharedState>,
10942    headers: HeaderMap,
10943    Path(name): Path<String>,
10944) -> Result<Json<serde_json::Value>, StatusCode> {
10945    let s = state.lock().unwrap();
10946    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10947
10948    match s.schedules.get(&name) {
10949        Some(entry) => Ok(Json(serde_json::to_value(entry).unwrap_or_default())),
10950        None => Ok(Json(serde_json::json!({
10951            "error": format!("schedule '{}' not found", name),
10952        }))),
10953    }
10954}
10955
10956/// DELETE /v1/schedules/{name} — remove a schedule.
10957async fn schedules_delete_handler(
10958    State(state): State<SharedState>,
10959    headers: HeaderMap,
10960    Path(name): Path<String>,
10961) -> Result<Json<serde_json::Value>, StatusCode> {
10962    let client = client_key_from_headers(&headers);
10963    let mut s = state.lock().unwrap();
10964    check_auth(&mut s, &headers, AccessLevel::Write)?;
10965
10966    match s.schedules.remove(&name) {
10967        Some(_) => {
10968            s.audit_log.record(
10969                &client,
10970                AuditAction::ConfigUpdate,
10971                &name,
10972                serde_json::json!({ "action": "schedule_delete", "flow": &name }),
10973                true,
10974            );
10975            Ok(Json(serde_json::json!({ "success": true, "deleted": name })))
10976        }
10977        None => Ok(Json(serde_json::json!({
10978            "success": false,
10979            "error": format!("schedule '{}' not found", name),
10980        }))),
10981    }
10982}
10983
10984/// POST /v1/schedules/{name}/toggle — enable or disable a schedule.
10985async fn schedules_toggle_handler(
10986    State(state): State<SharedState>,
10987    headers: HeaderMap,
10988    Path(name): Path<String>,
10989) -> Result<Json<serde_json::Value>, StatusCode> {
10990    let client = client_key_from_headers(&headers);
10991    let mut s = state.lock().unwrap();
10992    check_auth(&mut s, &headers, AccessLevel::Write)?;
10993
10994    match s.schedules.get_mut(&name) {
10995        Some(entry) => {
10996            entry.enabled = !entry.enabled;
10997            let new_state = entry.enabled;
10998            s.audit_log.record(
10999                &client,
11000                AuditAction::ConfigUpdate,
11001                &name,
11002                serde_json::json!({
11003                    "action": "schedule_toggle",
11004                    "flow": &name,
11005                    "enabled": new_state,
11006                }),
11007                true,
11008            );
11009            Ok(Json(serde_json::json!({
11010                "success": true,
11011                "flow_name": name,
11012                "enabled": new_state,
11013            })))
11014        }
11015        None => Ok(Json(serde_json::json!({
11016            "success": false,
11017            "error": format!("schedule '{}' not found", name),
11018        }))),
11019    }
11020}
11021
11022/// GET /v1/schedules/:name/history — execution history for a schedule.
11023async fn schedules_history_handler(
11024    State(state): State<SharedState>,
11025    headers: HeaderMap,
11026    Path(name): Path<String>,
11027    Query(params): Query<std::collections::HashMap<String, String>>,
11028) -> Result<Json<serde_json::Value>, StatusCode> {
11029    let s = state.lock().unwrap();
11030    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
11031
11032    match s.schedules.get(&name) {
11033        Some(entry) => {
11034            let limit: usize = params.get("limit")
11035                .and_then(|v| v.parse().ok())
11036                .unwrap_or(50);
11037
11038            let history: Vec<&ScheduleRun> = entry.history.iter().rev().take(limit).collect();
11039            let success_count = entry.history.iter().filter(|r| r.success).count();
11040            let error_count = entry.history.iter().filter(|r| !r.success).count();
11041            let avg_latency = if entry.history.is_empty() {
11042                0
11043            } else {
11044                entry.history.iter().map(|r| r.latency_ms).sum::<u64>() / entry.history.len() as u64
11045            };
11046
11047            Ok(Json(serde_json::json!({
11048                "schedule": name,
11049                "flow_name": entry.flow_name,
11050                "total_runs": entry.history.len(),
11051                "success_count": success_count,
11052                "error_count": error_count,
11053                "avg_latency_ms": avg_latency,
11054                "history": history,
11055            })))
11056        }
11057        None => Ok(Json(serde_json::json!({
11058            "error": format!("schedule '{}' not found", name),
11059        }))),
11060    }
11061}
11062
11063/// POST /v1/schedules/tick — check all due schedules and execute them.
11064///
11065/// Poll-based scheduler tick: iterates all enabled schedules where
11066/// `now >= next_run`, executes each flow via `server_execute`, records
11067/// traces, and advances `next_run`.
11068async fn schedules_tick_handler(
11069    State(state): State<SharedState>,
11070    headers: HeaderMap,
11071) -> Result<Json<serde_json::Value>, StatusCode> {
11072    let req_start = Instant::now();
11073    let client = client_key_from_headers(&headers);
11074    {
11075        let mut s = state.lock().unwrap();
11076        check_auth(&mut s, &headers, AccessLevel::Write)?;
11077    }
11078
11079    let now = std::time::SystemTime::now()
11080        .duration_since(std::time::UNIX_EPOCH)
11081        .unwrap_or_default()
11082        .as_secs();
11083
11084    // Collect due schedules (flow_name, backend, source, source_file)
11085    let due: Vec<(String, String, String, String)> = {
11086        let s = state.lock().unwrap();
11087        s.schedules.iter()
11088            .filter(|(_, e)| e.enabled && now >= e.next_run)
11089            .filter_map(|(name, e)| {
11090                let history = s.versions.get_history(name);
11091                history.and_then(|h| h.active()).map(|active| {
11092                    (name.clone(), e.backend.clone(), active.source.clone(), active.source_file.clone())
11093                })
11094            })
11095            .collect()
11096    };
11097
11098    let mut results = Vec::new();
11099
11100    for (flow_name, backend, source, source_file) in &due {
11101        let (exec_result, _) = server_execute_full(&state, source, source_file, flow_name, backend);
11102
11103        match exec_result {
11104            Ok(mut er) => {
11105                let trace_entry = crate::trace_store::build_trace(
11106                    &er.flow_name,
11107                    &er.source_file,
11108                    &er.backend,
11109                    &client,
11110                    if er.success {
11111                        crate::trace_store::TraceStatus::Success
11112                    } else {
11113                        crate::trace_store::TraceStatus::Partial
11114                    },
11115                    er.steps_executed,
11116                    er.latency_ms,
11117                );
11118
11119                let mut s = state.lock().unwrap();
11120                let mut entry = trace_entry;
11121                entry.tokens_input = er.tokens_input;
11122                entry.tokens_output = er.tokens_output;
11123                entry.anchor_checks = er.anchor_checks;
11124                entry.anchor_breaches = er.anchor_breaches;
11125                entry.errors = er.errors;
11126                let trace_id = s.trace_store.record(entry);
11127                er.trace_id = trace_id;
11128
11129                // Update schedule state
11130                if let Some(sched) = s.schedules.get_mut(flow_name) {
11131                    sched.last_run = now;
11132                    sched.next_run = now + sched.interval_secs;
11133                    sched.run_count += 1;
11134                    if !er.success {
11135                        sched.error_count += 1;
11136                    }
11137                    sched.history.push(ScheduleRun {
11138                        timestamp: now,
11139                        success: er.success,
11140                        trace_id,
11141                        latency_ms: er.latency_ms,
11142                        error: None,
11143                    });
11144                    if sched.history.len() > 50 {
11145                        sched.history.remove(0);
11146                    }
11147                }
11148
11149                results.push(serde_json::json!({
11150                    "flow": flow_name,
11151                    "success": er.success,
11152                    "trace_id": trace_id,
11153                    "steps": er.steps_executed,
11154                    "latency_ms": er.latency_ms,
11155                }));
11156            }
11157            Err(e) => {
11158                let mut fail_entry = crate::trace_store::build_trace(
11159                    flow_name,
11160                    source_file,
11161                    backend,
11162                    &client,
11163                    crate::trace_store::TraceStatus::Failed,
11164                    0,
11165                    req_start.elapsed().as_millis() as u64,
11166                );
11167                fail_entry.errors = 1;
11168
11169                let mut s = state.lock().unwrap();
11170                let trace_id = s.trace_store.record(fail_entry);
11171                s.metrics.total_errors += 1;
11172
11173                let err_latency = req_start.elapsed().as_millis() as u64;
11174                if let Some(sched) = s.schedules.get_mut(flow_name) {
11175                    sched.last_run = now;
11176                    sched.next_run = now + sched.interval_secs;
11177                    sched.run_count += 1;
11178                    sched.error_count += 1;
11179                    sched.history.push(ScheduleRun {
11180                        timestamp: now,
11181                        success: false,
11182                        trace_id,
11183                        latency_ms: err_latency,
11184                        error: Some(e.clone()),
11185                    });
11186                    if sched.history.len() > 50 {
11187                        sched.history.remove(0);
11188                    }
11189                }
11190
11191                results.push(serde_json::json!({
11192                    "flow": flow_name,
11193                    "success": false,
11194                    "trace_id": trace_id,
11195                    "error": e,
11196                }));
11197            }
11198        }
11199    }
11200
11201    // Emit event
11202    {
11203        let mut s = state.lock().unwrap();
11204        s.event_bus.publish(
11205            "schedule.tick",
11206            serde_json::json!({
11207                "executed": results.len(),
11208                "timestamp": now,
11209            }),
11210            "server",
11211        );
11212        s.request_logger.record("POST", "/v1/schedules/tick", 200, req_start.elapsed(), &client);
11213    }
11214
11215    Ok(Json(serde_json::json!({
11216        "executed": results.len(),
11217        "results": results,
11218        "timestamp": now,
11219    })))
11220}
11221
11222// ── Trace replay endpoint ──────────────────────────────────────────────────
11223
11224/// Replay request — optional overrides for the original execution parameters.
11225#[derive(Debug, Deserialize)]
11226pub struct ReplayRequest {
11227    /// Override the backend (default: reuse original trace's backend).
11228    pub backend: Option<String>,
11229}
11230
11231/// Comparison of original vs replay trace fields.
11232#[derive(Debug, Serialize)]
11233struct ReplayDiff {
11234    status_changed: bool,
11235    original_status: String,
11236    replay_status: String,
11237    latency_delta_ms: i64,
11238    steps_delta: i64,
11239    errors_delta: i64,
11240}
11241
11242/// POST /v1/traces/{id}/replay — re-execute the flow that produced a trace.
11243///
11244/// Looks up the original trace by ID, finds the deployed source for the same
11245/// flow, re-executes it, records a new trace linked via `replay_of`, and
11246/// returns a comparison of original vs replay results.
11247async fn traces_replay_handler(
11248    State(state): State<SharedState>,
11249    headers: HeaderMap,
11250    Path(id): Path<u64>,
11251    body: Option<Json<ReplayRequest>>,
11252) -> Result<Json<serde_json::Value>, StatusCode> {
11253    let req_start = Instant::now();
11254    let client = client_key_from_headers(&headers);
11255    {
11256        let mut s = state.lock().unwrap();
11257        check_auth(&mut s, &headers, AccessLevel::Write)?;
11258        check_rate_limit(&mut s, &headers)?;
11259    }
11260
11261    // Look up the original trace
11262    let (flow_name, source_file, original_backend, original_status,
11263         original_steps, original_latency, original_errors) = {
11264        let s = state.lock().unwrap();
11265        match s.trace_store.get(id) {
11266            Some(entry) => (
11267                entry.flow_name.clone(),
11268                entry.source_file.clone(),
11269                entry.backend.clone(),
11270                entry.status.as_str().to_string(),
11271                entry.steps_executed,
11272                entry.latency_ms,
11273                entry.errors,
11274            ),
11275            None => {
11276                return Ok(Json(serde_json::json!({
11277                    "success": false,
11278                    "error": format!("trace {} not found", id),
11279                })));
11280            }
11281        }
11282    };
11283
11284    // Determine backend (override or original)
11285    let backend = body
11286        .as_ref()
11287        .and_then(|b| b.backend.clone())
11288        .unwrap_or(original_backend);
11289
11290    // Look up deployed source for the flow
11291    let source = {
11292        let s = state.lock().unwrap();
11293        let history = s.versions.get_history(&flow_name);
11294        match history.and_then(|h| h.active()) {
11295            Some(active) => active.source.clone(),
11296            None => {
11297                return Ok(Json(serde_json::json!({
11298                    "success": false,
11299                    "error": format!("flow '{}' no longer deployed — cannot replay", flow_name),
11300                })));
11301            }
11302        }
11303    };
11304
11305    // Execute (outside lock — full backend stack)
11306    let (result, _) = server_execute_full(&state, &source, &source_file, &flow_name, &backend);
11307
11308    match result {
11309        Ok(mut exec_result) => {
11310            // Build replay trace with link to original
11311            let mut trace_entry = crate::trace_store::build_trace(
11312                &exec_result.flow_name,
11313                &exec_result.source_file,
11314                &exec_result.backend,
11315                &client,
11316                if exec_result.success {
11317                    crate::trace_store::TraceStatus::Success
11318                } else {
11319                    crate::trace_store::TraceStatus::Partial
11320                },
11321                exec_result.steps_executed,
11322                exec_result.latency_ms,
11323            );
11324            trace_entry.tokens_input = exec_result.tokens_input;
11325            trace_entry.tokens_output = exec_result.tokens_output;
11326            trace_entry.anchor_checks = exec_result.anchor_checks;
11327            trace_entry.anchor_breaches = exec_result.anchor_breaches;
11328            trace_entry.errors = exec_result.errors;
11329            trace_entry.replay_of = Some(id);
11330
11331            let trace_id = {
11332                let mut s = state.lock().unwrap();
11333                let tid = s.trace_store.record(trace_entry);
11334
11335                // Audit trail
11336                s.audit_log.record(
11337                    &client,
11338                    AuditAction::Execute,
11339                    &exec_result.flow_name,
11340                    serde_json::json!({
11341                        "action": "replay",
11342                        "original_trace": id,
11343                        "replay_trace": tid,
11344                        "flow": &exec_result.flow_name,
11345                        "backend": &exec_result.backend,
11346                        "success": exec_result.success,
11347                    }),
11348                    exec_result.success,
11349                );
11350
11351                s.request_logger.record("POST", &format!("/v1/traces/{}/replay", id), 200, req_start.elapsed(), &client);
11352                tid
11353            };
11354
11355            exec_result.trace_id = trace_id;
11356
11357            // Emit event
11358            {
11359                let s = state.lock().unwrap();
11360                s.event_bus.publish(
11361                    "trace.replay",
11362                    serde_json::json!({
11363                        "original_trace": id,
11364                        "replay_trace": trace_id,
11365                        "flow": &exec_result.flow_name,
11366                        "success": exec_result.success,
11367                    }),
11368                    "server",
11369                );
11370            }
11371
11372            // Build diff
11373            let replay_status = if exec_result.success { "success" } else { "partial" };
11374            let diff = ReplayDiff {
11375                status_changed: original_status != replay_status,
11376                original_status: original_status.clone(),
11377                replay_status: replay_status.to_string(),
11378                latency_delta_ms: exec_result.latency_ms as i64 - original_latency as i64,
11379                steps_delta: exec_result.steps_executed as i64 - original_steps as i64,
11380                errors_delta: exec_result.errors as i64 - original_errors as i64,
11381            };
11382
11383            Ok(Json(serde_json::json!({
11384                "success": true,
11385                "original_trace_id": id,
11386                "replay_trace_id": trace_id,
11387                "flow": exec_result.flow_name,
11388                "backend": exec_result.backend,
11389                "steps_executed": exec_result.steps_executed,
11390                "latency_ms": exec_result.latency_ms,
11391                "errors": exec_result.errors,
11392                "step_names": exec_result.step_names,
11393                "diff": serde_json::to_value(&diff).unwrap_or_default(),
11394            })))
11395        }
11396        Err(e) => {
11397            // Record failed replay trace
11398            let mut entry = crate::trace_store::build_trace(
11399                &flow_name,
11400                &source_file,
11401                &backend,
11402                &client,
11403                crate::trace_store::TraceStatus::Failed,
11404                0,
11405                req_start.elapsed().as_millis() as u64,
11406            );
11407            entry.errors = 1;
11408            entry.replay_of = Some(id);
11409
11410            let trace_id = {
11411                let mut s = state.lock().unwrap();
11412                let tid = s.trace_store.record(entry);
11413                s.metrics.total_errors += 1;
11414                s.request_logger.record("POST", &format!("/v1/traces/{}/replay", id), 500, req_start.elapsed(), &client);
11415                tid
11416            };
11417
11418            Ok(Json(serde_json::json!({
11419                "success": false,
11420                "original_trace_id": id,
11421                "replay_trace_id": trace_id,
11422                "error": e,
11423                "diff": {
11424                    "status_changed": original_status != "failed",
11425                    "original_status": original_status,
11426                    "replay_status": "failed",
11427                },
11428            })))
11429        }
11430    }
11431}
11432
11433/// A flamegraph span node.
11434#[derive(Debug, Clone, Serialize)]
11435struct FlamegraphSpan {
11436    name: String,
11437    event_type: String,
11438    start_ms: u64,
11439    end_ms: u64,
11440    duration_ms: u64,
11441    detail: String,
11442    children: Vec<FlamegraphSpan>,
11443}
11444
11445/// GET /v1/traces/:id/flamegraph — generate flamegraph-style JSON from trace events.
11446async fn traces_flamegraph_handler(
11447    State(state): State<SharedState>,
11448    headers: HeaderMap,
11449    Path(id): Path<u64>,
11450) -> Result<Json<serde_json::Value>, StatusCode> {
11451    let s = state.lock().unwrap();
11452    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
11453
11454    let entry = match s.trace_store.get(id) {
11455        Some(e) => e,
11456        None => {
11457            return Ok(Json(serde_json::json!({
11458                "error": format!("trace {} not found", id),
11459            })));
11460        }
11461    };
11462
11463    // Build flamegraph from events
11464    let mut root_spans: Vec<FlamegraphSpan> = Vec::new();
11465    let mut stack: Vec<FlamegraphSpan> = Vec::new();
11466
11467    for ev in &entry.events {
11468        match ev.event_type.as_str() {
11469            "step_start" => {
11470                stack.push(FlamegraphSpan {
11471                    name: ev.step_name.clone(),
11472                    event_type: "step".into(),
11473                    start_ms: ev.offset_ms,
11474                    end_ms: ev.offset_ms, // will be updated on step_end
11475                    duration_ms: 0,
11476                    detail: ev.detail.clone(),
11477                    children: Vec::new(),
11478                });
11479            }
11480            "step_end" => {
11481                if let Some(mut span) = stack.pop() {
11482                    span.end_ms = ev.offset_ms;
11483                    span.duration_ms = ev.offset_ms.saturating_sub(span.start_ms);
11484                    if let Some(parent) = stack.last_mut() {
11485                        parent.children.push(span);
11486                    } else {
11487                        root_spans.push(span);
11488                    }
11489                }
11490            }
11491            _ => {
11492                // model_call, anchor_check, error, etc. → leaf span
11493                let leaf = FlamegraphSpan {
11494                    name: if ev.step_name.is_empty() { ev.event_type.clone() } else { ev.step_name.clone() },
11495                    event_type: ev.event_type.clone(),
11496                    start_ms: ev.offset_ms,
11497                    end_ms: ev.offset_ms,
11498                    duration_ms: 0,
11499                    detail: ev.detail.clone(),
11500                    children: Vec::new(),
11501                };
11502                if let Some(parent) = stack.last_mut() {
11503                    parent.children.push(leaf);
11504                } else {
11505                    root_spans.push(leaf);
11506                }
11507            }
11508        }
11509    }
11510
11511    // Flush any unclosed spans
11512    while let Some(mut span) = stack.pop() {
11513        span.end_ms = entry.latency_ms;
11514        span.duration_ms = entry.latency_ms.saturating_sub(span.start_ms);
11515        if let Some(parent) = stack.last_mut() {
11516            parent.children.push(span);
11517        } else {
11518            root_spans.push(span);
11519        }
11520    }
11521
11522    Ok(Json(serde_json::json!({
11523        "trace_id": id,
11524        "flow_name": entry.flow_name,
11525        "total_latency_ms": entry.latency_ms,
11526        "events_count": entry.events.len(),
11527        "spans": root_spans,
11528    })))
11529}
11530
11531/// Request body for trace comparison.
11532#[derive(Debug, Deserialize)]
11533pub struct TraceCompareRequest {
11534    /// Trace IDs to compare (2–20).
11535    pub ids: Vec<u64>,
11536}
11537
11538/// POST /v1/traces/compare — compare N traces across key metrics.
11539async fn traces_compare_handler(
11540    State(state): State<SharedState>,
11541    headers: HeaderMap,
11542    Json(payload): Json<TraceCompareRequest>,
11543) -> Result<Json<serde_json::Value>, StatusCode> {
11544    let s = state.lock().unwrap();
11545    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
11546
11547    if payload.ids.len() < 2 {
11548        return Ok(Json(serde_json::json!({
11549            "error": "at least 2 trace IDs required for comparison",
11550        })));
11551    }
11552    if payload.ids.len() > 20 {
11553        return Ok(Json(serde_json::json!({
11554            "error": "maximum 20 traces per comparison",
11555        })));
11556    }
11557
11558    let mut rows = Vec::new();
11559    let mut not_found = Vec::new();
11560    let mut latencies = Vec::new();
11561    let mut total_tokens_sum: u64 = 0;
11562    let mut total_errors: usize = 0;
11563    let mut flow_set = std::collections::HashSet::new();
11564    let mut backend_set = std::collections::HashSet::new();
11565
11566    for &id in &payload.ids {
11567        match s.trace_store.get(id) {
11568            Some(e) => {
11569                let tokens = e.tokens_input + e.tokens_output;
11570                latencies.push(e.latency_ms);
11571                total_tokens_sum += tokens;
11572                total_errors += e.errors;
11573                flow_set.insert(e.flow_name.clone());
11574                backend_set.insert(e.backend.clone());
11575
11576                rows.push(serde_json::json!({
11577                    "id": e.id,
11578                    "flow_name": e.flow_name,
11579                    "status": e.status.as_str(),
11580                    "latency_ms": e.latency_ms,
11581                    "steps_executed": e.steps_executed,
11582                    "tokens_input": e.tokens_input,
11583                    "tokens_output": e.tokens_output,
11584                    "tokens_total": tokens,
11585                    "errors": e.errors,
11586                    "retries": e.retries,
11587                    "anchor_checks": e.anchor_checks,
11588                    "anchor_breaches": e.anchor_breaches,
11589                    "backend": e.backend,
11590                    "timestamp": e.timestamp,
11591                }));
11592            }
11593            None => {
11594                not_found.push(id);
11595            }
11596        }
11597    }
11598
11599    let count = rows.len() as u64;
11600    let (avg_latency, min_latency, max_latency, latency_spread) = if !latencies.is_empty() {
11601        latencies.sort();
11602        let sum: u64 = latencies.iter().sum();
11603        let avg = sum / latencies.len() as u64;
11604        let min = latencies[0];
11605        let max = latencies[latencies.len() - 1];
11606        (avg, min, max, max - min)
11607    } else {
11608        (0, 0, 0, 0)
11609    };
11610
11611    Ok(Json(serde_json::json!({
11612        "compared": count,
11613        "not_found": not_found,
11614        "rows": rows,
11615        "summary": {
11616            "avg_latency_ms": avg_latency,
11617            "min_latency_ms": min_latency,
11618            "max_latency_ms": max_latency,
11619            "latency_spread_ms": latency_spread,
11620            "total_errors": total_errors,
11621            "avg_tokens": if count > 0 { total_tokens_sum / count } else { 0 },
11622            "unique_flows": flow_set.len(),
11623            "unique_backends": backend_set.len(),
11624            "flows": flow_set.into_iter().collect::<Vec<_>>(),
11625            "backends": backend_set.into_iter().collect::<Vec<_>>(),
11626        },
11627    })))
11628}
11629
11630/// Request body for trace timeline.
11631#[derive(Debug, Deserialize)]
11632pub struct TraceTimelineRequest {
11633    /// Trace IDs to include in the timeline.
11634    pub ids: Vec<u64>,
11635    /// Optional: only include events after this offset_ms (relative to earliest trace).
11636    #[serde(default)]
11637    pub from_ms: u64,
11638    /// Optional: only include events before this offset_ms (0 = no limit).
11639    #[serde(default)]
11640    pub to_ms: u64,
11641}
11642
11643/// A single event in the merged timeline.
11644#[derive(Debug, Clone, Serialize)]
11645struct TimelineEvent {
11646    /// Absolute timestamp (trace timestamp_secs * 1000 + event offset_ms).
11647    abs_ms: u64,
11648    /// Trace ID this event belongs to.
11649    trace_id: u64,
11650    /// Flow name of the parent trace.
11651    flow_name: String,
11652    /// Event type.
11653    event_type: String,
11654    /// Step name.
11655    step_name: String,
11656    /// Event detail.
11657    detail: String,
11658    /// Original offset within the trace.
11659    offset_ms: u64,
11660}
11661
11662/// POST /v1/traces/timeline — merged chronological timeline across traces.
11663async fn traces_timeline_handler(
11664    State(state): State<SharedState>,
11665    headers: HeaderMap,
11666    Json(payload): Json<TraceTimelineRequest>,
11667) -> Result<Json<serde_json::Value>, StatusCode> {
11668    let s = state.lock().unwrap();
11669    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
11670
11671    if payload.ids.is_empty() {
11672        return Ok(Json(serde_json::json!({
11673            "error": "at least 1 trace ID required",
11674        })));
11675    }
11676
11677    let mut timeline: Vec<TimelineEvent> = Vec::new();
11678    let mut not_found: Vec<u64> = Vec::new();
11679    let mut traces_included: Vec<serde_json::Value> = Vec::new();
11680
11681    for &id in &payload.ids {
11682        match s.trace_store.get(id) {
11683            Some(entry) => {
11684                let base_ms = entry.timestamp * 1000;
11685                traces_included.push(serde_json::json!({
11686                    "id": entry.id,
11687                    "flow_name": entry.flow_name,
11688                    "timestamp": entry.timestamp,
11689                    "events_count": entry.events.len(),
11690                }));
11691
11692                for ev in &entry.events {
11693                    let abs = base_ms + ev.offset_ms;
11694                    timeline.push(TimelineEvent {
11695                        abs_ms: abs,
11696                        trace_id: entry.id,
11697                        flow_name: entry.flow_name.clone(),
11698                        event_type: ev.event_type.clone(),
11699                        step_name: ev.step_name.clone(),
11700                        detail: ev.detail.clone(),
11701                        offset_ms: ev.offset_ms,
11702                    });
11703                }
11704            }
11705            None => not_found.push(id),
11706        }
11707    }
11708
11709    // Sort by absolute timestamp
11710    timeline.sort_by_key(|e| e.abs_ms);
11711
11712    // Apply time range filter if specified
11713    let earliest = timeline.first().map(|e| e.abs_ms).unwrap_or(0);
11714    let filtered: Vec<&TimelineEvent> = timeline.iter().filter(|e| {
11715        let relative = e.abs_ms.saturating_sub(earliest);
11716        let after_from = relative >= payload.from_ms;
11717        let before_to = payload.to_ms == 0 || relative <= payload.to_ms;
11718        after_from && before_to
11719    }).collect();
11720
11721    Ok(Json(serde_json::json!({
11722        "traces_included": traces_included,
11723        "not_found": not_found,
11724        "total_events": filtered.len(),
11725        "time_range": {
11726            "earliest_abs_ms": timeline.first().map(|e| e.abs_ms).unwrap_or(0),
11727            "latest_abs_ms": timeline.last().map(|e| e.abs_ms).unwrap_or(0),
11728            "span_ms": timeline.last().map(|e| e.abs_ms).unwrap_or(0).saturating_sub(
11729                timeline.first().map(|e| e.abs_ms).unwrap_or(0)
11730            ),
11731        },
11732        "timeline": filtered,
11733    })))
11734}
11735
11736/// Query parameters for trace heatmap.
11737#[derive(Debug, Deserialize)]
11738pub struct TraceHeatmapQuery {
11739    /// Bucket size in seconds (default 60).
11740    #[serde(default = "default_heatmap_bucket")]
11741    pub bucket_secs: u64,
11742    /// Time window in seconds (0 = all buffered).
11743    #[serde(default)]
11744    pub window: u64,
11745}
11746
11747fn default_heatmap_bucket() -> u64 { 60 }
11748
11749/// A single time bucket in the heatmap.
11750#[derive(Debug, Clone, Serialize)]
11751struct HeatmapBucket {
11752    /// Bucket start timestamp (Unix seconds).
11753    bucket_start: u64,
11754    /// Bucket end timestamp.
11755    bucket_end: u64,
11756    /// Number of traces in this bucket.
11757    count: u64,
11758    /// Average latency in ms.
11759    avg_latency_ms: u64,
11760    /// P50 latency in ms.
11761    p50_latency_ms: u64,
11762    /// Max latency in ms.
11763    max_latency_ms: u64,
11764    /// Traces with errors.
11765    error_count: u64,
11766    /// Error rate (0.0–1.0).
11767    error_rate: f64,
11768    /// Total tokens consumed.
11769    total_tokens: u64,
11770}
11771
11772/// GET /v1/traces/heatmap — latency/error heatmap across time buckets.
11773async fn traces_heatmap_handler(
11774    State(state): State<SharedState>,
11775    headers: HeaderMap,
11776    Query(params): Query<TraceHeatmapQuery>,
11777) -> Result<Json<serde_json::Value>, StatusCode> {
11778    let s = state.lock().unwrap();
11779    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
11780
11781    let bucket_secs = if params.bucket_secs == 0 { 60 } else { params.bucket_secs };
11782
11783    let now = std::time::SystemTime::now()
11784        .duration_since(std::time::UNIX_EPOCH)
11785        .unwrap_or_default()
11786        .as_secs();
11787    let cutoff = if params.window > 0 { now.saturating_sub(params.window) } else { 0 };
11788
11789    // Collect traces in window
11790    let entries: Vec<_> = s.trace_store.recent(s.trace_store.len(), None)
11791        .into_iter()
11792        .filter(|e| e.timestamp >= cutoff)
11793        .collect();
11794
11795    if entries.is_empty() {
11796        return Ok(Json(serde_json::json!({
11797            "bucket_secs": bucket_secs,
11798            "window": params.window,
11799            "total_traces": 0,
11800            "buckets": [],
11801        })));
11802    }
11803
11804    // Group by bucket
11805    let mut bucket_map: std::collections::BTreeMap<u64, Vec<&crate::trace_store::TraceEntry>> =
11806        std::collections::BTreeMap::new();
11807
11808    for e in &entries {
11809        let bucket_start = (e.timestamp / bucket_secs) * bucket_secs;
11810        bucket_map.entry(bucket_start).or_default().push(e);
11811    }
11812
11813    let buckets: Vec<HeatmapBucket> = bucket_map.into_iter().map(|(start, traces)| {
11814        let count = traces.len() as u64;
11815        let mut latencies: Vec<u64> = traces.iter().map(|t| t.latency_ms).collect();
11816        latencies.sort();
11817        let total_lat: u64 = latencies.iter().sum();
11818        let errors = traces.iter().filter(|t| t.errors > 0).count() as u64;
11819        let tokens: u64 = traces.iter().map(|t| t.tokens_input + t.tokens_output).sum();
11820
11821        let p50_idx = ((50 * latencies.len() + 99) / 100).min(latencies.len()) - 1;
11822
11823        HeatmapBucket {
11824            bucket_start: start,
11825            bucket_end: start + bucket_secs,
11826            count,
11827            avg_latency_ms: total_lat / count,
11828            p50_latency_ms: latencies[p50_idx.min(latencies.len() - 1)],
11829            max_latency_ms: *latencies.last().unwrap(),
11830            error_count: errors,
11831            error_rate: errors as f64 / count as f64,
11832            total_tokens: tokens,
11833        }
11834    }).collect();
11835
11836    Ok(Json(serde_json::json!({
11837        "bucket_secs": bucket_secs,
11838        "window": params.window,
11839        "total_traces": entries.len(),
11840        "total_buckets": buckets.len(),
11841        "buckets": buckets,
11842    })))
11843}
11844
11845/// A dependency edge between two daemons.
11846#[derive(Debug, Clone, Serialize)]
11847struct DependencyEdge {
11848    from: String,
11849    to: String,
11850    topic: String,
11851}
11852
11853/// A node in the dependency graph.
11854#[derive(Debug, Clone, Serialize)]
11855struct DependencyNode {
11856    name: String,
11857    state: DaemonState,
11858    trigger_topic: Option<String>,
11859    output_topic: Option<String>,
11860    upstream: Vec<String>,
11861    downstream: Vec<String>,
11862    depth: u32,
11863}
11864
11865/// GET /v1/daemons/dependencies — infer daemon-to-daemon dependencies from chain topology.
11866async fn daemons_dependencies_handler(
11867    State(state): State<SharedState>,
11868    headers: HeaderMap,
11869) -> Result<Json<serde_json::Value>, StatusCode> {
11870    let s = state.lock().unwrap();
11871    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
11872
11873    let daemons: Vec<&DaemonInfo> = s.daemons.values().collect();
11874
11875    // Build edges: daemon A's output_topic matches daemon B's trigger_topic
11876    let mut edges: Vec<DependencyEdge> = Vec::new();
11877    let mut upstream_map: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new();
11878    let mut downstream_map: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new();
11879
11880    for a in &daemons {
11881        if let Some(ref out_topic) = a.output_topic {
11882            for b in &daemons {
11883                if a.name == b.name { continue; }
11884                if let Some(ref trig) = b.trigger_topic {
11885                    // Exact match or wildcard
11886                    let matches = trig == out_topic
11887                        || trig == "*"
11888                        || (trig.ends_with(".*") && out_topic.starts_with(&trig[..trig.len()-2]));
11889                    if matches {
11890                        edges.push(DependencyEdge {
11891                            from: a.name.clone(),
11892                            to: b.name.clone(),
11893                            topic: out_topic.clone(),
11894                        });
11895                        downstream_map.entry(a.name.clone()).or_default().push(b.name.clone());
11896                        upstream_map.entry(b.name.clone()).or_default().push(a.name.clone());
11897                    }
11898                }
11899            }
11900        }
11901    }
11902
11903    // Compute depth via BFS from roots
11904    let roots: Vec<String> = daemons.iter()
11905        .filter(|d| !upstream_map.contains_key(&d.name))
11906        .map(|d| d.name.clone())
11907        .collect();
11908
11909    let mut depth_map: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
11910    let mut queue: std::collections::VecDeque<(String, u32)> = std::collections::VecDeque::new();
11911    for r in &roots {
11912        queue.push_back((r.clone(), 0));
11913        depth_map.insert(r.clone(), 0);
11914    }
11915    while let Some((name, depth)) = queue.pop_front() {
11916        if let Some(children) = downstream_map.get(&name) {
11917            for child in children {
11918                if !depth_map.contains_key(child) || depth_map[child] < depth + 1 {
11919                    depth_map.insert(child.clone(), depth + 1);
11920                    queue.push_back((child.clone(), depth + 1));
11921                }
11922            }
11923        }
11924    }
11925
11926    let leaves: Vec<String> = daemons.iter()
11927        .filter(|d| !downstream_map.contains_key(&d.name))
11928        .map(|d| d.name.clone())
11929        .collect();
11930
11931    // Build nodes
11932    let mut nodes: Vec<DependencyNode> = daemons.iter().map(|d| {
11933        DependencyNode {
11934            name: d.name.clone(),
11935            state: d.state,
11936            trigger_topic: d.trigger_topic.clone(),
11937            output_topic: d.output_topic.clone(),
11938            upstream: upstream_map.get(&d.name).cloned().unwrap_or_default(),
11939            downstream: downstream_map.get(&d.name).cloned().unwrap_or_default(),
11940            depth: depth_map.get(&d.name).copied().unwrap_or(0),
11941        }
11942    }).collect();
11943    nodes.sort_by_key(|n| (n.depth, n.name.clone()));
11944
11945    let max_depth = depth_map.values().copied().max().unwrap_or(0);
11946
11947    Ok(Json(serde_json::json!({
11948        "total_daemons": daemons.len(),
11949        "total_edges": edges.len(),
11950        "max_depth": max_depth,
11951        "roots": roots,
11952        "leaves": leaves,
11953        "nodes": nodes,
11954        "edges": edges,
11955    })))
11956}
11957
11958/// Request to enqueue a flow execution.
11959#[derive(Debug, Deserialize)]
11960pub struct EnqueueRequest {
11961    /// Flow name to execute.
11962    pub flow_name: String,
11963    /// Backend override (default "stub").
11964    #[serde(default = "default_execute_backend")]
11965    pub backend: String,
11966    /// Priority (1=highest, 10=lowest, default 5).
11967    #[serde(default = "default_priority")]
11968    pub priority: u32,
11969}
11970
11971fn default_priority() -> u32 { 5 }
11972
11973/// POST /v1/execute/enqueue — add a flow execution to the priority queue.
11974async fn execute_enqueue_handler(
11975    State(state): State<SharedState>,
11976    headers: HeaderMap,
11977    Json(payload): Json<EnqueueRequest>,
11978) -> Result<Json<serde_json::Value>, StatusCode> {
11979    let client = client_key_from_headers(&headers);
11980    let mut s = state.lock().unwrap();
11981    check_auth(&mut s, &headers, AccessLevel::Write)?;
11982
11983    let priority = payload.priority.clamp(1, 10);
11984    let now = std::time::SystemTime::now()
11985        .duration_since(std::time::UNIX_EPOCH)
11986        .unwrap_or_default()
11987        .as_secs();
11988
11989    let id = s.execution_queue_next_id;
11990    s.execution_queue_next_id += 1;
11991
11992    let item = QueuedExecution {
11993        id,
11994        flow_name: payload.flow_name.clone(),
11995        backend: payload.backend.clone(),
11996        priority,
11997        client_key: client.clone(),
11998        enqueued_at: now,
11999        status: "pending".into(),
12000    };
12001
12002    // Insert sorted by priority (stable: same priority preserves FIFO)
12003    let pos = s.execution_queue.iter().position(|q| q.priority > priority)
12004        .unwrap_or(s.execution_queue.len());
12005    s.execution_queue.insert(pos, item);
12006
12007    // Cap queue at 100
12008    if s.execution_queue.len() > 100 {
12009        s.execution_queue.truncate(100);
12010    }
12011
12012    Ok(Json(serde_json::json!({
12013        "success": true,
12014        "queue_id": id,
12015        "flow_name": payload.flow_name,
12016        "priority": priority,
12017        "position": pos,
12018        "queue_length": s.execution_queue.len(),
12019    })))
12020}
12021
12022/// GET /v1/execute/queue — view the current execution queue.
12023async fn execute_queue_handler(
12024    State(state): State<SharedState>,
12025    headers: HeaderMap,
12026) -> Result<Json<serde_json::Value>, StatusCode> {
12027    let s = state.lock().unwrap();
12028    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12029
12030    let pending: Vec<&QueuedExecution> = s.execution_queue.iter()
12031        .filter(|q| q.status == "pending")
12032        .collect();
12033
12034    Ok(Json(serde_json::json!({
12035        "total": s.execution_queue.len(),
12036        "pending": pending.len(),
12037        "queue": s.execution_queue,
12038    })))
12039}
12040
12041/// POST /v1/execute/dequeue — take the next item from the queue and mark it processing.
12042async fn execute_dequeue_handler(
12043    State(state): State<SharedState>,
12044    headers: HeaderMap,
12045) -> Result<Json<serde_json::Value>, StatusCode> {
12046    let mut s = state.lock().unwrap();
12047    check_auth(&mut s, &headers, AccessLevel::Write)?;
12048
12049    // Find first pending item (queue is already priority-sorted)
12050    match s.execution_queue.iter_mut().find(|q| q.status == "pending") {
12051        Some(item) => {
12052            item.status = "processing".into();
12053            Ok(Json(serde_json::json!({
12054                "success": true,
12055                "queue_id": item.id,
12056                "flow_name": item.flow_name,
12057                "backend": item.backend,
12058                "priority": item.priority,
12059                "client_key": item.client_key,
12060                "enqueued_at": item.enqueued_at,
12061            })))
12062        }
12063        None => Ok(Json(serde_json::json!({
12064            "success": false,
12065            "message": "queue is empty",
12066        }))),
12067    }
12068}
12069
12070/// Compute per-flow cost from buffered traces using pricing config.
12071fn compute_flow_costs(
12072    trace_store: &crate::trace_store::TraceStore,
12073    pricing: &CostPricing,
12074) -> Vec<FlowCostSummary> {
12075    let mut flow_map: HashMap<String, (u64, u64, u64, String)> = HashMap::new(); // (execs, input_tok, output_tok, backend)
12076
12077    let entries = trace_store.recent(trace_store.len(), None);
12078    for e in entries {
12079        let entry = flow_map.entry(e.flow_name.clone()).or_insert((0, 0, 0, e.backend.clone()));
12080        entry.0 += 1;
12081        entry.1 += e.tokens_input;
12082        entry.2 += e.tokens_output;
12083        entry.3 = e.backend.clone(); // last backend used
12084    }
12085
12086    let mut costs: Vec<FlowCostSummary> = flow_map.into_iter().map(|(name, (execs, inp, outp, backend))| {
12087        let input_price = pricing.input_per_million.get(&backend).copied().unwrap_or(0.0);
12088        let output_price = pricing.output_per_million.get(&backend).copied().unwrap_or(0.0);
12089        let cost = (inp as f64 / 1_000_000.0) * input_price + (outp as f64 / 1_000_000.0) * output_price;
12090
12091        FlowCostSummary {
12092            flow_name: name,
12093            executions: execs,
12094            total_input_tokens: inp,
12095            total_output_tokens: outp,
12096            estimated_cost_usd: (cost * 10000.0).round() / 10000.0, // 4 decimal places
12097        }
12098    }).collect();
12099    costs.sort_by(|a, b| b.estimated_cost_usd.partial_cmp(&a.estimated_cost_usd).unwrap_or(std::cmp::Ordering::Equal));
12100    costs
12101}
12102
12103/// GET /v1/costs — aggregate cost summary across all flows.
12104async fn costs_handler(
12105    State(state): State<SharedState>,
12106    headers: HeaderMap,
12107) -> Result<Json<serde_json::Value>, StatusCode> {
12108    let s = state.lock().unwrap();
12109    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12110
12111    let costs = compute_flow_costs(&s.trace_store, &s.cost_pricing);
12112    let total_cost: f64 = costs.iter().map(|c| c.estimated_cost_usd).sum();
12113    let total_tokens: u64 = costs.iter().map(|c| c.total_input_tokens + c.total_output_tokens).sum();
12114
12115    Ok(Json(serde_json::json!({
12116        "total_estimated_cost_usd": (total_cost * 10000.0).round() / 10000.0,
12117        "total_tokens": total_tokens,
12118        "flows_count": costs.len(),
12119        "pricing": s.cost_pricing,
12120        "flows": costs,
12121    })))
12122}
12123
12124/// GET /v1/costs/:flow — cost details for a specific flow.
12125async fn costs_flow_handler(
12126    State(state): State<SharedState>,
12127    headers: HeaderMap,
12128    Path(flow): Path<String>,
12129) -> Result<Json<serde_json::Value>, StatusCode> {
12130    let s = state.lock().unwrap();
12131    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12132
12133    let costs = compute_flow_costs(&s.trace_store, &s.cost_pricing);
12134    match costs.iter().find(|c| c.flow_name == flow) {
12135        Some(cost) => Ok(Json(serde_json::to_value(cost).unwrap_or_default())),
12136        None => Ok(Json(serde_json::json!({
12137            "error": format!("no cost data for flow '{}'", flow),
12138        }))),
12139    }
12140}
12141
12142/// PUT /v1/costs/pricing — update backend pricing configuration.
12143async fn costs_pricing_handler(
12144    State(state): State<SharedState>,
12145    headers: HeaderMap,
12146    Json(payload): Json<CostPricing>,
12147) -> Result<Json<serde_json::Value>, StatusCode> {
12148    let client = client_key_from_headers(&headers);
12149    let mut s = state.lock().unwrap();
12150    check_auth(&mut s, &headers, AccessLevel::Admin)?;
12151
12152    s.cost_pricing = payload.clone();
12153    s.audit_log.record(
12154        &client, AuditAction::ConfigUpdate, "cost_pricing",
12155        serde_json::json!({"input_per_million": payload.input_per_million, "output_per_million": payload.output_per_million}),
12156        true,
12157    );
12158
12159    Ok(Json(serde_json::json!({
12160        "success": true,
12161        "pricing": s.cost_pricing,
12162    })))
12163}
12164
12165/// POST /v1/execute/drain — process all pending queue items sequentially.
12166async fn execute_drain_handler(
12167    State(state): State<SharedState>,
12168    headers: HeaderMap,
12169) -> Result<Json<serde_json::Value>, StatusCode> {
12170    let req_start = Instant::now();
12171    let client = client_key_from_headers(&headers);
12172    {
12173        let mut s = state.lock().unwrap();
12174        check_auth(&mut s, &headers, AccessLevel::Write)?;
12175    }
12176
12177    // Collect pending items
12178    let pending: Vec<(u64, String, String)> = {
12179        let mut s = state.lock().unwrap();
12180        s.execution_queue.iter_mut()
12181            .filter(|q| q.status == "pending")
12182            .map(|q| {
12183                q.status = "processing".into();
12184                (q.id, q.flow_name.clone(), q.backend.clone())
12185            })
12186            .collect()
12187    };
12188
12189    if pending.is_empty() {
12190        return Ok(Json(serde_json::json!({
12191            "drained": 0,
12192            "message": "queue empty",
12193        })));
12194    }
12195
12196    let mut results = Vec::new();
12197
12198    for (queue_id, flow_name, backend) in &pending {
12199        // Look up deployed source
12200        let source_info = {
12201            let s = state.lock().unwrap();
12202            s.versions.get_history(flow_name)
12203                .and_then(|h| h.active())
12204                .map(|v| (v.source.clone(), v.source_file.clone()))
12205        };
12206
12207        let (source, source_file) = match source_info {
12208            Some(info) => info,
12209            None => {
12210                // Mark failed
12211                let mut s = state.lock().unwrap();
12212                if let Some(item) = s.execution_queue.iter_mut().find(|q| q.id == *queue_id) {
12213                    item.status = "failed".into();
12214                }
12215                results.push(serde_json::json!({
12216                    "queue_id": queue_id,
12217                    "flow": flow_name,
12218                    "success": false,
12219                    "error": "flow not deployed",
12220                }));
12221                continue;
12222            }
12223        };
12224
12225        match server_execute_full(&state, &source, &source_file, flow_name, backend).0 {
12226            Ok(mut er) => {
12227                let mut trace_entry = crate::trace_store::build_trace(
12228                    &er.flow_name, &er.source_file, &er.backend, &client,
12229                    if er.success { crate::trace_store::TraceStatus::Success }
12230                    else { crate::trace_store::TraceStatus::Partial },
12231                    er.steps_executed, er.latency_ms,
12232                );
12233                trace_entry.tokens_input = er.tokens_input;
12234                trace_entry.tokens_output = er.tokens_output;
12235                trace_entry.errors = er.errors;
12236
12237                let mut s = state.lock().unwrap();
12238                let trace_id = s.trace_store.record(trace_entry);
12239                if let Some(item) = s.execution_queue.iter_mut().find(|q| q.id == *queue_id) {
12240                    item.status = if er.success { "completed" } else { "failed" }.into();
12241                }
12242
12243                results.push(serde_json::json!({
12244                    "queue_id": queue_id,
12245                    "flow": flow_name,
12246                    "success": er.success,
12247                    "trace_id": trace_id,
12248                    "latency_ms": er.latency_ms,
12249                    "steps": er.steps_executed,
12250                }));
12251            }
12252            Err(e) => {
12253                let mut s = state.lock().unwrap();
12254                s.metrics.total_errors += 1;
12255                if let Some(item) = s.execution_queue.iter_mut().find(|q| q.id == *queue_id) {
12256                    item.status = "failed".into();
12257                }
12258                results.push(serde_json::json!({
12259                    "queue_id": queue_id,
12260                    "flow": flow_name,
12261                    "success": false,
12262                    "error": e,
12263                }));
12264            }
12265        }
12266    }
12267
12268    let succeeded = results.iter().filter(|r| r["success"] == true).count();
12269    let failed = results.iter().filter(|r| r["success"] == false).count();
12270    let total_latency = req_start.elapsed().as_millis() as u64;
12271
12272    // Audit
12273    {
12274        let mut s = state.lock().unwrap();
12275        s.audit_log.record(
12276            &client, AuditAction::Execute, "queue_drain",
12277            serde_json::json!({"drained": results.len(), "succeeded": succeeded, "failed": failed}),
12278            true,
12279        );
12280    }
12281
12282    Ok(Json(serde_json::json!({
12283        "drained": results.len(),
12284        "succeeded": succeeded,
12285        "failed": failed,
12286        "total_latency_ms": total_latency,
12287        "results": results,
12288    })))
12289}
12290
12291/// Request to set a cost budget for a flow.
12292#[derive(Debug, Deserialize)]
12293pub struct SetBudgetRequest {
12294    /// Maximum cost in USD.
12295    pub max_cost_usd: f64,
12296    /// Warning threshold (0.0–1.0, default 0.8).
12297    #[serde(default = "default_warn_threshold")]
12298    pub warn_threshold: f64,
12299}
12300
12301fn default_warn_threshold() -> f64 { 0.8 }
12302
12303/// PUT /v1/costs/:flow/budget — set a cost budget for a flow.
12304async fn costs_budget_set_handler(
12305    State(state): State<SharedState>,
12306    headers: HeaderMap,
12307    Path(flow): Path<String>,
12308    Json(payload): Json<SetBudgetRequest>,
12309) -> Result<Json<serde_json::Value>, StatusCode> {
12310    let client = client_key_from_headers(&headers);
12311    let mut s = state.lock().unwrap();
12312    check_auth(&mut s, &headers, AccessLevel::Admin)?;
12313
12314    let threshold = payload.warn_threshold.clamp(0.0, 1.0);
12315    s.cost_budgets.insert(flow.clone(), CostBudget {
12316        max_cost_usd: payload.max_cost_usd,
12317        warn_threshold: threshold,
12318    });
12319
12320    s.audit_log.record(
12321        &client, AuditAction::ConfigUpdate, &format!("cost_budget:{}", flow),
12322        serde_json::json!({"max_cost_usd": payload.max_cost_usd, "warn_threshold": threshold}),
12323        true,
12324    );
12325
12326    Ok(Json(serde_json::json!({
12327        "success": true,
12328        "flow": flow,
12329        "max_cost_usd": payload.max_cost_usd,
12330        "warn_threshold": threshold,
12331    })))
12332}
12333
12334/// DELETE /v1/costs/:flow/budget — remove a cost budget for a flow.
12335async fn costs_budget_delete_handler(
12336    State(state): State<SharedState>,
12337    headers: HeaderMap,
12338    Path(flow): Path<String>,
12339) -> Result<Json<serde_json::Value>, StatusCode> {
12340    let mut s = state.lock().unwrap();
12341    check_auth(&mut s, &headers, AccessLevel::Admin)?;
12342
12343    let removed = s.cost_budgets.remove(&flow).is_some();
12344    Ok(Json(serde_json::json!({
12345        "success": removed,
12346        "flow": flow,
12347    })))
12348}
12349
12350/// GET /v1/costs/alerts — check all flows against their budgets.
12351async fn costs_alerts_handler(
12352    State(state): State<SharedState>,
12353    headers: HeaderMap,
12354) -> Result<Json<serde_json::Value>, StatusCode> {
12355    let s = state.lock().unwrap();
12356    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12357
12358    let costs = compute_flow_costs(&s.trace_store, &s.cost_pricing);
12359    let mut alerts: Vec<CostAlert> = Vec::new();
12360
12361    for (flow_name, budget) in &s.cost_budgets {
12362        let current_cost = costs.iter()
12363            .find(|c| &c.flow_name == flow_name)
12364            .map(|c| c.estimated_cost_usd)
12365            .unwrap_or(0.0);
12366
12367        let usage_pct = if budget.max_cost_usd > 0.0 {
12368            current_cost / budget.max_cost_usd
12369        } else {
12370            0.0
12371        };
12372
12373        if usage_pct >= 1.0 {
12374            alerts.push(CostAlert {
12375                flow_name: flow_name.clone(),
12376                current_cost_usd: current_cost,
12377                budget_usd: budget.max_cost_usd,
12378                usage_pct: (usage_pct * 10000.0).round() / 10000.0,
12379                level: "exceeded".into(),
12380            });
12381        } else if usage_pct >= budget.warn_threshold {
12382            alerts.push(CostAlert {
12383                flow_name: flow_name.clone(),
12384                current_cost_usd: current_cost,
12385                budget_usd: budget.max_cost_usd,
12386                usage_pct: (usage_pct * 10000.0).round() / 10000.0,
12387                level: "warning".into(),
12388            });
12389        }
12390    }
12391
12392    alerts.sort_by(|a, b| b.usage_pct.partial_cmp(&a.usage_pct).unwrap_or(std::cmp::Ordering::Equal));
12393
12394    Ok(Json(serde_json::json!({
12395        "total_budgets": s.cost_budgets.len(),
12396        "alerts_count": alerts.len(),
12397        "alerts": alerts,
12398    })))
12399}
12400
12401/// Per-day cost data point for forecasting.
12402#[derive(Debug, Clone, Serialize)]
12403pub struct DailyCostPoint {
12404    pub day_offset: i64,
12405    pub date: String,
12406    pub cost_usd: f64,
12407    pub executions: u64,
12408}
12409
12410/// Cost forecast result for a flow (or aggregate).
12411#[derive(Debug, Clone, Serialize)]
12412pub struct CostForecast {
12413    pub flow: String,
12414    pub historical_days: usize,
12415    pub forecast_days: u64,
12416    pub daily_history: Vec<DailyCostPoint>,
12417    pub forecast: Vec<DailyCostPoint>,
12418    pub trend_slope_usd_per_day: f64,
12419    pub total_forecast_cost_usd: f64,
12420}
12421
12422/// GET /v1/costs/forecast — predict future costs based on historical daily trends.
12423/// Query params: flow (optional), days (forecast horizon, default 7).
12424async fn costs_forecast_handler(
12425    State(state): State<SharedState>,
12426    headers: HeaderMap,
12427    Query(params): Query<std::collections::HashMap<String, String>>,
12428) -> Result<Json<serde_json::Value>, StatusCode> {
12429    let s = state.lock().unwrap();
12430    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12431
12432    let flow_filter = params.get("flow").cloned();
12433    let forecast_days = params.get("days").and_then(|d| d.parse::<u64>().ok()).unwrap_or(7);
12434    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
12435    let secs_per_day: u64 = 86400;
12436
12437    // Collect trace entries, optionally filtered by flow
12438    let entries = s.trace_store.recent(s.trace_store.len(), None);
12439    let filtered: Vec<_> = entries.iter().filter(|e| {
12440        flow_filter.as_ref().map_or(true, |f| &e.flow_name == f)
12441    }).collect();
12442
12443    if filtered.is_empty() {
12444        let flow_label = flow_filter.unwrap_or_else(|| "*".into());
12445        return Ok(Json(serde_json::json!({
12446            "flow": flow_label,
12447            "historical_days": 0,
12448            "forecast_days": forecast_days,
12449            "daily_history": [],
12450            "forecast": [],
12451            "trend_slope_usd_per_day": 0.0,
12452            "total_forecast_cost_usd": 0.0,
12453        })));
12454    }
12455
12456    // Find time range and bucket by day
12457    let min_ts = filtered.iter().map(|e| e.timestamp).min().unwrap_or(now);
12458    let day_zero = min_ts / secs_per_day; // day index of earliest trace
12459    let today = now / secs_per_day;
12460    let num_days = ((today - day_zero) + 1) as usize;
12461
12462    // Accumulate cost per day bucket
12463    let mut day_costs: Vec<(f64, u64)> = vec![(0.0, 0); num_days]; // (cost, executions)
12464    for e in &filtered {
12465        let day_idx = ((e.timestamp / secs_per_day) - day_zero) as usize;
12466        if day_idx < num_days {
12467            let backend = &e.backend;
12468            let input_price = s.cost_pricing.input_per_million.get(backend).copied().unwrap_or(0.0);
12469            let output_price = s.cost_pricing.output_per_million.get(backend).copied().unwrap_or(0.0);
12470            let cost = (e.tokens_input as f64 / 1_000_000.0) * input_price
12471                     + (e.tokens_output as f64 / 1_000_000.0) * output_price;
12472            day_costs[day_idx].0 += cost;
12473            day_costs[day_idx].1 += 1;
12474        }
12475    }
12476
12477    // Build historical daily points
12478    let daily_history: Vec<DailyCostPoint> = day_costs.iter().enumerate().map(|(i, (cost, execs))| {
12479        let day_ts = (day_zero + i as u64) * secs_per_day;
12480        DailyCostPoint {
12481            day_offset: i as i64,
12482            date: format_unix_day(day_ts),
12483            cost_usd: (*cost * 10000.0).round() / 10000.0,
12484            executions: *execs,
12485        }
12486    }).collect();
12487
12488    // Linear regression: y = a + b*x where x = day_offset, y = cost
12489    let n = daily_history.len() as f64;
12490    let sum_x: f64 = daily_history.iter().map(|p| p.day_offset as f64).sum();
12491    let sum_y: f64 = daily_history.iter().map(|p| p.cost_usd).sum();
12492    let sum_xy: f64 = daily_history.iter().map(|p| p.day_offset as f64 * p.cost_usd).sum();
12493    let sum_x2: f64 = daily_history.iter().map(|p| (p.day_offset as f64).powi(2)).sum();
12494
12495    let denom = n * sum_x2 - sum_x * sum_x;
12496    let (slope, intercept) = if denom.abs() < 1e-12 {
12497        // Flat — use average
12498        (0.0, if n > 0.0 { sum_y / n } else { 0.0 })
12499    } else {
12500        let b = (n * sum_xy - sum_x * sum_y) / denom;
12501        let a = (sum_y - b * sum_x) / n;
12502        (b, a)
12503    };
12504
12505    // Generate forecast points
12506    let last_offset = num_days as i64;
12507    let forecast: Vec<DailyCostPoint> = (0..forecast_days).map(|d| {
12508        let offset = last_offset + d as i64;
12509        let predicted = (intercept + slope * offset as f64).max(0.0);
12510        let day_ts = (day_zero + offset as u64) * secs_per_day;
12511        DailyCostPoint {
12512            day_offset: offset,
12513            date: format_unix_day(day_ts),
12514            cost_usd: (predicted * 10000.0).round() / 10000.0,
12515            executions: 0,
12516        }
12517    }).collect();
12518
12519    let total_forecast: f64 = forecast.iter().map(|p| p.cost_usd).sum();
12520    let flow_label = flow_filter.unwrap_or_else(|| "*".into());
12521
12522    Ok(Json(serde_json::json!({
12523        "flow": flow_label,
12524        "historical_days": num_days,
12525        "forecast_days": forecast_days,
12526        "daily_history": daily_history,
12527        "forecast": forecast,
12528        "trend_slope_usd_per_day": (slope * 10000.0).round() / 10000.0,
12529        "total_forecast_cost_usd": (total_forecast * 10000.0).round() / 10000.0,
12530    })))
12531}
12532
12533/// Format Unix timestamp to YYYY-MM-DD string.
12534fn format_unix_day(ts: u64) -> String {
12535    // Simple conversion without chrono: days since epoch
12536    let days = ts / 86400;
12537    // Approximate: good enough for display
12538    let y = 1970 + (days as i64 * 400 / 146097);
12539    let mut remaining = days as i64 - ((y - 1970) * 365 + (y - 1970) / 4 - (y - 1970) / 100 + (y - 1970) / 400);
12540    let mut year = y;
12541    if remaining < 0 {
12542        year -= 1;
12543        remaining += if is_leap(year) { 366 } else { 365 };
12544    }
12545    while remaining >= if is_leap(year) { 366 } else { 365 } {
12546        remaining -= if is_leap(year) { 366 } else { 365 };
12547        year += 1;
12548    }
12549    let leap = is_leap(year);
12550    let month_days = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
12551    let mut month = 0usize;
12552    for (i, &md) in month_days.iter().enumerate() {
12553        if remaining < md as i64 { month = i; break; }
12554        remaining -= md as i64;
12555    }
12556    format!("{:04}-{:02}-{:02}", year, month + 1, remaining + 1)
12557}
12558
12559fn is_leap(y: i64) -> bool {
12560    y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)
12561}
12562
12563/// GET /v1/backends — list registered backends with status.
12564async fn backends_list_handler(
12565    State(state): State<SharedState>,
12566    headers: HeaderMap,
12567) -> Result<Json<serde_json::Value>, StatusCode> {
12568    let s = state.lock().unwrap();
12569    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12570
12571    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
12572
12573    // Merge supported backends with registry entries
12574    let mut entries: Vec<serde_json::Value> = Vec::new();
12575    for &name in crate::backend::SUPPORTED_BACKENDS {
12576        let registered = s.backend_registry.get(name);
12577        let has_env_key = std::env::var(format!("{}_API_KEY", name.to_uppercase())).is_ok();
12578        let has_server_key = registered.map_or(false, |r| !r.api_key.is_empty());
12579
12580        entries.push(serde_json::json!({
12581            "name": name,
12582            "enabled": registered.map_or(true, |r| r.enabled),
12583            "key_source": if has_server_key { "server" } else if has_env_key { "env" } else { "none" },
12584            "status": registered.map_or("unknown".to_string(), |r| r.status.clone()),
12585            "last_check_at": registered.map_or(0, |r| r.last_check_at),
12586            "last_check_latency_ms": registered.map_or(0, |r| r.last_check_latency_ms),
12587            "total_calls": registered.map_or(0, |r| r.total_calls),
12588            "total_errors": registered.map_or(0, |r| r.total_errors),
12589        }));
12590    }
12591
12592    Ok(Json(serde_json::json!({
12593        "backends": entries,
12594        "total": entries.len(),
12595    })))
12596}
12597
12598/// PUT /v1/backends/{name} — register or update a backend with server-managed API key.
12599async fn backends_put_handler(
12600    State(state): State<SharedState>,
12601    headers: HeaderMap,
12602    Path(name): Path<String>,
12603    Json(payload): Json<serde_json::Value>,
12604) -> Result<Json<serde_json::Value>, StatusCode> {
12605    let mut s = state.lock().unwrap();
12606    let client = client_key_from_headers(&headers);
12607    check_auth(&mut s, &headers, AccessLevel::Admin)?;
12608
12609    // Validate backend name
12610    if !crate::backend::SUPPORTED_BACKENDS.contains(&name.as_str()) {
12611        return Ok(Json(serde_json::json!({
12612            "error": format!("Unknown backend '{}'. Supported: {:?}", name, crate::backend::SUPPORTED_BACKENDS),
12613        })));
12614    }
12615
12616    let api_key = payload.get("api_key").and_then(|v| v.as_str()).unwrap_or("").to_string();
12617    let enabled = payload.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
12618
12619    let entry = s.backend_registry.entry(name.clone()).or_insert_with(|| BackendRegistryEntry {
12620        name: name.clone(),
12621        api_key: String::new(),
12622        enabled: true,
12623        status: "unknown".into(),
12624        last_check_at: 0,
12625        last_check_latency_ms: 0,
12626        total_calls: 0,
12627        total_errors: 0,
12628        total_tokens_input: 0,
12629        total_tokens_output: 0,
12630        total_latency_ms: 0,
12631        last_call_at: 0,
12632        fallback_chain: Vec::new(),
12633        consecutive_failures: 0,
12634        circuit_open_until: 0,
12635        circuit_breaker_threshold: 5,
12636        circuit_breaker_cooldown_secs: 60,
12637            total_cost_usd: 0.0, max_rpm: 0, max_tpm: 0, rpm_window_start: 0, rpm_count: 0, tpm_count: 0,
12638    });
12639
12640    if !api_key.is_empty() {
12641        entry.api_key = api_key;
12642    }
12643    entry.enabled = enabled;
12644    let has_key = !entry.api_key.is_empty();
12645    let status = entry.status.clone();
12646
12647    s.audit_log.record(&client, AuditAction::ConfigUpdate, "backend_registry",
12648        serde_json::json!({"action": "put", "backend": &name, "enabled": enabled, "has_key": has_key}), true);
12649
12650    Ok(Json(serde_json::json!({
12651        "success": true,
12652        "backend": name,
12653        "enabled": enabled,
12654        "has_key": has_key,
12655        "status": status,
12656    })))
12657}
12658
12659/// DELETE /v1/backends/{name} — remove a backend from registry (reverts to env-only).
12660async fn backends_delete_handler(
12661    State(state): State<SharedState>,
12662    headers: HeaderMap,
12663    Path(name): Path<String>,
12664) -> Result<Json<serde_json::Value>, StatusCode> {
12665    let mut s = state.lock().unwrap();
12666    let client = client_key_from_headers(&headers);
12667    check_auth(&mut s, &headers, AccessLevel::Admin)?;
12668
12669    let removed = s.backend_registry.remove(&name).is_some();
12670
12671    s.audit_log.record(&client, AuditAction::ConfigUpdate, "backend_registry",
12672        serde_json::json!({"action": "delete", "backend": &name, "removed": removed}), removed);
12673
12674    Ok(Json(serde_json::json!({"success": removed, "backend": name})))
12675}
12676
12677/// POST /v1/backends/{name}/check — health-check a backend by attempting a minimal API call.
12678async fn backends_check_handler(
12679    State(state): State<SharedState>,
12680    headers: HeaderMap,
12681    Path(name): Path<String>,
12682) -> Result<Json<serde_json::Value>, StatusCode> {
12683    // Validate and get key outside lock
12684    let api_key = {
12685        let s = state.lock().unwrap();
12686        check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12687
12688        if !crate::backend::SUPPORTED_BACKENDS.contains(&name.as_str()) {
12689            return Ok(Json(serde_json::json!({"error": format!("Unknown backend '{}'", name)})));
12690        }
12691
12692        // Prefer server registry key, fallback to env
12693        let server_key = s.backend_registry.get(&name).map(|r| r.api_key.clone()).unwrap_or_default();
12694        if !server_key.is_empty() {
12695            server_key
12696        } else {
12697            crate::backend::get_api_key(&name).unwrap_or_default()
12698        }
12699    };
12700
12701    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
12702    let check_start = Instant::now();
12703
12704    // Attempt a minimal call (1-token response)
12705    let result = crate::backend::call(
12706        &name,
12707        &api_key,
12708        "You are a health check. Reply with OK.",
12709        "health",
12710        Some(5),
12711    );
12712
12713    let latency_ms = check_start.elapsed().as_millis() as u64;
12714    let (status, error_msg) = match &result {
12715        Ok(_) => ("healthy".to_string(), None),
12716        Err(e) => {
12717            let msg = e.message.clone();
12718            if msg.contains("not set") || msg.contains("API_KEY") {
12719                ("no_key".to_string(), Some(msg))
12720            } else if msg.contains("timeout") || msg.contains("connect") {
12721                ("unreachable".to_string(), Some(msg))
12722            } else {
12723                ("degraded".to_string(), Some(msg))
12724            }
12725        }
12726    };
12727
12728    // Update registry entry + record health history
12729    let transition;
12730    {
12731        let mut s = state.lock().unwrap();
12732        let entry = s.backend_registry.entry(name.clone()).or_insert_with(|| BackendRegistryEntry {
12733            name: name.clone(),
12734            api_key: String::new(),
12735            enabled: true,
12736            status: "unknown".into(),
12737            last_check_at: 0,
12738            last_check_latency_ms: 0,
12739            total_calls: 0,
12740            total_errors: 0,
12741            total_tokens_input: 0,
12742            total_tokens_output: 0,
12743            total_latency_ms: 0,
12744            last_call_at: 0,
12745            fallback_chain: Vec::new(),
12746            consecutive_failures: 0,
12747            circuit_open_until: 0,
12748            circuit_breaker_threshold: 5,
12749            circuit_breaker_cooldown_secs: 60,
12750            total_cost_usd: 0.0, max_rpm: 0, max_tpm: 0, rpm_window_start: 0, rpm_count: 0, tpm_count: 0,
12751        });
12752        let previous_status = entry.status.clone();
12753        entry.status = status.clone();
12754        entry.last_check_at = now;
12755        entry.last_check_latency_ms = latency_ms;
12756        transition = previous_status != status;
12757
12758        // Update probe consecutive counters
12759        if let Some(probe) = s.backend_health_probes.get_mut(&name) {
12760            if status == "healthy" {
12761                probe.consecutive_ok += 1;
12762                probe.consecutive_fail = 0;
12763            } else {
12764                probe.consecutive_fail += 1;
12765                probe.consecutive_ok = 0;
12766            }
12767        }
12768
12769        // Record health history (cap at 100 entries per backend)
12770        let record = HealthCheckRecord {
12771            timestamp: now,
12772            status: status.clone(),
12773            latency_ms,
12774            error: error_msg.clone(),
12775            previous_status,
12776        };
12777        let history = s.backend_health_history.entry(name.clone()).or_insert_with(Vec::new);
12778        history.push(record);
12779        if history.len() > 100 {
12780            history.remove(0);
12781        }
12782    }
12783
12784    Ok(Json(serde_json::json!({
12785        "backend": name,
12786        "status": status,
12787        "latency_ms": latency_ms,
12788        "error": error_msg,
12789        "transition": transition,
12790    })))
12791}
12792
12793/// Resolve API key for a backend: server registry → env var → error.
12794/// GET /v1/backends/{name}/metrics — detailed call metrics for a specific backend.
12795async fn backends_metrics_handler(
12796    State(state): State<SharedState>,
12797    headers: HeaderMap,
12798    Path(name): Path<String>,
12799) -> Result<Json<serde_json::Value>, StatusCode> {
12800    let s = state.lock().unwrap();
12801    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12802
12803    if !crate::backend::SUPPORTED_BACKENDS.contains(&name.as_str()) {
12804        return Ok(Json(serde_json::json!({"error": format!("Unknown backend '{}'", name)})));
12805    }
12806
12807    match s.backend_registry.get(&name) {
12808        Some(entry) => {
12809            let avg_latency = if entry.total_calls > 0 {
12810                entry.total_latency_ms as f64 / entry.total_calls as f64
12811            } else {
12812                0.0
12813            };
12814            let error_rate = if entry.total_calls > 0 {
12815                entry.total_errors as f64 / entry.total_calls as f64
12816            } else {
12817                0.0
12818            };
12819            let total_tokens = entry.total_tokens_input + entry.total_tokens_output;
12820
12821            Ok(Json(serde_json::json!({
12822                "backend": name,
12823                "enabled": entry.enabled,
12824                "status": entry.status,
12825                "total_calls": entry.total_calls,
12826                "total_errors": entry.total_errors,
12827                "error_rate": (error_rate * 10000.0).round() / 10000.0,
12828                "total_tokens_input": entry.total_tokens_input,
12829                "total_tokens_output": entry.total_tokens_output,
12830                "total_tokens": total_tokens,
12831                "total_latency_ms": entry.total_latency_ms,
12832                "avg_latency_ms": (avg_latency * 100.0).round() / 100.0,
12833                "last_call_at": entry.last_call_at,
12834                "total_cost_usd": entry.total_cost_usd,
12835                "last_check_at": entry.last_check_at,
12836                "last_check_latency_ms": entry.last_check_latency_ms,
12837            })))
12838        }
12839        None => Ok(Json(serde_json::json!({
12840            "backend": name,
12841            "enabled": true,
12842            "status": "unknown",
12843            "total_calls": 0,
12844            "total_errors": 0,
12845            "error_rate": 0.0,
12846            "total_tokens_input": 0,
12847            "total_tokens_output": 0,
12848            "total_tokens": 0,
12849            "total_latency_ms": 0,
12850            "avg_latency_ms": 0.0,
12851            "total_cost_usd": 0.0,
12852            "last_call_at": 0,
12853            "last_check_at": 0,
12854            "last_check_latency_ms": 0,
12855        }))),
12856    }
12857}
12858
12859/// PUT /v1/backends/{name}/limits — set rate limits for a backend.
12860/// Body: { "max_rpm": 60, "max_tpm": 100000 } (0 = unlimited)
12861async fn backends_limits_put_handler(
12862    State(state): State<SharedState>,
12863    headers: HeaderMap,
12864    Path(name): Path<String>,
12865    Json(payload): Json<serde_json::Value>,
12866) -> Result<Json<serde_json::Value>, StatusCode> {
12867    let mut s = state.lock().unwrap();
12868    let client = client_key_from_headers(&headers);
12869    check_auth(&mut s, &headers, AccessLevel::Admin)?;
12870
12871    let max_rpm = payload.get("max_rpm").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
12872    let max_tpm = payload.get("max_tpm").and_then(|v| v.as_u64()).unwrap_or(0);
12873
12874    if let Some(entry) = s.backend_registry.get_mut(&name) {
12875        entry.max_rpm = max_rpm;
12876        entry.max_tpm = max_tpm;
12877    } else {
12878        return Ok(Json(serde_json::json!({"error": format!("backend '{}' not in registry", name)})));
12879    }
12880
12881    s.audit_log.record(&client, AuditAction::ConfigUpdate, "backend_limits",
12882        serde_json::json!({"action": "set", "backend": &name, "max_rpm": max_rpm, "max_tpm": max_tpm}), true);
12883
12884    Ok(Json(serde_json::json!({
12885        "success": true,
12886        "backend": name,
12887        "max_rpm": max_rpm,
12888        "max_tpm": max_tpm,
12889    })))
12890}
12891
12892/// GET /v1/backends/{name}/limits — view rate limits and current usage.
12893async fn backends_limits_get_handler(
12894    State(state): State<SharedState>,
12895    headers: HeaderMap,
12896    Path(name): Path<String>,
12897) -> Result<Json<serde_json::Value>, StatusCode> {
12898    let s = state.lock().unwrap();
12899    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12900
12901    let now = std::time::SystemTime::now()
12902        .duration_since(std::time::UNIX_EPOCH)
12903        .unwrap_or_default()
12904        .as_secs();
12905
12906    match s.backend_registry.get(&name) {
12907        Some(entry) => {
12908            let window_remaining = if entry.rpm_window_start + 60 > now {
12909                entry.rpm_window_start + 60 - now
12910            } else { 60 };
12911            Ok(Json(serde_json::json!({
12912                "backend": name,
12913                "max_rpm": entry.max_rpm,
12914                "max_tpm": entry.max_tpm,
12915                "current_rpm": entry.rpm_count,
12916                "current_tpm": entry.tpm_count,
12917                "window_remaining_secs": window_remaining,
12918                "rpm_limited": entry.max_rpm > 0 && entry.rpm_count >= entry.max_rpm,
12919                "tpm_limited": entry.max_tpm > 0 && entry.tpm_count >= entry.max_tpm,
12920            })))
12921        }
12922        None => Ok(Json(serde_json::json!({"error": format!("backend '{}' not in registry", name)}))),
12923    }
12924}
12925
12926/// GET /v1/backends/{name}/fallback — view fallback chain for a backend.
12927async fn backends_fallback_get_handler(
12928    State(state): State<SharedState>,
12929    headers: HeaderMap,
12930    Path(name): Path<String>,
12931) -> Result<Json<serde_json::Value>, StatusCode> {
12932    let s = state.lock().unwrap();
12933    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12934
12935    let chain = s.backend_registry.get(&name)
12936        .map(|e| e.fallback_chain.clone())
12937        .unwrap_or_default();
12938
12939    Ok(Json(serde_json::json!({
12940        "backend": name,
12941        "fallback_chain": chain,
12942    })))
12943}
12944
12945/// PUT /v1/backends/{name}/fallback — set fallback chain for a backend.
12946async fn backends_fallback_put_handler(
12947    State(state): State<SharedState>,
12948    headers: HeaderMap,
12949    Path(name): Path<String>,
12950    Json(payload): Json<serde_json::Value>,
12951) -> Result<Json<serde_json::Value>, StatusCode> {
12952    let mut s = state.lock().unwrap();
12953    let client = client_key_from_headers(&headers);
12954    check_auth(&mut s, &headers, AccessLevel::Admin)?;
12955
12956    let chain: Vec<String> = payload.get("chain")
12957        .and_then(|v| v.as_array())
12958        .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
12959        .unwrap_or_default();
12960
12961    // Validate: no self-reference, all must be supported backends
12962    if chain.contains(&name) {
12963        return Ok(Json(serde_json::json!({"error": "fallback chain cannot contain the backend itself"})));
12964    }
12965    for fb in &chain {
12966        if !crate::backend::SUPPORTED_BACKENDS.contains(&fb.as_str()) {
12967            return Ok(Json(serde_json::json!({"error": format!("unknown backend '{}' in chain", fb)})));
12968        }
12969    }
12970
12971    let entry = s.backend_registry.entry(name.clone()).or_insert_with(|| BackendRegistryEntry {
12972        name: name.clone(),
12973        api_key: String::new(),
12974        enabled: true,
12975        status: "unknown".into(),
12976        last_check_at: 0,
12977        last_check_latency_ms: 0,
12978        total_calls: 0,
12979        total_errors: 0,
12980        total_tokens_input: 0,
12981        total_tokens_output: 0,
12982        total_latency_ms: 0,
12983        last_call_at: 0,
12984        fallback_chain: Vec::new(),
12985        consecutive_failures: 0,
12986        circuit_open_until: 0,
12987        circuit_breaker_threshold: 5,
12988        circuit_breaker_cooldown_secs: 60,
12989        total_cost_usd: 0.0, max_rpm: 0, max_tpm: 0, rpm_window_start: 0, rpm_count: 0, tpm_count: 0,
12990    });
12991    entry.fallback_chain = chain.clone();
12992
12993    s.audit_log.record(&client, AuditAction::ConfigUpdate, "backend_fallback",
12994        serde_json::json!({"action": "set", "backend": &name, "chain": &chain}), true);
12995
12996    Ok(Json(serde_json::json!({"success": true, "backend": name, "fallback_chain": chain})))
12997}
12998
12999/// Backend score for ranking/selection.
13000#[derive(Debug, Clone, Serialize)]
13001pub struct BackendScore {
13002    pub name: String,
13003    pub enabled: bool,
13004    pub circuit_open: bool,
13005    pub total_calls: u64,
13006    pub error_rate: f64,
13007    pub avg_latency_ms: f64,
13008    pub cost_per_call_usd: f64,
13009    pub total_cost_usd: f64,
13010    /// Composite score (higher = better). Strategy-dependent.
13011    pub score: f64,
13012}
13013
13014/// Compute scores for all backends in registry based on strategy.
13015/// Strategy: "cheapest" | "fastest" | "most_reliable" | "balanced"
13016fn compute_backend_scores(state: &ServerState, strategy: &str) -> Vec<BackendScore> {
13017    let now = std::time::SystemTime::now()
13018        .duration_since(std::time::UNIX_EPOCH)
13019        .unwrap_or_default()
13020        .as_secs();
13021
13022    let mut scores: Vec<BackendScore> = state.backend_registry.values().filter(|e| e.enabled).map(|e| {
13023        let error_rate = if e.total_calls > 0 { e.total_errors as f64 / e.total_calls as f64 } else { 0.0 };
13024        let avg_latency = if e.total_calls > 0 { e.total_latency_ms as f64 / e.total_calls as f64 } else { 0.0 };
13025        let cost_per_call = if e.total_calls > 0 { e.total_cost_usd / e.total_calls as f64 } else { 0.0 };
13026        let circuit_open = e.circuit_open_until > 0 && now < e.circuit_open_until;
13027
13028        // Base score: 100 for usable backends, 0 for circuit-open
13029        let mut score = if circuit_open { 0.0 } else { 100.0 };
13030
13031        if !circuit_open && e.total_calls > 0 {
13032            match strategy {
13033                "cheapest" => {
13034                    // Lower cost → higher score. Normalize: max $0.10/call → score 0
13035                    score = (100.0 - cost_per_call * 1000.0).max(0.0);
13036                }
13037                "fastest" => {
13038                    // Lower latency → higher score. 0ms → 100, 5000ms → 0
13039                    score = (100.0 - avg_latency / 50.0).max(0.0);
13040                }
13041                "most_reliable" => {
13042                    // Lower error rate → higher score
13043                    score = (1.0 - error_rate) * 100.0;
13044                }
13045                "balanced" | _ => {
13046                    // Weighted composite: 40% reliability + 30% speed + 30% cost
13047                    let reliability = (1.0 - error_rate) * 100.0;
13048                    let speed = (100.0 - avg_latency / 50.0).max(0.0);
13049                    let cost_score = (100.0 - cost_per_call * 1000.0).max(0.0);
13050                    score = reliability * 0.4 + speed * 0.3 + cost_score * 0.3;
13051                }
13052            }
13053        }
13054
13055        BackendScore {
13056            name: e.name.clone(),
13057            enabled: e.enabled,
13058            circuit_open,
13059            total_calls: e.total_calls,
13060            error_rate: (error_rate * 10000.0).round() / 10000.0,
13061            avg_latency_ms: (avg_latency * 100.0).round() / 100.0,
13062            cost_per_call_usd: (cost_per_call * 10000.0).round() / 10000.0,
13063            total_cost_usd: e.total_cost_usd,
13064            score: (score * 100.0).round() / 100.0,
13065        }
13066    }).collect();
13067
13068    scores.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
13069    scores
13070}
13071
13072/// GET /v1/backends/ranking — rank all backends by strategy.
13073/// Query param: strategy (cheapest|fastest|most_reliable|balanced, default balanced).
13074async fn backends_ranking_handler(
13075    State(state): State<SharedState>,
13076    headers: HeaderMap,
13077    Query(params): Query<std::collections::HashMap<String, String>>,
13078) -> Result<Json<serde_json::Value>, StatusCode> {
13079    let s = state.lock().unwrap();
13080    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
13081
13082    let strategy = params.get("strategy").map(|s| s.as_str()).unwrap_or("balanced");
13083    let scores = compute_backend_scores(&s, strategy);
13084
13085    Ok(Json(serde_json::json!({
13086        "strategy": strategy,
13087        "backends": scores,
13088        "recommended": scores.first().map(|s| s.name.clone()),
13089    })))
13090}
13091
13092/// POST /v1/backends/select — auto-select optimal backend for execution.
13093/// Body: { "strategy": "cheapest|fastest|most_reliable|balanced" }
13094/// Returns the best backend name and its score.
13095async fn backends_select_handler(
13096    State(state): State<SharedState>,
13097    headers: HeaderMap,
13098    Json(payload): Json<serde_json::Value>,
13099) -> Result<Json<serde_json::Value>, StatusCode> {
13100    let s = state.lock().unwrap();
13101    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
13102
13103    let strategy = payload.get("strategy").and_then(|s| s.as_str()).unwrap_or("balanced");
13104    let scores = compute_backend_scores(&s, strategy);
13105
13106    match scores.first() {
13107        Some(best) => Ok(Json(serde_json::json!({
13108            "selected": best.name,
13109            "strategy": strategy,
13110            "score": best.score,
13111            "error_rate": best.error_rate,
13112            "avg_latency_ms": best.avg_latency_ms,
13113            "cost_per_call_usd": best.cost_per_call_usd,
13114            "circuit_open": best.circuit_open,
13115            "alternatives": scores.iter().skip(1).take(3).map(|s| {
13116                serde_json::json!({"name": s.name, "score": s.score})
13117            }).collect::<Vec<_>>(),
13118        }))),
13119        None => Ok(Json(serde_json::json!({
13120            "error": "no enabled backends with metrics available",
13121            "strategy": strategy,
13122        }))),
13123    }
13124}
13125
13126/// GET /v1/backends/dashboard — aggregate backend dashboard.
13127/// Returns per-backend summary (calls, cost, limits, circuit state, ranking)
13128/// plus fleet-wide aggregates and the balanced optimizer ranking.
13129async fn backends_dashboard_handler(
13130    State(state): State<SharedState>,
13131    headers: HeaderMap,
13132) -> Result<Json<serde_json::Value>, StatusCode> {
13133    let s = state.lock().unwrap();
13134    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
13135
13136    let now = std::time::SystemTime::now()
13137        .duration_since(std::time::UNIX_EPOCH)
13138        .unwrap_or_default()
13139        .as_secs();
13140
13141    // Per-backend summaries
13142    let mut backends_summary: Vec<serde_json::Value> = Vec::new();
13143    let mut fleet_total_calls: u64 = 0;
13144    let mut fleet_total_errors: u64 = 0;
13145    let mut fleet_total_tokens_input: u64 = 0;
13146    let mut fleet_total_tokens_output: u64 = 0;
13147    let mut fleet_total_cost_usd: f64 = 0.0;
13148    let mut fleet_total_latency_ms: u64 = 0;
13149    let mut backends_enabled: u32 = 0;
13150    let mut backends_circuit_open: u32 = 0;
13151    let mut backends_degraded: u32 = 0;
13152
13153    for entry in s.backend_registry.values() {
13154        let avg_latency = if entry.total_calls > 0 {
13155            entry.total_latency_ms as f64 / entry.total_calls as f64
13156        } else {
13157            0.0
13158        };
13159        let error_rate = if entry.total_calls > 0 {
13160            entry.total_errors as f64 / entry.total_calls as f64
13161        } else {
13162            0.0
13163        };
13164        let circuit_open = entry.circuit_open_until > 0 && now < entry.circuit_open_until;
13165        let circuit_state = if circuit_open {
13166            "open"
13167        } else if entry.consecutive_failures > 0 {
13168            "half-open"
13169        } else {
13170            "closed"
13171        };
13172
13173        let rpm_remaining = if entry.max_rpm > 0 {
13174            let in_window = now.saturating_sub(entry.rpm_window_start) < 60;
13175            if in_window { entry.max_rpm.saturating_sub(entry.rpm_count) } else { entry.max_rpm }
13176        } else {
13177            0
13178        };
13179        let tpm_remaining = if entry.max_tpm > 0 {
13180            let in_window = now.saturating_sub(entry.rpm_window_start) < 60;
13181            if in_window { entry.max_tpm.saturating_sub(entry.tpm_count) } else { entry.max_tpm }
13182        } else {
13183            0
13184        };
13185
13186        // Fleet aggregates
13187        fleet_total_calls += entry.total_calls;
13188        fleet_total_errors += entry.total_errors;
13189        fleet_total_tokens_input += entry.total_tokens_input;
13190        fleet_total_tokens_output += entry.total_tokens_output;
13191        fleet_total_cost_usd += entry.total_cost_usd;
13192        fleet_total_latency_ms += entry.total_latency_ms;
13193        if entry.enabled { backends_enabled += 1; }
13194        if circuit_open { backends_circuit_open += 1; }
13195        if entry.status == "degraded" { backends_degraded += 1; }
13196
13197        let mut rate_limits = serde_json::json!({
13198            "max_rpm": entry.max_rpm,
13199            "max_tpm": entry.max_tpm,
13200        });
13201        if entry.max_rpm > 0 {
13202            rate_limits["rpm_remaining"] = serde_json::json!(rpm_remaining);
13203        }
13204        if entry.max_tpm > 0 {
13205            rate_limits["tpm_remaining"] = serde_json::json!(tpm_remaining);
13206        }
13207
13208        backends_summary.push(serde_json::json!({
13209            "name": entry.name,
13210            "enabled": entry.enabled,
13211            "status": entry.status,
13212            "circuit_state": circuit_state,
13213            "consecutive_failures": entry.consecutive_failures,
13214            "total_calls": entry.total_calls,
13215            "total_errors": entry.total_errors,
13216            "error_rate": (error_rate * 10000.0).round() / 10000.0,
13217            "total_tokens_input": entry.total_tokens_input,
13218            "total_tokens_output": entry.total_tokens_output,
13219            "avg_latency_ms": (avg_latency * 100.0).round() / 100.0,
13220            "total_cost_usd": (entry.total_cost_usd * 10000.0).round() / 10000.0,
13221            "last_call_at": entry.last_call_at,
13222            "rate_limits": rate_limits,
13223            "fallback_chain": entry.fallback_chain,
13224        }));
13225    }
13226
13227    // Fleet-wide averages
13228    let fleet_avg_latency = if fleet_total_calls > 0 {
13229        fleet_total_latency_ms as f64 / fleet_total_calls as f64
13230    } else {
13231        0.0
13232    };
13233    let fleet_error_rate = if fleet_total_calls > 0 {
13234        fleet_total_errors as f64 / fleet_total_calls as f64
13235    } else {
13236        0.0
13237    };
13238
13239    // Balanced ranking
13240    let ranking = compute_backend_scores(&s, "balanced");
13241
13242    Ok(Json(serde_json::json!({
13243        "fleet": {
13244            "total_backends": s.backend_registry.len(),
13245            "backends_enabled": backends_enabled,
13246            "backends_circuit_open": backends_circuit_open,
13247            "backends_degraded": backends_degraded,
13248            "total_calls": fleet_total_calls,
13249            "total_errors": fleet_total_errors,
13250            "fleet_error_rate": (fleet_error_rate * 10000.0).round() / 10000.0,
13251            "total_tokens_input": fleet_total_tokens_input,
13252            "total_tokens_output": fleet_total_tokens_output,
13253            "total_tokens": fleet_total_tokens_input + fleet_total_tokens_output,
13254            "total_cost_usd": (fleet_total_cost_usd * 10000.0).round() / 10000.0,
13255            "avg_latency_ms": (fleet_avg_latency * 100.0).round() / 100.0,
13256        },
13257        "backends": backends_summary,
13258        "ranking": {
13259            "strategy": "balanced",
13260            "scores": ranking,
13261            "recommended": ranking.first().map(|s| s.name.clone()),
13262        },
13263    })))
13264}
13265
13266/// GET /v1/backends/{name}/health — health check history with transition analysis.
13267async fn backends_health_handler(
13268    State(state): State<SharedState>,
13269    headers: HeaderMap,
13270    Path(name): Path<String>,
13271) -> Result<Json<serde_json::Value>, StatusCode> {
13272    let s = state.lock().unwrap();
13273    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
13274
13275    let history = s.backend_health_history.get(&name).cloned().unwrap_or_default();
13276    let probe = s.backend_health_probes.get(&name);
13277    let registry = s.backend_registry.get(&name);
13278
13279    let current_status = registry.map(|r| r.status.as_str()).unwrap_or("unknown");
13280    let last_check_at = registry.map(|r| r.last_check_at).unwrap_or(0);
13281
13282    // Compute transition count
13283    let transitions: Vec<&HealthCheckRecord> = history.iter()
13284        .filter(|r| r.status != r.previous_status && r.previous_status != "unknown")
13285        .collect();
13286
13287    // Uptime calculation: count healthy checks / total checks
13288    let total_checks = history.len();
13289    let healthy_checks = history.iter().filter(|r| r.status == "healthy").count();
13290    let uptime_pct = if total_checks > 0 {
13291        (healthy_checks as f64 / total_checks as f64 * 10000.0).round() / 100.0
13292    } else {
13293        0.0
13294    };
13295
13296    // Average latency from history
13297    let avg_latency = if total_checks > 0 {
13298        let total: u64 = history.iter().map(|r| r.latency_ms).sum();
13299        (total as f64 / total_checks as f64 * 100.0).round() / 100.0
13300    } else {
13301        0.0
13302    };
13303
13304    let probe_info = probe.map(|p| serde_json::json!({
13305        "interval_secs": p.interval_secs,
13306        "unhealthy_threshold": p.unhealthy_threshold,
13307        "healthy_threshold": p.healthy_threshold,
13308        "timeout_ms": p.timeout_ms,
13309        "enabled": p.enabled,
13310        "consecutive_ok": p.consecutive_ok,
13311        "consecutive_fail": p.consecutive_fail,
13312    }));
13313
13314    Ok(Json(serde_json::json!({
13315        "backend": name,
13316        "current_status": current_status,
13317        "last_check_at": last_check_at,
13318        "probe": probe_info,
13319        "history": {
13320            "total_checks": total_checks,
13321            "healthy_checks": healthy_checks,
13322            "uptime_pct": uptime_pct,
13323            "avg_latency_ms": avg_latency,
13324            "transitions": transitions.len(),
13325            "records": history,
13326        },
13327    })))
13328}
13329
13330/// PUT /v1/backends/{name}/probe — configure health probe for a backend.
13331/// Body: { "interval_secs": 300, "unhealthy_threshold": 3, "healthy_threshold": 2, "timeout_ms": 10000, "enabled": true }
13332async fn backends_probe_put_handler(
13333    State(state): State<SharedState>,
13334    headers: HeaderMap,
13335    Path(name): Path<String>,
13336    Json(payload): Json<serde_json::Value>,
13337) -> Result<Json<serde_json::Value>, StatusCode> {
13338    let mut s = state.lock().unwrap();
13339    let client = client_key_from_headers(&headers);
13340    check_auth(&mut s, &headers, AccessLevel::Write)?;
13341
13342    let probe = s.backend_health_probes.entry(name.clone()).or_insert_with(|| {
13343        let mut p = BackendHealthProbe::default();
13344        p.backend = name.clone();
13345        p
13346    });
13347
13348    if let Some(v) = payload.get("interval_secs").and_then(|v| v.as_u64()) {
13349        probe.interval_secs = v;
13350    }
13351    if let Some(v) = payload.get("unhealthy_threshold").and_then(|v| v.as_u64()) {
13352        probe.unhealthy_threshold = v as u32;
13353    }
13354    if let Some(v) = payload.get("healthy_threshold").and_then(|v| v.as_u64()) {
13355        probe.healthy_threshold = v as u32;
13356    }
13357    if let Some(v) = payload.get("timeout_ms").and_then(|v| v.as_u64()) {
13358        probe.timeout_ms = v;
13359    }
13360    if let Some(v) = payload.get("enabled").and_then(|v| v.as_bool()) {
13361        probe.enabled = v;
13362    }
13363
13364    let probe_snapshot = probe.clone();
13365
13366    s.audit_log.record(&client, AuditAction::ConfigUpdate, "backend_probe",
13367        serde_json::json!({"action": "configure", "backend": &name}), true);
13368
13369    Ok(Json(serde_json::json!({
13370        "success": true,
13371        "backend": name,
13372        "probe": {
13373            "interval_secs": probe_snapshot.interval_secs,
13374            "unhealthy_threshold": probe_snapshot.unhealthy_threshold,
13375            "healthy_threshold": probe_snapshot.healthy_threshold,
13376            "timeout_ms": probe_snapshot.timeout_ms,
13377            "enabled": probe_snapshot.enabled,
13378        },
13379    })))
13380}
13381
13382/// GET /v1/backends/{name}/probe — get probe configuration.
13383async fn backends_probe_get_handler(
13384    State(state): State<SharedState>,
13385    headers: HeaderMap,
13386    Path(name): Path<String>,
13387) -> Result<Json<serde_json::Value>, StatusCode> {
13388    let s = state.lock().unwrap();
13389    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
13390
13391    match s.backend_health_probes.get(&name) {
13392        Some(probe) => Ok(Json(serde_json::json!({
13393            "backend": name,
13394            "probe": {
13395                "interval_secs": probe.interval_secs,
13396                "unhealthy_threshold": probe.unhealthy_threshold,
13397                "healthy_threshold": probe.healthy_threshold,
13398                "timeout_ms": probe.timeout_ms,
13399                "enabled": probe.enabled,
13400                "consecutive_ok": probe.consecutive_ok,
13401                "consecutive_fail": probe.consecutive_fail,
13402            },
13403        }))),
13404        None => Ok(Json(serde_json::json!({
13405            "backend": name,
13406            "probe": null,
13407            "message": "no probe configured",
13408        }))),
13409    }
13410}
13411
13412/// GET /v1/backends/health — fleet-wide health summary across all backends.
13413async fn backends_fleet_health_handler(
13414    State(state): State<SharedState>,
13415    headers: HeaderMap,
13416) -> Result<Json<serde_json::Value>, StatusCode> {
13417    let s = state.lock().unwrap();
13418    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
13419
13420    let mut backends_summary: Vec<serde_json::Value> = Vec::new();
13421    let mut total_healthy = 0u32;
13422    let mut total_degraded = 0u32;
13423    let mut total_unreachable = 0u32;
13424    let mut total_unknown = 0u32;
13425
13426    for (bname, entry) in &s.backend_registry {
13427        match entry.status.as_str() {
13428            "healthy" => total_healthy += 1,
13429            "degraded" => total_degraded += 1,
13430            "unreachable" => total_unreachable += 1,
13431            _ => total_unknown += 1,
13432        }
13433
13434        let history = s.backend_health_history.get(bname);
13435        let check_count = history.map(|h| h.len()).unwrap_or(0);
13436        let healthy_count = history.map(|h| h.iter().filter(|r| r.status == "healthy").count()).unwrap_or(0);
13437        let uptime = if check_count > 0 {
13438            (healthy_count as f64 / check_count as f64 * 10000.0).round() / 100.0
13439        } else {
13440            0.0
13441        };
13442
13443        let probe = s.backend_health_probes.get(bname);
13444
13445        backends_summary.push(serde_json::json!({
13446            "name": bname,
13447            "status": entry.status,
13448            "enabled": entry.enabled,
13449            "last_check_at": entry.last_check_at,
13450            "last_check_latency_ms": entry.last_check_latency_ms,
13451            "check_count": check_count,
13452            "uptime_pct": uptime,
13453            "probe_enabled": probe.map(|p| p.enabled).unwrap_or(false),
13454        }));
13455    }
13456
13457    backends_summary.sort_by(|a, b| {
13458        a["name"].as_str().unwrap_or("").cmp(b["name"].as_str().unwrap_or(""))
13459    });
13460
13461    Ok(Json(serde_json::json!({
13462        "fleet_health": {
13463            "total": s.backend_registry.len(),
13464            "healthy": total_healthy,
13465            "degraded": total_degraded,
13466            "unreachable": total_unreachable,
13467            "unknown": total_unknown,
13468        },
13469        "backends": backends_summary,
13470    })))
13471}
13472
13473/// Attempt execution with fallback: try primary, then each fallback in order.
13474/// Returns (result, actual_backend_used).
13475fn execute_with_fallback(
13476    state: &std::sync::Mutex<ServerState>,
13477    source: &str,
13478    source_file: &str,
13479    flow_name: &str,
13480    primary_backend: &str,
13481    primary_key: Option<&str>,
13482    // §Fase 37.b (D1) — the parsed request body for the Request
13483    // Binding Contract, threaded through to `runner::execute_server_flow`.
13484    request_body: Option<&serde_json::Value>,
13485    // §Fase 37.y (D3) — path + query maps threaded alongside the body
13486    // so the runner's binder sees the full three-source set.
13487    request_path: &std::collections::HashMap<String, String>,
13488    request_query: &std::collections::HashMap<String, String>,
13489) -> (Result<ServerExecutionResult, String>, String) {
13490    // Try primary
13491    let result = server_execute(
13492        source,
13493        source_file,
13494        flow_name,
13495        primary_backend,
13496        primary_key,
13497        request_body,
13498        request_path,
13499        request_query,
13500    );
13501    if result.is_ok() {
13502        return (result, primary_backend.to_string());
13503    }
13504
13505    // Get fallback chain from registry
13506    let chain = {
13507        let s = state.lock().unwrap();
13508        s.backend_registry.get(primary_backend)
13509            .map(|e| e.fallback_chain.clone())
13510            .unwrap_or_default()
13511    };
13512
13513    if chain.is_empty() {
13514        return (result, primary_backend.to_string());
13515    }
13516
13517    // Try each fallback
13518    let primary_err = result.unwrap_err();
13519    for fallback_backend in &chain {
13520        let fb_key = {
13521            let s = state.lock().unwrap();
13522            resolve_backend_key(&s, fallback_backend).ok()
13523        };
13524        let fb_result = server_execute(
13525            source,
13526            source_file,
13527            flow_name,
13528            fallback_backend,
13529            fb_key.as_deref(),
13530            request_body,
13531            request_path,
13532            request_query,
13533        );
13534        if fb_result.is_ok() {
13535            return (fb_result, fallback_backend.clone());
13536        }
13537    }
13538
13539    // All fallbacks failed — return original error
13540    (Err(primary_err), primary_backend.to_string())
13541}
13542
13543/// Full execution pipeline: resolve key → execute with fallback → record metrics.
13544/// Replaces the pattern: resolve_backend_key + server_execute + record_backend_metrics.
13545/// Call sites only need state, source, source_file, flow_name, backend.
13546fn server_execute_full(
13547    state: &std::sync::Mutex<ServerState>,
13548    source: &str,
13549    source_file: &str,
13550    flow_name: &str,
13551    backend: &str,
13552) -> (Result<ServerExecutionResult, String>, String) {
13553    // Auto-backend: if "auto", use optimizer to select best backend
13554    let effective_backend = if backend == "auto" {
13555        let s = state.lock().unwrap();
13556        let scores = compute_backend_scores(&s, "balanced");
13557        scores.first().map(|s| s.name.clone()).unwrap_or_else(|| "stub".to_string())
13558    } else {
13559        backend.to_string()
13560    };
13561
13562    // Check rate limit before execution
13563    {
13564        let mut s = state.lock().unwrap();
13565        if let Err(e) = check_backend_rate_limit(&mut s, &effective_backend) {
13566            return (Err(e), effective_backend);
13567        }
13568    }
13569
13570    // Resolve key from registry
13571    let resolved_key = {
13572        let s = state.lock().unwrap();
13573        resolve_backend_key(&s, &effective_backend).ok()
13574    };
13575
13576    // Execute with fallback chain. §Fase 37.b — `server_execute_full`
13577    // serves non-dynamic-route callers (CLI-style RPC, batch, pipeline
13578    // stages); there is no HTTP request body to bind, so `None`.
13579    // §Fase 37.y — non-dynamic-route callers also have no path or
13580    // query captures; empty maps.
13581    let empty_path = std::collections::HashMap::new();
13582    let empty_query = std::collections::HashMap::new();
13583    let (result, actual_backend) = execute_with_fallback(
13584        state, source, source_file, flow_name, &effective_backend, resolved_key.as_deref(),
13585        None,
13586        &empty_path,
13587        &empty_query,
13588    );
13589
13590    // Record metrics
13591    if let Ok(ref er) = result {
13592        let mut s = state.lock().unwrap();
13593        record_backend_metrics(
13594            &mut s, &actual_backend, er.success,
13595            er.tokens_input, er.tokens_output, er.latency_ms,
13596        );
13597    } else {
13598        let mut s = state.lock().unwrap();
13599        record_backend_metrics(&mut s, &actual_backend, false, 0, 0, 0);
13600    }
13601
13602    (result, actual_backend)
13603}
13604
13605pub fn resolve_backend_key(state: &ServerState, backend: &str) -> Result<String, String> {
13606    // 1. Server registry (inline key, enabled check, circuit breaker)
13607    if let Some(entry) = state.backend_registry.get(backend) {
13608        if !entry.enabled {
13609            return Err(format!("Backend '{}' is disabled in registry", backend));
13610        }
13611        // Circuit breaker check
13612        if entry.circuit_open_until > 0 {
13613            let now = std::time::SystemTime::now()
13614                .duration_since(std::time::UNIX_EPOCH)
13615                .unwrap_or_default()
13616                .as_secs();
13617            if now < entry.circuit_open_until {
13618                return Err(format!(
13619                    "Backend '{}' circuit is open ({} consecutive failures, recovers in {}s)",
13620                    backend, entry.consecutive_failures,
13621                    entry.circuit_open_until.saturating_sub(now)
13622                ));
13623            }
13624            // Cooldown expired — allow through (half-open state, success will close it)
13625        }
13626        if !entry.api_key.is_empty() {
13627            return Ok(entry.api_key.clone());
13628        }
13629    }
13630
13631    // 2. Per-tenant AWS SM cache (sync, zero-latency fast path)
13632    let tenant_id = crate::tenant::current_tenant_id();
13633    if let Some(key) = state.tenant_secrets.get_cached(&tenant_id, backend) {
13634        return Ok(key);
13635    }
13636
13637    // 3. Global env-var fallback
13638    crate::backend::get_api_key(backend).map_err(|e| e.message)
13639}
13640
13641// ── MCP Exposition (ℰMCP Server) ────────────────────────────────────────
13642
13643/// Extract anchor names from a flow's source for CSP constraint schema.
13644/// Best-effort: compiles the source and extracts anchor names from IR.
13645fn extract_flow_anchors(source: &str, flow_name: &str) -> Vec<String> {
13646    let tokens = match crate::lexer::Lexer::new(source, "mcp_schema").tokenize() {
13647        Ok(t) => t,
13648        Err(_) => return vec![],
13649    };
13650    let mut parser = crate::parser::Parser::new(tokens);
13651    let program = match parser.parse() {
13652        Ok(p) => p,
13653        Err(_) => return vec![],
13654    };
13655    let ir = crate::ir_generator::IRGenerator::new().generate(&program);
13656    ir.anchors.iter().map(|a| a.name.clone()).collect()
13657}
13658
13659/// Extract personas from a flow's deployed source for MCP prompts exposition.
13660fn extract_personas(source: &str) -> Vec<(String, Vec<String>, String, Option<f64>, String)> {
13661    let tokens = match crate::lexer::Lexer::new(source, "mcp_prompts").tokenize() {
13662        Ok(t) => t,
13663        Err(_) => return vec![],
13664    };
13665    let mut parser = crate::parser::Parser::new(tokens);
13666    let program = match parser.parse() {
13667        Ok(p) => p,
13668        Err(_) => return vec![],
13669    };
13670    let ir = crate::ir_generator::IRGenerator::new().generate(&program);
13671    ir.personas.iter().map(|p| (
13672        p.name.clone(), p.domain.clone(), p.tone.clone(),
13673        p.confidence_threshold, p.description.clone(),
13674    )).collect()
13675}
13676
13677/// Extract contexts from a flow's deployed source for MCP prompts exposition.
13678fn extract_contexts(source: &str) -> Vec<(String, String, String, Option<i64>, Option<f64>)> {
13679    let tokens = match crate::lexer::Lexer::new(source, "mcp_prompts").tokenize() {
13680        Ok(t) => t,
13681        Err(_) => return vec![],
13682    };
13683    let mut parser = crate::parser::Parser::new(tokens);
13684    let program = match parser.parse() {
13685        Ok(p) => p,
13686        Err(_) => return vec![],
13687    };
13688    let ir = crate::ir_generator::IRGenerator::new().generate(&program);
13689    ir.contexts.iter().map(|c| (
13690        c.name.clone(), c.memory_scope.clone(), c.depth.clone(),
13691        c.max_tokens, c.temperature,
13692    )).collect()
13693}
13694
13695/// The 47 cognitive primitives of AXON — the formal instruction set.
13696pub const AXON_COGNITIVE_PRIMITIVES: &[&str] = &[
13697    // Top-level declarations (20)
13698    "persona", "context", "flow", "anchor", "tool", "memory", "type",
13699    "agent", "shield", "pix", "psyche", "corpus", "dataspace",
13700    "ots", "mandate", "compute", "daemon", "axonstore", "axonendpoint", "lambda",
13701    // Step-level primitives (27)
13702    "step", "reason", "validate", "refine", "weave", "probe",
13703    "use", "remember", "recall",
13704    "know", "believe", "speculate", "doubt",
13705    "par", "hibernate", "deliberate", "consensus", "forge",
13706    "stream", "navigate", "drill", "trail", "corroborate",
13707    "focus", "associate", "aggregate", "explore",
13708];
13709
13710/// MCP tool descriptor for exposition.
13711#[derive(Debug, Clone, Serialize)]
13712pub struct McpExposedTool {
13713    pub name: String,
13714    pub description: String,
13715    pub input_schema: serde_json::Value,
13716}
13717
13718/// POST /v1/mcp — JSON-RPC 2.0 endpoint implementing MCP server protocol.
13719/// Exposes deployed AXON flows as MCP tools and server state as MCP resources.
13720/// Methods: initialize, tools/list, tools/call, resources/list, resources/read.
13721async fn mcp_handler(
13722    State(state): State<SharedState>,
13723    headers: HeaderMap,
13724    body: String,
13725) -> Result<Json<serde_json::Value>, StatusCode> {
13726    let rpc: serde_json::Value = serde_json::from_str(&body).unwrap_or_default();
13727    let method = rpc.get("method").and_then(|m| m.as_str()).unwrap_or("");
13728    let id = rpc.get("id").and_then(|i| i.as_u64()).unwrap_or(0);
13729    let params = rpc.get("params").cloned().unwrap_or(serde_json::json!({}));
13730
13731    match method {
13732        "initialize" => {
13733            Ok(Json(serde_json::json!({
13734                "jsonrpc": "2.0",
13735                "id": id,
13736                "result": {
13737                    "protocolVersion": "2024-11-05",
13738                    "capabilities": {
13739                        "tools": { "listChanged": false },
13740                        "resources": { "subscribe": false, "listChanged": false },
13741                        "prompts": { "listChanged": false }
13742                    },
13743                    "serverInfo": {
13744                        "name": "axon-server",
13745                        "version": env!("CARGO_PKG_VERSION"),
13746                    }
13747                }
13748            })))
13749        }
13750        "tools/list" => {
13751            let s = state.lock().unwrap();
13752            check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
13753
13754            let mut tools: Vec<serde_json::Value> = Vec::new();
13755            // Each deployed flow → MCP tool with CSP-derived schema (§5.3)
13756            for summary in s.versions.list_flows() {
13757                if let Some(active) = s.versions.get_active(&summary.flow_name) {
13758                    // Extract anchor constraints from flow IR for CSP schema
13759                    let anchors = extract_flow_anchors(&active.source, &summary.flow_name);
13760                    tools.push(serde_json::json!({
13761                        "name": format!("axon_{}", summary.flow_name),
13762                        "description": format!(
13763                            "Execute AXON flow '{}' (v{}) — ℰMCP tool with epistemic guarantees",
13764                            summary.flow_name, active.version
13765                        ),
13766                        "inputSchema": {
13767                            "type": "object",
13768                            "properties": {
13769                                "backend": {
13770                                    "type": "string",
13771                                    "description": "LLM backend provider",
13772                                    "default": "stub",
13773                                    "enum": crate::backend::SUPPORTED_BACKENDS,
13774                                },
13775                                "input": {
13776                                    "type": "string",
13777                                    "description": "Input data for the flow"
13778                                }
13779                            },
13780                            // CSP constraints (§5.3): anchors that bound the output space
13781                            "_axon_csp": {
13782                                "constraints": anchors,
13783                                "effect_row": "<io, epistemic:speculate>",
13784                                "output_taint": "Uncertainty",
13785                            }
13786                        }
13787                    }));
13788                }
13789            }
13790
13791            // AxonStore cognitive tools (CSP §5.3 schemas, Theorem 5.1)
13792            for store in s.axon_stores.values() {
13793                // persist tool — raw write, c=1.0, δ=raw
13794                tools.push(serde_json::json!({
13795                    "name": format!("axon_as_{}_persist", store.name),
13796                    "description": format!(
13797                        "Persist key-value entry into AxonStore '{}' (ontology: {}) — ΛD: c=1.0, δ=raw",
13798                        store.name, store.ontology
13799                    ),
13800                    "inputSchema": {
13801                        "type": "object",
13802                        "properties": {
13803                            "key": {
13804                                "type": "string",
13805                                "description": "Storage key for the entry",
13806                            },
13807                            "value": {
13808                                "description": "Entry payload (any JSON value)",
13809                            }
13810                        },
13811                        "required": ["key", "value"],
13812                        "_axon_csp": {
13813                            "constraints": [
13814                                format!("ontology ∈ {}", store.ontology),
13815                                "Theorem 5.1: raw persist → c=1.0",
13816                            ],
13817                            "effect_row": "<io, epistemic:know>",
13818                            "output_taint": "Raw",
13819                        }
13820                    }
13821                }));
13822
13823                // retrieve tool — read, c preserved from entry
13824                tools.push(serde_json::json!({
13825                    "name": format!("axon_as_{}_retrieve", store.name),
13826                    "description": format!(
13827                        "Retrieve entry by key from AxonStore '{}' with ΛD envelope — epistemic state preserved",
13828                        store.name
13829                    ),
13830                    "inputSchema": {
13831                        "type": "object",
13832                        "properties": {
13833                            "key": {
13834                                "type": "string",
13835                                "description": "Storage key to retrieve",
13836                            }
13837                        },
13838                        "required": ["key"],
13839                        "_axon_csp": {
13840                            "constraints": ["key ∈ store.entries", "envelope faithfully returned"],
13841                            "effect_row": "<io, epistemic:believe>",
13842                            "output_taint": "Preserved",
13843                        }
13844                    }
13845                }));
13846
13847                // mutate tool — derived, c≤0.99, δ=derived (Theorem 5.1)
13848                tools.push(serde_json::json!({
13849                    "name": format!("axon_as_{}_mutate", store.name),
13850                    "description": format!(
13851                        "Mutate existing entry in AxonStore '{}' — ΛD: c≤0.99, δ=derived (Theorem 5.1: only raw may carry c=1.0)",
13852                        store.name
13853                    ),
13854                    "inputSchema": {
13855                        "type": "object",
13856                        "properties": {
13857                            "key": {
13858                                "type": "string",
13859                                "description": "Key of entry to mutate (must exist)",
13860                            },
13861                            "value": {
13862                                "description": "New value (any JSON)",
13863                            }
13864                        },
13865                        "required": ["key", "value"],
13866                        "_axon_csp": {
13867                            "constraints": [
13868                                "key ∈ store.entries (pre-condition)",
13869                                "Theorem 5.1: mutation → c clamped ≤0.99, δ=derived",
13870                            ],
13871                            "effect_row": "<io, epistemic:speculate>",
13872                            "output_taint": "Uncertainty",
13873                        }
13874                    }
13875                }));
13876
13877                // purge tool — destructive delete
13878                tools.push(serde_json::json!({
13879                    "name": format!("axon_as_{}_purge", store.name),
13880                    "description": format!(
13881                        "Purge entry from AxonStore '{}' — irreversible deletion with audit trail",
13882                        store.name
13883                    ),
13884                    "inputSchema": {
13885                        "type": "object",
13886                        "properties": {
13887                            "key": {
13888                                "type": "string",
13889                                "description": "Key of entry to purge (must exist)",
13890                            }
13891                        },
13892                        "required": ["key"],
13893                        "_axon_csp": {
13894                            "constraints": ["key ∈ store.entries (pre-condition)", "irreversible"],
13895                            "effect_row": "<io, epistemic:know>",
13896                            "output_taint": "Void",
13897                        }
13898                    }
13899                }));
13900            }
13901
13902            // Dataspace cognitive tools (CSP §5.3 schemas)
13903            for ds in s.dataspaces.values() {
13904                // ingest tool
13905                tools.push(serde_json::json!({
13906                    "name": format!("axon_ds_{}_ingest", ds.name),
13907                    "description": format!(
13908                        "Ingest data into dataspace '{}' (ontology: {}) — ΛD: c=1.0, δ=raw",
13909                        ds.name, ds.ontology
13910                    ),
13911                    "inputSchema": {
13912                        "type": "object",
13913                        "properties": {
13914                            "ontology": {
13915                                "type": "string",
13916                                "description": "Ontological type tag for the entry",
13917                                "default": &ds.ontology,
13918                            },
13919                            "data": {
13920                                "description": "Entry payload (any JSON value)",
13921                            },
13922                            "tags": {
13923                                "type": "array",
13924                                "items": { "type": "string" },
13925                                "description": "Tags for filtering and grouping",
13926                            }
13927                        },
13928                        "required": ["data"],
13929                        "_axon_csp": {
13930                            "constraints": [format!("ontology ∈ {}", ds.ontology)],
13931                            "effect_row": "<io, epistemic:know>",
13932                            "output_taint": "Raw",
13933                        }
13934                    }
13935                }));
13936
13937                // focus tool
13938                tools.push(serde_json::json!({
13939                    "name": format!("axon_ds_{}_focus", ds.name),
13940                    "description": format!(
13941                        "Filter entries in dataspace '{}' by ontology/tags — ΛD: c≤0.99, δ=derived (Theorem 5.1)",
13942                        ds.name
13943                    ),
13944                    "inputSchema": {
13945                        "type": "object",
13946                        "properties": {
13947                            "ontology": {
13948                                "type": "string",
13949                                "description": "Filter by ontological type",
13950                            },
13951                            "tags": {
13952                                "type": "array",
13953                                "items": { "type": "string" },
13954                                "description": "Filter by tags (all must match)",
13955                            },
13956                            "limit": {
13957                                "type": "integer",
13958                                "description": "Max results to return",
13959                                "default": 100,
13960                            }
13961                        },
13962                        "_axon_csp": {
13963                            "constraints": ["result ⊆ dataspace", "Theorem 5.1: derived"],
13964                            "effect_row": "<io, epistemic:speculate>",
13965                            "output_taint": "Uncertainty",
13966                        }
13967                    }
13968                }));
13969
13970                // aggregate tool
13971                tools.push(serde_json::json!({
13972                    "name": format!("axon_ds_{}_aggregate", ds.name),
13973                    "description": format!(
13974                        "Aggregate entries in dataspace '{}' (count/sum/avg/min/max) — ΛD: c≤0.99, δ=aggregated",
13975                        ds.name
13976                    ),
13977                    "inputSchema": {
13978                        "type": "object",
13979                        "properties": {
13980                            "op": {
13981                                "type": "string",
13982                                "enum": ["count", "sum", "avg", "min", "max"],
13983                                "description": "Aggregation operation",
13984                            },
13985                            "field": {
13986                                "type": "string",
13987                                "description": "Dot-path to numeric field (e.g., 'score')",
13988                            },
13989                            "ontology": {
13990                                "type": "string",
13991                                "description": "Filter by ontological type before aggregating",
13992                            }
13993                        },
13994                        "required": ["op"],
13995                        "_axon_csp": {
13996                            "constraints": ["op ∈ {count,sum,avg,min,max}", "Theorem 5.1: aggregated"],
13997                            "effect_row": "<io, epistemic:speculate>",
13998                            "output_taint": "Uncertainty",
13999                        }
14000                    }
14001                }));
14002            }
14003
14004            // Shield cognitive tools (CSP §5.3 schemas)
14005            for sh in s.shields.values() {
14006                // evaluate tool
14007                tools.push(serde_json::json!({
14008                    "name": format!("axon_sh_{}_evaluate", sh.name),
14009                    "description": format!(
14010                        "Evaluate content against shield '{}' ({} rules, mode: {}) — ΛD: c≤0.99, δ=derived",
14011                        sh.name, sh.rules.len(), sh.mode
14012                    ),
14013                    "inputSchema": {
14014                        "type": "object",
14015                        "properties": {
14016                            "content": {
14017                                "type": "string",
14018                                "description": "Content to evaluate against guardrails",
14019                            },
14020                            "direction": {
14021                                "type": "string",
14022                                "enum": ["input", "output"],
14023                                "description": "Direction: input (pre-execution) or output (post-execution)",
14024                                "default": "input",
14025                            }
14026                        },
14027                        "required": ["content"],
14028                        "_axon_csp": {
14029                            "constraints": [
14030                                format!("mode ∈ {}", sh.mode),
14031                                "Theorem 5.1: pattern matching is approximate (δ=derived)",
14032                            ],
14033                            "effect_row": "<io, epistemic:speculate>",
14034                            "output_taint": "Uncertainty",
14035                        }
14036                    }
14037                }));
14038            }
14039
14040            // Corpus cognitive tools (CSP §5.3 schemas)
14041            for corpus in s.corpora.values() {
14042                // search tool
14043                tools.push(serde_json::json!({
14044                    "name": format!("axon_corpus_{}_search", corpus.name),
14045                    "description": format!(
14046                        "Search corpus '{}' ({} docs, ontology: {}) — ΛD: c≤0.99, δ=derived",
14047                        corpus.name, corpus.documents.len(), corpus.ontology
14048                    ),
14049                    "inputSchema": {
14050                        "type": "object",
14051                        "properties": {
14052                            "query": {
14053                                "type": "string",
14054                                "description": "Search query (keyword-based)",
14055                            },
14056                            "tags": {
14057                                "type": "array",
14058                                "items": { "type": "string" },
14059                                "description": "Filter by tags (all must match)",
14060                            },
14061                            "limit": {
14062                                "type": "integer",
14063                                "description": "Max results",
14064                                "default": 10,
14065                            }
14066                        },
14067                        "required": ["query"],
14068                        "_axon_csp": {
14069                            "constraints": [
14070                                format!("ontology ∈ {}", corpus.ontology),
14071                                "Theorem 5.1: relevance scoring is approximate",
14072                            ],
14073                            "effect_row": "<io, epistemic:speculate>",
14074                            "output_taint": "Uncertainty",
14075                        }
14076                    }
14077                }));
14078
14079                // cite tool
14080                tools.push(serde_json::json!({
14081                    "name": format!("axon_corpus_{}_cite", corpus.name),
14082                    "description": format!(
14083                        "Generate citations from corpus '{}' — ΛD: c≤0.99, δ=derived (excerpt extraction is interpretive)",
14084                        corpus.name
14085                    ),
14086                    "inputSchema": {
14087                        "type": "object",
14088                        "properties": {
14089                            "query": {
14090                                "type": "string",
14091                                "description": "Citation query",
14092                            },
14093                            "max_citations": {
14094                                "type": "integer",
14095                                "description": "Max citations to return",
14096                                "default": 5,
14097                            },
14098                            "excerpt_length": {
14099                                "type": "integer",
14100                                "description": "Excerpt length in characters",
14101                                "default": 200,
14102                            }
14103                        },
14104                        "required": ["query"],
14105                        "_axon_csp": {
14106                            "constraints": ["Theorem 5.1: citation extraction is interpretive (δ=derived)"],
14107                            "effect_row": "<io, epistemic:speculate>",
14108                            "output_taint": "Uncertainty",
14109                        }
14110                    }
14111                }));
14112            }
14113
14114            // Compute cognitive tool (CSP §5.3 schema)
14115            tools.push(serde_json::json!({
14116                "name": "axon_compute_evaluate",
14117                "description": "Evaluate arithmetic/symbolic expression — ΛD: c=1.0 exact, c=0.99 approximate",
14118                "inputSchema": {
14119                    "type": "object",
14120                    "properties": {
14121                        "expression": {
14122                            "type": "string",
14123                            "description": "Math expression (e.g., '2*(3+4)^2', 'sqrt(x^2+y^2)')",
14124                        },
14125                        "variables": {
14126                            "type": "object",
14127                            "description": "Named variables (e.g., {\"x\": 10, \"y\": 5})",
14128                        }
14129                    },
14130                    "required": ["expression"],
14131                    "_axon_csp": {
14132                        "constraints": ["exact int → c=1.0", "float/transcendental → c=0.99", "Theorem 5.1"],
14133                        "effect_row": "<compute, epistemic:know|speculate>",
14134                        "output_taint": "Exact|Uncertainty",
14135                    }
14136                }
14137            }));
14138
14139            // Mandate cognitive tools (CSP §5.3 schemas)
14140            for mandate in s.mandates.values() {
14141                tools.push(serde_json::json!({
14142                    "name": format!("axon_mandate_{}_evaluate", mandate.name),
14143                    "description": format!(
14144                        "Evaluate access request against mandate '{}' ({} rules) — ΛD: c=1.0 explicit match, c=0.99 default deny",
14145                        mandate.name, mandate.rules.len()
14146                    ),
14147                    "inputSchema": {
14148                        "type": "object",
14149                        "properties": {
14150                            "subject": {
14151                                "type": "string",
14152                                "description": "Subject (role or principal)",
14153                                "default": "anonymous",
14154                            },
14155                            "action": {
14156                                "type": "string",
14157                                "description": "Action to authorize (e.g., 'execute', 'read', 'delete')",
14158                            },
14159                            "resource": {
14160                                "type": "string",
14161                                "description": "Resource path (e.g., '/v1/flows/analyze')",
14162                            }
14163                        },
14164                        "required": ["action", "resource"],
14165                        "_axon_csp": {
14166                            "constraints": ["first-match-wins with priority ordering", "default deny if no rule matches"],
14167                            "effect_row": "<io, epistemic:know|speculate>",
14168                            "output_taint": "Raw|Uncertainty",
14169                        }
14170                    }
14171                }));
14172            }
14173
14174            // Forge cognitive tools (CSP §5.3 schemas)
14175            for forge in s.forges.values() {
14176                let template_names: Vec<&str> = forge.templates.keys().map(|k| k.as_str()).collect();
14177                tools.push(serde_json::json!({
14178                    "name": format!("axon_forge_{}_render", forge.name),
14179                    "description": format!(
14180                        "Render template artifact in forge '{}' (templates: {:?}) — ΛD: c=0.99, δ=derived",
14181                        forge.name, template_names
14182                    ),
14183                    "inputSchema": {
14184                        "type": "object",
14185                        "properties": {
14186                            "template": {
14187                                "type": "string",
14188                                "description": "Template name to render",
14189                            },
14190                            "variables": {
14191                                "type": "object",
14192                                "description": "Variables for {{placeholder}} substitution",
14193                            }
14194                        },
14195                        "required": ["template", "variables"],
14196                        "_axon_csp": {
14197                            "constraints": ["all {{variables}} must be provided", "Theorem 5.1: template rendering is derived"],
14198                            "effect_row": "<io, epistemic:believe>",
14199                            "output_taint": "Uncertainty",
14200                        }
14201                    }
14202                }));
14203            }
14204
14205            Ok(Json(serde_json::json!({
14206                "jsonrpc": "2.0",
14207                "id": id,
14208                "result": { "tools": tools }
14209            })))
14210        }
14211        "tools/call" => {
14212            let tool_name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
14213            let arguments = params.get("arguments").cloned().unwrap_or(serde_json::json!({}));
14214
14215            // ── Dataspace tool dispatch (axon_ds_{name}_{op}) ──
14216            if let Some(ds_suffix) = tool_name.strip_prefix("axon_ds_") {
14217                // Parse: "{dataspace_name}_{op}" where op is ingest|focus|aggregate
14218                let (ds_name, op) = if let Some(pos) = ds_suffix.rfind('_') {
14219                    (&ds_suffix[..pos], &ds_suffix[pos+1..])
14220                } else {
14221                    return Ok(Json(serde_json::json!({
14222                        "jsonrpc": "2.0", "id": id,
14223                        "error": { "code": -32602, "message": format!("invalid dataspace tool name: {}", tool_name) },
14224                        "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14225                    })));
14226                };
14227
14228                let mut s = state.lock().unwrap();
14229                let client = client_key_from_headers(&headers);
14230                check_auth(&mut s, &headers, AccessLevel::Write)?;
14231
14232                let ds = match s.dataspaces.get_mut(ds_name) {
14233                    Some(d) => d,
14234                    None => return Ok(Json(serde_json::json!({
14235                        "jsonrpc": "2.0", "id": id,
14236                        "error": { "code": -32602, "message": format!("dataspace '{}' not found", ds_name) },
14237                        "_axon_blame": { "blame": "caller", "reason": "CT-2: referenced non-existent dataspace" }
14238                    }))),
14239                };
14240
14241                match op {
14242                    "ingest" => {
14243                        let entry_ontology = arguments.get("ontology").and_then(|v| v.as_str())
14244                            .unwrap_or(&ds.ontology).to_string();
14245                        let data = arguments.get("data").cloned().unwrap_or(serde_json::json!(null));
14246                        let tags: Vec<String> = arguments.get("tags")
14247                            .and_then(|v| serde_json::from_value(v.clone()).ok())
14248                            .unwrap_or_default();
14249
14250                        let now = std::time::SystemTime::now()
14251                            .duration_since(std::time::UNIX_EPOCH)
14252                            .unwrap_or_default()
14253                            .as_secs();
14254
14255                        let entry_id = format!("ds_{}_{}", ds_name, ds.next_id);
14256                        ds.next_id += 1;
14257
14258                        let envelope = EpistemicEnvelope::raw_config(&entry_ontology, &client);
14259                        let entry = DataspaceEntry {
14260                            id: entry_id.clone(),
14261                            ontology: entry_ontology.clone(),
14262                            data: data.clone(),
14263                            envelope,
14264                            ingested_at: now,
14265                            tags,
14266                        };
14267                        ds.entries.insert(entry_id.clone(), entry);
14268                        ds.total_ops += 1;
14269
14270                        return Ok(Json(serde_json::json!({
14271                            "jsonrpc": "2.0", "id": id,
14272                            "result": {
14273                                "content": [{ "type": "text", "text": format!("Ingested entry {} into dataspace {}", entry_id, ds_name) }],
14274                                "isError": false,
14275                                "_axon": {
14276                                    "dataspace": ds_name, "entry_id": entry_id,
14277                                    "epistemic_envelope": { "certainty": 1.0, "derivation": "raw" },
14278                                    "lattice_position": "know",
14279                                    "effect_row": ["io", "epistemic:know"],
14280                                    "blame": "none",
14281                                }
14282                            }
14283                        })));
14284                    }
14285                    "focus" => {
14286                        let filter_ontology = arguments.get("ontology").and_then(|v| v.as_str());
14287                        let filter_tags: Option<Vec<String>> = arguments.get("tags")
14288                            .and_then(|v| serde_json::from_value(v.clone()).ok());
14289                        let limit = arguments.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
14290
14291                        let results: Vec<serde_json::Value> = ds.entries.values()
14292                            .filter(|e| {
14293                                if let Some(ont) = filter_ontology {
14294                                    if e.ontology != ont { return false; }
14295                                }
14296                                if let Some(ref tags) = filter_tags {
14297                                    if !tags.iter().all(|t| e.tags.contains(t)) { return false; }
14298                                }
14299                                true
14300                            })
14301                            .take(limit)
14302                            .map(|e| serde_json::json!({
14303                                "id": e.id, "ontology": e.ontology, "data": e.data, "tags": e.tags,
14304                            }))
14305                            .collect();
14306
14307                        let result_text = serde_json::to_string_pretty(&results).unwrap_or_default();
14308
14309                        return Ok(Json(serde_json::json!({
14310                            "jsonrpc": "2.0", "id": id,
14311                            "result": {
14312                                "content": [{ "type": "text", "text": result_text }],
14313                                "isError": false,
14314                                "_axon": {
14315                                    "dataspace": ds_name, "matched": results.len(),
14316                                    "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
14317                                    "lattice_position": "speculate",
14318                                    "effect_row": ["io", "epistemic:speculate"],
14319                                    "blame": "none",
14320                                }
14321                            }
14322                        })));
14323                    }
14324                    "aggregate" => {
14325                        let agg_op = arguments.get("op").and_then(|v| v.as_str()).unwrap_or("count");
14326                        let field = arguments.get("field").and_then(|v| v.as_str()).unwrap_or("");
14327                        let filter_ontology = arguments.get("ontology").and_then(|v| v.as_str());
14328
14329                        let filtered: Vec<&DataspaceEntry> = ds.entries.values()
14330                            .filter(|e| filter_ontology.map_or(true, |ont| e.ontology == ont))
14331                            .collect();
14332
14333                        let extract_number = |entry: &DataspaceEntry| -> Option<f64> {
14334                            let parts: Vec<&str> = field.split('.').collect();
14335                            let mut current = &entry.data;
14336                            for part in &parts {
14337                                if *part == "data" { continue; }
14338                                current = current.get(part)?;
14339                            }
14340                            current.as_f64()
14341                        };
14342
14343                        let result_val: serde_json::Value = match agg_op {
14344                            "count" => serde_json::json!(filtered.len()),
14345                            "sum" => {
14346                                let sum: f64 = filtered.iter().filter_map(|e| extract_number(e)).sum();
14347                                serde_json::json!(sum)
14348                            }
14349                            "avg" => {
14350                                let vals: Vec<f64> = filtered.iter().filter_map(|e| extract_number(e)).collect();
14351                                if vals.is_empty() { serde_json::json!(0.0) }
14352                                else { serde_json::json!((vals.iter().sum::<f64>() / vals.len() as f64 * 10000.0).round() / 10000.0) }
14353                            }
14354                            "min" => {
14355                                let min = filtered.iter().filter_map(|e| extract_number(e)).fold(f64::INFINITY, f64::min);
14356                                if min.is_infinite() { serde_json::json!(null) } else { serde_json::json!(min) }
14357                            }
14358                            "max" => {
14359                                let max = filtered.iter().filter_map(|e| extract_number(e)).fold(f64::NEG_INFINITY, f64::max);
14360                                if max.is_infinite() { serde_json::json!(null) } else { serde_json::json!(max) }
14361                            }
14362                            _ => return Ok(Json(serde_json::json!({
14363                                "jsonrpc": "2.0", "id": id,
14364                                "error": { "code": -32602, "message": format!("unknown aggregate op '{}'", agg_op) },
14365                                "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14366                            }))),
14367                        };
14368
14369                        return Ok(Json(serde_json::json!({
14370                            "jsonrpc": "2.0", "id": id,
14371                            "result": {
14372                                "content": [{ "type": "text", "text": format!("{}: {}", agg_op, result_val) }],
14373                                "isError": false,
14374                                "_axon": {
14375                                    "dataspace": ds_name, "op": agg_op, "result": result_val,
14376                                    "entries_considered": filtered.len(),
14377                                    "epistemic_envelope": { "certainty": 0.99, "derivation": "aggregated" },
14378                                    "lattice_position": "speculate",
14379                                    "effect_row": ["io", "epistemic:speculate"],
14380                                    "blame": "none",
14381                                }
14382                            }
14383                        })));
14384                    }
14385                    _ => {
14386                        return Ok(Json(serde_json::json!({
14387                            "jsonrpc": "2.0", "id": id,
14388                            "error": { "code": -32602, "message": format!("unknown dataspace op '{}' in tool '{}'", op, tool_name) },
14389                            "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14390                        })));
14391                    }
14392                }
14393            }
14394
14395            // ── AxonStore tool dispatch (axon_as_{name}_{op}) ──
14396            if let Some(as_suffix) = tool_name.strip_prefix("axon_as_") {
14397                // Parse: "{store_name}_{op}" where op is persist|retrieve|mutate|purge
14398                let (store_name, op) = if let Some(pos) = as_suffix.rfind('_') {
14399                    (&as_suffix[..pos], &as_suffix[pos+1..])
14400                } else {
14401                    return Ok(Json(serde_json::json!({
14402                        "jsonrpc": "2.0", "id": id,
14403                        "error": { "code": -32602, "message": format!("invalid axonstore tool name: {}", tool_name) },
14404                        "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14405                    })));
14406                };
14407
14408                let mut s = state.lock().unwrap();
14409                let client = client_key_from_headers(&headers);
14410                check_auth(&mut s, &headers, AccessLevel::Write)?;
14411
14412                let store = match s.axon_stores.get_mut(store_name) {
14413                    Some(st) => st,
14414                    None => return Ok(Json(serde_json::json!({
14415                        "jsonrpc": "2.0", "id": id,
14416                        "error": { "code": -32602, "message": format!("axonstore '{}' not found", store_name) },
14417                        "_axon_blame": { "blame": "caller", "reason": "CT-2: referenced non-existent axonstore" }
14418                    }))),
14419                };
14420
14421                match op {
14422                    "persist" => {
14423                        let key = arguments.get("key").and_then(|v| v.as_str()).unwrap_or("").to_string();
14424                        let value = arguments.get("value").cloned().unwrap_or(serde_json::json!(null));
14425
14426                        if key.is_empty() {
14427                            return Ok(Json(serde_json::json!({
14428                                "jsonrpc": "2.0", "id": id,
14429                                "error": { "code": -32602, "message": "key is required" },
14430                                "_axon_blame": { "blame": "caller", "reason": "CT-2: missing required parameter" }
14431                            })));
14432                        }
14433
14434                        let now = std::time::SystemTime::now()
14435                            .duration_since(std::time::UNIX_EPOCH)
14436                            .unwrap_or_default()
14437                            .as_secs();
14438
14439                        // ΛD: persist = raw write → c=1.0, δ=raw
14440                        let envelope = EpistemicEnvelope::raw_config(&store.ontology, &client);
14441
14442                        let entry = AxonStoreEntry {
14443                            key: key.clone(),
14444                            value: value.clone(),
14445                            envelope,
14446                            created_at: now,
14447                            updated_at: now,
14448                            version: 1,
14449                        };
14450
14451                        store.entries.insert(key.clone(), entry);
14452                        store.total_ops += 1;
14453
14454                        return Ok(Json(serde_json::json!({
14455                            "jsonrpc": "2.0", "id": id,
14456                            "result": {
14457                                "content": [{ "type": "text", "text": format!("Persisted key '{}' in axonstore '{}'", key, store_name) }],
14458                                "isError": false,
14459                                "_axon": {
14460                                    "store": store_name, "key": key, "version": 1,
14461                                    "epistemic_envelope": { "certainty": 1.0, "derivation": "raw" },
14462                                    "lattice_position": "know",
14463                                    "effect_row": ["io", "epistemic:know"],
14464                                    "blame": "none",
14465                                }
14466                            }
14467                        })));
14468                    }
14469                    "retrieve" => {
14470                        let key = arguments.get("key").and_then(|v| v.as_str()).unwrap_or("").to_string();
14471
14472                        if key.is_empty() {
14473                            return Ok(Json(serde_json::json!({
14474                                "jsonrpc": "2.0", "id": id,
14475                                "error": { "code": -32602, "message": "key is required" },
14476                                "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14477                            })));
14478                        }
14479
14480                        match store.entries.get(&key) {
14481                            Some(entry) => {
14482                                let result_text = serde_json::to_string_pretty(&serde_json::json!({
14483                                    "key": entry.key,
14484                                    "value": entry.value,
14485                                    "version": entry.version,
14486                                    "envelope": {
14487                                        "ontology": entry.envelope.ontology,
14488                                        "certainty": entry.envelope.certainty,
14489                                        "provenance": entry.envelope.provenance,
14490                                        "derivation": entry.envelope.derivation,
14491                                    }
14492                                })).unwrap_or_default();
14493
14494                                return Ok(Json(serde_json::json!({
14495                                    "jsonrpc": "2.0", "id": id,
14496                                    "result": {
14497                                        "content": [{ "type": "text", "text": result_text }],
14498                                        "isError": false,
14499                                        "_axon": {
14500                                            "store": store_name, "key": key, "found": true,
14501                                            "epistemic_envelope": {
14502                                                "certainty": entry.envelope.certainty,
14503                                                "derivation": &entry.envelope.derivation,
14504                                            },
14505                                            "lattice_position": "believe",
14506                                            "effect_row": ["io", "epistemic:believe"],
14507                                            "blame": "none",
14508                                        }
14509                                    }
14510                                })));
14511                            }
14512                            None => {
14513                                return Ok(Json(serde_json::json!({
14514                                    "jsonrpc": "2.0", "id": id,
14515                                    "result": {
14516                                        "content": [{ "type": "text", "text": format!("Key '{}' not found in axonstore '{}'", key, store_name) }],
14517                                        "isError": false,
14518                                        "_axon": {
14519                                            "store": store_name, "key": key, "found": false,
14520                                            "epistemic_envelope": { "certainty": 0.0, "derivation": "absent" },
14521                                            "lattice_position": "doubt",
14522                                            "effect_row": ["io", "epistemic:doubt"],
14523                                            "blame": "none",
14524                                        }
14525                                    }
14526                                })));
14527                            }
14528                        }
14529                    }
14530                    "mutate" => {
14531                        let key = arguments.get("key").and_then(|v| v.as_str()).unwrap_or("").to_string();
14532                        let value = arguments.get("value").cloned().unwrap_or(serde_json::json!(null));
14533
14534                        if key.is_empty() {
14535                            return Ok(Json(serde_json::json!({
14536                                "jsonrpc": "2.0", "id": id,
14537                                "error": { "code": -32602, "message": "key is required" },
14538                                "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14539                            })));
14540                        }
14541
14542                        match store.entries.get_mut(&key) {
14543                            Some(entry) => {
14544                                let now = std::time::SystemTime::now()
14545                                    .duration_since(std::time::UNIX_EPOCH)
14546                                    .unwrap_or_default()
14547                                    .as_secs();
14548
14549                                entry.value = value;
14550                                entry.version += 1;
14551                                entry.updated_at = now;
14552                                // ΛD Theorem 5.1: mutation degrades certainty
14553                                entry.envelope = EpistemicEnvelope::derived(&store.ontology, 0.99, &client);
14554
14555                                store.total_ops += 1;
14556                                let version = entry.version;
14557
14558                                return Ok(Json(serde_json::json!({
14559                                    "jsonrpc": "2.0", "id": id,
14560                                    "result": {
14561                                        "content": [{ "type": "text", "text": format!("Mutated key '{}' in axonstore '{}' → v{}", key, store_name, version) }],
14562                                        "isError": false,
14563                                        "_axon": {
14564                                            "store": store_name, "key": key, "version": version,
14565                                            "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
14566                                            "lattice_position": "speculate",
14567                                            "effect_row": ["io", "epistemic:speculate"],
14568                                            "blame": "none",
14569                                        }
14570                                    }
14571                                })));
14572                            }
14573                            None => {
14574                                return Ok(Json(serde_json::json!({
14575                                    "jsonrpc": "2.0", "id": id,
14576                                    "error": { "code": -32602, "message": format!("key '{}' not found in axonstore '{}'", key, store_name) },
14577                                    "_axon_blame": { "blame": "caller", "reason": "CT-2: mutate target absent" }
14578                                })));
14579                            }
14580                        }
14581                    }
14582                    "purge" => {
14583                        let key = arguments.get("key").and_then(|v| v.as_str()).unwrap_or("").to_string();
14584
14585                        if key.is_empty() {
14586                            return Ok(Json(serde_json::json!({
14587                                "jsonrpc": "2.0", "id": id,
14588                                "error": { "code": -32602, "message": "key is required" },
14589                                "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14590                            })));
14591                        }
14592
14593                        match store.entries.remove(&key) {
14594                            Some(_) => {
14595                                store.total_ops += 1;
14596
14597                                return Ok(Json(serde_json::json!({
14598                                    "jsonrpc": "2.0", "id": id,
14599                                    "result": {
14600                                        "content": [{ "type": "text", "text": format!("Purged key '{}' from axonstore '{}'", key, store_name) }],
14601                                        "isError": false,
14602                                        "_axon": {
14603                                            "store": store_name, "key": key, "purged": true,
14604                                            "epistemic_envelope": { "certainty": 1.0, "derivation": "void" },
14605                                            "lattice_position": "know",
14606                                            "effect_row": ["io", "epistemic:know"],
14607                                            "blame": "none",
14608                                        }
14609                                    }
14610                                })));
14611                            }
14612                            None => {
14613                                return Ok(Json(serde_json::json!({
14614                                    "jsonrpc": "2.0", "id": id,
14615                                    "error": { "code": -32602, "message": format!("key '{}' not found in axonstore '{}'", key, store_name) },
14616                                    "_axon_blame": { "blame": "caller", "reason": "CT-2: purge target absent" }
14617                                })));
14618                            }
14619                        }
14620                    }
14621                    _ => {
14622                        return Ok(Json(serde_json::json!({
14623                            "jsonrpc": "2.0", "id": id,
14624                            "error": { "code": -32602, "message": format!("unknown axonstore op '{}' in tool '{}'", op, tool_name) },
14625                            "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14626                        })));
14627                    }
14628                }
14629            }
14630
14631            // ── Shield tool dispatch (axon_sh_{name}_evaluate) ──
14632            if let Some(sh_suffix) = tool_name.strip_prefix("axon_sh_") {
14633                let (sh_name, op) = if let Some(pos) = sh_suffix.rfind('_') {
14634                    (&sh_suffix[..pos], &sh_suffix[pos+1..])
14635                } else {
14636                    return Ok(Json(serde_json::json!({
14637                        "jsonrpc": "2.0", "id": id,
14638                        "error": { "code": -32602, "message": format!("invalid shield tool name: {}", tool_name) },
14639                        "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14640                    })));
14641                };
14642
14643                if op == "evaluate" {
14644                    let mut s = state.lock().unwrap();
14645                    check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
14646
14647                    let content = arguments.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
14648                    let direction = arguments.get("direction").and_then(|v| v.as_str()).unwrap_or("input");
14649
14650                    let shield = match s.shields.get_mut(sh_name) {
14651                        Some(sh) => sh,
14652                        None => return Ok(Json(serde_json::json!({
14653                            "jsonrpc": "2.0", "id": id,
14654                            "error": { "code": -32602, "message": format!("shield '{}' not found", sh_name) },
14655                            "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14656                        }))),
14657                    };
14658
14659                    let mode_ok = match shield.mode.as_str() {
14660                        "both" => true,
14661                        m => m == direction,
14662                    };
14663                    if !mode_ok {
14664                        return Ok(Json(serde_json::json!({
14665                            "jsonrpc": "2.0", "id": id,
14666                            "error": { "code": -32602, "message": format!("shield mode '{}' incompatible with direction '{}'", shield.mode, direction) },
14667                        })));
14668                    }
14669
14670                    let result = shield.evaluate(&content);
14671                    shield.total_evaluations += 1;
14672                    if result.blocked { shield.total_blocks += 1; }
14673
14674                    let certainty = if result.rules_triggered == 0 { 0.95 } else { 0.85 };
14675                    let result_text = serde_json::to_string_pretty(&serde_json::json!({
14676                        "blocked": result.blocked,
14677                        "warnings": result.warnings,
14678                        "redactions": result.redactions,
14679                        "content": result.content,
14680                        "rules_evaluated": result.rules_evaluated,
14681                        "rules_triggered": result.rules_triggered,
14682                    })).unwrap_or_default();
14683
14684                    return Ok(Json(serde_json::json!({
14685                        "jsonrpc": "2.0", "id": id,
14686                        "result": {
14687                            "content": [{ "type": "text", "text": result_text }],
14688                            "isError": false,
14689                            "_axon": {
14690                                "shield": sh_name, "blocked": result.blocked,
14691                                "epistemic_envelope": { "certainty": certainty, "derivation": "derived" },
14692                                "lattice_position": if result.blocked { "doubt" } else { "speculate" },
14693                                "effect_row": ["io", "epistemic:speculate"],
14694                                "blame": "none",
14695                            }
14696                        }
14697                    })));
14698                } else {
14699                    return Ok(Json(serde_json::json!({
14700                        "jsonrpc": "2.0", "id": id,
14701                        "error": { "code": -32602, "message": format!("unknown shield op '{}' in tool '{}'", op, tool_name) },
14702                        "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14703                    })));
14704                }
14705            }
14706
14707            // ── Corpus tool dispatch (axon_corpus_{name}_{op}) ──
14708            if let Some(corpus_suffix) = tool_name.strip_prefix("axon_corpus_") {
14709                let (corpus_name, op) = if let Some(pos) = corpus_suffix.rfind('_') {
14710                    (&corpus_suffix[..pos], &corpus_suffix[pos+1..])
14711                } else {
14712                    return Ok(Json(serde_json::json!({
14713                        "jsonrpc": "2.0", "id": id,
14714                        "error": { "code": -32602, "message": format!("invalid corpus tool name: {}", tool_name) },
14715                        "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14716                    })));
14717                };
14718
14719                let mut s = state.lock().unwrap();
14720                let client = client_key_from_headers(&headers);
14721                check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
14722
14723                let corpus = match s.corpora.get_mut(corpus_name) {
14724                    Some(c) => c,
14725                    None => return Ok(Json(serde_json::json!({
14726                        "jsonrpc": "2.0", "id": id,
14727                        "error": { "code": -32602, "message": format!("corpus '{}' not found", corpus_name) },
14728                        "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14729                    }))),
14730                };
14731
14732                match op {
14733                    "search" => {
14734                        let query = arguments.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string();
14735                        let filter_tags: Option<Vec<String>> = arguments.get("tags")
14736                            .and_then(|v| serde_json::from_value(v.clone()).ok());
14737                        let limit = arguments.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
14738
14739                        if query.is_empty() {
14740                            return Ok(Json(serde_json::json!({
14741                                "jsonrpc": "2.0", "id": id,
14742                                "error": { "code": -32602, "message": "query is required" },
14743                                "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14744                            })));
14745                        }
14746
14747                        let query_lower = query.to_lowercase();
14748                        let query_terms: Vec<&str> = query_lower.split_whitespace().collect();
14749
14750                        let mut scored: Vec<serde_json::Value> = Vec::new();
14751                        for doc in corpus.documents.values() {
14752                            if let Some(ref tags) = filter_tags {
14753                                if !tags.iter().all(|t| doc.tags.contains(t)) { continue; }
14754                            }
14755                            let content_lower = doc.content.to_lowercase();
14756                            let title_lower = doc.title.to_lowercase();
14757                            let mut hits = 0.0f64;
14758                            for term in &query_terms {
14759                                hits += content_lower.matches(term).count() as f64;
14760                                hits += title_lower.matches(term).count() as f64 * 3.0;
14761                            }
14762                            if hits > 0.0 {
14763                                let total_words = doc.word_count.max(1) as f64 + doc.title.split_whitespace().count() as f64;
14764                                let relevance = (hits / total_words).min(1.0);
14765                                scored.push(serde_json::json!({
14766                                    "document_id": doc.id, "title": doc.title,
14767                                    "relevance": (relevance * 10000.0).round() / 10000.0,
14768                                }));
14769                            }
14770                        }
14771                        scored.sort_by(|a, b| b["relevance"].as_f64().unwrap_or(0.0)
14772                            .partial_cmp(&a["relevance"].as_f64().unwrap_or(0.0)).unwrap_or(std::cmp::Ordering::Equal));
14773                        scored.truncate(limit);
14774                        corpus.total_ops += 1;
14775
14776                        let result_text = serde_json::to_string_pretty(&scored).unwrap_or_default();
14777
14778                        return Ok(Json(serde_json::json!({
14779                            "jsonrpc": "2.0", "id": id,
14780                            "result": {
14781                                "content": [{ "type": "text", "text": result_text }],
14782                                "isError": false,
14783                                "_axon": {
14784                                    "corpus": corpus_name, "query": query, "matched": scored.len(),
14785                                    "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
14786                                    "lattice_position": "speculate",
14787                                    "effect_row": ["io", "epistemic:speculate"],
14788                                    "blame": "none",
14789                                }
14790                            }
14791                        })));
14792                    }
14793                    "cite" => {
14794                        let query = arguments.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string();
14795                        let max_citations = arguments.get("max_citations").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
14796                        let excerpt_length = arguments.get("excerpt_length").and_then(|v| v.as_u64()).unwrap_or(200) as usize;
14797
14798                        if query.is_empty() {
14799                            return Ok(Json(serde_json::json!({
14800                                "jsonrpc": "2.0", "id": id,
14801                                "error": { "code": -32602, "message": "query is required" },
14802                                "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14803                            })));
14804                        }
14805
14806                        let query_lower = query.to_lowercase();
14807                        let ontology = corpus.ontology.clone();
14808                        let mut citations: Vec<serde_json::Value> = Vec::new();
14809
14810                        for doc in corpus.documents.values() {
14811                            let content_lower = doc.content.to_lowercase();
14812                            if let Some(pos) = content_lower.find(&query_lower) {
14813                                let start = pos.saturating_sub(excerpt_length / 4);
14814                                let end = (pos + query.len() + excerpt_length * 3 / 4).min(doc.content.len());
14815                                let excerpt = &doc.content[start..end];
14816                                let relevance = 1.0 - (pos as f64 / doc.content.len().max(1) as f64 * 0.1);
14817                                citations.push(serde_json::json!({
14818                                    "document_id": doc.id, "title": doc.title,
14819                                    "excerpt": excerpt,
14820                                    "relevance": (relevance.min(1.0) * 10000.0).round() / 10000.0,
14821                                }));
14822                            }
14823                        }
14824                        citations.sort_by(|a, b| b["relevance"].as_f64().unwrap_or(0.0)
14825                            .partial_cmp(&a["relevance"].as_f64().unwrap_or(0.0)).unwrap_or(std::cmp::Ordering::Equal));
14826                        citations.truncate(max_citations);
14827                        corpus.total_ops += 1;
14828
14829                        let result_text = serde_json::to_string_pretty(&citations).unwrap_or_default();
14830
14831                        return Ok(Json(serde_json::json!({
14832                            "jsonrpc": "2.0", "id": id,
14833                            "result": {
14834                                "content": [{ "type": "text", "text": result_text }],
14835                                "isError": false,
14836                                "_axon": {
14837                                    "corpus": corpus_name, "query": query, "citations": citations.len(),
14838                                    "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
14839                                    "lattice_position": "speculate",
14840                                    "effect_row": ["io", "epistemic:speculate"],
14841                                    "blame": "none",
14842                                }
14843                            }
14844                        })));
14845                    }
14846                    _ => {
14847                        return Ok(Json(serde_json::json!({
14848                            "jsonrpc": "2.0", "id": id,
14849                            "error": { "code": -32602, "message": format!("unknown corpus op '{}' in tool '{}'", op, tool_name) },
14850                            "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14851                        })));
14852                    }
14853                }
14854            }
14855
14856            // ── Compute tool dispatch (axon_compute_evaluate) ──
14857            if tool_name == "axon_compute_evaluate" {
14858                let expression = arguments.get("expression").and_then(|v| v.as_str()).unwrap_or("").to_string();
14859                let variables: HashMap<String, f64> = arguments.get("variables")
14860                    .and_then(|v| serde_json::from_value(v.clone()).ok())
14861                    .unwrap_or_default();
14862
14863                if expression.is_empty() {
14864                    return Ok(Json(serde_json::json!({
14865                        "jsonrpc": "2.0", "id": id,
14866                        "error": { "code": -32602, "message": "expression is required" },
14867                        "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14868                    })));
14869                }
14870
14871                match compute_evaluate(&expression, &variables) {
14872                    Ok(result) => {
14873                        let result_text = format!("{} = {}", result.expression, result.value);
14874                        let lattice = if result.exact { "know" } else { "speculate" };
14875                        return Ok(Json(serde_json::json!({
14876                            "jsonrpc": "2.0", "id": id,
14877                            "result": {
14878                                "content": [{ "type": "text", "text": result_text }],
14879                                "isError": false,
14880                                "_axon": {
14881                                    "value": result.value, "exact": result.exact,
14882                                    "epistemic_envelope": { "certainty": result.certainty, "derivation": result.derivation },
14883                                    "lattice_position": lattice,
14884                                    "effect_row": ["compute", format!("epistemic:{}", lattice)],
14885                                    "blame": "none",
14886                                }
14887                            }
14888                        })));
14889                    }
14890                    Err(e) => {
14891                        return Ok(Json(serde_json::json!({
14892                            "jsonrpc": "2.0", "id": id,
14893                            "error": { "code": -32602, "message": e },
14894                            "_axon_blame": { "blame": "caller", "reason": "CT-2: invalid expression" }
14895                        })));
14896                    }
14897                }
14898            }
14899
14900            // ── Mandate tool dispatch (axon_mandate_{name}_evaluate) ──
14901            if let Some(mandate_suffix) = tool_name.strip_prefix("axon_mandate_") {
14902                let (mandate_name, op) = if let Some(pos) = mandate_suffix.rfind('_') {
14903                    (&mandate_suffix[..pos], &mandate_suffix[pos+1..])
14904                } else {
14905                    return Ok(Json(serde_json::json!({
14906                        "jsonrpc": "2.0", "id": id,
14907                        "error": { "code": -32602, "message": format!("invalid mandate tool: {}", tool_name) },
14908                        "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14909                    })));
14910                };
14911
14912                if op == "evaluate" {
14913                    let mut s = state.lock().unwrap();
14914                    check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
14915
14916                    let subject = arguments.get("subject").and_then(|v| v.as_str()).unwrap_or("anonymous");
14917                    let action = arguments.get("action").and_then(|v| v.as_str()).unwrap_or("");
14918                    let resource = arguments.get("resource").and_then(|v| v.as_str()).unwrap_or("");
14919
14920                    if action.is_empty() || resource.is_empty() {
14921                        return Ok(Json(serde_json::json!({
14922                            "jsonrpc": "2.0", "id": id,
14923                            "error": { "code": -32602, "message": "action and resource are required" },
14924                            "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14925                        })));
14926                    }
14927
14928                    let policy = match s.mandates.get_mut(mandate_name) {
14929                        Some(m) => m,
14930                        None => return Ok(Json(serde_json::json!({
14931                            "jsonrpc": "2.0", "id": id,
14932                            "error": { "code": -32602, "message": format!("mandate '{}' not found", mandate_name) },
14933                            "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14934                        }))),
14935                    };
14936
14937                    let result = policy.evaluate(subject, action, resource);
14938                    policy.total_evaluations += 1;
14939                    if !result.allowed { policy.total_denials += 1; }
14940
14941                    let result_text = format!("{}: {} {} on {}", result.effect, subject, action, resource);
14942                    let lattice = if result.certainty == 1.0 { "know" } else { "speculate" };
14943
14944                    return Ok(Json(serde_json::json!({
14945                        "jsonrpc": "2.0", "id": id,
14946                        "result": {
14947                            "content": [{ "type": "text", "text": result_text }],
14948                            "isError": false,
14949                            "_axon": {
14950                                "mandate": mandate_name, "allowed": result.allowed,
14951                                "effect": result.effect, "matched_rule": result.matched_rule,
14952                                "epistemic_envelope": { "certainty": result.certainty, "derivation": result.derivation },
14953                                "lattice_position": lattice,
14954                                "effect_row": ["io", format!("epistemic:{}", lattice)],
14955                                "blame": "none",
14956                            }
14957                        }
14958                    })));
14959                } else {
14960                    return Ok(Json(serde_json::json!({
14961                        "jsonrpc": "2.0", "id": id,
14962                        "error": { "code": -32602, "message": format!("unknown mandate op '{}'", op) },
14963                        "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14964                    })));
14965                }
14966            }
14967
14968            // ── Forge tool dispatch (axon_forge_{name}_render) ──
14969            if let Some(forge_suffix) = tool_name.strip_prefix("axon_forge_") {
14970                let (forge_name, op) = if let Some(pos) = forge_suffix.rfind('_') {
14971                    (&forge_suffix[..pos], &forge_suffix[pos+1..])
14972                } else {
14973                    return Ok(Json(serde_json::json!({
14974                        "jsonrpc": "2.0", "id": id,
14975                        "error": { "code": -32602, "message": format!("invalid forge tool: {}", tool_name) },
14976                        "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14977                    })));
14978                };
14979
14980                if op == "render" {
14981                    let mut s = state.lock().unwrap();
14982                    check_auth(&mut s, &headers, AccessLevel::Write)?;
14983
14984                    let template_name = arguments.get("template").and_then(|v| v.as_str()).unwrap_or("").to_string();
14985                    let variables: HashMap<String, String> = arguments.get("variables")
14986                        .and_then(|v| serde_json::from_value(v.clone()).ok())
14987                        .unwrap_or_default();
14988
14989                    if template_name.is_empty() {
14990                        return Ok(Json(serde_json::json!({
14991                            "jsonrpc": "2.0", "id": id,
14992                            "error": { "code": -32602, "message": "template is required" },
14993                            "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14994                        })));
14995                    }
14996
14997                    let forge = match s.forges.get_mut(forge_name) {
14998                        Some(f) => f,
14999                        None => return Ok(Json(serde_json::json!({
15000                            "jsonrpc": "2.0", "id": id,
15001                            "error": { "code": -32602, "message": format!("forge '{}' not found", forge_name) },
15002                            "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15003                        }))),
15004                    };
15005
15006                    match forge.render(&template_name, &variables) {
15007                        Ok(artifact) => {
15008                            return Ok(Json(serde_json::json!({
15009                                "jsonrpc": "2.0", "id": id,
15010                                "result": {
15011                                    "content": [{ "type": "text", "text": artifact.content }],
15012                                    "isError": false,
15013                                    "_axon": {
15014                                        "forge": forge_name, "artifact_id": artifact.id,
15015                                        "template": artifact.template_name, "format": artifact.format,
15016                                        "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
15017                                        "lattice_position": "believe",
15018                                        "effect_row": ["io", "epistemic:believe"],
15019                                        "blame": "none",
15020                                    }
15021                                }
15022                            })));
15023                        }
15024                        Err(e) => {
15025                            return Ok(Json(serde_json::json!({
15026                                "jsonrpc": "2.0", "id": id,
15027                                "error": { "code": -32602, "message": e },
15028                                "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15029                            })));
15030                        }
15031                    }
15032                } else {
15033                    return Ok(Json(serde_json::json!({
15034                        "jsonrpc": "2.0", "id": id,
15035                        "error": { "code": -32602, "message": format!("unknown forge op '{}'", op) },
15036                        "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15037                    })));
15038                }
15039            }
15040
15041            // Strip "axon_" prefix to get flow name
15042            let flow_name = tool_name.strip_prefix("axon_").unwrap_or(tool_name);
15043            let backend = arguments.get("backend").and_then(|b| b.as_str()).unwrap_or("stub");
15044
15045            // Resolve source and key — blame: Caller if flow not found
15046            let (source, source_file, resolved_key, tenant_secrets_arc) = {
15047                let s = state.lock().unwrap();
15048                check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
15049                let history = s.versions.get_history(flow_name);
15050                let ts = s.tenant_secrets.clone();
15051                match history.and_then(|h| h.active()) {
15052                    Some(active) => {
15053                        let key = resolve_backend_key(&s, backend).ok();
15054                        (active.source.clone(), active.source_file.clone(), key, ts)
15055                    }
15056                    None => {
15057                        // Blame::Caller (CT-2) — invalid tool name
15058                        return Ok(Json(serde_json::json!({
15059                            "jsonrpc": "2.0",
15060                            "id": id,
15061                            "error": {
15062                                "code": -32602,
15063                                "message": format!("flow '{}' not deployed", flow_name)
15064                            },
15065                            "_axon_blame": {
15066                                "blame": "caller",
15067                                "reason": "CT-2: caller referenced non-existent flow",
15068                                "flow": flow_name,
15069                            }
15070                        })));
15071                    }
15072                }
15073            };
15074
15075            // Async SM fetch for cold cache (M3): if registry + cache both miss, try SM now
15076            let resolved_key = if resolved_key.is_none() {
15077                let tenant_id = crate::tenant::current_tenant_id();
15078                tenant_secrets_arc.get_api_key(&tenant_id, backend).await.ok()
15079            } else {
15080                resolved_key
15081            };
15082
15083            // Execute
15084            // §Fase 37.y — async-fetch path serves non-dynamic-route
15085            // callers; empty path + query maps.
15086            let empty_path = std::collections::HashMap::new();
15087            let empty_query = std::collections::HashMap::new();
15088            let result = server_execute(
15089                &source, &source_file, flow_name, backend, resolved_key.as_deref(), None,
15090                &empty_path,
15091                &empty_query,
15092            );
15093
15094            match result {
15095                Ok(exec_result) => {
15096                    // Record backend metrics
15097                    {
15098                        let mut s = state.lock().unwrap();
15099                        record_backend_metrics(
15100                            &mut s, &exec_result.backend, exec_result.success,
15101                            exec_result.tokens_input, exec_result.tokens_output,
15102                            exec_result.latency_ms,
15103                        );
15104                    }
15105
15106                    // ΛD Epistemic Envelope — computed, not hardcoded
15107                    // ψ = ⟨T, V, E⟩ where E = ⟨c, τ, ρ, δ⟩
15108                    // All MCP-sourced execution is δ=derived (not raw data)
15109                    // Certainty based on execution outcome:
15110                    //   success + no anchor breaches → c=0.85 (speculate, not know)
15111                    //   success + anchor breaches → c=0.5 (doubt)
15112                    //   failure → c=0.1 (near ⊥)
15113                    let certainty = if exec_result.success && exec_result.anchor_breaches == 0 {
15114                        0.85 // speculate: succeeded, anchors held
15115                    } else if exec_result.success {
15116                        0.5  // doubt: succeeded but anchors breached
15117                    } else {
15118                        0.1  // near ⊥: execution failed
15119                    };
15120                    let epistemic_envelope = EpistemicEnvelope::derived(
15121                        &format!("mcp:tool:{}", flow_name),
15122                        certainty,
15123                        &format!("emcp:axon_server:{}:{}", flow_name, exec_result.backend),
15124                    );
15125
15126                    // Effect row — computed from actual execution
15127                    let mut effects = vec!["io".to_string()];
15128                    if backend != "stub" {
15129                        effects.push("network".to_string());
15130                    }
15131                    // Map certainty to epistemic effect
15132                    let epistemic_effect = if certainty >= 0.85 {
15133                        "epistemic:speculate"
15134                    } else if certainty >= 0.5 {
15135                        "epistemic:doubt"
15136                    } else {
15137                        "epistemic:uncertain"
15138                    };
15139                    effects.push(epistemic_effect.to_string());
15140
15141                    // Epistemic lattice position
15142                    let lattice_position = if certainty >= 0.85 {
15143                        "speculate"
15144                    } else if certainty >= 0.5 {
15145                        "doubt"
15146                    } else {
15147                        "⊥"
15148                    };
15149
15150                    let output_text = exec_result.step_results.join("\n");
15151                    Ok(Json(serde_json::json!({
15152                        "jsonrpc": "2.0",
15153                        "id": id,
15154                        "result": {
15155                            "content": [{
15156                                "type": "text",
15157                                "text": output_text,
15158                            }],
15159                            "isError": !exec_result.success,
15160                            // ℰMCP epistemic metadata (formally derived)
15161                            "_axon": {
15162                                "flow": flow_name,
15163                                "backend": exec_result.backend,
15164                                "steps_executed": exec_result.steps_executed,
15165                                "latency_ms": exec_result.latency_ms,
15166                                "tokens_input": exec_result.tokens_input,
15167                                "tokens_output": exec_result.tokens_output,
15168                                "anchor_checks": exec_result.anchor_checks,
15169                                "anchor_breaches": exec_result.anchor_breaches,
15170                                // ΛD: full epistemic envelope ψ = ⟨T, V, E⟩
15171                                "epistemic_envelope": {
15172                                    "ontology": epistemic_envelope.ontology,
15173                                    "certainty": epistemic_envelope.certainty,
15174                                    "temporal_start": epistemic_envelope.temporal_start,
15175                                    "temporal_end": epistemic_envelope.temporal_end,
15176                                    "provenance": epistemic_envelope.provenance,
15177                                    "derivation": epistemic_envelope.derivation,
15178                                },
15179                                // Lattice position: ⊥ ⊑ doubt ⊑ speculate ⊑ believe ⊑ know
15180                                "lattice_position": lattice_position,
15181                                // Effect row: <io, network?, epistemic:X>
15182                                "effect_row": effects,
15183                                // Blame: none on success
15184                                "blame": "none",
15185                            }
15186                        }
15187                    })))
15188                }
15189                Err(e) => {
15190                    // Blame assignment (Findler-Felleisen CT-2/CT-3)
15191                    let blame = if e.contains("Backend error") || e.contains("timeout") || e.contains("connect") {
15192                        "network"  // infrastructure failure
15193                    } else if e.contains("not found") || e.contains("parse error") || e.contains("lex error") {
15194                        "server"   // AXON server failed to compile/execute
15195                    } else {
15196                        "server"   // default: server-side failure
15197                    };
15198
15199                    Ok(Json(serde_json::json!({
15200                        "jsonrpc": "2.0",
15201                        "id": id,
15202                        "result": {
15203                            "content": [{
15204                                "type": "text",
15205                                "text": format!("Execution error: {}", e),
15206                            }],
15207                            "isError": true,
15208                            "_axon": {
15209                                "blame": blame,
15210                                "epistemic_envelope": {
15211                                    "ontology": format!("mcp:tool:{}:error", flow_name),
15212                                    "certainty": 0.0,
15213                                    "derivation": "failed",
15214                                    "provenance": format!("emcp:axon_server:{}", flow_name),
15215                                },
15216                                "lattice_position": "⊥",
15217                                "effect_row": ["io", "epistemic:uncertain"],
15218                            }
15219                        }
15220                    })))
15221                }
15222            }
15223        }
15224        "resources/list" => {
15225            let s = state.lock().unwrap();
15226            check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
15227
15228            let mut resources: Vec<serde_json::Value> = Vec::new();
15229
15230            // axon://traces/recent — recent execution traces
15231            resources.push(serde_json::json!({
15232                "uri": "axon://traces/recent",
15233                "name": "Recent Traces",
15234                "description": "Last 20 execution traces with epistemic metadata",
15235                "mimeType": "application/json",
15236            }));
15237
15238            // axon://metrics — server metrics snapshot
15239            resources.push(serde_json::json!({
15240                "uri": "axon://metrics",
15241                "name": "Server Metrics",
15242                "description": "Current server metrics (requests, errors, latency, tokens)",
15243                "mimeType": "application/json",
15244            }));
15245
15246            // axon://backends — backend registry status
15247            resources.push(serde_json::json!({
15248                "uri": "axon://backends",
15249                "name": "Backend Registry",
15250                "description": "LLM backend status, metrics, circuit breaker state",
15251                "mimeType": "application/json",
15252            }));
15253
15254            // axon://flows — deployed flows
15255            resources.push(serde_json::json!({
15256                "uri": "axon://flows",
15257                "name": "Deployed Flows",
15258                "description": "All deployed AXON flows with version info",
15259                "mimeType": "application/json",
15260            }));
15261
15262            // axon://dataspaces — dataspace registry
15263            resources.push(serde_json::json!({
15264                "uri": "axon://dataspaces",
15265                "name": "Dataspaces",
15266                "description": "Cognitive data navigation containers with ΛD epistemic envelopes",
15267                "mimeType": "application/json",
15268            }));
15269
15270            // axon://dataspaces/{name} — individual dataspaces
15271            for ds in s.dataspaces.values() {
15272                resources.push(serde_json::json!({
15273                    "uri": format!("axon://dataspaces/{}", ds.name),
15274                    "name": format!("Dataspace: {}", ds.name),
15275                    "description": format!("{} — {} entries, {} associations, ontology: {}", ds.name, ds.entries.len(), ds.associations.len(), ds.ontology),
15276                    "mimeType": "application/json",
15277                }));
15278            }
15279
15280            // axon://axonstores — axonstore registry
15281            resources.push(serde_json::json!({
15282                "uri": "axon://axonstores",
15283                "name": "AxonStores",
15284                "description": "Cognitive durable persistence stores with ΛD epistemic envelopes",
15285                "mimeType": "application/json",
15286            }));
15287
15288            // axon://axonstores/{name} — individual axonstores
15289            for st in s.axon_stores.values() {
15290                resources.push(serde_json::json!({
15291                    "uri": format!("axon://axonstores/{}", st.name),
15292                    "name": format!("AxonStore: {}", st.name),
15293                    "description": format!("{} — {} entries, ontology: {}, {} ops", st.name, st.entries.len(), st.ontology, st.total_ops),
15294                    "mimeType": "application/json",
15295                }));
15296            }
15297
15298            // axon://shields — shield registry
15299            resources.push(serde_json::json!({
15300                "uri": "axon://shields",
15301                "name": "Shields",
15302                "description": "Cognitive guardrail instances with deny_list/pattern/pii/length rules",
15303                "mimeType": "application/json",
15304            }));
15305            for sh in s.shields.values() {
15306                resources.push(serde_json::json!({
15307                    "uri": format!("axon://shields/{}", sh.name),
15308                    "name": format!("Shield: {}", sh.name),
15309                    "description": format!("{} — {} rules, mode: {}, {} evals, {} blocks", sh.name, sh.rules.len(), sh.mode, sh.total_evaluations, sh.total_blocks),
15310                    "mimeType": "application/json",
15311                }));
15312            }
15313
15314            // axon://corpora — corpus registry
15315            resources.push(serde_json::json!({
15316                "uri": "axon://corpora",
15317                "name": "Corpora",
15318                "description": "Document corpus instances with search and citation",
15319                "mimeType": "application/json",
15320            }));
15321            for corpus in s.corpora.values() {
15322                resources.push(serde_json::json!({
15323                    "uri": format!("axon://corpora/{}", corpus.name),
15324                    "name": format!("Corpus: {}", corpus.name),
15325                    "description": format!("{} — {} docs, ontology: {}", corpus.name, corpus.documents.len(), corpus.ontology),
15326                    "mimeType": "application/json",
15327                }));
15328            }
15329
15330            // axon://mandates — mandate policy registry
15331            resources.push(serde_json::json!({
15332                "uri": "axon://mandates",
15333                "name": "Mandates",
15334                "description": "Authorization policies with priority-ordered rule evaluation",
15335                "mimeType": "application/json",
15336            }));
15337            for mandate in s.mandates.values() {
15338                resources.push(serde_json::json!({
15339                    "uri": format!("axon://mandates/{}", mandate.name),
15340                    "name": format!("Mandate: {}", mandate.name),
15341                    "description": format!("{} — {} rules, {} evals", mandate.name, mandate.rules.len(), mandate.total_evaluations),
15342                    "mimeType": "application/json",
15343                }));
15344            }
15345
15346            // axon://forges — forge session registry
15347            resources.push(serde_json::json!({
15348                "uri": "axon://forges",
15349                "name": "Forges",
15350                "description": "Template-based artifact generation sessions",
15351                "mimeType": "application/json",
15352            }));
15353            for forge in s.forges.values() {
15354                resources.push(serde_json::json!({
15355                    "uri": format!("axon://forges/{}", forge.name),
15356                    "name": format!("Forge: {}", forge.name),
15357                    "description": format!("{} — {} templates, {} artifacts", forge.name, forge.templates.len(), forge.artifacts.len()),
15358                    "mimeType": "application/json",
15359                }));
15360            }
15361
15362            // axon://traces/{id} — individual traces (template)
15363            for entry in s.trace_store.recent(10, None) {
15364                resources.push(serde_json::json!({
15365                    "uri": format!("axon://traces/{}", entry.id),
15366                    "name": format!("Trace #{} ({})", entry.id, entry.flow_name),
15367                    "description": format!("{} — {} steps, {}ms", entry.status.as_str(), entry.steps_executed, entry.latency_ms),
15368                    "mimeType": "application/json",
15369                }));
15370            }
15371
15372            Ok(Json(serde_json::json!({
15373                "jsonrpc": "2.0",
15374                "id": id,
15375                "result": { "resources": resources }
15376            })))
15377        }
15378        "resources/read" => {
15379            let uri = params.get("uri").and_then(|u| u.as_str()).unwrap_or("");
15380
15381            let s = state.lock().unwrap();
15382            check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
15383
15384            let (content, mime) = if uri == "axon://traces/recent" {
15385                let traces: Vec<serde_json::Value> = s.trace_store.recent(20, None).iter().map(|e| {
15386                    serde_json::json!({
15387                        "id": e.id, "flow": e.flow_name, "status": e.status.as_str(),
15388                        "steps": e.steps_executed, "latency_ms": e.latency_ms,
15389                        "tokens_input": e.tokens_input, "tokens_output": e.tokens_output,
15390                        "backend": e.backend, "timestamp": e.timestamp,
15391                        "_epistemic": {
15392                            "derivation": "derived",
15393                            "certainty": if e.status.as_str() == "success" { 0.85 } else { 0.3 },
15394                            "lattice": if e.status.as_str() == "success" { "speculate" } else { "doubt" },
15395                        }
15396                    })
15397                }).collect();
15398                (serde_json::to_string_pretty(&traces).unwrap_or_default(), "application/json")
15399            } else if uri == "axon://metrics" {
15400                let m = &s.metrics;
15401                let content = serde_json::json!({
15402                    "total_requests": m.total_requests,
15403                    "total_errors": m.total_errors,
15404                    "deploy_count": s.deploy_count,
15405                    "flows_deployed": s.versions.flow_count(),
15406                    "traces_stored": s.trace_store.len(),
15407                    "backends_registered": s.backend_registry.len(),
15408                    "alert_rules": s.alert_rules.len(),
15409                    "fired_alerts": s.fired_alerts.len(),
15410                });
15411                (serde_json::to_string_pretty(&content).unwrap_or_default(), "application/json")
15412            } else if uri == "axon://backends" {
15413                let backends: Vec<serde_json::Value> = s.backend_registry.values().map(|e| {
15414                    serde_json::json!({
15415                        "name": e.name, "enabled": e.enabled, "status": e.status,
15416                        "total_calls": e.total_calls, "total_errors": e.total_errors,
15417                        "circuit_open_until": e.circuit_open_until,
15418                        "consecutive_failures": e.consecutive_failures,
15419                        "fallback_chain": e.fallback_chain,
15420                    })
15421                }).collect();
15422                (serde_json::to_string_pretty(&backends).unwrap_or_default(), "application/json")
15423            } else if uri == "axon://flows" {
15424                let flows: Vec<serde_json::Value> = s.versions.list_flows().iter().map(|f| {
15425                    serde_json::json!({
15426                        "name": f.flow_name,
15427                        "active_version": f.active_version,
15428                        "total_versions": f.total_versions,
15429                        "deploy_count": f.deploy_count,
15430                    })
15431                }).collect();
15432                (serde_json::to_string_pretty(&flows).unwrap_or_default(), "application/json")
15433            } else if uri == "axon://dataspaces" {
15434                let spaces: Vec<serde_json::Value> = s.dataspaces.values().map(|ds| {
15435                    serde_json::json!({
15436                        "name": ds.name,
15437                        "ontology": ds.ontology,
15438                        "entry_count": ds.entries.len(),
15439                        "association_count": ds.associations.len(),
15440                        "total_ops": ds.total_ops,
15441                        "created_at": ds.created_at,
15442                        "_epistemic": {
15443                            "ontology": "dataspace:registry",
15444                            "derivation": "raw",
15445                            "certainty": 1.0,
15446                            "provenance": "axon_server:dataspaces",
15447                        }
15448                    })
15449                }).collect();
15450                (serde_json::to_string_pretty(&spaces).unwrap_or_default(), "application/json")
15451            } else if let Some(ds_name) = uri.strip_prefix("axon://dataspaces/") {
15452                match s.dataspaces.get(ds_name) {
15453                    Some(ds) => {
15454                        let entries: Vec<serde_json::Value> = ds.entries.values().map(|e| {
15455                            serde_json::json!({
15456                                "id": e.id,
15457                                "ontology": e.ontology,
15458                                "data": e.data,
15459                                "tags": e.tags,
15460                                "ingested_at": e.ingested_at,
15461                                "_epistemic": {
15462                                    "ontology": &e.envelope.ontology,
15463                                    "certainty": e.envelope.certainty,
15464                                    "derivation": &e.envelope.derivation,
15465                                    "provenance": &e.envelope.provenance,
15466                                    "temporal_start": &e.envelope.temporal_start,
15467                                    "temporal_end": &e.envelope.temporal_end,
15468                                }
15469                            })
15470                        }).collect();
15471                        let associations: Vec<serde_json::Value> = ds.associations.iter().map(|a| {
15472                            serde_json::json!({
15473                                "from": a.from, "to": a.to,
15474                                "relation": a.relation,
15475                                "certainty": a.certainty,
15476                                "created_at": a.created_at,
15477                            })
15478                        }).collect();
15479                        let content = serde_json::json!({
15480                            "name": ds.name,
15481                            "ontology": ds.ontology,
15482                            "entries": entries,
15483                            "associations": associations,
15484                            "total_ops": ds.total_ops,
15485                            "_epistemic": {
15486                                "ontology": format!("dataspace:{}", ds.name),
15487                                "derivation": "raw",
15488                                "certainty": 1.0,
15489                                "provenance": format!("axon_server:dataspace:{}", ds.name),
15490                            }
15491                        });
15492                        (serde_json::to_string_pretty(&content).unwrap_or_default(), "application/json")
15493                    }
15494                    None => {
15495                        return Ok(Json(serde_json::json!({
15496                            "jsonrpc": "2.0", "id": id,
15497                            "error": { "code": -32602, "message": format!("dataspace '{}' not found", ds_name) },
15498                            "_axon_blame": { "blame": "caller", "reason": "CT-2: referenced non-existent dataspace" }
15499                        })));
15500                    }
15501                }
15502            } else if uri == "axon://axonstores" {
15503                let stores: Vec<serde_json::Value> = s.axon_stores.values().map(|st| {
15504                    serde_json::json!({
15505                        "name": st.name,
15506                        "ontology": st.ontology,
15507                        "entry_count": st.entries.len(),
15508                        "total_ops": st.total_ops,
15509                        "created_at": st.created_at,
15510                        "_epistemic": {
15511                            "ontology": "axonstore:registry",
15512                            "derivation": "raw",
15513                            "certainty": 1.0,
15514                            "provenance": "axon_server:axon_stores",
15515                        }
15516                    })
15517                }).collect();
15518                (serde_json::to_string_pretty(&stores).unwrap_or_default(), "application/json")
15519            } else if let Some(store_name) = uri.strip_prefix("axon://axonstores/") {
15520                match s.axon_stores.get(store_name) {
15521                    Some(st) => {
15522                        let entries: Vec<serde_json::Value> = st.entries.values().map(|e| {
15523                            serde_json::json!({
15524                                "key": e.key,
15525                                "value": e.value,
15526                                "version": e.version,
15527                                "created_at": e.created_at,
15528                                "updated_at": e.updated_at,
15529                                "_epistemic": {
15530                                    "ontology": &e.envelope.ontology,
15531                                    "certainty": e.envelope.certainty,
15532                                    "derivation": &e.envelope.derivation,
15533                                    "provenance": &e.envelope.provenance,
15534                                    "temporal_start": &e.envelope.temporal_start,
15535                                    "temporal_end": &e.envelope.temporal_end,
15536                                }
15537                            })
15538                        }).collect();
15539                        let content = serde_json::json!({
15540                            "name": st.name,
15541                            "ontology": st.ontology,
15542                            "entries": entries,
15543                            "total_ops": st.total_ops,
15544                            "_epistemic": {
15545                                "ontology": format!("axonstore:{}", st.name),
15546                                "derivation": "raw",
15547                                "certainty": 1.0,
15548                                "provenance": format!("axon_server:axonstore:{}", st.name),
15549                            }
15550                        });
15551                        (serde_json::to_string_pretty(&content).unwrap_or_default(), "application/json")
15552                    }
15553                    None => {
15554                        return Ok(Json(serde_json::json!({
15555                            "jsonrpc": "2.0", "id": id,
15556                            "error": { "code": -32602, "message": format!("axonstore '{}' not found", store_name) },
15557                            "_axon_blame": { "blame": "caller", "reason": "CT-2: referenced non-existent axonstore" }
15558                        })));
15559                    }
15560                }
15561            } else if let Some(id_str) = uri.strip_prefix("axon://traces/") {
15562                if let Ok(trace_id) = id_str.parse::<u64>() {
15563                    match s.trace_store.get(trace_id) {
15564                        Some(e) => {
15565                            let content = serde_json::json!({
15566                                "id": e.id, "flow": e.flow_name, "status": e.status.as_str(),
15567                                "backend": e.backend, "client": e.client_key,
15568                                "steps": e.steps_executed, "latency_ms": e.latency_ms,
15569                                "tokens_input": e.tokens_input, "tokens_output": e.tokens_output,
15570                                "anchor_checks": e.anchor_checks, "anchor_breaches": e.anchor_breaches,
15571                                "errors": e.errors, "timestamp": e.timestamp,
15572                                "_epistemic": {
15573                                    "ontology": format!("trace:{}", e.flow_name),
15574                                    "derivation": "raw",
15575                                    "certainty": 1.0,
15576                                    "provenance": format!("axon_server:trace_store:{}", e.id),
15577                                }
15578                            });
15579                            (serde_json::to_string_pretty(&content).unwrap_or_default(), "application/json")
15580                        }
15581                        None => {
15582                            return Ok(Json(serde_json::json!({
15583                                "jsonrpc": "2.0", "id": id,
15584                                "error": { "code": -32602, "message": format!("trace {} not found", trace_id) }
15585                            })));
15586                        }
15587                    }
15588                } else {
15589                    return Ok(Json(serde_json::json!({
15590                        "jsonrpc": "2.0", "id": id,
15591                        "error": { "code": -32602, "message": format!("invalid trace id in URI: {}", uri) }
15592                    })));
15593                }
15594            } else if uri == "axon://shields" {
15595                let shields: Vec<serde_json::Value> = s.shields.values().map(|sh| {
15596                    serde_json::json!({
15597                        "name": sh.name, "mode": sh.mode, "rule_count": sh.rules.len(),
15598                        "total_evaluations": sh.total_evaluations, "total_blocks": sh.total_blocks,
15599                        "_epistemic": { "derivation": "raw", "certainty": 1.0 }
15600                    })
15601                }).collect();
15602                (serde_json::to_string_pretty(&shields).unwrap_or_default(), "application/json")
15603            } else if let Some(sh_name) = uri.strip_prefix("axon://shields/") {
15604                match s.shields.get(sh_name) {
15605                    Some(sh) => {
15606                        let content = serde_json::json!({
15607                            "name": sh.name, "mode": sh.mode, "rules": sh.rules,
15608                            "total_evaluations": sh.total_evaluations, "total_blocks": sh.total_blocks,
15609                            "_epistemic": { "derivation": "raw", "certainty": 1.0, "provenance": format!("axon_server:shield:{}", sh.name) }
15610                        });
15611                        (serde_json::to_string_pretty(&content).unwrap_or_default(), "application/json")
15612                    }
15613                    None => return Ok(Json(serde_json::json!({
15614                        "jsonrpc": "2.0", "id": id,
15615                        "error": { "code": -32602, "message": format!("shield '{}' not found", sh_name) },
15616                        "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15617                    }))),
15618                }
15619            } else if uri == "axon://corpora" {
15620                let corpora: Vec<serde_json::Value> = s.corpora.values().map(|c| {
15621                    serde_json::json!({
15622                        "name": c.name, "ontology": c.ontology, "document_count": c.documents.len(),
15623                        "total_ops": c.total_ops,
15624                        "_epistemic": { "derivation": "raw", "certainty": 1.0 }
15625                    })
15626                }).collect();
15627                (serde_json::to_string_pretty(&corpora).unwrap_or_default(), "application/json")
15628            } else if let Some(corpus_name) = uri.strip_prefix("axon://corpora/") {
15629                match s.corpora.get(corpus_name) {
15630                    Some(corpus) => {
15631                        let docs: Vec<serde_json::Value> = corpus.documents.values().map(|d| {
15632                            serde_json::json!({
15633                                "id": d.id, "title": d.title, "word_count": d.word_count,
15634                                "tags": d.tags, "source": d.source,
15635                                "_epistemic": { "certainty": d.envelope.certainty, "derivation": &d.envelope.derivation }
15636                            })
15637                        }).collect();
15638                        let content = serde_json::json!({
15639                            "name": corpus.name, "ontology": corpus.ontology,
15640                            "documents": docs, "total_ops": corpus.total_ops,
15641                            "_epistemic": { "derivation": "raw", "certainty": 1.0, "provenance": format!("axon_server:corpus:{}", corpus.name) }
15642                        });
15643                        (serde_json::to_string_pretty(&content).unwrap_or_default(), "application/json")
15644                    }
15645                    None => return Ok(Json(serde_json::json!({
15646                        "jsonrpc": "2.0", "id": id,
15647                        "error": { "code": -32602, "message": format!("corpus '{}' not found", corpus_name) },
15648                        "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15649                    }))),
15650                }
15651            } else if uri == "axon://mandates" {
15652                let mandates: Vec<serde_json::Value> = s.mandates.values().map(|m| {
15653                    serde_json::json!({
15654                        "name": m.name, "description": m.description, "rule_count": m.rules.len(),
15655                        "total_evaluations": m.total_evaluations, "total_denials": m.total_denials,
15656                        "_epistemic": { "derivation": "raw", "certainty": 1.0 }
15657                    })
15658                }).collect();
15659                (serde_json::to_string_pretty(&mandates).unwrap_or_default(), "application/json")
15660            } else if let Some(mandate_name) = uri.strip_prefix("axon://mandates/") {
15661                match s.mandates.get(mandate_name) {
15662                    Some(m) => {
15663                        let content = serde_json::json!({
15664                            "name": m.name, "description": m.description, "rules": m.rules,
15665                            "total_evaluations": m.total_evaluations, "total_denials": m.total_denials,
15666                            "_epistemic": { "derivation": "raw", "certainty": 1.0, "provenance": format!("axon_server:mandate:{}", m.name) }
15667                        });
15668                        (serde_json::to_string_pretty(&content).unwrap_or_default(), "application/json")
15669                    }
15670                    None => return Ok(Json(serde_json::json!({
15671                        "jsonrpc": "2.0", "id": id,
15672                        "error": { "code": -32602, "message": format!("mandate '{}' not found", mandate_name) },
15673                        "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15674                    }))),
15675                }
15676            } else if uri == "axon://forges" {
15677                let forges: Vec<serde_json::Value> = s.forges.values().map(|f| {
15678                    serde_json::json!({
15679                        "name": f.name, "template_count": f.templates.len(),
15680                        "artifact_count": f.artifacts.len(),
15681                        "_epistemic": { "derivation": "raw", "certainty": 1.0 }
15682                    })
15683                }).collect();
15684                (serde_json::to_string_pretty(&forges).unwrap_or_default(), "application/json")
15685            } else if let Some(forge_name) = uri.strip_prefix("axon://forges/") {
15686                match s.forges.get(forge_name) {
15687                    Some(f) => {
15688                        let templates: Vec<serde_json::Value> = f.templates.values().map(|t| {
15689                            serde_json::json!({ "name": t.name, "format": t.format, "variables": t.variables })
15690                        }).collect();
15691                        let content = serde_json::json!({
15692                            "name": f.name, "templates": templates,
15693                            "artifact_count": f.artifacts.len(),
15694                            "_epistemic": { "derivation": "raw", "certainty": 1.0, "provenance": format!("axon_server:forge:{}", f.name) }
15695                        });
15696                        (serde_json::to_string_pretty(&content).unwrap_or_default(), "application/json")
15697                    }
15698                    None => return Ok(Json(serde_json::json!({
15699                        "jsonrpc": "2.0", "id": id,
15700                        "error": { "code": -32602, "message": format!("forge '{}' not found", forge_name) },
15701                        "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15702                    }))),
15703                }
15704            } else {
15705                return Ok(Json(serde_json::json!({
15706                    "jsonrpc": "2.0", "id": id,
15707                    "error": { "code": -32602, "message": format!("unknown resource URI: {}", uri) }
15708                })));
15709            };
15710
15711            Ok(Json(serde_json::json!({
15712                "jsonrpc": "2.0",
15713                "id": id,
15714                "result": {
15715                    "contents": [{
15716                        "uri": uri,
15717                        "mimeType": mime,
15718                        "text": content,
15719                    }]
15720                }
15721            })))
15722        }
15723        "prompts/list" => {
15724            let s = state.lock().unwrap();
15725            check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
15726
15727            let mut prompts: Vec<serde_json::Value> = Vec::new();
15728
15729            // Each deployed flow's personas → MCP prompts
15730            for summary in s.versions.list_flows() {
15731                if let Some(active) = s.versions.get_active(&summary.flow_name) {
15732                    let personas = extract_personas(&active.source);
15733                    for (name, domain, tone, confidence, desc) in &personas {
15734                        prompts.push(serde_json::json!({
15735                            "name": format!("{}:{}", summary.flow_name, name),
15736                            "description": if desc.is_empty() {
15737                                format!("Persona '{}' from flow '{}' — domain: {:?}, tone: {}", name, summary.flow_name, domain, tone)
15738                            } else {
15739                                desc.clone()
15740                            },
15741                            "arguments": [
15742                                {
15743                                    "name": "input",
15744                                    "description": "User message to process with this persona",
15745                                    "required": true,
15746                                },
15747                                {
15748                                    "name": "backend",
15749                                    "description": "LLM backend to use",
15750                                    "required": false,
15751                                },
15752                            ],
15753                            "_axon_persona": {
15754                                "domain": domain,
15755                                "tone": tone,
15756                                "confidence_threshold": confidence,
15757                            }
15758                        }));
15759                    }
15760                }
15761            }
15762
15763            // Cognitive workflow prompt templates
15764            prompts.push(serde_json::json!({
15765                "name": "workflow:research",
15766                "description": "Research workflow: probe sources → weave synthesis → forge artifact. Guided multi-source information gathering with attributed output.",
15767                "arguments": [
15768                    { "name": "question", "description": "Research question to investigate", "required": true },
15769                    { "name": "sources", "description": "Comma-separated source list (e.g., 'corpus:papers,axonstore:facts')", "required": false },
15770                    { "name": "output_format", "description": "Output format: markdown, text, json", "required": false },
15771                ],
15772            }));
15773            prompts.push(serde_json::json!({
15774                "name": "workflow:decide",
15775                "description": "Decision workflow: drill options → corroborate claims → deliberate with pros/cons → decide. Structured decision-making with epistemic audit trail.",
15776                "arguments": [
15777                    { "name": "question", "description": "Decision to make", "required": true },
15778                    { "name": "options", "description": "Comma-separated options to consider", "required": true },
15779                    { "name": "max_depth", "description": "Drill exploration depth (default: 3)", "required": false },
15780                ],
15781            }));
15782            prompts.push(serde_json::json!({
15783                "name": "workflow:secure_transfer",
15784                "description": "Secure transfer workflow: axonstore persist → shield validate → ots one-time delivery → mandate authorize. Security-hardened credential pipeline.",
15785                "arguments": [
15786                    { "name": "payload", "description": "Content to securely transfer", "required": true },
15787                    { "name": "ttl_secs", "description": "One-time secret TTL in seconds (default: 3600)", "required": false },
15788                    { "name": "recipient_role", "description": "Authorized recipient role", "required": false },
15789                ],
15790            }));
15791            prompts.push(serde_json::json!({
15792                "name": "workflow:reflect",
15793                "description": "Metacognitive workflow: psyche introspect → probe knowledge gaps → weave synthesis. Self-reflective learning loop.",
15794                "arguments": [
15795                    { "name": "context", "description": "Cognitive context to reflect on", "required": true },
15796                    { "name": "depth", "description": "Reflection depth: shallow, medium, deep", "required": false },
15797                ],
15798            }));
15799            prompts.push(serde_json::json!({
15800                "name": "workflow:analyze_image",
15801                "description": "Visual analysis workflow: pix register → annotate objects → compute metrics → report via axonendpoint. Image understanding pipeline.",
15802                "arguments": [
15803                    { "name": "image_source", "description": "Image URL or path", "required": true },
15804                    { "name": "analysis_type", "description": "Analysis: objects, text, features, all", "required": false },
15805                ],
15806            }));
15807
15808            Ok(Json(serde_json::json!({
15809                "jsonrpc": "2.0",
15810                "id": id,
15811                "result": {
15812                    "prompts": prompts,
15813                    "_axon_primitives": {
15814                        "count": AXON_COGNITIVE_PRIMITIVES.len(),
15815                        "inventory": AXON_COGNITIVE_PRIMITIVES,
15816                        "categories": {
15817                            "declarations": ["persona", "context", "flow", "anchor", "tool", "memory", "type",
15818                                            "agent", "shield", "pix", "psyche", "corpus", "dataspace",
15819                                            "ots", "mandate", "compute", "daemon", "axonstore", "axonendpoint", "lambda"],
15820                            "epistemic": ["know", "believe", "speculate", "doubt"],
15821                            "execution": ["step", "reason", "validate", "refine", "weave", "probe", "use",
15822                                         "remember", "recall", "par", "hibernate", "deliberate", "consensus", "forge"],
15823                            "navigation": ["stream", "navigate", "drill", "trail", "corroborate",
15824                                          "focus", "associate", "aggregate", "explore"],
15825                        },
15826                    }
15827                }
15828            })))
15829        }
15830        "prompts/get" => {
15831            let prompt_name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
15832            let arguments = params.get("arguments").cloned().unwrap_or(serde_json::json!({}));
15833
15834            let s = state.lock().unwrap();
15835            check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
15836
15837            // ── Workflow prompt templates ──
15838            if let Some(workflow_name) = prompt_name.strip_prefix("workflow:") {
15839                let workflow_prompt = match workflow_name {
15840                    "research" => {
15841                        let question = arguments.get("question").and_then(|v| v.as_str()).unwrap_or("(no question)");
15842                        let sources = arguments.get("sources").and_then(|v| v.as_str()).unwrap_or("corpus:default,axonstore:default");
15843                        let format = arguments.get("output_format").and_then(|v| v.as_str()).unwrap_or("markdown");
15844                        serde_json::json!({
15845                            "description": "Research workflow: probe → weave → forge",
15846                            "messages": [{
15847                                "role": "user",
15848                                "content": { "type": "text", "text": format!(
15849                                    "Execute AXON research workflow:\n\n\
15850                                    1. PROBE: Investigate '{}' across sources [{}]\n\
15851                                    2. WEAVE: Synthesize findings with source attribution\n\
15852                                    3. FORGE: Render as {} artifact\n\n\
15853                                    ΛD: All outputs are derived (c≤0.99). Cite sources.",
15854                                    question, sources, format
15855                                )}
15856                            }],
15857                            "_axon": {
15858                                "workflow": "probe→weave→forge",
15859                                "primitives": ["probe", "weave", "forge"],
15860                                "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
15861                            }
15862                        })
15863                    }
15864                    "decide" => {
15865                        let question = arguments.get("question").and_then(|v| v.as_str()).unwrap_or("(no question)");
15866                        let options = arguments.get("options").and_then(|v| v.as_str()).unwrap_or("option_a,option_b");
15867                        let depth = arguments.get("max_depth").and_then(|v| v.as_str()).unwrap_or("3");
15868                        serde_json::json!({
15869                            "description": "Decision workflow: drill → corroborate → deliberate",
15870                            "messages": [{
15871                                "role": "user",
15872                                "content": { "type": "text", "text": format!(
15873                                    "Execute AXON decision workflow:\n\n\
15874                                    1. DRILL: Explore options [{}] recursively (depth: {})\n\
15875                                    2. CORROBORATE: Verify key claims with cross-source evidence\n\
15876                                    3. DELIBERATE: Evaluate pros/cons and select best option\n\n\
15877                                    Question: {}\n\
15878                                    ΛD: Certainty based on evidence margin.",
15879                                    options, depth, question
15880                                )}
15881                            }],
15882                            "_axon": {
15883                                "workflow": "drill→corroborate→deliberate",
15884                                "primitives": ["drill", "corroborate", "deliberate"],
15885                                "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
15886                            }
15887                        })
15888                    }
15889                    "secure_transfer" => {
15890                        let ttl = arguments.get("ttl_secs").and_then(|v| v.as_str()).unwrap_or("3600");
15891                        let role = arguments.get("recipient_role").and_then(|v| v.as_str()).unwrap_or("operator");
15892                        serde_json::json!({
15893                            "description": "Secure transfer workflow: axonstore → shield → ots → mandate",
15894                            "messages": [{
15895                                "role": "user",
15896                                "content": { "type": "text", "text": format!(
15897                                    "Execute AXON secure transfer workflow:\n\n\
15898                                    1. AXONSTORE: Persist payload securely\n\
15899                                    2. SHIELD: Validate no credential leakage in outputs\n\
15900                                    3. OTS: Create one-time secret (TTL: {}s)\n\
15901                                    4. MANDATE: Authorize access for role '{}'\n\n\
15902                                    ΛD: Checkpoint raw, delivery ephemeral.",
15903                                    ttl, role
15904                                )}
15905                            }],
15906                            "_axon": {
15907                                "workflow": "axonstore→shield→ots→mandate",
15908                                "primitives": ["axonstore", "shield", "ots", "mandate"],
15909                                "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
15910                            }
15911                        })
15912                    }
15913                    "reflect" => {
15914                        let context = arguments.get("context").and_then(|v| v.as_str()).unwrap_or("(no context)");
15915                        let depth = arguments.get("depth").and_then(|v| v.as_str()).unwrap_or("medium");
15916                        serde_json::json!({
15917                            "description": "Metacognitive workflow: psyche → probe → weave",
15918                            "messages": [{
15919                                "role": "user",
15920                                "content": { "type": "text", "text": format!(
15921                                    "Execute AXON metacognitive workflow ({} depth):\n\n\
15922                                    1. PSYCHE: Self-reflect on '{}' — identify gaps, biases, strengths\n\
15923                                    2. PROBE: Investigate identified knowledge gaps\n\
15924                                    3. WEAVE: Synthesize original knowledge + new findings\n\n\
15925                                    ΛD: All self-reflection is derived (c≤0.99).",
15926                                    depth, context
15927                                )}
15928                            }],
15929                            "_axon": {
15930                                "workflow": "psyche→probe→weave",
15931                                "primitives": ["psyche", "probe", "weave"],
15932                                "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
15933                            }
15934                        })
15935                    }
15936                    "analyze_image" => {
15937                        let source = arguments.get("image_source").and_then(|v| v.as_str()).unwrap_or("(no source)");
15938                        let analysis = arguments.get("analysis_type").and_then(|v| v.as_str()).unwrap_or("all");
15939                        serde_json::json!({
15940                            "description": "Visual analysis workflow: pix → compute → axonendpoint",
15941                            "messages": [{
15942                                "role": "user",
15943                                "content": { "type": "text", "text": format!(
15944                                    "Execute AXON visual analysis workflow:\n\n\
15945                                    1. PIX: Register image '{}' and annotate (type: {})\n\
15946                                    2. COMPUTE: Calculate scene metrics from annotations\n\
15947                                    3. AXONENDPOINT: Report results to monitoring endpoint\n\n\
15948                                    ΛD: Image metadata raw, annotations derived.",
15949                                    source, analysis
15950                                )}
15951                            }],
15952                            "_axon": {
15953                                "workflow": "pix→compute→axonendpoint",
15954                                "primitives": ["pix", "compute", "axonendpoint"],
15955                                "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
15956                            }
15957                        })
15958                    }
15959                    _ => {
15960                        return Ok(Json(serde_json::json!({
15961                            "jsonrpc": "2.0", "id": id,
15962                            "error": { "code": -32602, "message": format!("unknown workflow prompt: {}", workflow_name) },
15963                            "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15964                        })));
15965                    }
15966                };
15967
15968                return Ok(Json(serde_json::json!({
15969                    "jsonrpc": "2.0", "id": id,
15970                    "result": workflow_prompt,
15971                })));
15972            }
15973
15974            // Parse "flow:persona" format
15975            let parts: Vec<&str> = prompt_name.splitn(2, ':').collect();
15976            if parts.len() != 2 {
15977                return Ok(Json(serde_json::json!({
15978                    "jsonrpc": "2.0", "id": id,
15979                    "error": { "code": -32602, "message": format!("prompt name must be 'flow:persona' or 'workflow:name', got '{}'", prompt_name) }
15980                })));
15981            }
15982            let (flow_name, persona_name) = (parts[0], parts[1]);
15983
15984            let active = match s.versions.get_active(flow_name) {
15985                Some(v) => v,
15986                None => return Ok(Json(serde_json::json!({
15987                    "jsonrpc": "2.0", "id": id,
15988                    "error": { "code": -32602, "message": format!("flow '{}' not deployed", flow_name) },
15989                    "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15990                }))),
15991            };
15992
15993            let personas = extract_personas(&active.source);
15994            let contexts = extract_contexts(&active.source);
15995
15996            let persona = personas.iter().find(|(n, _, _, _, _)| n == persona_name);
15997            match persona {
15998                Some((name, domain, tone, confidence, desc)) => {
15999                    // Build system prompt from persona + context
16000                    let mut system_parts = vec![
16001                        format!("You are {}, an AXON cognitive persona.", name),
16002                    ];
16003                    if !domain.is_empty() {
16004                        system_parts.push(format!("Domain expertise: {}.", domain.join(", ")));
16005                    }
16006                    if !tone.is_empty() {
16007                        system_parts.push(format!("Communication tone: {}.", tone));
16008                    }
16009                    if let Some(ct) = confidence {
16010                        system_parts.push(format!("Confidence threshold: {:.0}%.", ct * 100.0));
16011                    }
16012                    if !desc.is_empty() {
16013                        system_parts.push(desc.clone());
16014                    }
16015
16016                    // Include first context as additional system context
16017                    if let Some((ctx_name, scope, depth, max_tok, temp)) = contexts.first() {
16018                        system_parts.push(format!("Context '{}': scope={}, depth={}.", ctx_name, scope, depth));
16019                        if let Some(t) = temp {
16020                            system_parts.push(format!("Temperature: {}.", t));
16021                        }
16022                    }
16023
16024                    let system_message = system_parts.join(" ");
16025
16026                    // Build user message from arguments
16027                    let user_input = params.get("arguments")
16028                        .and_then(|a| a.get("input"))
16029                        .and_then(|i| i.as_str())
16030                        .unwrap_or("(no input provided)");
16031
16032                    Ok(Json(serde_json::json!({
16033                        "jsonrpc": "2.0",
16034                        "id": id,
16035                        "result": {
16036                            "description": format!("Prompt for persona '{}' in flow '{}'", name, flow_name),
16037                            "messages": [
16038                                { "role": "user", "content": { "type": "text", "text": format!("{}\n\n{}", system_message, user_input) } },
16039                            ],
16040                            "_axon": {
16041                                "persona": name,
16042                                "flow": flow_name,
16043                                "domain": domain,
16044                                "tone": tone,
16045                                "confidence_threshold": confidence,
16046                                "contexts": contexts.iter().map(|(n, s, d, mt, t)| {
16047                                    serde_json::json!({"name": n, "scope": s, "depth": d, "max_tokens": mt, "temperature": t})
16048                                }).collect::<Vec<_>>(),
16049                                "epistemic_envelope": {
16050                                    "ontology": format!("mcp:prompt:{}:{}", flow_name, name),
16051                                    "certainty": 0.95,
16052                                    "derivation": "derived",
16053                                    "provenance": format!("emcp:axon_server:prompt:{}:{}", flow_name, name),
16054                                },
16055                                "lattice_position": "speculate",
16056                                "primitives_used": AXON_COGNITIVE_PRIMITIVES.len(),
16057                            }
16058                        }
16059                    })))
16060                }
16061                None => {
16062                    Ok(Json(serde_json::json!({
16063                        "jsonrpc": "2.0", "id": id,
16064                        "error": { "code": -32602, "message": format!("persona '{}' not found in flow '{}'", persona_name, flow_name) },
16065                        "_axon_blame": { "blame": "caller", "reason": "CT-2: referenced non-existent persona" }
16066                    })))
16067                }
16068            }
16069        }
16070        _ => {
16071            Ok(Json(serde_json::json!({
16072                "jsonrpc": "2.0",
16073                "id": id,
16074                "error": {
16075                    "code": -32601,
16076                    "message": format!("method '{}' not found", method)
16077                }
16078            })))
16079        }
16080    }
16081}
16082
16083/// GET /v1/mcp/tools — list exposed MCP tools (convenience endpoint, non-JSON-RPC).
16084async fn mcp_tools_list_handler(
16085    State(state): State<SharedState>,
16086    headers: HeaderMap,
16087) -> Result<Json<serde_json::Value>, StatusCode> {
16088    let s = state.lock().unwrap();
16089    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
16090
16091    let mut tools: Vec<McpExposedTool> = Vec::new();
16092    for summary in s.versions.list_flows() {
16093        if let Some(active) = s.versions.get_active(&summary.flow_name) {
16094            tools.push(McpExposedTool {
16095                name: format!("axon_{}", summary.flow_name),
16096                description: format!("Execute AXON flow '{}' (v{})", summary.flow_name, active.version),
16097                input_schema: serde_json::json!({
16098                    "type": "object",
16099                    "properties": {
16100                        "backend": { "type": "string", "default": "stub" },
16101                        "input": { "type": "string" }
16102                    }
16103                }),
16104            });
16105        }
16106    }
16107
16108    Ok(Json(serde_json::json!({
16109        "tools": tools,
16110        "total": tools.len(),
16111        "protocol": "MCP 2024-11-05",
16112        "server": "axon-server",
16113    })))
16114}
16115
16116/// POST /v1/mcp/stream — MCP tools/call with streaming output via algebraic effects.
16117///
16118/// Executes the flow, emits tokens via StreamEmitter (the algebraic effect handler
16119/// h: F_Σ(B) → M_IO(B)), publishes to EventBus, and returns stream metadata
16120/// with ΛD epistemic envelope. Clients consume tokens via SSE on the topic URL.
16121///
16122/// Stream(τ) = νX. (StreamChunk × EpistemicState × X)
16123/// Each chunk carries its lattice position and effect row.
16124async fn mcp_stream_handler(
16125    State(state): State<SharedState>,
16126    headers: HeaderMap,
16127    Json(payload): Json<serde_json::Value>,
16128) -> Result<Json<serde_json::Value>, StatusCode> {
16129    let client = client_key_from_headers(&headers);
16130    {
16131        let mut s = state.lock().unwrap();
16132        check_auth(&mut s, &headers, AccessLevel::Write)?;
16133    }
16134
16135    let tool_name = payload.get("name").and_then(|n| n.as_str()).unwrap_or("");
16136    let arguments = payload.get("arguments").cloned().unwrap_or(serde_json::json!({}));
16137    let flow_name = tool_name.strip_prefix("axon_").unwrap_or(tool_name);
16138    let backend = arguments.get("backend").and_then(|b| b.as_str()).unwrap_or("stub");
16139
16140    // Resolve source and key
16141    let (source, source_file, resolved_key, tenant_secrets_arc) = {
16142        let s = state.lock().unwrap();
16143        let ts = s.tenant_secrets.clone();
16144        let history = s.versions.get_history(flow_name);
16145        match history.and_then(|h| h.active()) {
16146            Some(active) => {
16147                let key = resolve_backend_key(&s, backend).ok();
16148                (active.source.clone(), active.source_file.clone(), key, ts)
16149            }
16150            None => {
16151                return Ok(Json(serde_json::json!({
16152                    "error": format!("flow '{}' not deployed", flow_name),
16153                    "_axon_blame": { "blame": "caller", "reason": "CT-2" },
16154                })));
16155            }
16156        }
16157    };
16158
16159    // Async SM fetch for cold cache (M3)
16160    let resolved_key = if resolved_key.is_none() {
16161        let tenant_id = crate::tenant::current_tenant_id();
16162        tenant_secrets_arc.get_api_key(&tenant_id, backend).await.ok()
16163    } else {
16164        resolved_key
16165    };
16166
16167    // Execute
16168    // §Fase 37.y — this path serves non-dynamic-route callers; pass
16169    // empty path + query maps.
16170    let empty_path = std::collections::HashMap::new();
16171    let empty_query = std::collections::HashMap::new();
16172    match server_execute(
16173        &source, &source_file, flow_name, backend, resolved_key.as_deref(), None,
16174        &empty_path, &empty_query,
16175    ) {
16176        Ok(mut er) => {
16177            // Record trace
16178            let mut trace_entry = crate::trace_store::build_trace(
16179                &er.flow_name, &er.source_file, &er.backend, &client,
16180                if er.success { crate::trace_store::TraceStatus::Success }
16181                else { crate::trace_store::TraceStatus::Partial },
16182                er.steps_executed, er.latency_ms,
16183            );
16184            trace_entry.tokens_input = er.tokens_input;
16185            trace_entry.tokens_output = er.tokens_output;
16186            trace_entry.errors = er.errors;
16187
16188            let (trace_id, stream_topic, token_count) = {
16189                let mut s = state.lock().unwrap();
16190                let tid = s.trace_store.record(trace_entry);
16191
16192                // Algebraic Effect Handler: StreamEmitter
16193                // h: F_Σ(B) → M_IO(B) — captures perform(Emit(v)) and publishes
16194                let mut emitter = StreamEmitter::new(tid, &er.flow_name);
16195                for (i, step_name) in er.step_names.iter().enumerate() {
16196                    if let Some(chunks) = er.step_results.get(i).map(|r| {
16197                        if r.is_empty() { vec![] }
16198                        else {
16199                            r.split_whitespace()
16200                                .collect::<Vec<&str>>()
16201                                .chunks(3)
16202                                .map(|c| c.join(" "))
16203                                .collect()
16204                        }
16205                    }) {
16206                        emitter.emit_chunks(step_name, &chunks);
16207                    }
16208                }
16209                emitter.finalize();
16210                let tc = emitter.token_count();
16211                emitter.publish_to_bus(&s.event_bus);
16212
16213                // Record backend metrics
16214                record_backend_metrics(
16215                    &mut s, &er.backend, er.success,
16216                    er.tokens_input, er.tokens_output, er.latency_ms,
16217                );
16218
16219                let topic = format!("flow.stream.{}", tid);
16220                (tid, topic, tc)
16221            };
16222
16223            er.trace_id = trace_id;
16224
16225            // ΛD Epistemic Envelope
16226            let certainty = if er.success && er.anchor_breaches == 0 { 0.85 }
16227                else if er.success { 0.5 } else { 0.1 };
16228            let envelope = EpistemicEnvelope::derived(
16229                &format!("mcp:stream:{}", flow_name), certainty,
16230                &format!("emcp:axon_server:stream:{}:{}", flow_name, er.backend),
16231            );
16232
16233            // Effect row
16234            let mut effects = vec!["io".to_string()];
16235            if backend != "stub" { effects.push("network".into()); }
16236            let epistemic_effect = if certainty >= 0.85 { "epistemic:speculate" }
16237                else if certainty >= 0.5 { "epistemic:doubt" }
16238                else { "epistemic:uncertain" };
16239            effects.push(epistemic_effect.into());
16240
16241            let lattice = if certainty >= 0.85 { "speculate" }
16242                else if certainty >= 0.5 { "doubt" } else { "⊥" };
16243
16244            Ok(Json(serde_json::json!({
16245                "success": er.success,
16246                "trace_id": trace_id,
16247                "flow": er.flow_name,
16248                "backend": er.backend,
16249                "stream": {
16250                    "topic": stream_topic,
16251                    "token_count": token_count,
16252                    "consume_url": format!("/v1/events/stream?topic={}", stream_topic),
16253                    "protocol": "SSE (Server-Sent Events)",
16254                    // Stream(τ) = νX. (StreamChunk × EpistemicState × X)
16255                    "coinductive_type": "Stream(τ) = νX. (StreamChunk × EpistemicState × X)",
16256                },
16257                "algebraic_effect": {
16258                    "handler": "StreamEmitter: h: F_Σ(B) → M_IO(B)",
16259                    "operation": "perform(Emit(token))",
16260                    "materialization": format!("EventBus.publish(\"{}\")", stream_topic),
16261                },
16262                "_axon": {
16263                    "epistemic_envelope": {
16264                        "ontology": envelope.ontology,
16265                        "certainty": envelope.certainty,
16266                        "temporal_start": envelope.temporal_start,
16267                        "temporal_end": envelope.temporal_end,
16268                        "provenance": envelope.provenance,
16269                        "derivation": envelope.derivation,
16270                    },
16271                    "lattice_position": lattice,
16272                    "effect_row": effects,
16273                    "blame": "none",
16274                    "anchor_checks": er.anchor_checks,
16275                    "anchor_breaches": er.anchor_breaches,
16276                },
16277            })))
16278        }
16279        Err(e) => {
16280            let blame = if e.contains("Backend error") || e.contains("timeout") { "network" }
16281                else { "server" };
16282            Ok(Json(serde_json::json!({
16283                "success": false,
16284                "error": e,
16285                "_axon": {
16286                    "blame": blame,
16287                    "lattice_position": "⊥",
16288                    "epistemic_envelope": {
16289                        "ontology": format!("mcp:stream:{}:error", flow_name),
16290                        "certainty": 0.0,
16291                        "derivation": "failed",
16292                    },
16293                },
16294            })))
16295        }
16296    }
16297}
16298
16299/// GET /v1/dashboard — comprehensive server status overview.
16300/// GET /v1/primitives — cognitive primitive inventory with runtime wiring status.
16301/// Reports all 47 AXON cognitive primitives, their category, whether they are
16302/// wired to runtime (HTTP API or MCP), and ΛD alignment metadata.
16303async fn primitives_handler(
16304    State(state): State<SharedState>,
16305    headers: HeaderMap,
16306) -> Result<Json<serde_json::Value>, StatusCode> {
16307    let s = state.lock().unwrap();
16308    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
16309
16310    // Runtime wiring map: primitive → (status, endpoint/mechanism, phase)
16311    let wired: HashMap<&str, (&str, &str, &str)> = [
16312        // Declarations wired to runtime
16313        ("axonstore", ("wired", "/v1/axonstore/* + MCP tools/call (persist/retrieve/mutate/purge/transact)", "G2+G8")),
16314        ("dataspace", ("wired", "/v1/dataspace/* (ingest/focus/associate/aggregate/explore)", "G3")),
16315        ("flow", ("wired", "/v1/deploy + /v1/execute + /v1/inspect", "D")),
16316        ("persona", ("wired", "MCP prompts/list + prompts/get", "E6")),
16317        ("context", ("wired", "MCP prompts/get (system prompt enrichment)", "E6")),
16318        ("anchor", ("wired", "/v1/execute (anchor_checks, anchor_breaches)", "D")),
16319        ("tool", ("wired", "/v1/tools/* (registry, dispatch, CSP §5.3)", "D")),
16320        ("memory", ("wired", "/v1/session/remember + /v1/session/recall", "D")),
16321        ("daemon", ("wired", "/v1/daemons/* (lifecycle, supervisor)", "D")),
16322        ("agent", ("wired", "/v1/execute/pipeline (multi-flow orchestration)", "D")),
16323        ("type", ("wired", "IR type system (lex → parse → type check)", "B")),
16324        ("lambda", ("wired", "IR lambda expressions in compiler", "B")),
16325        // Step primitives wired to runtime
16326        ("step", ("wired", "/v1/execute (step_results, steps_executed)", "D")),
16327        ("reason", ("wired", "runner.rs execute_real (reason step type)", "B")),
16328        ("validate", ("wired", "/v1/flows/{name}/validate", "D")),
16329        ("use", ("wired", "tool dispatch in runner", "D")),
16330        ("remember", ("wired", "/v1/session/remember", "D")),
16331        ("recall", ("wired", "/v1/session/recall", "D")),
16332        ("stream", ("wired", "/v1/execute/stream (SSE, algebraic effects)", "D")),
16333        ("par", ("wired", "runner.rs parallel step execution", "D")),
16334        // Epistemic primitives
16335        ("know", ("wired", "EpistemicEnvelope c=1.0 (lattice top for raw)", "E7")),
16336        ("believe", ("wired", "epistemic lattice position in MCP", "E7")),
16337        ("speculate", ("wired", "EpistemicEnvelope c=0.85 (MCP tool result)", "E7")),
16338        ("doubt", ("wired", "EpistemicEnvelope c=0.5 (anchor breaches)", "E7")),
16339        // Navigation primitives (dataspace)
16340        ("focus", ("wired", "/v1/dataspace/{name}/focus + MCP tools/call", "G3+G5")),
16341        ("associate", ("wired", "/v1/dataspace/{name}/associate", "G3")),
16342        ("aggregate", ("wired", "/v1/dataspace/{name}/aggregate + MCP tools/call", "G3+G5")),
16343        ("explore", ("wired", "/v1/dataspace/{name}/explore", "G3")),
16344        // Persistence primitives (axonstore)
16345        ("navigate", ("wired", "dataspace focus+explore navigation pattern", "G3")),
16346        // Pending primitives
16347        ("shield", ("wired", "/v1/shields/* (create/evaluate/rules with deny_list/pattern/pii/length)", "G9")),
16348        ("pix", ("wired", "/v1/pix/* (image/annotate with bbox and visual classification)", "G27")),
16349        ("psyche", ("wired", "/v1/psyche/* (insight/complete with self-awareness scoring)", "G25")),
16350        ("corpus", ("wired", "/v1/corpus/* (ingest/search/cite with ΛD envelopes)", "G11")),
16351        ("ots", ("wired", "/v1/ots/* (create/retrieve-once with TTL and ephemeral destruction)", "G24")),
16352        ("mandate", ("wired", "/v1/mandates/* (policy CRUD, evaluate with priority-ordered first-match)", "G13")),
16353        ("compute", ("wired", "/v1/compute/* (evaluate/batch/functions with ΛD exactness tracking)", "G12")),
16354        ("axonendpoint", ("wired", "/v1/endpoints/* (bind/call with URL templates and auth config)", "G26")),
16355        ("refine", ("wired", "/v1/refine/* (start/iterate/status with convergence tracking)", "G14")),
16356        ("weave", ("wired", "/v1/weaves/* (strand/synthesize with attribution and weighted certainty)", "G17")),
16357        ("probe", ("wired", "/v1/probes/* (create/query/complete with multi-source findings)", "G16")),
16358        ("hibernate", ("wired", "/v1/hibernate/* (checkpoint/suspend/resume with state preservation)", "G23")),
16359        ("deliberate", ("wired", "/v1/deliberate/* (option/evaluate/eliminate/decide with scoring)", "G21")),
16360        ("consensus", ("wired", "/v1/consensus/* (vote/resolve with quorum and agreement scoring)", "G22")),
16361        ("forge", ("wired", "/v1/forges/* (template/render with {{variable}} substitution)", "G20")),
16362        ("drill", ("wired", "/v1/drills/* (expand/complete with depth-limited exploration tree)", "G19")),
16363        ("trail", ("wired", "/v1/trails/* (start/step/complete with step-by-step trace)", "G15")),
16364        ("corroborate", ("wired", "/v1/corroborate/* (evidence/verify with agreement scoring)", "G18")),
16365    ].into_iter().collect();
16366
16367    let mut declarations: Vec<serde_json::Value> = Vec::new();
16368    let mut step_primitives: Vec<serde_json::Value> = Vec::new();
16369    let mut epistemic: Vec<serde_json::Value> = Vec::new();
16370    let mut navigation: Vec<serde_json::Value> = Vec::new();
16371
16372    let decl_names = ["persona", "context", "flow", "anchor", "tool", "memory", "type",
16373        "agent", "shield", "pix", "psyche", "corpus", "dataspace",
16374        "ots", "mandate", "compute", "daemon", "axonstore", "axonendpoint", "lambda"];
16375    let step_names = ["step", "reason", "validate", "refine", "weave", "probe",
16376        "use", "remember", "recall", "par", "hibernate", "deliberate", "consensus", "forge"];
16377    let epi_names = ["know", "believe", "speculate", "doubt"];
16378    let nav_names = ["stream", "navigate", "drill", "trail", "corroborate",
16379        "focus", "associate", "aggregate", "explore"];
16380
16381    let mut total_wired = 0u32;
16382    let mut total_pending = 0u32;
16383
16384    let build_entry = |name: &str, wired: &HashMap<&str, (&str, &str, &str)>| -> serde_json::Value {
16385        let (status, endpoint, phase) = wired.get(name).copied().unwrap_or(("unknown", "—", "—"));
16386        serde_json::json!({
16387            "name": name,
16388            "status": status,
16389            "endpoint": endpoint,
16390            "wired_in_phase": phase,
16391        })
16392    };
16393
16394    for name in &decl_names {
16395        let entry = build_entry(name, &wired);
16396        if entry["status"] == "wired" { total_wired += 1; } else { total_pending += 1; }
16397        declarations.push(entry);
16398    }
16399    for name in &step_names {
16400        let entry = build_entry(name, &wired);
16401        if entry["status"] == "wired" { total_wired += 1; } else { total_pending += 1; }
16402        step_primitives.push(entry);
16403    }
16404    for name in &epi_names {
16405        let entry = build_entry(name, &wired);
16406        if entry["status"] == "wired" { total_wired += 1; } else { total_pending += 1; }
16407        epistemic.push(entry);
16408    }
16409    for name in &nav_names {
16410        let entry = build_entry(name, &wired);
16411        if entry["status"] == "wired" { total_wired += 1; } else { total_pending += 1; }
16412        navigation.push(entry);
16413    }
16414
16415    let total = total_wired + total_pending;
16416    let coverage = if total > 0 { (total_wired as f64 / total as f64 * 10000.0).round() / 100.0 } else { 0.0 };
16417
16418    Ok(Json(serde_json::json!({
16419        "total_primitives": total,
16420        "wired": total_wired,
16421        "pending": total_pending,
16422        "coverage_percent": coverage,
16423        "categories": {
16424            "declarations": { "count": decl_names.len(), "primitives": declarations },
16425            "step": { "count": step_names.len(), "primitives": step_primitives },
16426            "epistemic": { "count": epi_names.len(), "primitives": epistemic },
16427            "navigation": { "count": nav_names.len(), "primitives": navigation },
16428        },
16429        "lambda_d_alignment": {
16430            "epistemic_envelope": "EpistemicEnvelope ψ = ⟨T, V, E⟩ where E = ⟨c, τ, ρ, δ⟩",
16431            "theorem_5_1": "Epistemic Degradation: only raw may carry c=1.0, derived ≤ 0.99",
16432            "lattice": "⊥ ⊑ doubt ⊑ speculate ⊑ believe ⊑ know",
16433            "blame_calculus": "Findler-Felleisen CT-2 (caller) / CT-3 (server) / Network",
16434            "csp": "CSP §5.3: tools as constraint satisfaction, anchors as constraints",
16435            "effect_rows": "<io, network?, epistemic:X> computed from backend and certainty",
16436        },
16437    })))
16438}
16439
16440async fn dashboard_handler(
16441    State(state): State<SharedState>,
16442    headers: HeaderMap,
16443) -> Result<Json<serde_json::Value>, StatusCode> {
16444    let mut s = state.lock().unwrap();
16445    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
16446
16447    let uptime_secs = s.started_at.elapsed().as_secs();
16448    let days = uptime_secs / 86400;
16449    let hours = (uptime_secs % 86400) / 3600;
16450    let minutes = (uptime_secs % 3600) / 60;
16451
16452    let bus_stats = s.event_bus.stats();
16453    let sup_counts = s.supervisor.state_counts();
16454    let wh_stats = s.webhooks.stats();
16455    let trace_stats = s.trace_store.stats();
16456
16457    // Daemon states summary
16458    let daemon_states: Vec<serde_json::Value> = s.daemons.values().map(|d| {
16459        serde_json::json!({
16460            "name": d.name,
16461            "state": d.state,
16462            "events": d.event_count,
16463        })
16464    }).collect();
16465
16466    // Active schedules
16467    let sched_enabled = s.schedules.values().filter(|e| e.enabled).count();
16468    let sched_errors: u64 = s.schedules.values().map(|e| e.error_count).sum();
16469
16470    // Cost summary
16471    let costs = compute_flow_costs(&s.trace_store, &s.cost_pricing);
16472    let total_cost: f64 = costs.iter().map(|c| c.estimated_cost_usd).sum();
16473
16474    // Budget alerts
16475    let alert_count = s.cost_budgets.iter().filter(|(flow, budget)| {
16476        let cost = costs.iter().find(|c| &c.flow_name == *flow).map(|c| c.estimated_cost_usd).unwrap_or(0.0);
16477        let pct = if budget.max_cost_usd > 0.0 { cost / budget.max_cost_usd } else { 0.0 };
16478        pct >= budget.warn_threshold
16479    }).count();
16480
16481    // Queue status
16482    let queue_pending = s.execution_queue.iter().filter(|q| q.status == "pending").count();
16483    let queue_processing = s.execution_queue.iter().filter(|q| q.status == "processing").count();
16484
16485    // Webhook retry/dead
16486    let retry_count = s.webhooks.retry_queue_len();
16487    let dead_count = s.webhooks.dead_letters_len();
16488
16489    // Client rate metrics
16490    let client_metrics = s.rate_limiter.client_metrics();
16491    let total_rejected: u64 = client_metrics.iter().map(|c| c.rejected).sum();
16492
16493    Ok(Json(serde_json::json!({
16494        "server": {
16495            "uptime_secs": uptime_secs,
16496            "uptime_formatted": format!("{}d {}h {}m", days, hours, minutes),
16497            "version": AXON_VERSION,
16498            "total_requests": s.metrics.total_requests,
16499            "total_errors": s.metrics.total_errors,
16500            "total_deployments": s.metrics.total_deployments,
16501        },
16502        "daemons": {
16503            "total": s.daemons.len(),
16504            "states": sup_counts,
16505            "list": daemon_states,
16506        },
16507        "event_bus": {
16508            "events_published": bus_stats.events_published,
16509            "events_delivered": bus_stats.events_delivered,
16510            "events_dropped": bus_stats.events_dropped,
16511            "topics": bus_stats.topics_seen.len(),
16512            "subscribers": bus_stats.active_subscribers,
16513        },
16514        "traces": {
16515            "buffered": s.trace_store.len(),
16516            "total_recorded": trace_stats.total_recorded,
16517            "avg_latency_ms": trace_stats.avg_latency_ms,
16518            "max_latency_ms": trace_stats.max_latency_ms,
16519            "retention_ttl_secs": s.trace_store.config().max_age_secs,
16520        },
16521        "schedules": {
16522            "total": s.schedules.len(),
16523            "enabled": sched_enabled,
16524            "total_errors": sched_errors,
16525        },
16526        "costs": {
16527            "total_estimated_usd": (total_cost * 10000.0).round() / 10000.0,
16528            "flows_tracked": costs.len(),
16529            "budget_alerts": alert_count,
16530        },
16531        "execution_queue": {
16532            "total": s.execution_queue.len(),
16533            "pending": queue_pending,
16534            "processing": queue_processing,
16535        },
16536        "webhooks": {
16537            "total": wh_stats.total_webhooks,
16538            "active": wh_stats.active_webhooks,
16539            "retry_queue": retry_count,
16540            "dead_letters": dead_count,
16541        },
16542        "rate_limiter": {
16543            "enabled": s.rate_limiter.config().enabled,
16544            "clients": s.rate_limiter.client_count(),
16545            "total_rejected": total_rejected,
16546        },
16547        "sessions": {
16548            "scopes": s.scoped_sessions.scope_count(),
16549            "total_memory": s.scoped_sessions.total_memory_count(),
16550            "total_store": s.scoped_sessions.total_store_count(),
16551        },
16552        "config_snapshots": s.config_snapshots.len(),
16553    })))
16554}
16555
16556/// An API route descriptor.
16557#[derive(Debug, Clone, Serialize)]
16558struct ApiRoute {
16559    method: &'static str,
16560    path: &'static str,
16561    description: &'static str,
16562    category: &'static str,
16563}
16564
16565/// Build the static API route table.
16566fn api_route_table() -> Vec<ApiRoute> {
16567    vec![
16568        ApiRoute { method: "GET", path: "/v1/health", description: "Full health report", category: "health" },
16569        ApiRoute { method: "GET", path: "/v1/health/live", description: "Liveness probe", category: "health" },
16570        ApiRoute { method: "GET", path: "/v1/health/ready", description: "Readiness probe", category: "health" },
16571        ApiRoute { method: "GET", path: "/v1/health/components", description: "Component-level health checks", category: "health" },
16572        ApiRoute { method: "GET", path: "/v1/version", description: "AXON version info", category: "server" },
16573        ApiRoute { method: "GET", path: "/v1/uptime", description: "Detailed server uptime with hourly buckets", category: "server" },
16574        ApiRoute { method: "GET", path: "/v1/dashboard", description: "Comprehensive server status overview", category: "server" },
16575        ApiRoute { method: "GET", path: "/v1/docs", description: "API documentation (this endpoint)", category: "server" },
16576        ApiRoute { method: "GET", path: "/v1/metrics", description: "Execution metrics", category: "metrics" },
16577        ApiRoute { method: "GET", path: "/v1/metrics/prometheus", description: "Prometheus exposition format", category: "metrics" },
16578        ApiRoute { method: "POST", path: "/v1/deploy", description: "Compile and deploy .axon source", category: "execution" },
16579        ApiRoute { method: "POST", path: "/v1/execute", description: "Execute a deployed flow", category: "execution" },
16580        ApiRoute { method: "POST", path: "/v1/execute/enqueue", description: "Enqueue flow execution with priority", category: "execution" },
16581        ApiRoute { method: "GET", path: "/v1/execute/queue", description: "View execution queue", category: "execution" },
16582        ApiRoute { method: "POST", path: "/v1/execute/dequeue", description: "Take next item from queue", category: "execution" },
16583        ApiRoute { method: "POST", path: "/v1/execute/drain", description: "Process all pending queue items", category: "execution" },
16584        ApiRoute { method: "POST", path: "/v1/estimate", description: "Estimate execution cost (tokens/USD)", category: "execution" },
16585        ApiRoute { method: "GET", path: "/v1/costs", description: "Aggregate per-flow cost summary", category: "costs" },
16586        ApiRoute { method: "GET", path: "/v1/costs/:flow", description: "Cost details for a specific flow", category: "costs" },
16587        ApiRoute { method: "PUT", path: "/v1/costs/pricing", description: "Update backend pricing config", category: "costs" },
16588        ApiRoute { method: "PUT", path: "/v1/costs/:flow/budget", description: "Set cost budget for a flow", category: "costs" },
16589        ApiRoute { method: "DELETE", path: "/v1/costs/:flow/budget", description: "Remove cost budget", category: "costs" },
16590        ApiRoute { method: "GET", path: "/v1/costs/alerts", description: "Check flows against cost budgets", category: "costs" },
16591        ApiRoute { method: "GET", path: "/v1/traces", description: "Query execution traces (list/filter)", category: "traces" },
16592        ApiRoute { method: "GET", path: "/v1/traces/:id", description: "Get a specific trace by ID", category: "traces" },
16593        ApiRoute { method: "GET", path: "/v1/traces/stats", description: "Aggregate trace analytics", category: "traces" },
16594        ApiRoute { method: "GET", path: "/v1/traces/search", description: "Full-text search across traces", category: "traces" },
16595        ApiRoute { method: "GET", path: "/v1/traces/aggregate", description: "Aggregated metrics with percentiles", category: "traces" },
16596        ApiRoute { method: "GET", path: "/v1/traces/heatmap", description: "Latency/error heatmap across time buckets", category: "traces" },
16597        ApiRoute { method: "GET", path: "/v1/traces/export", description: "Export traces as JSONL/CSV/Prometheus", category: "traces" },
16598        ApiRoute { method: "GET", path: "/v1/traces/diff", description: "Compare two traces side-by-side", category: "traces" },
16599        ApiRoute { method: "POST", path: "/v1/traces/compare", description: "Compare N traces across metrics", category: "traces" },
16600        ApiRoute { method: "POST", path: "/v1/traces/timeline", description: "Merged chronological timeline", category: "traces" },
16601        ApiRoute { method: "GET|PUT", path: "/v1/traces/retention", description: "Trace retention policy (max_age_secs)", category: "traces" },
16602        ApiRoute { method: "POST", path: "/v1/traces/evict", description: "Manually trigger TTL-based eviction", category: "traces" },
16603        ApiRoute { method: "DELETE", path: "/v1/traces/bulk", description: "Bulk delete traces by IDs", category: "traces" },
16604        ApiRoute { method: "POST", path: "/v1/traces/bulk/annotate", description: "Bulk annotate traces by IDs", category: "traces" },
16605        ApiRoute { method: "POST", path: "/v1/traces/:id/annotate", description: "Add annotation to a trace", category: "traces" },
16606        ApiRoute { method: "GET", path: "/v1/traces/:id/annotations", description: "List annotations for a trace", category: "traces" },
16607        ApiRoute { method: "POST", path: "/v1/traces/:id/replay", description: "Re-execute and compare results", category: "traces" },
16608        ApiRoute { method: "GET", path: "/v1/traces/:id/flamegraph", description: "Flamegraph-style span tree", category: "traces" },
16609        ApiRoute { method: "GET", path: "/v1/daemons", description: "List registered daemons", category: "daemons" },
16610        ApiRoute { method: "GET|DELETE", path: "/v1/daemons/:name", description: "Get/delete individual daemon", category: "daemons" },
16611        ApiRoute { method: "POST", path: "/v1/daemons/:name/run", description: "Execute daemon's flow", category: "daemons" },
16612        ApiRoute { method: "POST", path: "/v1/daemons/:name/pause", description: "Pause a daemon", category: "daemons" },
16613        ApiRoute { method: "POST", path: "/v1/daemons/:name/resume", description: "Resume a paused daemon", category: "daemons" },
16614        ApiRoute { method: "GET", path: "/v1/daemons/:name/events", description: "Lifecycle events for a daemon", category: "daemons" },
16615        ApiRoute { method: "GET", path: "/v1/daemons/dependencies", description: "Inferred daemon dependency graph", category: "daemons" },
16616        ApiRoute { method: "GET|PUT|DELETE", path: "/v1/daemons/:name/trigger", description: "Daemon event trigger binding", category: "triggers" },
16617        ApiRoute { method: "GET", path: "/v1/triggers", description: "List all trigger bindings", category: "triggers" },
16618        ApiRoute { method: "POST", path: "/v1/triggers/dispatch", description: "Dispatch event to triggered daemons", category: "triggers" },
16619        ApiRoute { method: "POST", path: "/v1/triggers/replay", description: "Replay historical events", category: "triggers" },
16620        ApiRoute { method: "GET", path: "/v1/events/history", description: "Recent event bus history", category: "events" },
16621        ApiRoute { method: "GET|PUT|DELETE", path: "/v1/daemons/:name/chain", description: "Daemon output chain binding", category: "chains" },
16622        ApiRoute { method: "GET", path: "/v1/chains", description: "List all chain bindings", category: "chains" },
16623        ApiRoute { method: "GET", path: "/v1/chains/graph", description: "Chain topology as DOT/Mermaid", category: "chains" },
16624        ApiRoute { method: "GET|POST", path: "/v1/schedules", description: "List/create scheduled executions", category: "schedules" },
16625        ApiRoute { method: "GET|DELETE", path: "/v1/schedules/:name", description: "Get/delete individual schedule", category: "schedules" },
16626        ApiRoute { method: "POST", path: "/v1/schedules/:name/toggle", description: "Enable/disable a schedule", category: "schedules" },
16627        ApiRoute { method: "GET", path: "/v1/schedules/:name/history", description: "Schedule execution history", category: "schedules" },
16628        ApiRoute { method: "POST", path: "/v1/schedules/tick", description: "Poll-based scheduler tick", category: "schedules" },
16629        ApiRoute { method: "GET", path: "/v1/rate-limit", description: "Rate limit status", category: "auth" },
16630        ApiRoute { method: "GET|POST|DELETE", path: "/v1/keys", description: "API key management", category: "auth" },
16631        ApiRoute { method: "GET|POST", path: "/v1/webhooks", description: "Webhook management", category: "webhooks" },
16632        ApiRoute { method: "GET", path: "/v1/webhooks/stats", description: "Webhook aggregate stats", category: "webhooks" },
16633        ApiRoute { method: "GET", path: "/v1/webhooks/retry-queue", description: "Pending webhook retries", category: "webhooks" },
16634        ApiRoute { method: "GET", path: "/v1/webhooks/dead-letters", description: "Failed webhook deliveries", category: "webhooks" },
16635        ApiRoute { method: "GET|PUT", path: "/v1/config", description: "Runtime server configuration", category: "config" },
16636        ApiRoute { method: "POST", path: "/v1/config/save", description: "Save config to disk", category: "config" },
16637        ApiRoute { method: "POST", path: "/v1/config/load", description: "Load config from disk", category: "config" },
16638        ApiRoute { method: "GET|POST", path: "/v1/config/snapshots", description: "Config snapshot management", category: "config" },
16639        ApiRoute { method: "POST", path: "/v1/config/snapshots/restore", description: "Restore from named snapshot", category: "config" },
16640        ApiRoute { method: "GET", path: "/v1/audit", description: "Query audit trail entries", category: "audit" },
16641        ApiRoute { method: "GET", path: "/v1/audit/stats", description: "Audit trail statistics", category: "audit" },
16642        ApiRoute { method: "GET", path: "/v1/audit/export", description: "Export audit trail as JSONL/CSV", category: "audit" },
16643        ApiRoute { method: "GET|PUT", path: "/v1/cors", description: "CORS configuration", category: "config" },
16644        ApiRoute { method: "GET|PUT", path: "/v1/middleware", description: "Request middleware config/stats", category: "config" },
16645        ApiRoute { method: "GET", path: "/v1/inspect", description: "List deployed flows", category: "inspect" },
16646        ApiRoute { method: "GET", path: "/v1/inspect/:name", description: "Introspect flow by name", category: "inspect" },
16647        ApiRoute { method: "GET", path: "/v1/inspect/:name/graph", description: "Flow graph export", category: "inspect" },
16648        ApiRoute { method: "GET", path: "/v1/session/:scope/export", description: "Export scoped session data", category: "session" },
16649        ApiRoute { method: "GET", path: "/v1/logs", description: "Query recent request logs", category: "logs" },
16650        ApiRoute { method: "GET", path: "/v1/logs/stats", description: "Aggregate request statistics", category: "logs" },
16651        ApiRoute { method: "POST", path: "/v1/shutdown", description: "Initiate graceful shutdown (admin)", category: "server" },
16652    ]
16653}
16654
16655/// GET /v1/docs — API documentation with route listing.
16656async fn docs_handler() -> Json<serde_json::Value> {
16657    let routes = api_route_table();
16658
16659    // Group by category
16660    let mut categories: std::collections::BTreeMap<&str, Vec<&ApiRoute>> = std::collections::BTreeMap::new();
16661    for r in &routes {
16662        categories.entry(r.category).or_default().push(r);
16663    }
16664
16665    let category_summaries: Vec<serde_json::Value> = categories.iter().map(|(cat, rs)| {
16666        serde_json::json!({
16667            "category": cat,
16668            "endpoints": rs.len(),
16669        })
16670    }).collect();
16671
16672    Json(serde_json::json!({
16673        "api_version": "v1",
16674        "total_endpoints": routes.len(),
16675        "categories": category_summaries,
16676        "routes": routes,
16677    }))
16678}
16679
16680/// Request for sandboxed flow execution.
16681#[derive(Debug, Deserialize)]
16682pub struct SandboxRequest {
16683    /// Flow name to execute.
16684    pub flow_name: String,
16685    /// Backend override (default "stub").
16686    #[serde(default = "default_execute_backend")]
16687    pub backend: String,
16688    /// Maximum steps allowed (0 = unlimited, default 50).
16689    #[serde(default = "default_sandbox_max_steps")]
16690    pub max_steps: usize,
16691    /// Timeout in milliseconds (0 = no timeout, default 5000).
16692    #[serde(default = "default_sandbox_timeout_ms")]
16693    pub timeout_ms: u64,
16694    /// Maximum total tokens (0 = unlimited, default 10000).
16695    #[serde(default = "default_sandbox_max_tokens")]
16696    pub max_tokens: u64,
16697    /// Whether to record a trace (default false — sandbox is isolated).
16698    #[serde(default)]
16699    pub record_trace: bool,
16700}
16701
16702fn default_sandbox_max_steps() -> usize { 50 }
16703fn default_sandbox_timeout_ms() -> u64 { 5000 }
16704fn default_sandbox_max_tokens() -> u64 { 10000 }
16705
16706/// Sandbox execution result.
16707#[derive(Debug, Clone, Serialize)]
16708pub struct SandboxResult {
16709    pub success: bool,
16710    pub flow_name: String,
16711    pub backend: String,
16712    pub steps_executed: usize,
16713    pub latency_ms: u64,
16714    pub tokens_input: u64,
16715    pub tokens_output: u64,
16716    pub errors: usize,
16717    pub step_names: Vec<String>,
16718    pub limits_applied: SandboxLimits,
16719    pub limits_hit: Vec<String>,
16720    pub trace_id: Option<u64>,
16721    pub sandboxed: bool,
16722}
16723
16724/// Applied sandbox limits.
16725#[derive(Debug, Clone, Serialize)]
16726pub struct SandboxLimits {
16727    pub max_steps: usize,
16728    pub timeout_ms: u64,
16729    pub max_tokens: u64,
16730}
16731
16732/// POST /v1/execute/sandbox — execute a flow in an isolated sandbox with resource limits.
16733async fn execute_sandbox_handler(
16734    State(state): State<SharedState>,
16735    headers: HeaderMap,
16736    Json(payload): Json<SandboxRequest>,
16737) -> Result<Json<serde_json::Value>, StatusCode> {
16738    let req_start = Instant::now();
16739    let client = client_key_from_headers(&headers);
16740    {
16741        let mut s = state.lock().unwrap();
16742        check_auth(&mut s, &headers, AccessLevel::Write)?;
16743    }
16744
16745    // Look up deployed source
16746    let (source, source_file) = {
16747        let s = state.lock().unwrap();
16748        match s.versions.get_history(&payload.flow_name)
16749            .and_then(|h| h.active())
16750            .map(|v| (v.source.clone(), v.source_file.clone()))
16751        {
16752            Some(info) => info,
16753            None => return Ok(Json(serde_json::json!({
16754                "success": false,
16755                "error": format!("flow '{}' not deployed", payload.flow_name),
16756                "sandboxed": true,
16757            }))),
16758        }
16759    };
16760
16761    // Execute
16762    let (exec_result, _) = server_execute_full(&state, &source, &source_file, &payload.flow_name, &payload.backend);
16763
16764    let limits = SandboxLimits {
16765        max_steps: payload.max_steps,
16766        timeout_ms: payload.timeout_ms,
16767        max_tokens: payload.max_tokens,
16768    };
16769
16770    match exec_result {
16771        Ok(er) => {
16772            let latency = req_start.elapsed().as_millis() as u64;
16773            let total_tokens = er.tokens_input + er.tokens_output;
16774
16775            // Check limits
16776            let mut limits_hit = Vec::new();
16777            if payload.max_steps > 0 && er.steps_executed > payload.max_steps {
16778                limits_hit.push("max_steps".into());
16779            }
16780            if payload.timeout_ms > 0 && latency > payload.timeout_ms {
16781                limits_hit.push("timeout_ms".into());
16782            }
16783            if payload.max_tokens > 0 && total_tokens > payload.max_tokens {
16784                limits_hit.push("max_tokens".into());
16785            }
16786
16787            // Optionally record trace
16788            let trace_id = if payload.record_trace {
16789                let mut entry = crate::trace_store::build_trace(
16790                    &er.flow_name, &er.source_file, &er.backend, &client,
16791                    if er.success { crate::trace_store::TraceStatus::Success }
16792                    else { crate::trace_store::TraceStatus::Partial },
16793                    er.steps_executed, er.latency_ms,
16794                );
16795                entry.tokens_input = er.tokens_input;
16796                entry.tokens_output = er.tokens_output;
16797                entry.errors = er.errors;
16798                let mut s = state.lock().unwrap();
16799                Some(s.trace_store.record(entry))
16800            } else {
16801                None
16802            };
16803
16804            let result = SandboxResult {
16805                success: er.success && limits_hit.is_empty(),
16806                flow_name: er.flow_name,
16807                backend: er.backend,
16808                steps_executed: er.steps_executed,
16809                latency_ms: latency,
16810                tokens_input: er.tokens_input,
16811                tokens_output: er.tokens_output,
16812                errors: er.errors,
16813                step_names: er.step_names,
16814                limits_applied: limits,
16815                limits_hit,
16816                trace_id,
16817                sandboxed: true,
16818            };
16819
16820            Ok(Json(serde_json::to_value(&result).unwrap_or_default()))
16821        }
16822        Err(e) => {
16823            Ok(Json(serde_json::json!({
16824                "success": false,
16825                "flow_name": payload.flow_name,
16826                "error": e,
16827                "latency_ms": req_start.elapsed().as_millis() as u64,
16828                "limits_applied": limits,
16829                "sandboxed": true,
16830            })))
16831        }
16832    }
16833}
16834
16835/// Result of a hot-reload check for a single flow.
16836#[derive(Debug, Clone, Serialize)]
16837pub struct ReloadResult {
16838    pub flow_name: String,
16839    pub source_file: String,
16840    pub previous_hash: String,
16841    pub current_hash: String,
16842    pub changed: bool,
16843    pub redeployed: bool,
16844    pub error: Option<String>,
16845}
16846
16847/// POST /v1/deploy/reload — hot-reload all deployed flows by re-reading source files.
16848async fn deploy_reload_handler(
16849    State(state): State<SharedState>,
16850    headers: HeaderMap,
16851) -> Result<Json<serde_json::Value>, StatusCode> {
16852    let client = client_key_from_headers(&headers);
16853    {
16854        let mut s = state.lock().unwrap();
16855        check_auth(&mut s, &headers, AccessLevel::Admin)?;
16856    }
16857
16858    // Collect flow info
16859    let flows: Vec<(String, String, String, String)> = {
16860        let s = state.lock().unwrap();
16861        s.daemons.keys().filter_map(|name| {
16862            s.versions.get_history(name)
16863                .and_then(|h| h.active())
16864                .map(|v| (name.clone(), v.source_file.clone(), v.source_hash.clone(), v.backend.clone()))
16865        }).collect()
16866    };
16867
16868    let mut results = Vec::new();
16869
16870    for (flow_name, source_file, prev_hash, backend) in &flows {
16871        // Try to read source file from disk
16872        let disk_source = match std::fs::read_to_string(source_file) {
16873            Ok(s) => s,
16874            Err(e) => {
16875                results.push(ReloadResult {
16876                    flow_name: flow_name.clone(),
16877                    source_file: source_file.clone(),
16878                    previous_hash: prev_hash.clone(),
16879                    current_hash: String::new(),
16880                    changed: false,
16881                    redeployed: false,
16882                    error: Some(format!("cannot read file: {}", e)),
16883                });
16884                continue;
16885            }
16886        };
16887
16888        // Compute hash
16889        let current_hash = {
16890            let mut hash: u64 = 0xcbf29ce484222325;
16891            for byte in disk_source.bytes() {
16892                hash ^= byte as u64;
16893                hash = hash.wrapping_mul(0x100000001b3);
16894            }
16895            format!("{:016x}", hash)[..12].to_string()
16896        };
16897
16898        if current_hash == *prev_hash {
16899            results.push(ReloadResult {
16900                flow_name: flow_name.clone(),
16901                source_file: source_file.clone(),
16902                previous_hash: prev_hash.clone(),
16903                current_hash,
16904                changed: false,
16905                redeployed: false,
16906                error: None,
16907            });
16908            continue;
16909        }
16910
16911        // Changed — redeploy via compilation
16912        let tokens = match crate::lexer::Lexer::new(&disk_source, source_file).tokenize() {
16913            Ok(t) => t,
16914            Err(e) => {
16915                results.push(ReloadResult {
16916                    flow_name: flow_name.clone(),
16917                    source_file: source_file.clone(),
16918                    previous_hash: prev_hash.clone(),
16919                    current_hash,
16920                    changed: true,
16921                    redeployed: false,
16922                    error: Some(format!("lex error: {e:?}")),
16923                });
16924                continue;
16925            }
16926        };
16927
16928        let mut parser = crate::parser::Parser::new(tokens);
16929        let program = match parser.parse() {
16930            Ok(p) => p,
16931            Err(e) => {
16932                results.push(ReloadResult {
16933                    flow_name: flow_name.clone(),
16934                    source_file: source_file.clone(),
16935                    previous_hash: prev_hash.clone(),
16936                    current_hash,
16937                    changed: true,
16938                    redeployed: false,
16939                    error: Some(format!("parse error: {e:?}")),
16940                });
16941                continue;
16942            }
16943        };
16944
16945        let ir = crate::ir_generator::IRGenerator::new().generate(&program);
16946        let flow_names: Vec<String> = ir.flows.iter().map(|f| f.name.clone()).collect();
16947
16948        // Register new version
16949        {
16950            let mut s = state.lock().unwrap();
16951            s.versions.record_deploy(&flow_names, &disk_source, source_file, backend);
16952            s.deploy_count += 1;
16953            s.event_bus.publish(
16954                "deploy.reload",
16955                serde_json::json!({"flow": flow_name, "hash": &current_hash}),
16956                "server",
16957            );
16958        }
16959
16960        results.push(ReloadResult {
16961            flow_name: flow_name.clone(),
16962            source_file: source_file.clone(),
16963            previous_hash: prev_hash.clone(),
16964            current_hash,
16965            changed: true,
16966            redeployed: true,
16967            error: None,
16968        });
16969    }
16970
16971    let changed = results.iter().filter(|r| r.changed).count();
16972    let redeployed = results.iter().filter(|r| r.redeployed).count();
16973    let errors = results.iter().filter(|r| r.error.is_some()).count();
16974
16975    // Audit
16976    {
16977        let mut s = state.lock().unwrap();
16978        s.audit_log.record(
16979            &client, AuditAction::Deploy, "hot_reload",
16980            serde_json::json!({"checked": results.len(), "changed": changed, "redeployed": redeployed, "errors": errors}),
16981            true,
16982        );
16983    }
16984
16985    Ok(Json(serde_json::json!({
16986        "checked": results.len(),
16987        "changed": changed,
16988        "redeployed": redeployed,
16989        "errors": errors,
16990        "results": results,
16991    })))
16992}
16993
16994/// POST /v1/execute/process — dequeue next pending item, execute it, record trace, update status.
16995async fn execute_process_handler(
16996    State(state): State<SharedState>,
16997    headers: HeaderMap,
16998) -> Result<Json<serde_json::Value>, StatusCode> {
16999    let req_start = Instant::now();
17000    let client = client_key_from_headers(&headers);
17001    {
17002        let mut s = state.lock().unwrap();
17003        check_auth(&mut s, &headers, AccessLevel::Write)?;
17004    }
17005
17006    // Dequeue next pending
17007    let item = {
17008        let mut s = state.lock().unwrap();
17009        match s.execution_queue.iter_mut().find(|q| q.status == "pending") {
17010            Some(q) => {
17011                q.status = "processing".into();
17012                Some((q.id, q.flow_name.clone(), q.backend.clone(), q.priority))
17013            }
17014            None => None,
17015        }
17016    };
17017
17018    let (queue_id, flow_name, backend, priority) = match item {
17019        Some(i) => i,
17020        None => return Ok(Json(serde_json::json!({
17021            "success": false,
17022            "message": "no pending items in queue",
17023        }))),
17024    };
17025
17026    // Look up deployed source
17027    let source_info = {
17028        let s = state.lock().unwrap();
17029        s.versions.get_history(&flow_name)
17030            .and_then(|h| h.active())
17031            .map(|v| (v.source.clone(), v.source_file.clone()))
17032    };
17033
17034    let (source, source_file) = match source_info {
17035        Some(info) => info,
17036        None => {
17037            let mut s = state.lock().unwrap();
17038            if let Some(q) = s.execution_queue.iter_mut().find(|q| q.id == queue_id) {
17039                q.status = "failed".into();
17040            }
17041            return Ok(Json(serde_json::json!({
17042                "success": false,
17043                "queue_id": queue_id,
17044                "flow": flow_name,
17045                "error": "flow not deployed",
17046            })));
17047        }
17048    };
17049
17050    // Execute
17051    match server_execute_full(&state, &source, &source_file, &flow_name, &backend).0 {
17052        Ok(mut er) => {
17053            let mut trace_entry = crate::trace_store::build_trace(
17054                &er.flow_name, &er.source_file, &er.backend, &client,
17055                if er.success { crate::trace_store::TraceStatus::Success }
17056                else { crate::trace_store::TraceStatus::Partial },
17057                er.steps_executed, er.latency_ms,
17058            );
17059            trace_entry.tokens_input = er.tokens_input;
17060            trace_entry.tokens_output = er.tokens_output;
17061            trace_entry.errors = er.errors;
17062
17063            let trace_id = {
17064                let mut s = state.lock().unwrap();
17065                let tid = s.trace_store.record(trace_entry);
17066                if let Some(q) = s.execution_queue.iter_mut().find(|q| q.id == queue_id) {
17067                    q.status = if er.success { "completed" } else { "failed" }.into();
17068                }
17069                tid
17070            };
17071
17072            Ok(Json(serde_json::json!({
17073                "success": er.success,
17074                "queue_id": queue_id,
17075                "flow": flow_name,
17076                "backend": backend,
17077                "priority": priority,
17078                "trace_id": trace_id,
17079                "steps_executed": er.steps_executed,
17080                "latency_ms": er.latency_ms,
17081                "tokens_input": er.tokens_input,
17082                "tokens_output": er.tokens_output,
17083                "errors": er.errors,
17084                "total_latency_ms": req_start.elapsed().as_millis() as u64,
17085            })))
17086        }
17087        Err(e) => {
17088            let mut s = state.lock().unwrap();
17089            s.metrics.total_errors += 1;
17090            if let Some(q) = s.execution_queue.iter_mut().find(|q| q.id == queue_id) {
17091                q.status = "failed".into();
17092            }
17093            Ok(Json(serde_json::json!({
17094                "success": false,
17095                "queue_id": queue_id,
17096                "flow": flow_name,
17097                "error": e,
17098                "total_latency_ms": req_start.elapsed().as_millis() as u64,
17099            })))
17100        }
17101    }
17102}
17103
17104/// Request for dry-run execution.
17105#[derive(Debug, Deserialize)]
17106pub struct DryRunRequest {
17107    /// Flow name to validate.
17108    pub flow_name: String,
17109    /// Backend for cost estimation (default "stub").
17110    #[serde(default = "default_execute_backend")]
17111    pub backend: String,
17112}
17113
17114/// POST /v1/execute/dry-run — compile and validate without executing.
17115///
17116/// Returns step plan, dependency analysis, cost estimate, and type check results.
17117async fn execute_dry_run_handler(
17118    State(state): State<SharedState>,
17119    headers: HeaderMap,
17120    Json(payload): Json<DryRunRequest>,
17121) -> Result<Json<serde_json::Value>, StatusCode> {
17122    let s = state.lock().unwrap();
17123    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
17124
17125    let history = match s.versions.get_history(&payload.flow_name) {
17126        Some(h) => h,
17127        None => return Ok(Json(serde_json::json!({"error": format!("flow '{}' not deployed", payload.flow_name)}))),
17128    };
17129    let active = match history.active() {
17130        Some(v) => v,
17131        None => return Ok(Json(serde_json::json!({"error": format!("no active version for '{}'", payload.flow_name)}))),
17132    };
17133
17134    let source = active.source.clone();
17135    let source_file = active.source_file.clone();
17136    let source_hash = active.source_hash.clone();
17137    let version = active.version;
17138    drop(s);
17139
17140    // Lex
17141    let tokens = match crate::lexer::Lexer::new(&source, &source_file).tokenize() {
17142        Ok(t) => t,
17143        Err(e) => return Ok(Json(serde_json::json!({"error": format!("lex error: {e:?}"), "phase": "lexer"}))),
17144    };
17145    let token_count = tokens.len();
17146
17147    // Parse
17148    let mut parser = crate::parser::Parser::new(tokens);
17149    let program = match parser.parse() {
17150        Ok(p) => p,
17151        Err(e) => return Ok(Json(serde_json::json!({"error": format!("parse error: {e:?}"), "phase": "parser"}))),
17152    };
17153
17154    // Type check
17155    let type_errors = crate::type_checker::TypeChecker::new(&program).check();
17156    let type_error_msgs: Vec<String> = type_errors.iter().map(|e| format!("{e:?}")).collect();
17157
17158    // IR
17159    let ir = crate::ir_generator::IRGenerator::new().generate(&program);
17160    let ir_flow = match ir.flows.iter().find(|f| f.name == payload.flow_name) {
17161        Some(f) => f,
17162        None => return Ok(Json(serde_json::json!({"error": format!("flow '{}' not in IR", payload.flow_name)}))),
17163    };
17164
17165    // Step plan
17166    let steps: Vec<serde_json::Value> = ir_flow.steps.iter().filter_map(|node| {
17167        if let crate::ir_nodes::IRFlowNode::Step(step) = node {
17168            Some(serde_json::json!({
17169                "name": step.name,
17170                "has_tool": step.use_tool.is_some(),
17171                "has_probe": step.probe.is_some(),
17172                "output_type": step.output_type,
17173                "persona": step.persona_ref,
17174            }))
17175        } else {
17176            None
17177        }
17178    }).collect();
17179
17180    // Dependency analysis
17181    let step_infos: Vec<crate::step_deps::StepInfo> = ir_flow.steps.iter().filter_map(|node| {
17182        if let crate::ir_nodes::IRFlowNode::Step(step) = node {
17183            Some(crate::step_deps::StepInfo {
17184                name: step.name.clone(),
17185                step_type: step.node_type.to_string(),
17186                user_prompt: step.ask.clone(),
17187                argument: step.use_tool.as_ref()
17188                    .and_then(|t| t.get("argument").and_then(|a| a.as_str()).map(String::from))
17189                    .unwrap_or_default(),
17190            })
17191        } else {
17192            None
17193        }
17194    }).collect();
17195    let dep_graph = crate::step_deps::analyze(&step_infos);
17196
17197    // Cost estimate
17198    let pricing = {
17199        let s = state.lock().unwrap();
17200        s.cost_pricing.clone()
17201    };
17202    let input_price = pricing.input_per_million.get(&payload.backend).copied().unwrap_or(0.0);
17203    let output_price = pricing.output_per_million.get(&payload.backend).copied().unwrap_or(0.0);
17204    // Rough estimate: ~500 tokens per step
17205    let est_tokens_per_step = 500u64;
17206    let est_input = est_tokens_per_step * steps.len() as u64;
17207    let est_output = est_input / 2;
17208    let est_cost = (est_input as f64 / 1_000_000.0) * input_price + (est_output as f64 / 1_000_000.0) * output_price;
17209
17210    Ok(Json(serde_json::json!({
17211        "dry_run": true,
17212        "flow_name": payload.flow_name,
17213        "version": version,
17214        "source_hash": source_hash,
17215        "backend": payload.backend,
17216        "compilation": {
17217            "success": true,
17218            "token_count": token_count,
17219            "type_errors": type_error_msgs,
17220            "type_errors_count": type_error_msgs.len(),
17221        },
17222        "step_plan": {
17223            "total_steps": steps.len(),
17224            "steps": steps,
17225        },
17226        "dependencies": {
17227            "max_depth": dep_graph.max_depth,
17228            "parallel_groups": dep_graph.parallel_groups,
17229            "unresolved_refs": dep_graph.unresolved_refs,
17230        },
17231        "cost_estimate": {
17232            "backend": payload.backend,
17233            "estimated_input_tokens": est_input,
17234            "estimated_output_tokens": est_output,
17235            "estimated_cost_usd": (est_cost * 10000.0).round() / 10000.0,
17236            "pricing_input_per_million": input_price,
17237            "pricing_output_per_million": output_price,
17238        },
17239    })))
17240}
17241
17242/// A stage in a multi-flow pipeline.
17243#[derive(Debug, Clone, Deserialize)]
17244pub struct PipelineStage {
17245    /// Flow name to execute.
17246    pub flow_name: String,
17247    /// Backend override (default "stub").
17248    #[serde(default = "default_execute_backend")]
17249    pub backend: String,
17250}
17251
17252/// Request for multi-flow pipeline execution.
17253#[derive(Debug, Deserialize)]
17254pub struct PipelineRequest {
17255    /// Ordered list of stages to execute sequentially.
17256    pub stages: Vec<PipelineStage>,
17257    /// Whether to stop on first failure (default true).
17258    #[serde(default = "default_stop_on_failure")]
17259    pub stop_on_failure: bool,
17260}
17261
17262fn default_stop_on_failure() -> bool { true }
17263
17264/// Result for a single pipeline stage.
17265#[derive(Debug, Clone, Serialize)]
17266pub struct PipelineStageResult {
17267    pub stage: usize,
17268    pub flow_name: String,
17269    pub success: bool,
17270    pub trace_id: u64,
17271    pub steps_executed: usize,
17272    pub latency_ms: u64,
17273    pub tokens_input: u64,
17274    pub tokens_output: u64,
17275    pub errors: usize,
17276    pub error_message: Option<String>,
17277}
17278
17279/// POST /v1/execute/pipeline — execute multiple flows in sequence.
17280async fn execute_pipeline_handler(
17281    State(state): State<SharedState>,
17282    headers: HeaderMap,
17283    Json(payload): Json<PipelineRequest>,
17284) -> Result<Json<serde_json::Value>, StatusCode> {
17285    let req_start = Instant::now();
17286    let client = client_key_from_headers(&headers);
17287    {
17288        let mut s = state.lock().unwrap();
17289        check_auth(&mut s, &headers, AccessLevel::Write)?;
17290    }
17291
17292    if payload.stages.is_empty() {
17293        return Ok(Json(serde_json::json!({
17294            "error": "pipeline must have at least 1 stage",
17295        })));
17296    }
17297    if payload.stages.len() > 20 {
17298        return Ok(Json(serde_json::json!({
17299            "error": "maximum 20 stages per pipeline",
17300        })));
17301    }
17302
17303    let mut results: Vec<PipelineStageResult> = Vec::new();
17304    let mut pipeline_success = true;
17305
17306    for (idx, stage) in payload.stages.iter().enumerate() {
17307        // Look up source
17308        let source_info = {
17309            let s = state.lock().unwrap();
17310            s.versions.get_history(&stage.flow_name)
17311                .and_then(|h| h.active())
17312                .map(|v| (v.source.clone(), v.source_file.clone()))
17313        };
17314
17315        let (source, source_file) = match source_info {
17316            Some(info) => info,
17317            None => {
17318                let stage_result = PipelineStageResult {
17319                    stage: idx,
17320                    flow_name: stage.flow_name.clone(),
17321                    success: false,
17322                    trace_id: 0,
17323                    steps_executed: 0,
17324                    latency_ms: 0,
17325                    tokens_input: 0,
17326                    tokens_output: 0,
17327                    errors: 1,
17328                    error_message: Some(format!("flow '{}' not deployed", stage.flow_name)),
17329                };
17330                results.push(stage_result);
17331                pipeline_success = false;
17332                if payload.stop_on_failure { break; }
17333                continue;
17334            }
17335        };
17336
17337        match server_execute_full(&state, &source, &source_file, &stage.flow_name, &stage.backend).0 {
17338            Ok(er) => {
17339                let mut entry = crate::trace_store::build_trace(
17340                    &er.flow_name, &er.source_file, &er.backend, &client,
17341                    if er.success { crate::trace_store::TraceStatus::Success }
17342                    else { crate::trace_store::TraceStatus::Partial },
17343                    er.steps_executed, er.latency_ms,
17344                );
17345                entry.tokens_input = er.tokens_input;
17346                entry.tokens_output = er.tokens_output;
17347                entry.errors = er.errors;
17348
17349                let trace_id = {
17350                    let mut s = state.lock().unwrap();
17351                    s.trace_store.record(entry)
17352                };
17353
17354                let stage_success = er.success;
17355                results.push(PipelineStageResult {
17356                    stage: idx,
17357                    flow_name: stage.flow_name.clone(),
17358                    success: stage_success,
17359                    trace_id,
17360                    steps_executed: er.steps_executed,
17361                    latency_ms: er.latency_ms,
17362                    tokens_input: er.tokens_input,
17363                    tokens_output: er.tokens_output,
17364                    errors: er.errors,
17365                    error_message: None,
17366                });
17367
17368                if !stage_success {
17369                    pipeline_success = false;
17370                    if payload.stop_on_failure { break; }
17371                }
17372            }
17373            Err(e) => {
17374                let mut s = state.lock().unwrap();
17375                s.metrics.total_errors += 1;
17376                drop(s);
17377
17378                results.push(PipelineStageResult {
17379                    stage: idx,
17380                    flow_name: stage.flow_name.clone(),
17381                    success: false,
17382                    trace_id: 0,
17383                    steps_executed: 0,
17384                    latency_ms: 0,
17385                    tokens_input: 0,
17386                    tokens_output: 0,
17387                    errors: 1,
17388                    error_message: Some(e),
17389                });
17390                pipeline_success = false;
17391                if payload.stop_on_failure { break; }
17392            }
17393        }
17394    }
17395
17396    let total_latency = req_start.elapsed().as_millis() as u64;
17397    let stages_completed = results.len();
17398    let stages_succeeded = results.iter().filter(|r| r.success).count();
17399    let total_tokens: u64 = results.iter().map(|r| r.tokens_input + r.tokens_output).sum();
17400
17401    Ok(Json(serde_json::json!({
17402        "success": pipeline_success,
17403        "total_stages": payload.stages.len(),
17404        "stages_completed": stages_completed,
17405        "stages_succeeded": stages_succeeded,
17406        "total_latency_ms": total_latency,
17407        "total_tokens": total_tokens,
17408        "stop_on_failure": payload.stop_on_failure,
17409        "stages": results,
17410    })))
17411}
17412
17413/// GET /v1/flows/:name/rules — get validation rules for a flow.
17414async fn flow_rules_get_handler(
17415    State(state): State<SharedState>,
17416    headers: HeaderMap,
17417    Path(name): Path<String>,
17418) -> Result<Json<serde_json::Value>, StatusCode> {
17419    let s = state.lock().unwrap();
17420    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
17421
17422    match s.flow_rules.get(&name) {
17423        Some(rules) => Ok(Json(serde_json::json!({
17424            "flow": name,
17425            "rules": rules,
17426        }))),
17427        None => Ok(Json(serde_json::json!({
17428            "flow": name,
17429            "rules": serde_json::Value::Null,
17430            "message": "no rules configured",
17431        }))),
17432    }
17433}
17434
17435/// PUT /v1/flows/:name/rules — set validation rules for a flow.
17436async fn flow_rules_put_handler(
17437    State(state): State<SharedState>,
17438    headers: HeaderMap,
17439    Path(name): Path<String>,
17440    Json(rules): Json<FlowValidationRules>,
17441) -> Result<Json<serde_json::Value>, StatusCode> {
17442    let client = client_key_from_headers(&headers);
17443    let mut s = state.lock().unwrap();
17444    check_auth(&mut s, &headers, AccessLevel::Admin)?;
17445
17446    s.flow_rules.insert(name.clone(), rules.clone());
17447    s.audit_log.record(
17448        &client, AuditAction::ConfigUpdate, &format!("flow_rules:{}", name),
17449        serde_json::to_value(&rules).unwrap_or_default(), true,
17450    );
17451
17452    Ok(Json(serde_json::json!({
17453        "success": true,
17454        "flow": name,
17455        "rules": rules,
17456    })))
17457}
17458
17459/// DELETE /v1/flows/:name/rules — remove validation rules for a flow.
17460async fn flow_rules_delete_handler(
17461    State(state): State<SharedState>,
17462    headers: HeaderMap,
17463    Path(name): Path<String>,
17464) -> Result<Json<serde_json::Value>, StatusCode> {
17465    let mut s = state.lock().unwrap();
17466    check_auth(&mut s, &headers, AccessLevel::Admin)?;
17467
17468    let removed = s.flow_rules.remove(&name).is_some();
17469    Ok(Json(serde_json::json!({
17470        "success": removed,
17471        "flow": name,
17472    })))
17473}
17474
17475/// POST /v1/flows/:name/validate — validate a flow against its configured rules.
17476async fn flow_validate_handler(
17477    State(state): State<SharedState>,
17478    headers: HeaderMap,
17479    Path(name): Path<String>,
17480) -> Result<Json<serde_json::Value>, StatusCode> {
17481    let s = state.lock().unwrap();
17482    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
17483
17484    let rules = match s.flow_rules.get(&name) {
17485        Some(r) => r.clone(),
17486        None => return Ok(Json(serde_json::json!({
17487            "flow": name,
17488            "valid": true,
17489            "message": "no rules configured — validation skipped",
17490            "violations": [],
17491        }))),
17492    };
17493
17494    // Get flow IR for validation
17495    let active = match s.versions.get_history(&name).and_then(|h| h.active()) {
17496        Some(v) => v,
17497        None => return Ok(Json(serde_json::json!({"error": format!("flow '{}' not deployed", name)}))),
17498    };
17499    let source = active.source.clone();
17500    let source_file = active.source_file.clone();
17501    let backend = active.backend.clone();
17502    drop(s);
17503
17504    // Compile
17505    let tokens = match crate::lexer::Lexer::new(&source, &source_file).tokenize() {
17506        Ok(t) => t,
17507        Err(e) => return Ok(Json(serde_json::json!({"error": format!("lex error: {e:?}")}))),
17508    };
17509    let mut parser = crate::parser::Parser::new(tokens);
17510    let program = match parser.parse() {
17511        Ok(p) => p,
17512        Err(e) => return Ok(Json(serde_json::json!({"error": format!("parse error: {e:?}")}))),
17513    };
17514    let ir = crate::ir_generator::IRGenerator::new().generate(&program);
17515    let ir_flow = match ir.flows.iter().find(|f| f.name == name) {
17516        Some(f) => f,
17517        None => return Ok(Json(serde_json::json!({"error": format!("flow '{}' not in IR", name)}))),
17518    };
17519
17520    // Validate
17521    let mut violations = Vec::new();
17522
17523    // max_steps
17524    let step_count = ir_flow.steps.iter().filter(|n| matches!(n, crate::ir_nodes::IRFlowNode::Step(_))).count();
17525    if rules.max_steps > 0 && step_count > rules.max_steps {
17526        violations.push(format!("step count {} exceeds max_steps {}", step_count, rules.max_steps));
17527    }
17528
17529    // banned_tools
17530    for node in &ir_flow.steps {
17531        if let crate::ir_nodes::IRFlowNode::Step(step) = node {
17532            if let Some(ref tool) = step.use_tool {
17533                if let Some(tool_name) = tool.get("name").and_then(|n| n.as_str()) {
17534                    if rules.banned_tools.iter().any(|b| b == tool_name) {
17535                        violations.push(format!("step '{}' uses banned tool '{}'", step.name, tool_name));
17536                    }
17537                }
17538            }
17539        }
17540    }
17541
17542    // allowed_backends
17543    if !rules.allowed_backends.is_empty() && !rules.allowed_backends.contains(&backend) {
17544        violations.push(format!("backend '{}' not in allowed list {:?}", backend, rules.allowed_backends));
17545    }
17546
17547    // max_cost
17548    if rules.max_cost_usd > 0.0 {
17549        let s = state.lock().unwrap();
17550        let costs = compute_flow_costs(&s.trace_store, &s.cost_pricing);
17551        if let Some(fc) = costs.iter().find(|c| c.flow_name == name) {
17552            if fc.estimated_cost_usd > rules.max_cost_usd {
17553                violations.push(format!("current cost ${:.4} exceeds max_cost_usd ${:.4}", fc.estimated_cost_usd, rules.max_cost_usd));
17554            }
17555        }
17556    }
17557
17558    let valid = violations.is_empty();
17559
17560    Ok(Json(serde_json::json!({
17561        "flow": name,
17562        "valid": valid,
17563        "violations_count": violations.len(),
17564        "violations": violations,
17565        "rules": rules,
17566    })))
17567}
17568
17569/// Request to set a correlation ID on a trace.
17570#[derive(Debug, Deserialize)]
17571pub struct CorrelateRequest {
17572    /// Correlation ID to assign.
17573    pub correlation_id: String,
17574}
17575
17576/// POST /v1/traces/:id/correlate — set a correlation ID on a trace.
17577async fn traces_correlate_handler(
17578    State(state): State<SharedState>,
17579    headers: HeaderMap,
17580    Path(id): Path<u64>,
17581    Json(payload): Json<CorrelateRequest>,
17582) -> Result<Json<serde_json::Value>, StatusCode> {
17583    let mut s = state.lock().unwrap();
17584    check_auth(&mut s, &headers, AccessLevel::Write)?;
17585
17586    if payload.correlation_id.is_empty() {
17587        return Ok(Json(serde_json::json!({
17588            "error": "correlation_id must not be empty",
17589        })));
17590    }
17591
17592    if s.trace_store.set_correlation(id, &payload.correlation_id) {
17593        Ok(Json(serde_json::json!({
17594            "success": true,
17595            "trace_id": id,
17596            "correlation_id": payload.correlation_id,
17597        })))
17598    } else {
17599        Ok(Json(serde_json::json!({
17600            "success": false,
17601            "error": format!("trace {} not found", id),
17602        })))
17603    }
17604}
17605
17606/// Query for correlated traces.
17607#[derive(Debug, Deserialize)]
17608pub struct CorrelatedQuery {
17609    /// Correlation ID to search for.
17610    pub correlation_id: String,
17611}
17612
17613/// GET /v1/traces/correlated — find all traces with a given correlation ID.
17614async fn traces_correlated_handler(
17615    State(state): State<SharedState>,
17616    headers: HeaderMap,
17617    Query(params): Query<CorrelatedQuery>,
17618) -> Result<Json<serde_json::Value>, StatusCode> {
17619    let s = state.lock().unwrap();
17620    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
17621
17622    let traces = s.trace_store.by_correlation(&params.correlation_id);
17623
17624    let entries: Vec<serde_json::Value> = traces.iter().map(|e| {
17625        serde_json::json!({
17626            "id": e.id,
17627            "flow_name": e.flow_name,
17628            "status": e.status.as_str(),
17629            "timestamp": e.timestamp,
17630            "latency_ms": e.latency_ms,
17631            "errors": e.errors,
17632            "backend": e.backend,
17633            "correlation_id": e.correlation_id,
17634        })
17635    }).collect();
17636
17637    Ok(Json(serde_json::json!({
17638        "correlation_id": params.correlation_id,
17639        "count": entries.len(),
17640        "traces": entries,
17641    })))
17642}
17643
17644/// Request to set a flow quota.
17645#[derive(Debug, Deserialize)]
17646pub struct SetQuotaRequest {
17647    /// Max executions per hour (0 = unlimited).
17648    #[serde(default)]
17649    pub max_per_hour: u64,
17650    /// Max executions per day (0 = unlimited).
17651    #[serde(default)]
17652    pub max_per_day: u64,
17653}
17654
17655/// GET /v1/flows/:name/quota — get quota status for a flow.
17656async fn flow_quota_get_handler(
17657    State(state): State<SharedState>,
17658    headers: HeaderMap,
17659    Path(name): Path<String>,
17660) -> Result<Json<serde_json::Value>, StatusCode> {
17661    let s = state.lock().unwrap();
17662    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
17663
17664    match s.flow_quotas.get(&name) {
17665        Some(quota) => Ok(Json(serde_json::json!({
17666            "flow": name,
17667            "quota": quota,
17668        }))),
17669        None => Ok(Json(serde_json::json!({
17670            "flow": name,
17671            "quota": serde_json::Value::Null,
17672            "message": "no quota configured",
17673        }))),
17674    }
17675}
17676
17677/// PUT /v1/flows/:name/quota — set execution quota for a flow.
17678async fn flow_quota_put_handler(
17679    State(state): State<SharedState>,
17680    headers: HeaderMap,
17681    Path(name): Path<String>,
17682    Json(payload): Json<SetQuotaRequest>,
17683) -> Result<Json<serde_json::Value>, StatusCode> {
17684    let client = client_key_from_headers(&headers);
17685    let mut s = state.lock().unwrap();
17686    check_auth(&mut s, &headers, AccessLevel::Admin)?;
17687
17688    let quota = FlowQuota {
17689        max_per_hour: payload.max_per_hour,
17690        max_per_day: payload.max_per_day,
17691        current_hour_count: 0,
17692        current_day_count: 0,
17693        hour_window_start: 0,
17694        day_window_start: 0,
17695    };
17696    s.flow_quotas.insert(name.clone(), quota.clone());
17697
17698    s.audit_log.record(
17699        &client, AuditAction::ConfigUpdate, &format!("flow_quota:{}", name),
17700        serde_json::json!({"max_per_hour": payload.max_per_hour, "max_per_day": payload.max_per_day}),
17701        true,
17702    );
17703
17704    Ok(Json(serde_json::json!({
17705        "success": true,
17706        "flow": name,
17707        "quota": quota,
17708    })))
17709}
17710
17711/// DELETE /v1/flows/:name/quota — remove quota for a flow.
17712async fn flow_quota_delete_handler(
17713    State(state): State<SharedState>,
17714    headers: HeaderMap,
17715    Path(name): Path<String>,
17716) -> Result<Json<serde_json::Value>, StatusCode> {
17717    let mut s = state.lock().unwrap();
17718    check_auth(&mut s, &headers, AccessLevel::Admin)?;
17719
17720    let removed = s.flow_quotas.remove(&name).is_some();
17721    Ok(Json(serde_json::json!({
17722        "success": removed,
17723        "flow": name,
17724    })))
17725}
17726
17727/// POST /v1/flows/:name/quota/check — check if an execution is allowed by quota.
17728async fn flow_quota_check_handler(
17729    State(state): State<SharedState>,
17730    headers: HeaderMap,
17731    Path(name): Path<String>,
17732) -> Result<Json<serde_json::Value>, StatusCode> {
17733    let mut s = state.lock().unwrap();
17734    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
17735
17736    match s.flow_quotas.get_mut(&name) {
17737        Some(quota) => {
17738            let (allowed, violations) = quota.check_and_record();
17739            Ok(Json(serde_json::json!({
17740                "flow": name,
17741                "allowed": allowed,
17742                "violations": violations,
17743                "current_hour": quota.current_hour_count,
17744                "current_day": quota.current_day_count,
17745                "max_per_hour": quota.max_per_hour,
17746                "max_per_day": quota.max_per_day,
17747            })))
17748        }
17749        None => Ok(Json(serde_json::json!({
17750            "flow": name,
17751            "allowed": true,
17752            "message": "no quota configured",
17753        }))),
17754    }
17755}
17756
17757/// A rollback safety warning.
17758#[derive(Debug, Clone, Serialize)]
17759pub struct RollbackWarning {
17760    pub category: String,
17761    pub severity: String, // "info", "warning", "blocker"
17762    pub message: String,
17763}
17764
17765/// POST /v1/versions/:name/rollback/check — pre-rollback safety validation.
17766async fn rollback_check_handler(
17767    State(state): State<SharedState>,
17768    headers: HeaderMap,
17769    Path(name): Path<String>,
17770    Json(payload): Json<RollbackRequest>,
17771) -> Result<Json<serde_json::Value>, StatusCode> {
17772    let s = state.lock().unwrap();
17773    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
17774
17775    // Check version exists
17776    let history = match s.versions.get_history(&name) {
17777        Some(h) => h,
17778        None => return Ok(Json(serde_json::json!({"error": format!("no version history for '{}'", name)}))),
17779    };
17780    let target_exists = history.versions.iter().any(|v| v.version == payload.version);
17781    if !target_exists {
17782        return Ok(Json(serde_json::json!({"error": format!("version {} not found for '{}'", payload.version, name)})));
17783    }
17784    let current_version = history.active_version;
17785
17786    let mut warnings: Vec<RollbackWarning> = Vec::new();
17787
17788    // Check: daemon running
17789    if let Some(daemon) = s.daemons.get(&name) {
17790        if daemon.state == DaemonState::Running {
17791            warnings.push(RollbackWarning {
17792                category: "daemon".into(),
17793                severity: "blocker".into(),
17794                message: format!("daemon '{}' is currently Running — stop or pause before rollback", name),
17795            });
17796        } else if daemon.state == DaemonState::Paused {
17797            warnings.push(RollbackWarning {
17798                category: "daemon".into(),
17799                severity: "info".into(),
17800                message: format!("daemon '{}' is Paused — will resume with rolled-back version", name),
17801            });
17802        }
17803    }
17804
17805    // Check: active schedules
17806    if let Some(sched) = s.schedules.get(&name) {
17807        if sched.enabled {
17808            warnings.push(RollbackWarning {
17809                category: "schedule".into(),
17810                severity: "warning".into(),
17811                message: format!("schedule '{}' is enabled — next tick will use rolled-back version", name),
17812            });
17813        }
17814    }
17815
17816    // Check: chain dependencies (other daemons depending on this flow)
17817    let downstream: Vec<String> = s.daemons.values()
17818        .filter(|d| d.trigger_topic.as_deref().map_or(false, |t| t.contains(&name)))
17819        .map(|d| d.name.clone())
17820        .collect();
17821    if !downstream.is_empty() {
17822        warnings.push(RollbackWarning {
17823            category: "chain".into(),
17824            severity: "warning".into(),
17825            message: format!("daemons triggered by '{}': {:?}", name, downstream),
17826        });
17827    }
17828
17829    // Check: execution queue has pending items for this flow
17830    let queued = s.execution_queue.iter()
17831        .filter(|q| q.flow_name == name && q.status == "pending")
17832        .count();
17833    if queued > 0 {
17834        warnings.push(RollbackWarning {
17835            category: "queue".into(),
17836            severity: "warning".into(),
17837            message: format!("{} pending queue items for '{}' — will execute with rolled-back version", queued, name),
17838        });
17839    }
17840
17841    // Check: active quota
17842    if s.flow_quotas.contains_key(&name) {
17843        warnings.push(RollbackWarning {
17844            category: "quota".into(),
17845            severity: "info".into(),
17846            message: format!("flow '{}' has active execution quota — quota state preserved", name),
17847        });
17848    }
17849
17850    // Check: validation rules
17851    if s.flow_rules.contains_key(&name) {
17852        warnings.push(RollbackWarning {
17853            category: "rules".into(),
17854            severity: "info".into(),
17855            message: format!("flow '{}' has validation rules — re-validate after rollback recommended", name),
17856        });
17857    }
17858
17859    let blockers = warnings.iter().filter(|w| w.severity == "blocker").count();
17860    let safe = blockers == 0;
17861
17862    Ok(Json(serde_json::json!({
17863        "flow": name,
17864        "current_version": current_version,
17865        "target_version": payload.version,
17866        "safe_to_rollback": safe,
17867        "warnings_count": warnings.len(),
17868        "blockers": blockers,
17869        "warnings": warnings,
17870    })))
17871}
17872
17873/// GET /v1/health/gates — view current readiness gates configuration and status.
17874async fn health_gates_get_handler(
17875    State(state): State<SharedState>,
17876    headers: HeaderMap,
17877) -> Result<Json<serde_json::Value>, StatusCode> {
17878    let s = state.lock().unwrap();
17879    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
17880
17881    let checks = evaluate_gates(&s);
17882    let all_passed = checks.iter().all(|c| c.passed);
17883
17884    Ok(Json(serde_json::json!({
17885        "gates": s.readiness_gates,
17886        "checks": checks,
17887        "all_passed": all_passed,
17888    })))
17889}
17890
17891/// PUT /v1/health/gates — update readiness gates configuration.
17892async fn health_gates_put_handler(
17893    State(state): State<SharedState>,
17894    headers: HeaderMap,
17895    Json(gates): Json<ReadinessGates>,
17896) -> Result<Json<serde_json::Value>, StatusCode> {
17897    let client = client_key_from_headers(&headers);
17898    let mut s = state.lock().unwrap();
17899    check_auth(&mut s, &headers, AccessLevel::Admin)?;
17900
17901    s.readiness_gates = gates.clone();
17902    s.audit_log.record(
17903        &client, AuditAction::ConfigUpdate, "readiness_gates",
17904        serde_json::to_value(&gates).unwrap_or_default(), true,
17905    );
17906
17907    Ok(Json(serde_json::json!({
17908        "success": true,
17909        "gates": gates,
17910    })))
17911}
17912
17913/// Evaluate all readiness gates against current server state.
17914fn evaluate_gates(s: &ServerState) -> Vec<GateCheckResult> {
17915    let gates = &s.readiness_gates;
17916    let mut checks = Vec::new();
17917
17918    // min_daemons
17919    if gates.min_daemons > 0 {
17920        let current = s.daemons.len();
17921        checks.push(GateCheckResult {
17922            gate: "min_daemons".into(),
17923            passed: current >= gates.min_daemons,
17924            detail: format!("{}/{} daemons registered", current, gates.min_daemons),
17925        });
17926    }
17927
17928    // required_flows
17929    for flow in &gates.required_flows {
17930        let deployed = s.versions.get_history(flow).and_then(|h| h.active()).is_some();
17931        checks.push(GateCheckResult {
17932            gate: format!("required_flow:{}", flow),
17933            passed: deployed,
17934            detail: if deployed { format!("'{}' deployed", flow) } else { format!("'{}' NOT deployed", flow) },
17935        });
17936    }
17937
17938    // max_error_rate
17939    if gates.max_error_rate > 0.0 && s.metrics.total_requests > 0 {
17940        let rate = s.metrics.total_errors as f64 / s.metrics.total_requests as f64;
17941        checks.push(GateCheckResult {
17942            gate: "max_error_rate".into(),
17943            passed: rate <= gates.max_error_rate,
17944            detail: format!("error rate {:.4} (max {:.4})", rate, gates.max_error_rate),
17945        });
17946    }
17947
17948    // min_uptime_secs
17949    if gates.min_uptime_secs > 0 {
17950        let uptime = s.started_at.elapsed().as_secs();
17951        checks.push(GateCheckResult {
17952            gate: "min_uptime_secs".into(),
17953            passed: uptime >= gates.min_uptime_secs,
17954            detail: format!("uptime {}s (min {}s)", uptime, gates.min_uptime_secs),
17955        });
17956    }
17957
17958    checks
17959}
17960
17961/// Query parameters for custom trace export.
17962#[derive(Debug, Deserialize)]
17963pub struct CustomExportQuery {
17964    /// Template string with variables: {{id}}, {{flow_name}}, {{status}}, {{timestamp}},
17965    /// {{latency_ms}}, {{steps}}, {{errors}}, {{backend}}, {{tokens_in}}, {{tokens_out}}.
17966    pub template: String,
17967    /// Max traces to export (default 100).
17968    #[serde(default = "default_custom_export_limit")]
17969    pub limit: usize,
17970    /// Optional flow name filter.
17971    pub flow_name: Option<String>,
17972}
17973
17974fn default_custom_export_limit() -> usize { 100 }
17975
17976/// Render a trace export template for a single trace entry.
17977fn render_trace_template(template: &str, e: &crate::trace_store::TraceEntry) -> String {
17978    template
17979        .replace("{{id}}", &e.id.to_string())
17980        .replace("{{flow_name}}", &e.flow_name)
17981        .replace("{{status}}", e.status.as_str())
17982        .replace("{{timestamp}}", &e.timestamp.to_string())
17983        .replace("{{latency_ms}}", &e.latency_ms.to_string())
17984        .replace("{{steps}}", &e.steps_executed.to_string())
17985        .replace("{{errors}}", &e.errors.to_string())
17986        .replace("{{backend}}", &e.backend)
17987        .replace("{{tokens_in}}", &e.tokens_input.to_string())
17988        .replace("{{tokens_out}}", &e.tokens_output.to_string())
17989        .replace("{{client}}", &e.client_key)
17990        .replace("{{source_file}}", &e.source_file)
17991        .replace("{{retries}}", &e.retries.to_string())
17992        .replace("{{correlation_id}}", e.correlation_id.as_deref().unwrap_or(""))
17993}
17994
17995/// GET /v1/traces/export/custom — export traces using a custom template.
17996async fn traces_export_custom_handler(
17997    State(state): State<SharedState>,
17998    headers: HeaderMap,
17999    Query(params): Query<CustomExportQuery>,
18000) -> Result<(StatusCode, [(String, String); 1], String), StatusCode> {
18001    let s = state.lock().unwrap();
18002    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
18003
18004    let filter = params.flow_name.as_ref().map(|f| {
18005        crate::trace_store::TraceFilter {
18006            flow_name: Some(f.clone()),
18007            ..Default::default()
18008        }
18009    });
18010
18011    let entries = s.trace_store.recent(params.limit, filter.as_ref());
18012
18013    let mut output = String::new();
18014    for e in &entries {
18015        output.push_str(&render_trace_template(&params.template, e));
18016        output.push('\n');
18017    }
18018
18019    Ok((
18020        StatusCode::OK,
18021        [("content-type".into(), "text/plain".into())],
18022        output,
18023    ))
18024}
18025
18026/// Request to set an endpoint rate limit.
18027#[derive(Debug, Deserialize)]
18028pub struct SetEndpointLimitRequest {
18029    /// Path prefix to match.
18030    pub path_prefix: String,
18031    /// Max requests per window.
18032    pub max_requests: u64,
18033    /// Window size in seconds.
18034    pub window_secs: u64,
18035}
18036
18037/// GET /v1/rate-limit/endpoints — list all per-endpoint rate limits.
18038async fn endpoint_rate_limits_list_handler(
18039    State(state): State<SharedState>,
18040    headers: HeaderMap,
18041) -> Result<Json<serde_json::Value>, StatusCode> {
18042    let s = state.lock().unwrap();
18043    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
18044
18045    let limits: Vec<&EndpointRateLimit> = s.endpoint_rate_limits.values().collect();
18046    Ok(Json(serde_json::json!({
18047        "count": limits.len(),
18048        "limits": limits,
18049    })))
18050}
18051
18052/// PUT /v1/rate-limit/endpoints — add or update an endpoint rate limit.
18053async fn endpoint_rate_limits_put_handler(
18054    State(state): State<SharedState>,
18055    headers: HeaderMap,
18056    Json(payload): Json<SetEndpointLimitRequest>,
18057) -> Result<Json<serde_json::Value>, StatusCode> {
18058    let client = client_key_from_headers(&headers);
18059    let mut s = state.lock().unwrap();
18060    check_auth(&mut s, &headers, AccessLevel::Admin)?;
18061
18062    let limit = EndpointRateLimit {
18063        path_prefix: payload.path_prefix.clone(),
18064        max_requests: payload.max_requests,
18065        window_secs: payload.window_secs,
18066        current_count: 0,
18067        window_start: 0,
18068    };
18069    s.endpoint_rate_limits.insert(payload.path_prefix.clone(), limit.clone());
18070
18071    s.audit_log.record(
18072        &client, AuditAction::ConfigUpdate, &format!("endpoint_rate_limit:{}", payload.path_prefix),
18073        serde_json::json!({"max_requests": payload.max_requests, "window_secs": payload.window_secs}),
18074        true,
18075    );
18076
18077    Ok(Json(serde_json::json!({
18078        "success": true,
18079        "limit": limit,
18080    })))
18081}
18082
18083/// DELETE /v1/rate-limit/endpoints — remove an endpoint rate limit.
18084async fn endpoint_rate_limits_delete_handler(
18085    State(state): State<SharedState>,
18086    headers: HeaderMap,
18087    Query(params): Query<std::collections::HashMap<String, String>>,
18088) -> Result<Json<serde_json::Value>, StatusCode> {
18089    let mut s = state.lock().unwrap();
18090    check_auth(&mut s, &headers, AccessLevel::Admin)?;
18091
18092    let path = params.get("path_prefix").cloned().unwrap_or_default();
18093    let removed = s.endpoint_rate_limits.remove(&path).is_some();
18094    Ok(Json(serde_json::json!({
18095        "success": removed,
18096        "path_prefix": path,
18097    })))
18098}
18099
18100/// Query parameters for event stream polling.
18101#[derive(Debug, Deserialize)]
18102pub struct EventStreamQuery {
18103    /// Only return events after this timestamp (Unix seconds). Use as cursor.
18104    #[serde(default)]
18105    pub since: u64,
18106    /// Max events to return (default 50).
18107    #[serde(default = "default_stream_limit")]
18108    pub limit: usize,
18109    /// Optional topic filter.
18110    pub topic: Option<String>,
18111}
18112
18113fn default_stream_limit() -> usize { 50 }
18114
18115/// A stream event in SSE-like format.
18116#[derive(Debug, Clone, Serialize)]
18117struct StreamEvent {
18118    id: u64,
18119    timestamp: u64,
18120    topic: String,
18121    source: String,
18122    payload: serde_json::Value,
18123}
18124
18125/// GET /v1/events/stream — poll-based event stream (SSE-compatible).
18126///
18127/// Returns events since a given cursor timestamp. Clients poll with
18128/// `since=<last_timestamp>` to get new events incrementally.
18129/// Response includes `last_id` for cursor tracking.
18130async fn events_stream_handler(
18131    State(state): State<SharedState>,
18132    headers: HeaderMap,
18133    Query(params): Query<EventStreamQuery>,
18134) -> Result<(StatusCode, [(String, String); 1], String), StatusCode> {
18135    let s = state.lock().unwrap();
18136    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
18137
18138    let events = s.event_bus.recent_events(params.limit, params.topic.as_deref());
18139
18140    // Filter by since cursor
18141    let filtered: Vec<_> = events.iter()
18142        .filter(|e| params.since == 0 || e.timestamp_secs > params.since)
18143        .collect();
18144
18145    // Format as SSE text/event-stream
18146    let mut output = String::new();
18147    let mut last_id: u64 = params.since;
18148
18149    for (idx, ev) in filtered.iter().enumerate() {
18150        let event_id = ev.timestamp_secs * 1000 + idx as u64; // pseudo-unique ID
18151        let data = serde_json::json!({
18152            "topic": ev.topic,
18153            "source": ev.source,
18154            "timestamp": ev.timestamp_secs,
18155            "payload": ev.payload,
18156        });
18157        output.push_str(&format!("id: {}\n", event_id));
18158        output.push_str(&format!("event: {}\n", ev.topic));
18159        output.push_str(&format!("data: {}\n\n", serde_json::to_string(&data).unwrap_or_default()));
18160
18161        if ev.timestamp_secs > last_id {
18162            last_id = ev.timestamp_secs;
18163        }
18164    }
18165
18166    // Add retry hint for SSE clients
18167    if output.is_empty() {
18168        output.push_str(":\n\n"); // SSE keepalive comment
18169    }
18170
18171    // Add custom header for cursor tracking
18172    Ok((
18173        StatusCode::OK,
18174        [("content-type".into(), "text/event-stream".into())],
18175        output,
18176    ))
18177}
18178
18179// ── Algebraic Effect Stream Bridge ────────────────────────────────────────
18180//
18181// This implements the handler h: F_Σ(B) → M_IO(B) from algebraic effects theory.
18182// The flow's pure deliberation emits Emit(token) intents; the StreamEmitter
18183// handler materializes them as EventBus events consumable via SSE.
18184
18185/// A stream emission record — the materialized algebraic effect.
18186#[derive(Debug, Clone, Serialize)]
18187pub struct StreamToken {
18188    /// Execution/trace ID this token belongs to.
18189    pub trace_id: u64,
18190    /// Flow name being executed.
18191    pub flow_name: String,
18192    /// Step name that emitted this token.
18193    pub step_name: String,
18194    /// Sequential token index within this execution.
18195    pub token_index: u64,
18196    /// The emitted token/chunk content.
18197    pub content: String,
18198    /// Whether this is the final token (stream complete).
18199    pub is_final: bool,
18200    /// Unix timestamp.
18201    pub timestamp: u64,
18202
18203    // ── Algebraic Effects & Epistemic Semantics ──
18204    //
18205    // Stream(τ) = νX. (StreamChunk × EpistemicState × X)
18206    // Each token is a coinductive observation carrying its epistemic level.
18207
18208    /// Epistemic state of this token in the lattice (⊥ ⊑ doubt ⊑ speculate ⊑ believe ⊑ know).
18209    /// Streaming tokens arrive as "speculate" — promoted to "know" only after
18210    /// anchor validation on the complete response.
18211    pub epistemic_state: String,
18212
18213    /// Effect row annotation: <effects, epistemic:level>.
18214    /// Declares what effects this token's production involved.
18215    /// E.g., "<io, epistemic:speculate>" for LLM-generated content,
18216    ///       "<pure, epistemic:know>" for validated results.
18217    pub effect_row: String,
18218
18219    // ── PIX/MDN Navigation Context ──
18220    //
18221    // When the token originates from a PIX navigate/drill operation,
18222    // these fields carry the structural navigation context.
18223
18224    /// PIX index reference (if token from PIX navigation).
18225    /// Links to IRPix.name — the document tree being navigated.
18226    #[serde(skip_serializing_if = "Option::is_none")]
18227    pub pix_ref: Option<String>,
18228
18229    /// Corpus reference for MDN multi-document navigation.
18230    /// Links to IRCorpus.name — the document graph being traversed.
18231    #[serde(skip_serializing_if = "Option::is_none")]
18232    pub corpus_ref: Option<String>,
18233
18234    /// Navigation trail — sequence of nodes visited during PIX tree traversal.
18235    /// Each entry is a node identifier from the document tree D = (N, E, ρ, κ).
18236    /// Implements the trail step from IRTrailStep.
18237    #[serde(skip_serializing_if = "Option::is_none")]
18238    pub nav_trail: Option<Vec<String>>,
18239
18240    /// MDN edge type that led to this document (cite|depend|elaborate|contradict|...).
18241    /// From the MDN relation type taxonomy.
18242    #[serde(skip_serializing_if = "Option::is_none")]
18243    pub mdn_edge_type: Option<String>,
18244
18245    /// Navigation depth in PIX tree or MDN graph at time of emission.
18246    #[serde(skip_serializing_if = "Option::is_none")]
18247    pub nav_depth: Option<u32>,
18248}
18249
18250/// The algebraic effect handler — bridges flow execution to EventBus streaming.
18251///
18252/// In algebraic effects terms, this is the Handler that captures the
18253/// evaluation context E[perform(Emit(v))] and translates it to:
18254///   Handler(v, λx. E[x]) → publish("flow.stream.{id}", v) ; resume(x)
18255pub struct StreamEmitter {
18256    trace_id: u64,
18257    flow_name: String,
18258    token_count: u64,
18259    tokens: Vec<StreamToken>,
18260}
18261
18262impl StreamEmitter {
18263    pub fn new(trace_id: u64, flow_name: &str) -> Self {
18264        StreamEmitter {
18265            trace_id,
18266            flow_name: flow_name.to_string(),
18267            token_count: 0,
18268            tokens: Vec::new(),
18269        }
18270    }
18271
18272    /// perform(Emit(content)) — the algebraic effect operation.
18273    /// Pure: records the intent without side effects.
18274    /// The handler (publish_to_bus) materializes it.
18275    /// perform(Emit(content)) — step-level algebraic effect.
18276    /// Epistemic state: "speculate" (unvalidated LLM output).
18277    pub fn emit(&mut self, step_name: &str, content: &str) {
18278        self.emit_with_context(step_name, content, "speculate", "<io, epistemic:speculate>", None);
18279    }
18280
18281    /// perform(Emit(content)) with full epistemic/navigation context.
18282    pub fn emit_with_context(
18283        &mut self,
18284        step_name: &str,
18285        content: &str,
18286        epistemic_state: &str,
18287        effect_row: &str,
18288        nav_ctx: Option<&NavigationContext>,
18289    ) {
18290        let now = std::time::SystemTime::now()
18291            .duration_since(std::time::UNIX_EPOCH)
18292            .unwrap_or_default()
18293            .as_secs();
18294
18295        self.token_count += 1;
18296        self.tokens.push(StreamToken {
18297            trace_id: self.trace_id,
18298            flow_name: self.flow_name.clone(),
18299            step_name: step_name.to_string(),
18300            token_index: self.token_count,
18301            content: content.to_string(),
18302            is_final: false,
18303            timestamp: now,
18304            epistemic_state: epistemic_state.to_string(),
18305            effect_row: effect_row.to_string(),
18306            pix_ref: nav_ctx.and_then(|c| c.pix_ref.clone()),
18307            corpus_ref: nav_ctx.and_then(|c| c.corpus_ref.clone()),
18308            nav_trail: nav_ctx.and_then(|c| c.nav_trail.clone()),
18309            mdn_edge_type: nav_ctx.and_then(|c| c.mdn_edge_type.clone()),
18310            nav_depth: nav_ctx.and_then(|c| c.nav_depth),
18311        });
18312    }
18313
18314    /// Emit PIX navigate result — epistemic state "believe" (external source, not yet validated).
18315    /// PIX retrieval: EffectRow = <io, epistemic:believe>
18316    pub fn emit_pix_navigate(&mut self, step_name: &str, content: &str, pix_ref: &str, trail: Vec<String>, depth: u32) {
18317        self.emit_with_context(step_name, content, "believe", "<io, epistemic:believe>", Some(&NavigationContext {
18318            pix_ref: Some(pix_ref.to_string()),
18319            corpus_ref: None,
18320            nav_trail: Some(trail),
18321            mdn_edge_type: None,
18322            nav_depth: Some(depth),
18323        }));
18324    }
18325
18326    /// Emit MDN graph traverse result — epistemic state "believe" with edge type.
18327    /// MDN retrieval: EffectRow = <io, network, epistemic:believe>
18328    pub fn emit_mdn_traverse(&mut self, step_name: &str, content: &str, corpus_ref: &str, edge_type: &str, depth: u32) {
18329        self.emit_with_context(step_name, content, "believe", "<io, network, epistemic:believe>", Some(&NavigationContext {
18330            pix_ref: None,
18331            corpus_ref: Some(corpus_ref.to_string()),
18332            nav_trail: None,
18333            mdn_edge_type: Some(edge_type.to_string()),
18334            nav_depth: Some(depth),
18335        }));
18336    }
18337
18338    /// Mark stream as complete — emit final sentinel.
18339    /// On finalization, if all anchors pass, epistemic state promotes to "know".
18340    pub fn finalize(&mut self) {
18341        self.finalize_with_epistemic("know", "<pure, epistemic:know>");
18342    }
18343
18344    /// Finalize with explicit epistemic state (e.g., "believe" if anchors didn't run).
18345    pub fn finalize_with_epistemic(&mut self, epistemic_state: &str, effect_row: &str) {
18346        let now = std::time::SystemTime::now()
18347            .duration_since(std::time::UNIX_EPOCH)
18348            .unwrap_or_default()
18349            .as_secs();
18350
18351        self.token_count += 1;
18352        self.tokens.push(StreamToken {
18353            trace_id: self.trace_id,
18354            flow_name: self.flow_name.clone(),
18355            step_name: "".to_string(),
18356            token_index: self.token_count,
18357            content: String::new(),
18358            is_final: true,
18359            timestamp: now,
18360            epistemic_state: epistemic_state.to_string(),
18361            effect_row: effect_row.to_string(),
18362            pix_ref: None, corpus_ref: None, nav_trail: None, mdn_edge_type: None, nav_depth: None,
18363        });
18364    }
18365
18366    /// Materialize: publish all buffered tokens to the EventBus.
18367    /// This is the natural transformation h: F_Σ(B) → M_IO(B).
18368    pub fn publish_to_bus(&self, bus: &crate::event_bus::EventBus) {
18369        let topic = format!("flow.stream.{}", self.trace_id);
18370        for token in &self.tokens {
18371            bus.publish(
18372                &topic,
18373                serde_json::to_value(token).unwrap_or_default(),
18374                &format!("stream:{}", self.flow_name),
18375            );
18376        }
18377    }
18378
18379    /// Emit token-level chunks for a step — coinductive stream observations.
18380    /// Each chunk: epistemic_state = "speculate", effect_row = <io, epistemic:speculate>.
18381    pub fn emit_chunks(&mut self, step_name: &str, chunks: &[String]) {
18382        for chunk in chunks {
18383            self.emit(step_name, chunk);
18384        }
18385    }
18386
18387    pub fn token_count(&self) -> u64 { self.token_count }
18388    pub fn tokens(&self) -> &[StreamToken] { &self.tokens }
18389}
18390
18391/// Navigation context for PIX/MDN-originated stream tokens.
18392pub struct NavigationContext {
18393    pub pix_ref: Option<String>,
18394    pub corpus_ref: Option<String>,
18395    pub nav_trail: Option<Vec<String>>,
18396    pub mdn_edge_type: Option<String>,
18397    pub nav_depth: Option<u32>,
18398}
18399
18400/// Request for streaming execution.
18401#[derive(Debug, Deserialize)]
18402pub struct StreamExecuteRequest {
18403    /// Flow name to execute.
18404    pub flow_name: String,
18405    /// Backend (default "stub").
18406    #[serde(default = "default_execute_backend")]
18407    pub backend: String,
18408    /// §Fase 37.b (D1) — the parsed HTTP request body, carried so the
18409    /// flow's declared parameters bind from it (the Request Binding
18410    /// Contract). `#[serde(default)]` ⇒ `None` for a `/v1/execute/sse`
18411    /// direct hit that sends only `{flow_name, backend}` (D5).
18412    #[serde(default)]
18413    pub request_body: Option<serde_json::Value>,
18414    /// §Fase 37.y (D3) — URL path captures (same shape as
18415    /// `ExecuteRequest.request_path`).
18416    #[serde(default)]
18417    pub request_path: HashMap<String, String>,
18418    /// §Fase 37.y (D3) — URL query string parsed name → value.
18419    #[serde(default)]
18420    pub request_query: HashMap<String, String>,
18421}
18422
18423/// POST /v1/execute/stream — execute a flow with algebraic effect streaming.
18424///
18425/// Executes the flow, emits per-step tokens via the StreamEmitter (algebraic
18426/// effect handler), publishes to EventBus as flow.stream.{trace_id}, and
18427/// returns the execution result with stream metadata.
18428///
18429/// Clients can consume the stream via:
18430///   GET /v1/events/stream?topic=flow.stream.{trace_id}
18431async fn execute_stream_handler(
18432    State(state): State<SharedState>,
18433    headers: HeaderMap,
18434    Json(payload): Json<StreamExecuteRequest>,
18435) -> Result<Json<serde_json::Value>, StatusCode> {
18436    let req_start = Instant::now();
18437    let client = client_key_from_headers(&headers);
18438    {
18439        let mut s = state.lock().unwrap();
18440        check_auth(&mut s, &headers, AccessLevel::Write)?;
18441    }
18442
18443    // Look up deployed source
18444    let (source, source_file) = {
18445        let s = state.lock().unwrap();
18446        match s.versions.get_history(&payload.flow_name)
18447            .and_then(|h| h.active())
18448            .map(|v| (v.source.clone(), v.source_file.clone()))
18449        {
18450            Some(info) => info,
18451            None => return Ok(Json(serde_json::json!({
18452                "error": format!("flow '{}' not deployed", payload.flow_name),
18453            }))),
18454        }
18455    };
18456
18457    // Execute
18458    match server_execute_full(&state, &source, &source_file, &payload.flow_name, &payload.backend).0 {
18459        Ok(mut er) => {
18460            // Record trace
18461            let mut trace_entry = crate::trace_store::build_trace(
18462                &er.flow_name, &er.source_file, &er.backend, &client,
18463                if er.success { crate::trace_store::TraceStatus::Success }
18464                else { crate::trace_store::TraceStatus::Partial },
18465                er.steps_executed, er.latency_ms,
18466            );
18467            trace_entry.tokens_input = er.tokens_input;
18468            trace_entry.tokens_output = er.tokens_output;
18469            trace_entry.errors = er.errors;
18470
18471            let (trace_id, stream_topic) = {
18472                let mut s = state.lock().unwrap();
18473                let tid = s.trace_store.record(trace_entry);
18474
18475                // === ALGEBRAIC EFFECT HANDLER ===
18476                // Create the StreamEmitter (the handler h)
18477                let mut emitter = StreamEmitter::new(tid, &er.flow_name);
18478
18479                // Token-level granularity: emit_chunks for each step
18480                // Each chunk is a perform(Emit(chunk)) — coinductive stream observation
18481                for (i, step_name) in er.step_names.iter().enumerate() {
18482                    if let Some(chunks) = er.step_results.get(i).map(|r| {
18483                        // Chunk by word boundaries (~3 words per token)
18484                        if r.is_empty() { vec![] }
18485                        else {
18486                            r.split_whitespace()
18487                                .collect::<Vec<&str>>()
18488                                .chunks(3)
18489                                .map(|c| c.join(" "))
18490                                .collect()
18491                        }
18492                    }) {
18493                        emitter.emit_chunks(step_name, &chunks);
18494                    }
18495                }
18496                emitter.finalize();
18497
18498                // Materialize: h(intent_tree) → IO effects
18499                // Publish to EventBus for SSE consumption
18500                emitter.publish_to_bus(&s.event_bus);
18501
18502                let topic = format!("flow.stream.{}", tid);
18503                (tid, topic)
18504            };
18505
18506            er.trace_id = trace_id;
18507
18508            Ok(Json(serde_json::json!({
18509                "success": er.success,
18510                "trace_id": trace_id,
18511                "flow": er.flow_name,
18512                "backend": er.backend,
18513                "steps_executed": er.steps_executed,
18514                "latency_ms": req_start.elapsed().as_millis() as u64,
18515                "tokens_input": er.tokens_input,
18516                "tokens_output": er.tokens_output,
18517                "stream": {
18518                    "topic": stream_topic,
18519                    "token_count": er.step_names.len() + 1, // steps + final
18520                    "consume_url": format!("/v1/events/stream?topic={}", stream_topic),
18521                    "sse_url": format!("/v1/events/stream?topic={}", stream_topic),
18522                },
18523                "algebraic_effect": {
18524                    "handler": "StreamEmitter",
18525                    "operation": "perform(Emit(token))",
18526                    "materialization": format!("EventBus.publish(\"{}\")", stream_topic),
18527                },
18528            })))
18529        }
18530        Err(e) => {
18531            let mut s = state.lock().unwrap();
18532            s.metrics.total_errors += 1;
18533            Ok(Json(serde_json::json!({
18534                "success": false,
18535                "error": e,
18536            })))
18537        }
18538    }
18539}
18540
18541// ──────────────────────────────────────────────────────────────────────────
18542// §Fase 30.d — Single-shot Server-Sent Events response path.
18543//
18544// Distinct from `execute_stream_handler` above (which is the two-stage
18545// pub/sub pattern returning a JSON envelope with `consume_url`; preserved
18546// per D8). This handler returns `Content-Type: text/event-stream` DIRECTLY:
18547// the response body IS the SSE stream. Adopters who declared
18548// `transport: sse` on their axonendpoint get this wire format on
18549// `POST /v1/execute/sse` — one HTTP call, one stream, terminates with
18550// `event: axon.complete`.
18551//
18552// Wire format (per plan vivo §4):
18553//   - Initial `retry: 5000` directive (W3C SSE reconnect hint)
18554//   - Per-step `event: axon.token`, `id: <monotonic>`, `data: { ... }`
18555//   - Final `event: axon.complete`, `id: <last>`, `data: { final envelope }`
18556//   - Error mid-stream → `event: axon.error`, then server closes
18557//
18558// Event-ID counter is per-trace and monotonic — adopter EventSource
18559// clients can resume after disconnect with the `Last-Event-ID` header
18560// (axum forwards it to a future handler if 30.f adds resume support;
18561// 30.d ships the wire shape without server-side resume).
18562//
18563// Cancel-safety: axum + hyper drop the Sse response when the client
18564// disconnects. The flow executor today is synchronous (runs to
18565// completion BEFORE the SSE iterator starts emitting), so there is no
18566// background task to abort. A future Fase that refactors the executor
18567// to true incremental streaming will add `tokio::task::JoinHandle::abort`
18568// on disconnect; the wire format does NOT change.
18569// ──────────────────────────────────────────────────────────────────────────
18570
18571use axum::response::sse::{Event, Sse};
18572use futures::stream::Stream;
18573use std::convert::Infallible;
18574
18575// §Fase 33.z.k.g.2 (v1.28.0) — Wire-format event builders retired.
18576// The v1.27.1 inline helpers `build_token_event` / `build_complete_event`
18577// / `build_tool_call_event` / `build_error_event` were replaced by the
18578// closed-catalog `WireFormatAdapter` dispatch in the SSE consumer loop
18579// (see `axon-rs/src/wire_format/`). Per-dialect adapters own:
18580//
18581//   - axon  → `event: axon.token` / `axon.complete` / `axon.tool_call` /
18582//             `axon.error` (D6 byte-compat baseline; AxonDialectAdapter
18583//             matches v1.27.1's inline emission verbatim).
18584//   - openai → `data: {"choices":[{"delta":{...}}]}` chunks + Q7
18585//              `axon_metadata` frame + literal `data: [DONE]`.
18586//   - anthropic → `event: message_start` / `content_block_*` /
18587//                 `message_delta` + Q7 `axon.metadata` + `message_stop`.
18588//
18589// Kimi + GLM dialects dispatch to `OpenAIDialectAdapter` (the wire is
18590// the OpenAI-compatible Chat Completions streaming format both
18591// providers publish). The closed catalog `{axon, openai, kimi, glm,
18592// anthropic}` is resolved at deploy via `resolve_effective_dialect`
18593// (type_checker.rs) + dispatched at request time via `select_adapter`
18594// (wire_format/mod.rs).
18595
18596/// Build the initial `retry:` directive event. Per W3C SSE spec, this
18597/// tells the client EventSource how long to wait before reconnecting
18598/// on stream drop. 5000ms is the published default in plan vivo §4.5.
18599fn build_retry_hint_event() -> Event {
18600    Event::default().retry(std::time::Duration::from_millis(5000))
18601}
18602
18603/// §Fase 33.c — Live-streaming execution surface.
18604///
18605/// Spawns the synchronous executor on `tokio::task::spawn_blocking` and
18606/// projects its progress onto a `tokio::sync::mpsc::UnboundedReceiver`
18607/// of `FlowExecutionEvent` values. Each event is **emitted as it
18608/// occurs** rather than batched after the flow completes — the SSE
18609/// handler (or any other consumer) sees them live.
18610///
18611/// ## Invariant (D2 + closed catalog)
18612///
18613/// The producer guarantees the following ordered shape:
18614///
18615/// ```text
18616///   FlowStart
18617///   (StepStart  StepToken*  StepComplete)*
18618///   FlowComplete  |  FlowError
18619/// ```
18620///
18621/// Exactly one terminator (`FlowComplete` OR `FlowError`) closes the
18622/// stream. After the terminator is sent the sender is dropped so the
18623/// consumer's `recv()` returns `None`.
18624///
18625/// ## Layer mapping
18626///
18627/// - **33.b** introduced `FlowExecutionEvent` and the cross-stack
18628///   drift-gated corpus.
18629/// - **33.c** (this function) is the producer the runner-level path
18630///   plugs into. For now the chunking remains "~3-word groups per
18631///   step output", mirroring the pre-33.c `StreamEmitter` behavior,
18632///   so the wire body stays byte-identical (D9). 33.d replaces this
18633///   with real per-token streaming from the backend.
18634/// - **33.e** (effect dispatcher) eventually owns the chunk-shape
18635///   decision via the declared `<stream:<policy>>` annotation.
18636/// §Fase 33.e — Resolve per-step `<stream:<policy>>` effects for a
18637/// flow's source. Returns a list of `(step_name, policy_slug)` pairs
18638/// for steps that declare a stream effect.
18639///
18640/// Pure best-effort: source-parse failures return an empty vec rather
18641/// than propagating up (the runtime path already handles malformed
18642/// source at a different layer; we don't want a parse failure here to
18643/// drop streaming).
18644fn resolve_stream_policies_for_flow(
18645    source: &str,
18646    source_file: &str,
18647    flow_name: &str,
18648) -> Vec<(String, &'static str)> {
18649    let tokens = match crate::lexer::Lexer::new(source, source_file).tokenize() {
18650        Ok(t) => t,
18651        Err(_) => return Vec::new(),
18652    };
18653    let mut parser = crate::parser::Parser::new(tokens);
18654    let program = match parser.parse() {
18655        Ok(p) => p,
18656        Err(_) => return Vec::new(),
18657    };
18658
18659    let flow = program.declarations.iter().find_map(|d| match d {
18660        crate::ast::Declaration::Flow(f) if f.name == flow_name => Some(f),
18661        _ => None,
18662    });
18663    let Some(flow) = flow else { return Vec::new() };
18664
18665    let mut out = Vec::new();
18666    for step in &flow.body {
18667        if let crate::ast::FlowStep::Step(node) = step {
18668            if let Some(policy) = crate::stream_effect_dispatcher::resolve_stream_effect_for_step(
18669                &node.name,
18670                flow,
18671                &program,
18672            ) {
18673                out.push((node.name.clone(), policy.slug()));
18674            }
18675        }
18676    }
18677    out
18678}
18679
18680/// §Fase 55.b/c — re-derive the flow's epistemic envelopes from source for
18681/// the streaming path. Parses + generates the IR, then DELEGATES to the
18682/// single shared `runner::derive_epistemic_envelopes_for_flow` the sync
18683/// runner also calls — so the streaming `axon.complete` and the sync
18684/// `FlowEnvelope` carry byte-identical `(base, scope, confidence)` triples
18685/// by construction (there is exactly one derivation, never two that could
18686/// drift — the §55.c parity invariant). Best-effort: a lex / parse failure
18687/// yields an empty vec and the stream proceeds unchanged (mirrors
18688/// `resolve_stream_policies_for_flow`).
18689pub fn resolve_epistemic_envelopes_for_flow(
18690    source: &str,
18691    source_file: &str,
18692    flow_name: &str,
18693) -> Vec<crate::epistemic_capture::EpistemicEnvelope> {
18694    let tokens = match crate::lexer::Lexer::new(source, source_file).tokenize() {
18695        Ok(t) => t,
18696        Err(_) => return Vec::new(),
18697    };
18698    let program = match crate::parser::Parser::new(tokens).parse() {
18699        Ok(p) => p,
18700        Err(_) => return Vec::new(),
18701    };
18702    let ir = crate::ir_generator::IRGenerator::new().generate(&program);
18703    crate::runner::derive_epistemic_envelopes_for_flow(&ir, flow_name)
18704}
18705
18706/// §Fase 33.f — Handles returned by [`server_execute_streaming`].
18707///
18708/// Bundles the [`FlowExecutionEvent`] receiver with an "exited"
18709/// signal the consumer can await to confirm the producer task has
18710/// terminated (e.g. for cancel-safety budgets in tests / metrics).
18711// §Fase 33.f / §Fase 50.d/2 (v2.4.0) — publicized so `axon-enterprise`'s
18712// runtime executor can drive real per-token SSE streaming via the
18713// public `server_execute_streaming` entry point. The fields are read by
18714// the SSE consumer (events receiver + per-step side-channels); making
18715// the struct `pub` exposes those readings to enterprise consumers
18716// without forcing them through the OSS axum handler.
18717pub struct StreamingExecution {
18718    pub events: tokio::sync::mpsc::UnboundedReceiver<
18719        crate::flow_execution_event::FlowExecutionEvent,
18720    >,
18721    /// Resolves once the producer's `spawn_blocking` task exits, for
18722    /// any reason (normal completion, cancellation, or send-failure
18723    /// on a dropped consumer). Pairs with the [`crate::cancel_token::CancellationFlag`]
18724    /// the producer observes between emits.
18725    pub exited: std::sync::Arc<tokio::sync::Notify>,
18726    /// §Fase 33.x.d — Per-step `EnforcementSummary` side-channel.
18727    ///
18728    /// Populated by the async streaming producer
18729    /// (`run_streaming_async_path`) as each step's
18730    /// [`crate::stream_effect_dispatcher::StreamPolicyEnforcer`]
18731    /// finishes draining. Read by the consumer (`execute_sse_handler`)
18732    /// when emitting `axon.complete` so the wire body includes the
18733    /// enforcement summary per step that had a declared
18734    /// `<stream:<policy>>` effect.
18735    ///
18736    /// Empty for:
18737    ///   - The legacy synchronous fallback path
18738    ///     (`run_streaming_legacy_path`) — preserves v1.24.0 wire
18739    ///     byte-compat for adopter shapes the streaming path defers.
18740    ///   - The async path when no step has a declared effect policy
18741    ///     — no enforcer is constructed; no summary to publish.
18742    ///
18743    /// The lock is `tokio::sync::Mutex` so the producer can hold it
18744    /// across `.await` points without risk of deadlock from
18745    /// `std::sync::Mutex` blocking the executor.
18746    pub enforcement_summaries: std::sync::Arc<
18747        tokio::sync::Mutex<
18748            std::collections::HashMap<String, EnforcementSummaryWire>,
18749        >,
18750    >,
18751    /// §Fase 33.x.f — Per-step audit record side-channel.
18752    ///
18753    /// Populated by the async streaming producer
18754    /// (`run_streaming_async_path`) after each step finishes
18755    /// draining: one
18756    /// [`crate::axonendpoint_replay::StepAuditRecord`] per
18757    /// `IRFlowNode::Step` that executed. Read by the SSE handler
18758    /// when emitting `axon.complete` if a `ReplayContext` is
18759    /// provided (route has `replay: true` declared) — the records
18760    /// land in the
18761    /// [`crate::axonendpoint_replay::AxonendpointReplayEntry::step_audit`]
18762    /// field so `GET /v1/replay/<trace_id>` returns the per-step
18763    /// sequence to regulators / auditors.
18764    ///
18765    /// Empty for:
18766    ///   - The legacy synchronous fallback path
18767    ///     (`run_streaming_legacy_path`) — preserves v1.24.0 wire +
18768    ///     replay byte-compat.
18769    ///   - SSE routes WITHOUT `replay: true` — recording the
18770    ///     side-channel costs ~one Mutex push per step regardless
18771    ///     of replay binding (cheap), but the entry is never written.
18772    pub step_audit_records: std::sync::Arc<
18773        tokio::sync::Mutex<Vec<crate::axonendpoint_replay::StepAuditRecord>>,
18774    >,
18775    /// §Fase 33.x.g — Runtime warning side-channel.
18776    ///
18777    /// Populated synchronously by `server_execute_streaming` BEFORE
18778    /// spawning the producer when the dispatch falls back to the
18779    /// legacy synchronous path. The W002 warning is recorded with
18780    /// the specific [`crate::runtime_warnings::FallbackMode`] tag
18781    /// identifying WHY streaming did not activate (unsupported
18782    /// flow shape / unknown backend / source compilation failed /
18783    /// backend lacks stream()).
18784    ///
18785    /// Read by the SSE consumer at FlowComplete time + projected
18786    /// onto `axon.complete.warnings` (wire). Also lands on
18787    /// `AxonendpointReplayEntry.runtime_warnings` (audit) when the
18788    /// route declares `replay: true`.
18789    ///
18790    /// D5 contract: NO silent degradation. Empty Vec on the happy
18791    /// path (the async streaming path activated correctly).
18792    pub runtime_warnings: std::sync::Arc<
18793        tokio::sync::Mutex<Vec<crate::runtime_warnings::RuntimeWarning>>,
18794    >,
18795}
18796
18797/// §Fase 33.z.e — Single hot path through the per-IRFlowNode dispatcher.
18798///
18799/// After 33.z.e this function has EXACTLY ONE branch: construct
18800/// `DispatchCtx` + spawn `streaming_via_dispatcher::run_streaming_via_dispatcher`.
18801/// No flag check, no async-vs-legacy fork, no fallback. The dispatcher
18802/// (Fase 33.y 45/45 IRFlowNode coverage) is the unified production
18803/// hot path. D1 invariant: zero `if/match` selecting between paths.
18804///
18805/// Prior paths retired in 33.z.e:
18806/// - 33.x.b `run_streaming_async_path` (canonical Step v1.25.0 hot
18807///   path) — DELETED (no callers after the dispatcher unification).
18808/// - 33.x.h-attached `run_streaming_legacy_path` (synthetic-burst
18809///   3-word chunking for non-canonical shapes) — DELETED.
18810/// - 33.x.h tokenizer_fallback flag path through legacy — DELETED.
18811/// - `AXON_STREAMING_VIA_DISPATCHER` runtime flag + RAII guard —
18812///   DELETED. No opt-out; the dispatcher is the only path.
18813///
18814/// Adopters on v1.26.0 who set `set_streaming_via_dispatcher(false)`
18815/// hit a compile error on the missing symbol — explicit failure
18816/// shape per the 33.y.l → 33.z.e deprecation cycle.
18817// §Fase 33.e/§Fase 50.d/2 (v2.4.0) — publicized so `axon-enterprise`'s
18818// runtime executor can drive real per-token SSE streaming without
18819// reimplementing the per-IRFlowNode dispatcher on its side. The
18820// function builds the StreamingExecution handle (events channel + per-
18821// step side-channels) the SSE consumer drives; enterprise consumers
18822// pass a freshly-constructed `SharedState` (see `ServerState::new`,
18823// also publicized in v2.4.0).
18824pub fn server_execute_streaming(
18825    state: SharedState,
18826    source: String,
18827    source_file: String,
18828    flow_name: String,
18829    backend: String,
18830    cancel: crate::cancel_token::CancellationFlag,
18831    // §Fase 35.j (Pillar IV) — the request's held capability slugs,
18832    // threaded into the dispatcher so the store handlers re-check
18833    // capability-gated stores.
18834    held_capabilities: Option<Vec<String>>,
18835    // §Fase 37.b (D1) — the parsed HTTP request body, threaded to the
18836    // dispatcher so the flow's declared parameters bind from it (the
18837    // Request Binding Contract). `None` for a request with no body.
18838    request_body: Option<serde_json::Value>,
18839    // §Fase 37.y (D3) — URL path captures from the dynamic-route
18840    // dispatcher. Empty map for non-dynamic-route callers.
18841    request_path: HashMap<String, String>,
18842    // §Fase 37.y (D3) — URL query string parsed name → value.
18843    request_query: HashMap<String, String>,
18844    // §Fase 58.g (D7) — optional per-tenant / per-server tool base URL,
18845    // threaded to the dispatcher so relative program-tool runtimes
18846    // resolve to `{base}/{slug}` (absolute runtimes stay verbatim, D5).
18847    // `None` → no resolution.
18848    tool_base_url: Option<String>,
18849) -> StreamingExecution {
18850    use crate::flow_execution_event::FlowExecutionEvent;
18851    let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<FlowExecutionEvent>();
18852    let exited = std::sync::Arc::new(tokio::sync::Notify::new());
18853    let exited_for_task = exited.clone();
18854
18855    // §Fase 33.x.d — Shared per-step EnforcementSummary side-channel.
18856    // The async producer writes one entry per step that has a declared
18857    // `<stream:<policy>>` effect; the consumer reads the map when
18858    // emitting `axon.complete` so the wire body includes the summary.
18859    // Empty map on the legacy path + on flows with no declared
18860    // effects = D4 byte-compat preserved.
18861    let enforcement_summaries: std::sync::Arc<
18862        tokio::sync::Mutex<std::collections::HashMap<String, EnforcementSummaryWire>>,
18863    > = std::sync::Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new()));
18864
18865    // §Fase 33.x.f — Shared per-step audit record side-channel.
18866    // The async producer writes one record per step that executes;
18867    // the SSE handler reads them at FlowComplete and lands them in
18868    // the AxonendpointReplayEntry when the route declares
18869    // `replay: true`. Empty Vec on the legacy path + on /v1/execute/sse
18870    // direct hits (no ReplayContext supplied).
18871    let step_audit_records: std::sync::Arc<
18872        tokio::sync::Mutex<Vec<crate::axonendpoint_replay::StepAuditRecord>>,
18873    > = std::sync::Arc::new(tokio::sync::Mutex::new(Vec::new()));
18874
18875    // §Fase 33.x.g — Closed-catalog runtime warnings side-channel.
18876    // The initial Vec is built synchronously below (BEFORE we wrap
18877    // in Arc<tokio::sync::Mutex>) so we don't need to lock in this
18878    // sync function context (`tokio::sync::Mutex::blocking_lock`
18879    // panics inside an async runtime). The async path leaves the
18880    // Vec empty (no warning — streaming activated correctly).
18881    let mut initial_warnings: Vec<crate::runtime_warnings::RuntimeWarning> = Vec::new();
18882
18883    // §Fase 33.x.b + 33.x.d — Resolve `auto` to a concrete backend
18884    // BEFORE the routing decision. The dynamic-route dispatch path
18885    // (`dispatch_dynamic_route_handler`) passes `backend: "auto"`
18886    // unconditionally; the streaming-resolver doesn't recognize
18887    // "auto", so without this step every dynamic-route flow would
18888    // fall through to the legacy synchronous path — and the
18889    // enforcer (D2) + per-chunk granularity (D1) would never
18890    // activate. Resolution mirrors `server_execute_full`'s
18891    // identical "auto" branch (compute_backend_scores +
18892    // fall-back to "stub") so wire byte-compat with v1.24.0 is
18893    // preserved for adopters that never set a real provider key.
18894    let effective_backend = if backend == "auto" {
18895        let scores = {
18896            let s = state.lock().unwrap();
18897            compute_backend_scores(&s, "balanced")
18898        };
18899        scores
18900            .first()
18901            .map(|s| s.name.clone())
18902            .unwrap_or_else(|| "stub".to_string())
18903    } else {
18904        backend.clone()
18905    };
18906
18907    // §Fase 33.z.e — Backend visibility check (preserved). Unknown
18908    // backends surface `axon-W002 UnknownBackend` on the wire via the
18909    // dispatcher's BackendError pathway; the warning is pre-populated
18910    // synchronously so `axon.complete.warnings[*]` carries it even if
18911    // the dispatcher task terminates fast.
18912    let backend_known =
18913        crate::backends::resolve_streaming_backend(&effective_backend).is_some();
18914    if !backend_known {
18915        let warning = crate::runtime_warnings::RuntimeWarning::streaming_not_supported(
18916            flow_name.clone(),
18917            effective_backend.clone(),
18918            crate::runtime_warnings::FallbackMode::UnknownBackend,
18919            format!("backend '{effective_backend}' not in streaming registry"),
18920        );
18921        initial_warnings.push(warning);
18922    }
18923
18924    // Wrap warnings in the side-channel Mutex so the consumer reads
18925    // asynchronously.
18926    let runtime_warnings: std::sync::Arc<
18927        tokio::sync::Mutex<Vec<crate::runtime_warnings::RuntimeWarning>>,
18928    > = std::sync::Arc::new(tokio::sync::Mutex::new(initial_warnings));
18929
18930    // §Fase 33.z.e — Single hot path through the per-IRFlowNode
18931    // dispatcher (D1 milestone). The 33.z.b feature-flagged graft +
18932    // 33.z.c default-on flip + 33.z.d 50-flow parity corpus together
18933    // proved out the dispatcher path as semantically equivalent for
18934    // canonical Step + structurally-graduated for orchestration /
18935    // cognitive / algebraic / wire / PIX / lambda shapes (45 of 45
18936    // IRFlowNode variants graduated per Fase 33.y compiler-enforced
18937    // exhaustive match).
18938    //
18939    // 33.z.e DELETES the flag (no opt-out) + the
18940    // `run_streaming_async_path` v1.25.0 path + the
18941    // `run_streaming_legacy_path` v1.24.0 fallback + the
18942    // `PlanError::LegacyOrchestrationRequired` variant +
18943    // `flow_plan::unsupported_feature_reason` + the
18944    // `FallbackMode::UnsupportedFlowShape` variant. After this
18945    // commit, `server_execute_streaming` has exactly ONE branch:
18946    // construct DispatchCtx → spawn dispatcher producer. D1
18947    // invariant: zero `if/match` selecting between paths.
18948    //
18949    // `state` is moved into the dispatcher spawn — the legacy
18950    // synchronous path (which needed `state` for `server_execute_full`)
18951    // is gone; only the runtime-warnings + step_audit + enforcement
18952    // side-channels persist into the dispatcher.
18953    let _ = state;
18954
18955    let tx_for_dispatcher = tx.clone();
18956    let exited_for_dispatcher = exited_for_task.clone();
18957    let cancel_for_dispatcher = cancel.clone();
18958    let enforcement_for_dispatcher = enforcement_summaries.clone();
18959    let audit_for_dispatcher = step_audit_records.clone();
18960    let warnings_for_dispatcher = runtime_warnings.clone();
18961    tokio::spawn(async move {
18962        crate::streaming_via_dispatcher::run_streaming_via_dispatcher(
18963            source,
18964            source_file,
18965            flow_name,
18966            effective_backend,
18967            cancel_for_dispatcher,
18968            tx_for_dispatcher,
18969            enforcement_for_dispatcher,
18970            audit_for_dispatcher,
18971            warnings_for_dispatcher,
18972            held_capabilities,
18973            request_body,
18974            // §Fase 37.y — path + query into the dispatcher.
18975            request_path,
18976            request_query,
18977            // §Fase 58.g (D7) — per-tenant tool base URL.
18978            tool_base_url,
18979        )
18980        .await;
18981        exited_for_dispatcher.notify_waiters();
18982    });
18983    drop(tx);
18984    StreamingExecution {
18985        events: rx,
18986        exited,
18987        enforcement_summaries,
18988        step_audit_records,
18989        runtime_warnings,
18990    }
18991}
18992
18993
18994/// POST /v1/execute/sse — single-shot SSE execution.
18995///
18996/// Identical request body shape to `/v1/execute` (`ExecuteRequest`).
18997/// Response Content-Type is `text/event-stream` regardless of the
18998/// flow's transport declaration on its axonendpoint — adopters that
18999/// hit this route are explicitly opting into SSE. (The auto-promotion
19000/// path that consults the axonendpoint `transport: sse` declaration
19001/// ships in 30.e as content-negotiation on the existing `/v1/execute`.)
19002///
19003/// Errors during flow execution become a single `axon.error` event +
19004/// connection close.
19005///
19006/// # 30.f → 33.c streaming architecture evolution
19007///
19008/// **30.f** introduced channel-fed streaming so the handler returned
19009/// the `Sse` response immediately and KeepAlive comments had an
19010/// inactivity window to fire into. Even so, all `axon.token` events
19011/// were batched in the producer task AFTER the synchronous executor
19012/// returned `ServerExecutionResult` — adopter clients saw them burst-
19013/// arrive at the end.
19014///
19015/// **33.c** closes the live-forwarding loop by consuming the
19016/// `FlowExecutionEvent` receiver from `server_execute_streaming` and
19017/// projecting each event onto the wire AS IT ARRIVES:
19018///
19019/// - `FlowStart` / `StepStart` / `StepComplete` are consumed but not
19020///   surfaced (preserves the byte-identical wire body that Fase
19021///   30+31+32 SSE tests depend on — D9).
19022/// - `StepToken` becomes one `axon.token` SSE event per chunk.
19023/// - `FlowComplete` becomes the `axon.complete` envelope.
19024/// - `FlowError` becomes the `axon.error` envelope.
19025///
19026/// The wire's trace_id is allocated up front via `trace_store.reserve_id()`
19027/// so every `axon.token` event carries the same id as the eventual
19028/// `axon.complete`. The trace entry is recorded with that id once the
19029/// `FlowExecutionEvent` channel closes, so the `/v1/replay/<trace_id>`
19030/// surface stays valid (D9 + Fase 30.f audit parity).
19031/// §Fase 33.x.f — Replay context for dynamic-route SSE responses.
19032///
19033/// Supplied by `dispatch_dynamic_route_handler` when the route has
19034/// `replay: true` declared. Carries the UUID trace_id (correlation
19035/// anchor matching `X-Axon-Trace-Id` header), endpoint metadata,
19036/// client auth context, and the request body bytes — everything
19037/// needed to build an `AxonendpointReplayEntry` at FlowComplete.
19038///
19039/// `None` for direct hits to `POST /v1/execute/sse` (the legacy
19040/// SSE path which has no axonendpoint declaration → no replay
19041/// binding semantics).
19042#[derive(Debug, Clone)]
19043pub(crate) struct SseReplayContext {
19044    pub trace_id_uuid: String,
19045    pub endpoint_name: String,
19046    pub method: String,
19047    pub path: String,
19048    pub client_id: String,
19049    pub capabilities_used: Vec<String>,
19050    pub request_body: Vec<u8>,
19051}
19052
19053/// Axum handler for `POST /v1/execute/sse`. Direct hits to this
19054/// route have no axonendpoint replay binding so we call the inner
19055/// fn with `replay_ctx: None`.
19056async fn execute_sse_handler(
19057    State(state): State<SharedState>,
19058    headers: HeaderMap,
19059    Json(payload): Json<StreamExecuteRequest>,
19060) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, StatusCode> {
19061    // §Fase 33.z.k.g — `/v1/execute/sse` direct hits default to the
19062    // axon W3C named-events dialect (D6 backwards-compat for this
19063    // legacy entrypoint; adopters on `/v1/execute/sse` never expressed
19064    // a dialect preference + their existing EventSource clients parse
19065    // `event: axon.token`).
19066    execute_sse_handler_inner(state, headers, payload, None, "axon".to_string()).await
19067}
19068
19069/// §Fase 33.x.f — Internal SSE handler with optional replay context.
19070///
19071/// Same SSE response shape as `execute_sse_handler` for both
19072/// `replay_ctx = None` (direct `/v1/execute/sse` hits) and
19073/// `replay_ctx = Some(...)` (dynamic-route SSE with replay binding).
19074/// When `replay_ctx` is `Some`, the consumer task additionally
19075/// writes an `AxonendpointReplayEntry` to the replay log at
19076/// FlowComplete time, populated with the per-step `step_audit`
19077/// records from the producer side-channel — so `GET /v1/replay/<uuid>`
19078/// returns the per-step sequence regulators need.
19079async fn execute_sse_handler_inner(
19080    state: SharedState,
19081    headers: HeaderMap,
19082    payload: StreamExecuteRequest,
19083    replay_ctx: Option<SseReplayContext>,
19084    // §Fase 33.z.k.g (v1.28.0) — SSE wire-format dialect. Closed
19085    // catalog `{axon, openai, kimi, glm, anthropic}` (resolved by the
19086    // caller via `type_checker::resolve_effective_dialect` per the
19087    // Q1 ratification: explicit `transport: sse(<dialect>)` wins;
19088    // otherwise algebraic-effect flows default to `openai`, type-
19089    // annotation-only flows default to `axon`).
19090    //
19091    // §33.z.k.g.2 consumes this value to drive `adapter.translate()`
19092    // in the consumer loop below — the producer's event-by-event
19093    // translation IS the dialect's wire-format projection. Axon
19094    // dialect emits W3C named events (`event: axon.token` +
19095    // `event: axon.complete`) byte-identical to v1.27.1; openai
19096    // emits `data: {"choices":[...]}` chunks + `data: [DONE]`;
19097    // anthropic emits `event: content_block_delta` +
19098    // `event: message_stop`. Kimi + GLM reuse the OpenAI wire.
19099    wire_dialect: String,
19100) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, StatusCode> {
19101    use crate::flow_execution_event::FlowExecutionEvent;
19102    use futures::SinkExt;
19103
19104    let req_start = Instant::now();
19105    let client = client_key_from_headers(&headers);
19106    {
19107        let mut s = state.lock().unwrap();
19108        check_auth(&mut s, &headers, AccessLevel::Write)?;
19109    }
19110
19111    // Look up deployed source. Not-deployed flows still get a
19112    // wire-format-valid SSE response (retry + axon.error) rather than
19113    // a 404, per the streaming-API expectation that the wire format
19114    // is honored even for failures.
19115    let source_info = {
19116        let s = state.lock().unwrap();
19117        s.versions
19118            .get_history(&payload.flow_name)
19119            .and_then(|h| h.active())
19120            .map(|v| (v.source.clone(), v.source_file.clone()))
19121    };
19122
19123    // 30.f: resolve the KeepAlive interval BEFORE spawning the
19124    // executor so the Sse response is fully configured before any
19125    // event flows. When the flow is not deployed we still use the
19126    // default 15s — the error event fires immediately so the
19127    // keepalive timer is moot, but the configuration stays consistent.
19128    let keepalive_duration = source_info
19129        .as_ref()
19130        .map(|(src, _)| resolve_keepalive_for_flow(src, &payload.flow_name))
19131        .unwrap_or_else(|| std::time::Duration::from_secs(15));
19132
19133    // Unified channel-fed stream for both the deployed + not-deployed
19134    // paths so the return type stays a single `impl Stream`. Capacity
19135    // 16 is comfortably above the retry + axon.error + ~N tokens +
19136    // complete shape; backpressure via `send().await` handles overflow
19137    // cleanly when adopter clients drain slowly.
19138    let (mut tx, rx) = futures::channel::mpsc::channel::<Result<Event, Infallible>>(16);
19139
19140    // Retry directive always leads — the W3C SSE reconnect hint must
19141    // reach the client before any data event. try_send is non-blocking
19142    // and the channel has capacity 16, so this never fails here.
19143    let _ = tx.try_send(Ok(build_retry_hint_event()));
19144
19145    match source_info {
19146        Some((source, source_file)) => {
19147            let state_for_task = state.clone();
19148            let flow_name_owned = payload.flow_name.clone();
19149            let backend_owned = payload.backend.clone();
19150            let client_owned = client.clone();
19151            let source_file_for_audit = source_file.clone();
19152            // §Fase 37.b (D1) — the parsed request body for the
19153            // Request Binding Contract, moved into the spawned task.
19154            let request_body_for_task = payload.request_body.clone();
19155            // §Fase 37.y — path captures + query string move into the
19156            // spawned executor task alongside the body.
19157            let request_path_for_task = payload.request_path.clone();
19158            let request_query_for_task = payload.request_query.clone();
19159            // §Fase 33.z.k.g.2 — clone the wire-format dialect for
19160            // the spawned task. The adapter is constructed inside
19161            // the spawn closure once `trace_id` is reserved + drives
19162            // the consumer loop's translation of `FlowExecutionEvent`
19163            // into per-dialect wire frames via `adapter.translate()`.
19164            let wire_dialect_for_task = wire_dialect.clone();
19165
19166            // 33.c: trace_id reservation precedes the executor spawn
19167            // so the first axon.token event already carries the id
19168            // adopters will use to bind the wire stream to the
19169            // `/v1/replay/<id>` audit row.
19170            let trace_id: u64 = {
19171                let mut s = state_for_task.lock().unwrap();
19172                s.trace_store.reserve_id()
19173            };
19174
19175            tokio::spawn(async move {
19176                // 33.c: live forwarding pipeline.
19177                //
19178                // server_execute_streaming spawns the synchronous
19179                // executor on spawn_blocking. As each FlowExecutionEvent
19180                // is produced we project it onto the wire IMMEDIATELY
19181                // — no batching after full execution. When real per-
19182                // token backends ship in 33.d, this loop will deliver
19183                // tokens as the network bytes arrive.
19184                // §Fase 33.e — Resolve declared `<stream:<policy>>`
19185                // effects once per execution so the axon.complete wire
19186                // envelope can surface the active policies. Best-effort:
19187                // parse failures yield an empty vec; the streaming path
19188                // continues unchanged.
19189                let effect_policies = resolve_stream_policies_for_flow(
19190                    &source,
19191                    &source_file,
19192                    &flow_name_owned,
19193                )
19194                .into_iter()
19195                .map(|(step, slug)| (step, slug.to_string()))
19196                .collect::<Vec<_>>();
19197
19198                // §Fase 55.b — resolve the epistemic envelopes once per
19199                // execution (same derivation as the sync path) so the
19200                // axon.complete envelope surfaces the Theorem 5.1 triples.
19201                let epistemic_envelopes = resolve_epistemic_envelopes_for_flow(
19202                    &source,
19203                    &source_file,
19204                    &flow_name_owned,
19205                );
19206
19207                // §Fase 33.f cancel-safety — bind the executor's
19208                // lifetime to this spawned task. If the task is
19209                // aborted (e.g. axum drops the Sse response because
19210                // the client disconnected), the `CancelOnDrop` guard
19211                // fires the cancellation flag the producer observes
19212                // between emit calls.
19213                let cancel = crate::cancel_token::CancellationFlag::new();
19214                let _cancel_guard =
19215                    crate::cancel_token::CancelOnDrop::new(cancel.clone());
19216
19217                let StreamingExecution {
19218                    events: mut event_rx,
19219                    exited: _producer_exited,
19220                    enforcement_summaries: enforcement_summaries_for_consumer,
19221                    step_audit_records: step_audit_records_for_consumer,
19222                    runtime_warnings: runtime_warnings_for_consumer,
19223                } = server_execute_streaming(
19224                    state_for_task.clone(),
19225                    source.clone(),
19226                    source_file.clone(),
19227                    flow_name_owned.clone(),
19228                    backend_owned.clone(),
19229                    cancel.clone(),
19230                    // §Fase 35.j — the request's held capabilities
19231                    // (JWT bearer `capabilities` claim) for the store
19232                    // handlers' Pillar IV runtime re-check.
19233                    Some(crate::auth_scope::extract_capabilities_from_bearer(
19234                        &headers,
19235                    )),
19236                    // §Fase 37.b — the parsed request body for the
19237                    // Request Binding Contract.
19238                    request_body_for_task,
19239                    // §Fase 37.y — path captures + query string.
19240                    request_path_for_task,
19241                    request_query_for_task,
19242                    // §Fase 58.g (D7) — env-driven tool base URL for the
19243                    // OSS streaming server (single-tenant per-process);
19244                    // enterprise threads its per-tenant override.
19245                    std::env::var("AXON_TOOL_BASE_URL").ok(),
19246                );
19247
19248                // §Fase 33.z.k.g.2 — Construct the wire-format adapter
19249                // for this request. The adapter owns its own monotonic
19250                // event-ID counter + per-dialect state (role-marker
19251                // emission flag for openai, content-block boundary
19252                // tracker for anthropic, etc.). Every internal
19253                // FlowExecutionEvent flows through `adapter.translate()`
19254                // → zero-or-more dialect-shaped wire frames. The Q5
19255                // invariant ("axon dialect backwards-compat is
19256                // indefinite") is preserved by `AxonDialectAdapter`
19257                // emitting byte-identical wire to the v1.27.1 inline
19258                // helpers; the openai + anthropic adapters project the
19259                // same per-token + per-tool-call + per-completion
19260                // information onto their respective spec-faithful
19261                // wires.
19262                let mut wire_adapter = crate::wire_format::select_adapter(
19263                    &wire_dialect_for_task,
19264                    trace_id,
19265                );
19266
19267                let mut steps_executed: usize = 0;
19268                let mut tokens_input: u64 = 0;
19269                let mut tokens_output: u64 = 0;
19270                let mut errors_seen: usize = 0;
19271                let mut flow_succeeded = true;
19272                let mut terminator_seen = false;
19273                let mut consumer_disconnected = false;
19274
19275                while let Some(event) = event_rx.recv().await {
19276                    // Pre-dispatch bookkeeping for arms that need
19277                    // accumulator updates BEFORE handing the event to
19278                    // the adapter. FlowStart / StepStart / StepToken /
19279                    // ToolCall / FlowError fall through to the unified
19280                    // `adapter.translate(&event)` call below; only
19281                    // StepComplete (tokens accumulator) and FlowComplete
19282                    // (envelope assembly + replay write) need pre-work.
19283                    let frames: Vec<Event> = match &event {
19284                        FlowExecutionEvent::StepComplete {
19285                            tokens_output: out,
19286                            ..
19287                        } => {
19288                            tokens_output = tokens_output.saturating_add(*out);
19289                            // step counter is authoritative on
19290                            // FlowComplete; we don't increment here to
19291                            // avoid double-counting.
19292                            wire_adapter.translate(&event)
19293                        }
19294                        FlowExecutionEvent::FlowComplete {
19295                            flow_name,
19296                            backend,
19297                            success,
19298                            steps_executed: se,
19299                            tokens_input: ti,
19300                            tokens_output: to,
19301                            latency_ms: _,
19302                            timestamp_ms: _,
19303                        } => {
19304                            steps_executed = *se;
19305                            tokens_input = *ti;
19306                            tokens_output = *to;
19307                            flow_succeeded = *success;
19308                            terminator_seen = true;
19309
19310                            let flow_name = flow_name.clone();
19311                            let backend = backend.clone();
19312
19313                            // §Fase 33.x.d — read enforcement_summaries
19314                            // from the shared side-channel that the
19315                            // streaming async producer populates as
19316                            // each step's enforcer drains. The map
19317                            // is empty for the legacy path + for
19318                            // flows with no declared `<stream:<policy>>`
19319                            // effect, so the wire stays byte-identical
19320                            // with v1.24.0 in those cases (D4).
19321                            let summaries_vec: Vec<(String, EnforcementSummaryWire)> = {
19322                                let guard = enforcement_summaries_for_consumer
19323                                    .lock()
19324                                    .await;
19325                                let mut ordered: Vec<(String, EnforcementSummaryWire)> =
19326                                    guard.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
19327                                // Sort by step name for deterministic wire shape.
19328                                ordered.sort_by(|a, b| a.0.cmp(&b.0));
19329                                ordered
19330                            };
19331                            // §Fase 33.x.g — Read the runtime
19332                            // warnings side-channel. Empty on the
19333                            // happy async-streaming path; carries
19334                            // one W002 entry when the legacy path
19335                            // was chosen.
19336                            let warnings_vec: Vec<crate::runtime_warnings::RuntimeWarning> = {
19337                                let guard = runtime_warnings_for_consumer.lock().await;
19338                                guard.clone()
19339                            };
19340
19341                            // §Fase 33.z.k.h — Read the per-step audit
19342                            // records UNCONDITIONALLY (was previously
19343                            // gated behind `replay_ctx.is_some()`).
19344                            // The records flow into the CompleteEnvelope
19345                            // so non-axon dialect adapters (openai +
19346                            // anthropic) can surface them on the Q7
19347                            // `axon_metadata` extension frame — adopters
19348                            // on those wires need per-step provenance
19349                            // for the vertical-regulator audit trails
19350                            // (PCI DSS Req 10 / FedRAMP AU-2 / FRE 502 /
19351                            // 21 CFR Part 11 §11.10). The replay-log
19352                            // write below reuses the same already-read
19353                            // vec to avoid double-locking the mutex.
19354                            let step_audit_vec: Vec<crate::axonendpoint_replay::StepAuditRecord> = {
19355                                let guard = step_audit_records_for_consumer.lock().await;
19356                                guard.clone()
19357                            };
19358
19359                            // §Fase 33.x.f — Write the SSE replay
19360                            // entry IF the route declared `replay: true`.
19361                            // Reads the producer's per-step audit
19362                            // records from the side-channel, builds the
19363                            // `AxonendpointReplayEntry`, appends to
19364                            // the replay log keyed by the UUID
19365                            // trace_id. Pre-condition: replay_ctx is
19366                            // Some (dispatcher set it when
19367                            // route.replay_enabled). The wire body the
19368                            // adopter sees is unchanged — this is a
19369                            // server-side write that surfaces only
19370                            // via `GET /v1/replay/<uuid>`.
19371                            if let Some(ref rctx) = replay_ctx {
19372                                let step_records = step_audit_vec.clone();
19373                                let replay_warnings = warnings_vec.clone();
19374                                let now_ms = std::time::SystemTime::now()
19375                                    .duration_since(std::time::UNIX_EPOCH)
19376                                    .map(|d| d.as_millis() as u64)
19377                                    .unwrap_or(0);
19378                                let request_body_hash_hex =
19379                                    crate::axonendpoint_replay::AxonendpointReplayLog::hash_body_hex(
19380                                        &rctx.request_body,
19381                                    );
19382                                let replay_entry =
19383                                    crate::axonendpoint_replay::AxonendpointReplayEntry {
19384                                        trace_id: rctx.trace_id_uuid.clone(),
19385                                        timestamp_ms: now_ms,
19386                                        endpoint_name: rctx.endpoint_name.clone(),
19387                                        flow_name: flow_name.clone(),
19388                                        method: rctx.method.clone(),
19389                                        path: rctx.path.clone(),
19390                                        client_id: rctx.client_id.clone(),
19391                                        capabilities_used: rctx.capabilities_used.clone(),
19392                                        request_body_hash_hex,
19393                                        request_body: rctx.request_body.clone(),
19394                                        response_status: 200,
19395                                        // SSE bodies aren't captured
19396                                        // server-side (per-event token
19397                                        // chain is Fase 34 scope); the
19398                                        // step_audit records are the
19399                                        // per-step audit trail.
19400                                        response_body_hash_hex: String::new(),
19401                                        response_content_type: "text/event-stream".to_string(),
19402                                        response_body: Vec::new(),
19403                                        model_version:
19404                                            "axon.runtime.dynamic_route.sse.v1".to_string(),
19405                                        deterministic:
19406                                            crate::axonendpoint_replay::is_backend_deterministic(
19407                                                &backend,
19408                                            ),
19409                                        step_audit: step_records,
19410                                        runtime_warnings: replay_warnings,
19411                                    };
19412                                let mut s = state_for_task.lock().unwrap();
19413                                s.axonendpoint_replay.append(replay_entry);
19414                            }
19415
19416                            // §Fase 33.z.k.g.2 — Build the
19417                            // CompleteEnvelope from the accumulated
19418                            // flow state + algebraic-policy side-
19419                            // channels, then ask the adapter to project
19420                            // it onto the dialect's wire-format
19421                            // convention. Axon embeds the fields
19422                            // directly on `axon.complete` (D4 byte-
19423                            // compat with v1.27.1 inline emission);
19424                            // openai + anthropic surface the
19425                            // algebraic-policy data via per-dialect
19426                            // `axon_metadata` / `axon.metadata` frames
19427                            // emitted from `flush_terminator()` (Q7).
19428                            let envelope = crate::wire_format::CompleteEnvelope {
19429                                trace_id,
19430                                flow_name: flow_name.clone(),
19431                                backend: backend.clone(),
19432                                success: flow_succeeded,
19433                                steps_executed,
19434                                tokens_input,
19435                                tokens_output,
19436                                latency_ms: req_start.elapsed().as_millis() as u64,
19437                                effect_policies: effect_policies.clone(),
19438                                enforcement_summaries: summaries_vec,
19439                                runtime_warnings: warnings_vec,
19440                                step_audit_records: step_audit_vec,
19441                                epistemic_envelopes: epistemic_envelopes.clone(),
19442                            };
19443                            wire_adapter.build_complete_envelope_event(&envelope)
19444                        }
19445                        FlowExecutionEvent::FlowError { .. } => {
19446                            errors_seen += 1;
19447                            flow_succeeded = false;
19448                            terminator_seen = true;
19449                            wire_adapter.translate(&event)
19450                        }
19451                        // FlowStart / StepStart / StepToken / ToolCall
19452                        // — pure projection through the adapter. Axon
19453                        // adapter silently consumes FlowStart +
19454                        // StepStart + StepComplete to preserve v1.27.1
19455                        // byte-compat; openai emits a role-marker
19456                        // frame for FlowStart + per-token content
19457                        // deltas; anthropic emits message_start +
19458                        // content_block boundary frames.
19459                        _ => wire_adapter.translate(&event),
19460                    };
19461
19462                    // §Fase 33.f cancel-safety — when the SSE tx
19463                    // returns Err the client disconnected (axum
19464                    // dropped the Sse response → rx dropped). Cancel
19465                    // the upstream producer so it stops emitting
19466                    // events into a dropped channel; break out of the
19467                    // consumer loop so the trace gets recorded with
19468                    // the partial-execution status (no FlowComplete
19469                    // observed). The adapter may emit MULTIPLE frames
19470                    // for a single internal event (e.g., anthropic's
19471                    // tool-use 3-frame burst, openai's role-marker +
19472                    // content-delta pair on FlowStart); cancel-on-err
19473                    // checks fire per frame so partial bursts close
19474                    // the channel cleanly.
19475                    for wire_event in frames {
19476                        if tx.send(Ok(wire_event)).await.is_err() {
19477                            cancel.cancel();
19478                            consumer_disconnected = true;
19479                            break;
19480                        }
19481                    }
19482                    if consumer_disconnected {
19483                        break;
19484                    }
19485                }
19486
19487                // §Fase 33.z.k.g.2 — Emit dialect-specific terminator
19488                // frames after the FlowComplete/FlowError translation.
19489                // Axon emits nothing (terminator is in-line with the
19490                // axon.complete frame); openai emits the Q7
19491                // axon_metadata frame + literal `data: [DONE]`;
19492                // anthropic emits axon.metadata + `event: message_stop`.
19493                if !consumer_disconnected {
19494                    for wire_event in wire_adapter.flush_terminator() {
19495                        if tx.send(Ok(wire_event)).await.is_err() {
19496                            break;
19497                        }
19498                    }
19499                }
19500
19501                // Defense in depth: if the producer dropped without
19502                // emitting a terminator we still record a trace + emit
19503                // an error event so the wire is well-formed. This
19504                // path is unreachable in 33.c's producer but the
19505                // closed-catalog invariant lives in the consumer too.
19506                // Route the synthetic FlowError through the adapter so
19507                // EVERY dialect surfaces the well-formed terminator
19508                // (anthropic emits message_delta; openai emits a final
19509                // chunk with finish_reason; axon emits axon.error).
19510                if !terminator_seen {
19511                    flow_succeeded = false;
19512                    errors_seen = errors_seen.saturating_add(1);
19513                    let synthetic_error = FlowExecutionEvent::FlowError {
19514                        flow_name: flow_name_owned.clone(),
19515                        error: "executor channel closed without terminator".to_string(),
19516                        timestamp_ms: 0,
19517                    };
19518                    for wire_event in wire_adapter.translate(&synthetic_error) {
19519                        let _ = tx.send(Ok(wire_event)).await;
19520                    }
19521                    for wire_event in wire_adapter.flush_terminator() {
19522                        let _ = tx.send(Ok(wire_event)).await;
19523                    }
19524                }
19525
19526                // Record the trace with the reserved id so audit /
19527                // observability surfaces match the JSON /v1/execute
19528                // path verbatim. Done AFTER the channel closes so we
19529                // have full visibility into the stream.
19530                {
19531                    let mut trace_entry = crate::trace_store::build_trace(
19532                        &flow_name_owned,
19533                        &source_file_for_audit,
19534                        &backend_owned,
19535                        &client_owned,
19536                        if flow_succeeded && errors_seen == 0 {
19537                            crate::trace_store::TraceStatus::Success
19538                        } else if !flow_succeeded && steps_executed == 0 {
19539                            crate::trace_store::TraceStatus::Failed
19540                        } else {
19541                            crate::trace_store::TraceStatus::Partial
19542                        },
19543                        steps_executed,
19544                        req_start.elapsed().as_millis() as u64,
19545                    );
19546                    trace_entry.tokens_input = tokens_input;
19547                    trace_entry.tokens_output = tokens_output;
19548                    trace_entry.errors = errors_seen;
19549                    let mut s = state_for_task.lock().unwrap();
19550                    s.trace_store.record_with_id(trace_entry, trace_id);
19551                    if !flow_succeeded {
19552                        s.metrics.total_errors += 1;
19553                    }
19554                }
19555                // tx drops here → rx stream ends → axum closes
19556                // the response body. KeepAlive stops firing.
19557            });
19558        }
19559        None => {
19560            // Not-deployed: emit a single error event + dialect
19561            // terminator + close. §Fase 33.z.k.g.2 — route through the
19562            // wire-format adapter so EVERY dialect surfaces a well-
19563            // formed error. Axon emits `event: axon.error`; openai
19564            // emits a final chunk with finish_reason + `data: [DONE]`;
19565            // anthropic emits message_delta + `event: message_stop`.
19566            // The wire shape is `retry: 5000` (already queued above)
19567            // followed by the dialect-specific error projection.
19568            let err_msg = format!("flow '{}' not deployed", payload.flow_name);
19569            let mut wire_adapter = crate::wire_format::select_adapter(&wire_dialect, 0);
19570            let synthetic_error = crate::flow_execution_event::FlowExecutionEvent::FlowError {
19571                flow_name: payload.flow_name.clone(),
19572                error: err_msg,
19573                timestamp_ms: 0,
19574            };
19575            for wire_event in wire_adapter.translate(&synthetic_error) {
19576                let _ = tx.try_send(Ok(wire_event));
19577            }
19578            for wire_event in wire_adapter.flush_terminator() {
19579                let _ = tx.try_send(Ok(wire_event));
19580            }
19581        }
19582    }
19583
19584    // 30.f: wire the configured KeepAlive into the Sse response. The
19585    // comment text "keepalive" emits as `: keepalive\n\n` per W3C SSE
19586    // §"comment line"; EventSource clients silently discard it, but
19587    // intermediate load balancers see it as wire activity and refrain
19588    // from tearing the TCP connection down.
19589    Ok(Sse::new(rx).keep_alive(
19590        axum::response::sse::KeepAlive::new()
19591            .interval(keepalive_duration)
19592            .text("keepalive"),
19593    ))
19594}
19595
19596// ──────────────────────────────────────────────────────────────────────────
19597// §Fase 30.e — Content-negotiation fallback (D4 + D5 ratified 2026-05-10)
19598//
19599// Adds an opt-in auto-promotion of `POST /v1/execute` from JSON to SSE
19600// when the client signals `Accept: text/event-stream` AND the deployed
19601// flow satisfies the language-level streaming contract. Strictly
19602// additive: every existing v1.20.0 client hitting /v1/execute without
19603// that Accept header gets the legacy JSON response verbatim (D9).
19604//
19605// Decision matrix (plan vivo §6.1):
19606//
19607//   axonendpoint.transport     │ flow has │ Accept:           │ Server
19608//   declaration on this flow   │ stream-eff│ text/event-stream │ response
19609//   ────────────────────────────┼─────────┼──────────────────┼────────
19610//   "sse" or "ndjson"          │ —       │ any              │ SSE  (D5)
19611//   "json" explicit            │ —       │ any              │ JSON (D5)
19612//   absent / not declared      │ no      │ any              │ JSON (D9)
19613//   absent / not declared      │ yes     │ absent or other  │ JSON
19614//   absent / not declared      │ yes     │ text/event-stream│ SSE  (D4)
19615//
19616// D5: explicit declaration on the axonendpoint always wins over the
19617// client's Accept header. Adopters who explicitly declare json/sse opt
19618// out of negotiation.
19619//
19620// D4: when no explicit declaration exists, the client's Accept header
19621// chooses — but ONLY when the flow can actually produce stream tokens
19622// (the language-level streaming contract from 30.c). Without the
19623// stream-effect proof, the server returns JSON regardless of Accept.
19624//
19625// # Predicate scope
19626//
19627// `flow_produces_stream_runtime` walks the Rust-side AST for two of
19628// the three formal disjuncts from 30.c:
19629//   (a) type-level — any StepNode with `output: Stream<T>` shape
19630//   (b) effect-level — any UseTool flow-step resolving to a tool with
19631//       `effects: <stream:<policy>>`
19632// Disjunct (c) (`perform Stream.Yield(...)`) is NOT detected here
19633// because the Rust frontend AST parses step bodies structurally; the
19634// `perform` expression survives only as part of GenericStep payload.
19635// Adopters relying ONLY on disjunct (c) fall through to JSON under
19636// D4; they have two clean opt-ins to SSE: hit /v1/execute/sse
19637// directly (30.d route), OR declare `transport: sse` on the
19638// axonendpoint (D5 force-promote here).
19639//
19640// Per-request source parse is acceptable for initial implementation;
19641// a future Fase can cache parsed declarations per deployment if
19642// throughput becomes a constraint.
19643// ──────────────────────────────────────────────────────────────────────────
19644
19645use axum::response::IntoResponse;
19646use crate::ast::{Declaration, FlowStep};
19647
19648/// Negotiation verdicts (plan vivo §6.1).
19649#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19650enum NegotiationDecision {
19651    /// Force SSE — declared `transport: sse|ndjson` (D5) OR
19652    /// content-negotiation fallback (D4) when Accept asked.
19653    PromoteToSse,
19654    /// Force JSON — explicit `transport: json` (D5), OR default
19655    /// path (no stream-effect proof OR no Accept header).
19656    StayJson,
19657}
19658
19659/// Walk a parsed program to extract the negotiation-relevant facts
19660/// for the named flow:
19661///   - Any axonendpoint declaration with `execute: <flow_name>` and
19662///     its declared `transport` value (empty string = absent).
19663///   - Whether the named flow itself produces a stream (Rust-side
19664///     subset of the 30.c predicate — disjuncts a + b only).
19665fn classify_negotiation_for_flow(
19666    program: &crate::ast::Program,
19667    flow_name: &str,
19668) -> NegotiationDecision {
19669    let mut endpoint_transport: Option<String> = None;
19670    let mut flow_has_stream_effect = false;
19671
19672    // First pass: collect endpoint transport for this flow + locate
19673    // the FlowDefinition for the predicate walk.
19674    let mut target_flow: Option<&crate::ast::FlowDefinition> = None;
19675    for decl in &program.declarations {
19676        match decl {
19677            Declaration::AxonEndpoint(ae) if ae.execute_flow == flow_name => {
19678                endpoint_transport = Some(ae.transport.clone());
19679            }
19680            Declaration::Flow(f) if f.name == flow_name => {
19681                target_flow = Some(f);
19682            }
19683            _ => {}
19684        }
19685    }
19686
19687    // Disjunct (a) + (b) walk: only relevant if we don't have an
19688    // explicit transport declaration that already decides the case.
19689    let needs_predicate_walk = match endpoint_transport.as_deref() {
19690        Some("sse") | Some("ndjson") => false, // D5 force SSE
19691        Some("json") => false,                  // D5 force JSON
19692        _ => true,                              // D4 negotiation territory
19693    };
19694
19695    if needs_predicate_walk {
19696        if let Some(flow) = target_flow {
19697            flow_has_stream_effect = flow_produces_stream_runtime(flow, program);
19698        }
19699    }
19700
19701    // Decision per plan vivo §6.1.
19702    match endpoint_transport.as_deref() {
19703        Some("sse") | Some("ndjson") => NegotiationDecision::PromoteToSse,
19704        Some("json") => NegotiationDecision::StayJson,
19705        _ => {
19706            if flow_has_stream_effect {
19707                // Caller decides via Accept header. We signal this
19708                // with PromoteToSse here AND the caller has already
19709                // verified the Accept header before calling us (the
19710                // wrapper handler short-circuits if Accept is absent).
19711                NegotiationDecision::PromoteToSse
19712            } else {
19713                NegotiationDecision::StayJson
19714            }
19715        }
19716    }
19717}
19718
19719/// Rust-side subset of the 30.c `produces_stream` predicate.
19720///
19721/// Covers disjuncts (a) type-level + (b) effect-level. Disjunct (c)
19722/// `perform Stream.Yield(...)` is NOT detected here (Rust AST step
19723/// bodies are parsed structurally; perform expressions don't surface
19724/// as discrete nodes). Adopters relying solely on disjunct (c) opt
19725/// into SSE via `transport: sse` declaration (D5) or the dedicated
19726/// `/v1/execute/sse` route (30.d) instead.
19727fn flow_produces_stream_runtime(
19728    flow: &crate::ast::FlowDefinition,
19729    program: &crate::ast::Program,
19730) -> bool {
19731    // Index tools by name for the disjunct (b) walk.
19732    let mut tools_by_name: std::collections::HashMap<&str, &crate::ast::ToolDefinition> =
19733        std::collections::HashMap::new();
19734    for decl in &program.declarations {
19735        if let Declaration::Tool(t) = decl {
19736            tools_by_name.insert(t.name.as_str(), t);
19737        }
19738    }
19739
19740    for step in &flow.body {
19741        // Disjunct (a): step.output_type matches the Stream<T> shape.
19742        if let FlowStep::Step(s) = step {
19743            let out = s.output_type.trim();
19744            if out.starts_with("Stream<") && out.ends_with('>') {
19745                return true;
19746            }
19747        }
19748        // Disjunct (b): UseTool flow-step → tool effects carry "stream:".
19749        if let FlowStep::UseTool(u) = step {
19750            if let Some(tool) = tools_by_name.get(u.tool_name.as_str()) {
19751                if let Some(ref effects) = tool.effects {
19752                    if effects.effects.iter().any(|e| e.starts_with("stream:")) {
19753                        return true;
19754                    }
19755                }
19756            }
19757        }
19758    }
19759    false
19760}
19761
19762/// Defensive source-text classifier — used ONLY when the Rust parser
19763/// fails on the source (e.g. step-body `output: Stream<T>` shape that
19764/// the Rust frontend doesn't yet accept). Brittle by design; the
19765/// long-term solution is to bring the Rust parser to v1.19.3 shape
19766/// (its own sub-fase). Until then, this bridge keeps 30.e robust
19767/// across the cross-stack parser drift.
19768///
19769/// Looks for the canonical streaming evidence on the wire:
19770///   (a) `output: Stream<` anywhere in the source
19771///   (b) `effects: <stream:` or `effects: stream:` anywhere
19772///   (c) `perform Stream.Yield(` anywhere
19773///
19774/// Returns PromoteToSse if any pattern is present OR if an
19775/// axonendpoint forces SSE; StayJson otherwise. The `flow_name`
19776/// arg is reserved for a future tighter match (currently we accept
19777/// any streaming evidence in the source; multi-flow files would
19778/// benefit from per-flow scoping but that requires partial parse).
19779fn classify_negotiation_via_source_text(
19780    source: &str,
19781    flow_name: &str,
19782) -> NegotiationDecision {
19783    // D5: explicit transport: json declaration on this flow → respect.
19784    if source_text_axonendpoint_has_transport(source, flow_name, "json") {
19785        return NegotiationDecision::StayJson;
19786    }
19787    // D5: explicit transport: sse|ndjson → force promote.
19788    if source_text_has_force_decl(source, flow_name) {
19789        return NegotiationDecision::PromoteToSse;
19790    }
19791    // D4 territory: predicate via text patterns.
19792    let has_stream_output = source.contains("output: Stream<")
19793        || source.contains("output:Stream<");
19794    let has_stream_effect = source.contains("stream:drop_oldest")
19795        || source.contains("stream:degrade_quality")
19796        || source.contains("stream:pause_upstream")
19797        || source.contains("stream:fail");
19798    let has_stream_yield = source.contains("Stream.Yield(")
19799        || source.contains("Stream.Yield (");
19800    if has_stream_output || has_stream_effect || has_stream_yield {
19801        NegotiationDecision::PromoteToSse
19802    } else {
19803        NegotiationDecision::StayJson
19804    }
19805}
19806
19807/// Does the source text declare an axonendpoint for the given flow
19808/// with `transport: sse|ndjson`? Used by both the parse-path and the
19809/// source-text-fallback path so the D5 force-decl check has a single
19810/// canonical implementation across both paths.
19811fn source_text_has_force_decl(source: &str, flow_name: &str) -> bool {
19812    source_text_axonendpoint_has_transport(source, flow_name, "sse")
19813        || source_text_axonendpoint_has_transport(source, flow_name, "ndjson")
19814}
19815
19816/// Does the source contain an `axonendpoint <name> { ... execute:
19817/// <flow_name> ... transport: <transport_value> ... }` block?
19818///
19819/// Walks `axonendpoint` keyword occurrences, finds the matching
19820/// closing brace via depth tracking (string-aware to avoid false
19821/// positives inside `path: "..."`), and checks whether the body
19822/// contains both `execute: <flow_name>` and `transport: <value>`.
19823fn source_text_axonendpoint_has_transport(
19824    source: &str,
19825    flow_name: &str,
19826    transport: &str,
19827) -> bool {
19828    let bytes = source.as_bytes();
19829    let kw = b"axonendpoint";
19830    let mut i = 0;
19831    while i + kw.len() <= bytes.len() {
19832        if &bytes[i..i + kw.len()] == kw {
19833            // Find the opening brace.
19834            let body_start = source[i..]
19835                .find('{')
19836                .map(|off| i + off + 1);
19837            if let Some(start) = body_start {
19838                // Find matching close — string-aware brace counter.
19839                let mut depth: i32 = 1;
19840                let mut j = start;
19841                let mut in_string = false;
19842                while j < bytes.len() && depth > 0 {
19843                    let c = bytes[j];
19844                    match c {
19845                        b'"' if !in_string => in_string = true,
19846                        b'"' if in_string => in_string = false,
19847                        b'\\' if in_string => {
19848                            j += 1;
19849                        } // skip escape sequence
19850                        b'{' if !in_string => depth += 1,
19851                        b'}' if !in_string => depth -= 1,
19852                        _ => {}
19853                    }
19854                    j += 1;
19855                }
19856                let body = &source[start..j.saturating_sub(1)];
19857                // Field-shape match: `execute: <flow_name>` (allow
19858                // whitespace + optional comma/newline after the
19859                // identifier).
19860                let has_execute = body.contains(&format!("execute: {flow_name}"))
19861                    || body.contains(&format!("execute:{flow_name}"));
19862                let has_transport = body.contains(&format!("transport: {transport}"))
19863                    || body.contains(&format!("transport:{transport}"));
19864                if has_execute && has_transport {
19865                    return true;
19866                }
19867                i = j;
19868                continue;
19869            }
19870        }
19871        i += 1;
19872    }
19873    false
19874}
19875
19876// ═══════════════════════════════════════════════════════════════════
19877//  §FASE 32.b — DYNAMIC AXONENDPOINT ROUTES (D1, D2, D3, D11)
19878// ═══════════════════════════════════════════════════════════════════
19879//
19880// Every `axonendpoint` declaration in a deployed program produces
19881// exactly one HTTP route at the declared (method, path). The path is
19882// no longer decorative metadata — it IS the canonical URL the adopter
19883// exposes (D1).
19884//
19885// Path conflict resolution is deterministic (D2): two axonendpoints
19886// declaring the same (method, path) tuple — within one program OR
19887// across two deploys of different flows — fail the deploy with a
19888// structured 409 response naming both endpoints. No silent "last
19889// wins" override.
19890//
19891// Method enum closed (D3): `axonendpoint.method ∈ {GET, POST, PUT,
19892// DELETE, PATCH}`. Other methods (HEAD, OPTIONS, CONNECT, TRACE) are
19893// runtime-managed (CORS preflight, etc.) and not adopter-declarable.
19894// Closed enum refuses interpretation drift.
19895//
19896// Cross-stack consistency (D11): the Python `AxonServer.create_app()`
19897// mirror registers routes via FastAPI's `app.add_api_route()`; both
19898// stacks produce byte-identical route sets from the same source.
19899// Drift gate over a shared corpus locks parity in CI.
19900//
19901// Pillar trace per D12:
19902//   MATHEMATICS — `(method, path) → DynamicEndpointRoute` is a pure
19903//                  function of the deployed program.
19904//   LOGIC      — path conflicts detected at deploy time; orphan
19905//                 paths impossible.
19906//   PHILOSOPHY — declarative source IS the HTTP behavior; auditors
19907//                 inspect source + know the REST surface verbatim.
19908//   COMPUTING  — strictly additive when adopter declares paths;
19909//                 /v1/execute preserved verbatim per D10.
19910
19911/// §Fase 32.e (D6) — Wire-format verdict for a dynamic route.
19912///
19913/// Total enum returned by `classify_dynamic_route_wire`. The dynamic
19914/// fallback handler maps each variant to the corresponding downstream
19915/// dispatch: `Sse` → `execute_sse_handler` (text/event-stream wire),
19916/// `Json` → `execute_handler` (application/json wire) with the
19917/// Fase 31.e `X-Axon-Stream-Available` header attached when the
19918/// underlying flow has stream effects.
19919#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19920pub enum DynamicRouteWire {
19921    Sse,
19922    Json,
19923}
19924
19925/// §Fase 32.e (D6) — Per-route negotiation classifier.
19926/// §Fase 33.z.k.1 (v1.27.1) — Extended with algebraic-effect override.
19927///
19928/// Total function over `(transport, transport_explicit,
19929/// implicit_transport, has_algebraic_stream_effect, client_wants_sse,
19930/// strict_mode)`. The decision is single-valued (`DynamicRouteWire`)
19931/// for every input combination.
19932///
19933/// The function is **pure** — same inputs always produce the same
19934/// output. The drift gate at the test layer locks the truth table
19935/// against accidental rewrites.
19936///
19937/// Truth table (post-33.z.k.1):
19938///
19939/// | explicit | transport | algebraic | implicit | strict | accept_sse | wire |
19940/// |----------|-----------|-----------|----------|--------|------------|------|
19941/// | true     | sse       | *         | *        | *      | *          | SSE  |
19942/// | true     | ndjson    | *         | *        | *      | *          | SSE  |
19943/// | true     | json      | *         | *        | *      | *          | JSON | ← D3 sacred opt-out
19944/// | false    | (n/a)     | **true**  | *        | *      | *          | SSE  | ← D11 algebraic-effect override (v1.27.1)
19945/// | false    | (n/a)     | false     | sse      | true   | *          | SSE  | ← D1 inference fires
19946/// | false    | (n/a)     | false     | sse      | false  | true       | SSE  | ← D4 Accept-fallback
19947/// | false    | (n/a)     | false     | sse      | false  | false      | JSON | ← D9 backwards-compat
19948/// | false    | (n/a)     | false     | json     | *      | *          | JSON |
19949/// | false    | (n/a)     | false     | ""       | *      | *          | JSON | ← pre-31.b inference
19950///
19951/// (Last row catches AST that was consumed without running the
19952/// `compute_implicit_transports` pass — defensive default to JSON.)
19953///
19954/// # §Fase 33.z.k.1 — Algebraic-effect override (v1.27.1)
19955///
19956/// When the `execute_flow` references a tool that declares
19957/// `effects: <stream:<policy>>` (i.e.,
19958/// `has_algebraic_stream_effect == true`), the route promotes to
19959/// `Sse` UNCONDITIONALLY — no `Accept: text/event-stream` header
19960/// required, no `AXON_STRICT_TYPE_DRIVEN_TRANSPORT=1` runtime flag
19961/// required. The justification: an algebraic effect declaration on
19962/// a tool is a LANGUAGE-LEVEL commitment (part of the type system),
19963/// not a client preference. The wire MUST honor it.
19964///
19965/// D3 `transport: json` explicit opt-out STILL WINS above the
19966/// override — adopters can deliberately suppress SSE by declaring
19967/// `transport: json` on the endpoint even when the tool has a
19968/// stream effect. This is the only escape valve.
19969///
19970/// The override is strictly additive over the pre-33.z.k.1 truth
19971/// table: every cell where the algebraic predicate is `false`
19972/// produces the same wire as before. Adopters who depended on
19973/// `output: Stream<T>` (type-annotation-only stream) for D6
19974/// backwards-compat still see JSON in legacy mode without `Accept:`.
19975pub fn classify_dynamic_route_wire(
19976    transport: &str,
19977    transport_explicit: bool,
19978    implicit_transport: &str,
19979    has_algebraic_stream_effect: bool,
19980    client_wants_sse: bool,
19981    strict_mode: bool,
19982) -> DynamicRouteWire {
19983    if transport_explicit {
19984        // Adopter declared explicitly — declaration wins regardless of
19985        // strict mode or Accept header. D3 + D5 sacred. Algebraic-effect
19986        // predicate does NOT override an explicit `transport: json`.
19987        return match transport {
19988            "sse" | "ndjson" => DynamicRouteWire::Sse,
19989            _ => DynamicRouteWire::Json,
19990        };
19991    }
19992    // §Fase 33.z.k.1 — Algebraic-effect override fires BEFORE the
19993    // D9 backwards-compat gate. A declared `effects: <stream:<policy>>`
19994    // on a tool is part of the language's type system; the wire MUST
19995    // honor it without client cooperation.
19996    if has_algebraic_stream_effect {
19997        return DynamicRouteWire::Sse;
19998    }
19999    // Implicit path — consult Fase 31.b's pre-computed `implicit_transport`.
20000    if implicit_transport == "sse" {
20001        // Flow has stream effects.
20002        if strict_mode || client_wants_sse {
20003            return DynamicRouteWire::Sse;
20004        }
20005    }
20006    DynamicRouteWire::Json
20007}
20008
20009#[cfg(test)]
20010mod dynamic_route_wire_truth_table {
20011    use super::{classify_dynamic_route_wire, DynamicRouteWire};
20012
20013    fn s() -> DynamicRouteWire { DynamicRouteWire::Sse }
20014    fn j() -> DynamicRouteWire { DynamicRouteWire::Json }
20015
20016    // §Fase 33.z.k.1 (v1.27.1) — Every assertion below pins the
20017    // 6-input signature. The algebraic-effect predicate defaults to
20018    // `false` for the pre-33.z.k.1 truth-table cells (the algebraic
20019    // override is exercised in the dedicated test block at the end).
20020
20021    #[test]
20022    fn explicit_sse_always_promotes() {
20023        for strict in [false, true] {
20024            for accept in [false, true] {
20025                for algebraic in [false, true] {
20026                    assert_eq!(classify_dynamic_route_wire("sse", true, "", algebraic, accept, strict), s());
20027                    assert_eq!(classify_dynamic_route_wire("sse", true, "sse", algebraic, accept, strict), s());
20028                    assert_eq!(classify_dynamic_route_wire("sse", true, "json", algebraic, accept, strict), s());
20029                }
20030            }
20031        }
20032    }
20033
20034    #[test]
20035    fn explicit_ndjson_promotes_to_sse_wire() {
20036        // ndjson currently maps onto the SSE handler per Fase 30 D2
20037        // (the wire format is still text/event-stream framing).
20038        assert_eq!(
20039            classify_dynamic_route_wire("ndjson", true, "", false, false, false),
20040            s()
20041        );
20042    }
20043
20044    #[test]
20045    fn explicit_json_is_sacred_opt_out_d3() {
20046        for strict in [false, true] {
20047            for accept in [false, true] {
20048                for algebraic in [false, true] {
20049                    // §Fase 33.z.k.1 — Even when the tool DECLARES a stream
20050                    // effect (`algebraic = true`), an explicit
20051                    // `transport: json` on the endpoint STILL wins. D3 is
20052                    // the only escape valve from the algebraic override.
20053                    assert_eq!(
20054                        classify_dynamic_route_wire("json", true, "sse", algebraic, accept, strict),
20055                        j(),
20056                        "D3 opt-out must hold for (accept={accept}, strict={strict}, algebraic={algebraic})"
20057                    );
20058                }
20059            }
20060        }
20061    }
20062
20063    #[test]
20064    fn implicit_sse_strict_mode_promotes_d1() {
20065        assert_eq!(classify_dynamic_route_wire("", false, "sse", false, false, true), s());
20066        assert_eq!(classify_dynamic_route_wire("", false, "sse", false, true, true), s());
20067    }
20068
20069    #[test]
20070    fn implicit_sse_legacy_with_accept_promotes_d4() {
20071        assert_eq!(classify_dynamic_route_wire("", false, "sse", false, true, false), s());
20072    }
20073
20074    #[test]
20075    fn implicit_sse_legacy_no_accept_stays_json_d9() {
20076        assert_eq!(classify_dynamic_route_wire("", false, "sse", false, false, false), j());
20077    }
20078
20079    #[test]
20080    fn implicit_json_always_stays_json() {
20081        for strict in [false, true] {
20082            for accept in [false, true] {
20083                assert_eq!(
20084                    classify_dynamic_route_wire("", false, "json", false, accept, strict),
20085                    j()
20086                );
20087            }
20088        }
20089    }
20090
20091    #[test]
20092    fn empty_implicit_defaults_to_json() {
20093        // Catches AST consumed without running compute_implicit_transports.
20094        // Algebraic predicate also defaults to false in that scenario, so
20095        // the route falls through to the JSON default cell.
20096        assert_eq!(classify_dynamic_route_wire("", false, "", false, true, true), j());
20097    }
20098
20099    // §Fase 33.z.k.1 — Algebraic-effect override truth-table.
20100    //
20101    // The override fires whenever the tool declares
20102    // `effects: <stream:<policy>>` AND the endpoint does NOT
20103    // explicitly opt out via `transport: json`. The wire is
20104    // unconditionally `Sse` across every combination of `strict_mode`
20105    // + `client_wants_sse` + `implicit_transport`.
20106
20107    #[test]
20108    fn algebraic_override_fires_without_strict_or_accept_d11() {
20109        // The canonical Kivi-shape adopter case: `step S { apply: T }`
20110        // where tool T declares `effects: <stream:drop_oldest>`. Client
20111        // sends POST without `Accept: text/event-stream`; server runs
20112        // without `AXON_STRICT_TYPE_DRIVEN_TRANSPORT=1`. Pre-33.z.k.1
20113        // this returned `Json` (D9 backwards-compat). Post-33.z.k.1
20114        // the algebraic-effect override promotes to `Sse`.
20115        assert_eq!(
20116            classify_dynamic_route_wire("", false, "sse", true, false, false),
20117            s(),
20118            "33.z.k.1 D11 algebraic-effect override: tool's declared \
20119             effects: <stream:...> MUST drive the wire to SSE \
20120             unconditionally (D6 backwards-compat is structurally \
20121             unobservable for tool-streaming flows)"
20122        );
20123    }
20124
20125    #[test]
20126    fn algebraic_override_unaffected_by_strict_or_accept() {
20127        // The override result is invariant under D6 strict_mode + D4
20128        // accept_sse — they only matter when the algebraic predicate
20129        // is false. With algebraic=true the wire is always Sse.
20130        for strict in [false, true] {
20131            for accept in [false, true] {
20132                for implicit in ["", "sse", "json"] {
20133                    assert_eq!(
20134                        classify_dynamic_route_wire("", false, implicit, true, accept, strict),
20135                        s(),
20136                        "33.z.k.1: algebraic=true MUST promote to Sse \
20137                         for (implicit={implicit:?}, accept={accept}, strict={strict})"
20138                    );
20139                }
20140            }
20141        }
20142    }
20143
20144    #[test]
20145    fn algebraic_override_does_not_fire_when_transport_json_explicit() {
20146        // D3 sacred above all — even with algebraic-effect signal, an
20147        // explicit `transport: json` declaration wins.
20148        for strict in [false, true] {
20149            for accept in [false, true] {
20150                assert_eq!(
20151                    classify_dynamic_route_wire("json", true, "sse", true, accept, strict),
20152                    j(),
20153                    "D3 dominates D11 algebraic override; explicit json \
20154                     opt-out is the only escape valve"
20155                );
20156            }
20157        }
20158    }
20159
20160    #[test]
20161    fn algebraic_override_fires_even_when_implicit_transport_empty() {
20162        // Defensive: if compute_implicit_transports never ran but the
20163        // algebraic predicate was set (e.g., manual route construction
20164        // in adopter test harness), the override still fires. This
20165        // matches the "algebraic effect is part of the type" principle —
20166        // the language commitment doesn't depend on a separate
20167        // transport-inference pass.
20168        assert_eq!(
20169            classify_dynamic_route_wire("", false, "", true, false, false),
20170            s()
20171        );
20172    }
20173}
20174
20175/// Closed method enum per D3. Adopter-declarable methods only;
20176/// HEAD/OPTIONS/CONNECT/TRACE are runtime-managed (CORS preflight,
20177/// etc.) and never registered from source.
20178pub const AXONENDPOINT_METHODS: &[&str] = &["GET", "POST", "PUT", "DELETE", "PATCH"];
20179
20180/// Metadata stored per registered dynamic route. Populated at deploy
20181/// time from `AxonEndpointDefinition`; consulted at request time by
20182/// the fallback handler to dispatch to the correct flow with the
20183/// correct transport semantics.
20184#[derive(Debug, Clone)]
20185pub struct DynamicEndpointRoute {
20186    /// The flow name the axonendpoint's `execute:` field declared.
20187    pub flow_name: String,
20188    /// The axonendpoint name (for diagnostic + audit).
20189    pub endpoint_name: String,
20190    /// Source file the axonendpoint was deployed from. Used by the
20191    /// negotiation classifier (Fase 30.e + Fase 31.d) for source-text
20192    /// dual-signal predicate.
20193    pub source_file: String,
20194    /// Full source of the deployed program. Required for runtime
20195    /// transport inference (Fase 31.b `produces_stream` predicate).
20196    pub source: String,
20197    /// `transport:` field verbatim — one of `{json, sse, ndjson}` per
20198    /// Fase 30 D2; empty string when omitted (Fase 31.b
20199    /// `transport_explicit == false` path).
20200    pub transport: String,
20201    /// Was `transport:` explicitly declared in source (Fase 31.b)?
20202    pub transport_explicit: bool,
20203    /// `keepalive:` field per Fase 30 D6; empty when omitted.
20204    pub keepalive: String,
20205    /// Inferred transport per Fase 31.b D1 (`"sse"` / `"json"` /
20206    /// empty if pre-compute).
20207    pub implicit_transport: String,
20208    /// §Fase 32.c — Declared body type per `body:` field on the source
20209    /// axonendpoint. Empty string when omitted (D9 backwards-compat —
20210    /// the fallback handler skips body-schema validation entirely).
20211    /// When non-empty, the value is looked up in
20212    /// `ServerState.dynamic_types` at request time and the request body
20213    /// is validated against the resolved `TypeSchema` before the flow
20214    /// dispatch. Schema mismatch returns 400 Bad Request with a
20215    /// structured `BodyValidationError`.
20216    pub body_type: String,
20217    /// §Fase 32.d — Declared output type per `output:` field on the
20218    /// source axonendpoint. Empty string when omitted (D9 backwards-
20219    /// compat — the fallback handler skips output-schema validation).
20220    /// When non-empty, the flow's response body is validated against
20221    /// the resolved `TypeSchema` BEFORE returning to the client.
20222    /// Per OWASP, validation failure returns a GENERIC 500 to the
20223    /// client + records the full diagnostic in `audit_log` so the
20224    /// adopter can fix the FLOW without schema details leaking to
20225    /// potentially malicious clients.
20226    ///
20227    /// Output validation only fires when the response Content-Type is
20228    /// `application/json` — SSE/ndjson streams are token-by-token and
20229    /// cannot be validated against a static type at the wire layer
20230    /// (a future fase may add per-event validation for typed streams).
20231    pub output_type: String,
20232    /// §Fase 32.g — Declared capability slugs the request bearer must
20233    /// hold for the endpoint to dispatch. Empty vec means "no auth
20234    /// gate" (D9 backwards-compat). The runtime checks
20235    /// `declared_requires ⊆ token_capabilities` (AND semantics — every
20236    /// declared capability must be present). On missing capability
20237    /// the fallback handler returns 403 Forbidden with structured
20238    /// `{error: "missing_capability", required, have, ...}` so the
20239    /// client knows precisely which capability is needed.
20240    pub requires_capabilities: Vec<String>,
20241    /// §Fase 32.h — Effective replay-binding boolean resolved at
20242    /// deploy time. `true` ⟹ every successful 2xx POST/PUT response
20243    /// is recorded in `ServerState.axonendpoint_replay` keyed by
20244    /// trace_id. Default is method-derived (POST/PUT → true;
20245    /// GET/DELETE → false); an explicit `replay: true | false`
20246    /// declaration overrides.
20247    pub replay_enabled: bool,
20248    /// §Fase 33.z.k.b (v1.28.0) — Selected SSE wire-format dialect.
20249    ///
20250    /// Populated when the source declares `transport: sse(<dialect>)`.
20251    /// Closed catalog from `AXONENDPOINT_TRANSPORT_DIALECTS`:
20252    /// `{axon, openai, anthropic}`. Empty string when the source
20253    /// declared a non-SSE transport, bare `transport: sse` without
20254    /// parens, or omitted `transport:` entirely.
20255    ///
20256    /// Copied from `AxonEndpointDefinition.transport_dialect` at
20257    /// route construction. The runtime resolves the effective
20258    /// dialect via `type_checker::resolve_effective_dialect()` —
20259    /// when this field is empty, the algebraic-effect predicate
20260    /// drives the Q1 default (openai for tool-streaming flows,
20261    /// axon for type-annotation-only).
20262    pub transport_dialect: String,
20263    /// §Fase 33.z.k.1 (v1.27.1) — Algebraic-effect override predicate.
20264    ///
20265    /// `true` when `execute_flow` references a tool that declares
20266    /// `effects: <stream:<policy>>`. Copied verbatim from
20267    /// `AxonEndpointDefinition.has_algebraic_stream_effect` (computed
20268    /// by `axon_frontend::type_checker::compute_implicit_transports`).
20269    ///
20270    /// Used by [`classify_dynamic_route_wire`] to OVERRIDE the v1.22.0
20271    /// D6 backwards-compat gate. A tool with a declared stream effect
20272    /// is a LANGUAGE-LEVEL commitment to streaming (algebraic effect
20273    /// is part of the type), not a client preference. The runtime
20274    /// honors the commitment without requiring an `Accept:
20275    /// text/event-stream` header (D4 negotiation) or the strict-mode
20276    /// runtime flag (D6). D3 explicit `transport: json` opt-out still
20277    /// wins above this override.
20278    pub has_algebraic_stream_effect: bool,
20279    /// §Fase 36.e (D3) — the route's declared execution backend.
20280    ///
20281    /// Resolved at deploy time: the `axonendpoint backend:` field
20282    /// (Fase 36.d, `AxonEndpointDefinition.backend`) if the source
20283    /// declared one; otherwise the deploy-request backend
20284    /// (`DeployRequest.backend`) when that was set to an explicit,
20285    /// concrete value (`deploy_handler` fills it as a deploy-scoped
20286    /// default for every route that did not declare its own).
20287    ///
20288    /// Empty string `""` ≡ "not declared" — the route resolves its
20289    /// backend at request time down the Fase 36 D1 precedence ladder
20290    /// (server default → environment-available `auto`; honest failure
20291    /// if nothing real resolves). When non-empty this value is rung 2
20292    /// of that ladder (`EndpointDeclared`).
20293    ///
20294    /// (D3 frames this as an "optional backend"; the struct realizes
20295    /// "optional" via its uniform empty-string-sentinel convention —
20296    /// shared with every sibling field — rather than introducing a
20297    /// lone `Option<String>`.)
20298    pub backend: String,
20299    /// §Fase 37.y (D1) — Path-parameter names extracted from the
20300    /// route's path string (mirrors `AxonEndpointDefinition.path_params`).
20301    /// Empty Vec when the path has no `{name}` placeholders. Used by
20302    /// `match_path_template` to (a) gate template matching only on
20303    /// routes that DECLARED placeholders and (b) skip the linear scan
20304    /// for legacy (placeholder-less) routes that go through the fast
20305    /// exact-string lookup path (D5 backwards-compat — pre-37.y route
20306    /// performance preserved).
20307    pub path_params: Vec<String>,
20308}
20309
20310/// §Fase 37.y (D1) — Match a registered axonendpoint path template
20311/// against an incoming request URL path; on match, return the captured
20312/// path-parameter values.
20313///
20314/// The template uses `{name}` placeholders (the same shape the parser
20315/// extracts into `AxonEndpointDefinition.path_params`); the actual URL
20316/// is the request's `uri.path()`. A segment that is NOT a placeholder
20317/// must match byte-identical between template and actual. A segment
20318/// that IS a placeholder matches any non-empty actual segment and
20319/// captures the actual value into the returned map.
20320///
20321/// Returns `Some(captures)` when:
20322///   - segment count matches (no `*` wildcards in v1.38.5; multi-
20323///     segment captures are honest-deferred per plan vivo §7);
20324///   - every non-placeholder segment matches byte-for-byte;
20325///   - every placeholder segment captures a non-empty actual segment.
20326///
20327/// Returns `None` otherwise.
20328///
20329/// Pure + total (no panics, no allocs beyond the captures map).
20330/// Empty template + empty actual returns `Some(empty)`; templates
20331/// without placeholders reduce to byte-equality check, so the helper
20332/// is also a drop-in replacement for the legacy exact-string lookup
20333/// when `path_params` is empty.
20334pub(crate) fn match_path_template(
20335    template: &str,
20336    actual: &str,
20337) -> Option<HashMap<String, String>> {
20338    // Split on '/' — both URLs use canonical path separators. We
20339    // tolerate trailing slashes by treating both `/api/x` and `/api/x/`
20340    // as equivalent: each splits into 3 segments with an empty last
20341    // segment.
20342    let tpl_parts: Vec<&str> = template.split('/').collect();
20343    let act_parts: Vec<&str> = actual.split('/').collect();
20344    if tpl_parts.len() != act_parts.len() {
20345        return None;
20346    }
20347    let mut captures: HashMap<String, String> = HashMap::new();
20348    for (tpl, act) in tpl_parts.iter().zip(act_parts.iter()) {
20349        if tpl.starts_with('{') && tpl.ends_with('}') && tpl.len() > 2 {
20350            let name = &tpl[1..tpl.len() - 1];
20351            // Validate the placeholder's identifier shape — the parser
20352            // already enforced it, but defense-in-depth catches a
20353            // malformed registered route (e.g. from a hand-edited
20354            // `dynamic_routes` map).
20355            let valid = !name.is_empty()
20356                && name.bytes().enumerate().all(|(idx, b)| {
20357                    if idx == 0 {
20358                        b.is_ascii_alphabetic() || b == b'_'
20359                    } else {
20360                        b.is_ascii_alphanumeric() || b == b'_'
20361                    }
20362                });
20363            if !valid {
20364                // Template malformed — treat as exact-string mismatch.
20365                if tpl != act {
20366                    return None;
20367                }
20368            } else {
20369                // Empty actual segment fails — `/api/{id}` must not
20370                // match `/api/` (the URL would have an empty `id`).
20371                if act.is_empty() {
20372                    return None;
20373                }
20374                captures.insert(name.to_string(), act.to_string());
20375            }
20376        } else if tpl != act {
20377            return None;
20378        }
20379    }
20380    Some(captures)
20381}
20382
20383/// §Fase 37.y (D2) — Parse a URL query string into a single-value
20384/// name → value map. Multi-value query keys (`?tag=a&tag=b`) collapse
20385/// to the FIRST value per v1.38.5 honest-scope semantics (multi-value
20386/// query binding deferred per plan vivo §7).
20387///
20388/// URL-decoding follows the standard `application/x-www-form-urlencoded`
20389/// rules: `+` decodes to space, `%XX` decodes to the byte. Malformed
20390/// encodings keep the raw bytes (never panics).
20391///
20392/// Returns an empty map when `query` is `None` or empty.
20393pub(crate) fn parse_query_string(query: Option<&str>) -> HashMap<String, String> {
20394    let mut out: HashMap<String, String> = HashMap::new();
20395    let Some(q) = query else {
20396        return out;
20397    };
20398    if q.is_empty() {
20399        return out;
20400    }
20401    for pair in q.split('&') {
20402        if pair.is_empty() {
20403            continue;
20404        }
20405        let (raw_name, raw_value) = match pair.find('=') {
20406            Some(idx) => (&pair[..idx], &pair[idx + 1..]),
20407            None => (pair, ""),
20408        };
20409        let name = url_decode(raw_name);
20410        if name.is_empty() {
20411            continue;
20412        }
20413        // First-value semantics: don't overwrite if a name already
20414        // appeared earlier in the query string.
20415        if !out.contains_key(&name) {
20416            out.insert(name, url_decode(raw_value));
20417        }
20418    }
20419    out
20420}
20421
20422/// Minimal URL-decode for query-string values. `%XX` → byte;
20423/// `+` → space; everything else verbatim. Never panics on malformed
20424/// input — invalid `%XX` sequences pass through unchanged.
20425fn url_decode(s: &str) -> String {
20426    let bytes = s.as_bytes();
20427    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
20428    let mut i = 0;
20429    while i < bytes.len() {
20430        match bytes[i] {
20431            b'+' => {
20432                out.push(b' ');
20433                i += 1;
20434            }
20435            b'%' if i + 2 < bytes.len() => {
20436                let hi = bytes[i + 1];
20437                let lo = bytes[i + 2];
20438                let from_hex = |b: u8| -> Option<u8> {
20439                    match b {
20440                        b'0'..=b'9' => Some(b - b'0'),
20441                        b'a'..=b'f' => Some(b - b'a' + 10),
20442                        b'A'..=b'F' => Some(b - b'A' + 10),
20443                        _ => None,
20444                    }
20445                };
20446                match (from_hex(hi), from_hex(lo)) {
20447                    (Some(h), Some(l)) => {
20448                        out.push((h << 4) | l);
20449                        i += 3;
20450                    }
20451                    _ => {
20452                        out.push(bytes[i]);
20453                        i += 1;
20454                    }
20455                }
20456            }
20457            other => {
20458                out.push(other);
20459                i += 1;
20460            }
20461        }
20462    }
20463    // Lossy decode from bytes — malformed UTF-8 from a hostile client
20464    // gets replacement chars rather than panic.
20465    String::from_utf8_lossy(&out).into_owned()
20466}
20467
20468/// Walk the program's AxonEndpoint declarations and produce the
20469/// route table that the deploy_handler will merge into
20470/// `ServerState.dynamic_routes`. Detects intra-program path
20471/// collisions (D2 within a single deploy) and returns a structured
20472/// `Err` naming both colliding endpoints.
20473///
20474/// Method enum validation (D3) is done at PARSER level (axon-frontend)
20475/// + Python parser; this function trusts that all methods reaching it
20476/// are in `AXONENDPOINT_METHODS`. Defensive: if an unknown method
20477/// slipped through, we skip with a warning logged to stderr (never
20478/// panic; never silently accept).
20479pub fn collect_axonendpoint_routes(
20480    program: &crate::ast::Program,
20481    source: &str,
20482    source_file: &str,
20483) -> Result<HashMap<(String, String), DynamicEndpointRoute>, String> {
20484    let mut routes: HashMap<(String, String), DynamicEndpointRoute> = HashMap::new();
20485    for decl in &program.declarations {
20486        if let crate::ast::Declaration::AxonEndpoint(ae) = decl {
20487            let method = ae.method.trim().to_ascii_uppercase();
20488            if !AXONENDPOINT_METHODS.contains(&method.as_str()) {
20489                eprintln!(
20490                    "axon-rs: axonendpoint '{}' declared invalid method '{}' \
20491                     (closed enum D3: {AXONENDPOINT_METHODS:?}); skipping route \
20492                     registration. Parser should have rejected this — defensive \
20493                     skip applied at runtime.",
20494                    ae.name, method
20495                );
20496                continue;
20497            }
20498            let path = ae.path.trim().to_string();
20499            if path.is_empty() || !path.starts_with('/') {
20500                eprintln!(
20501                    "axon-rs: axonendpoint '{}' declared invalid path '{}' \
20502                     (must be non-empty and start with '/'); skipping route \
20503                     registration.",
20504                    ae.name, path
20505                );
20506                continue;
20507            }
20508            let key = (method.clone(), path.clone());
20509            if let Some(existing) = routes.get(&key) {
20510                return Err(format!(
20511                    "Path collision (D2): axonendpoint '{}' and '{}' both \
20512                     declare `method: {method} path: {path}`. Resolve by \
20513                     editing one of the two axonendpoints to use a distinct \
20514                     (method, path) tuple.",
20515                    existing.endpoint_name, ae.name
20516                ));
20517            }
20518            routes.insert(
20519                key,
20520                DynamicEndpointRoute {
20521                    flow_name: ae.execute_flow.clone(),
20522                    endpoint_name: ae.name.clone(),
20523                    source_file: source_file.to_string(),
20524                    source: source.to_string(),
20525                    transport: ae.transport.clone(),
20526                    transport_explicit: ae.transport_explicit,
20527                    keepalive: ae.keepalive.clone(),
20528                    implicit_transport: ae.implicit_transport.clone(),
20529                    body_type: ae.body_type.clone(),
20530                    output_type: ae.output_type.clone(),
20531                    requires_capabilities: ae.requires_capabilities.clone(),
20532                    replay_enabled: crate::axonendpoint_replay::resolve_replay_enabled(
20533                        &method, ae.replay_explicit, ae.replay,
20534                    ),
20535                    // §Fase 33.z.k.b (v1.28.0) — wire-format dialect
20536                    // copied verbatim from the AST. Parser already
20537                    // validated it against the closed catalog.
20538                    transport_dialect: ae.transport_dialect.clone(),
20539                    // §Fase 33.z.k.1 (v1.27.1) — algebraic-effect override
20540                    // copied verbatim from the AST. The
20541                    // compute_implicit_transports pass on the frontend
20542                    // already cross-referenced the flow body against the
20543                    // program's tool declarations.
20544                    has_algebraic_stream_effect: ae.has_algebraic_stream_effect,
20545                    // §Fase 36.e (D3) — the `axonendpoint backend:`
20546                    // declaration (Fase 36.d), copied verbatim from the
20547                    // AST. The parser already validated it against the
20548                    // closed catalog. Empty when the source omitted
20549                    // `backend:`; `deploy_handler` may then fill it from
20550                    // an explicit `DeployRequest.backend` as a
20551                    // deploy-scoped default.
20552                    backend: ae.backend.clone(),
20553                    // §Fase 37.y (D1) — Path-parameter names extracted
20554                    // by the parser. Copied verbatim from the AST.
20555                    path_params: ae.path_params.clone(),
20556                },
20557            );
20558        }
20559    }
20560    Ok(routes)
20561}
20562
20563/// §Fase 36.e (D3) — apply the deploy-scoped backend default to a
20564/// freshly-collected route table.
20565///
20566/// A route that did not declare its own `axonendpoint backend:`
20567/// (Fase 36.d) inherits `deploy_backend` — the `DeployRequest.backend`
20568/// field — as a deploy-scoped default. The fill fires ONLY when
20569/// `deploy_backend` is an explicit, concrete choice: `"auto"` and the
20570/// empty string are transparent (a route left empty resolves down the
20571/// Fase 36 D1 ladder at request time, so D5's no-silent-`stub`
20572/// guarantee is preserved). A route that DID declare its own backend
20573/// is never overridden — the per-route source declaration outranks
20574/// the per-deploy default.
20575///
20576/// Pure + total + deterministic — the same `(routes, deploy_backend)`
20577/// always produces the same table. Exhaustively unit-testable.
20578pub fn apply_deploy_backend_default(
20579    routes: &mut HashMap<(String, String), DynamicEndpointRoute>,
20580    deploy_backend: &str,
20581) {
20582    if !crate::backend_resolution::is_explicit_backend(deploy_backend) {
20583        return;
20584    }
20585    for route in routes.values_mut() {
20586        if route.backend.is_empty() {
20587            route.backend = deploy_backend.to_string();
20588        }
20589    }
20590}
20591
20592/// §Fase 36.f (D1) — resolve the execution backend for one dynamic
20593/// route by the Backend Resolution Contract ladder.
20594///
20595/// A thin, pure adapter over [`backend_resolution::resolve_backend`]
20596/// that projects a `DynamicEndpointRoute` + the live environment onto
20597/// the ladder's inputs:
20598///
20599///   - **Rung 1 — request-explicit:** `None`. A per-request backend
20600///     override is not a surface on dynamic routes — a deployed route
20601///     is hit by plain HTTP carrying the adopter's own body schema,
20602///     not an `ExecuteRequest`. (`/v1/execute` is the rung-1 surface.)
20603///   - **Rung 2 — endpoint-declared:** `route.backend` (the
20604///     `axonendpoint backend:` field, 36.d, carried onto the route by
20605///     36.e). Empty / `"auto"` are transparent.
20606///   - **Rung 3 — server default:** `server_default` (the
20607///     `ServerConfig.default_backend`, wired by §Fase 36.g; `None`
20608///     until then).
20609///   - **Rungs 4a/4b — environment-available `auto`:** the
20610///     operator-tuned `registry_ranked` scores, else `env_available`
20611///     (the providers with an API key in the environment, 36.c).
20612///
20613/// Pure given its arguments — the caller does the I/O (locking the
20614/// registry, scanning the environment) and hands the results in, so
20615/// this stays deterministic and unit-testable. Returns the honest
20616/// `Err(NoBackendAvailable)` when every rung is empty.
20617pub fn resolve_route_backend(
20618    route: &DynamicEndpointRoute,
20619    registry_ranked: Vec<String>,
20620    env_available: Vec<String>,
20621    server_default: Option<String>,
20622) -> Result<
20623    crate::backend_resolution::BackendResolution,
20624    crate::backend_resolution::NoBackendAvailable,
20625> {
20626    crate::backend_resolution::resolve_backend(
20627        &crate::backend_resolution::BackendResolutionInputs {
20628            request_backend: None,
20629            endpoint_backend: Some(route.backend.clone()),
20630            server_default,
20631            registry_ranked,
20632            env_available,
20633        },
20634    )
20635}
20636
20637/// §Fase 36.j (D8) — inject the `backend_resolution` observability
20638/// object into a dynamic-route JSON response body.
20639///
20640/// The resolved backend and the precedence rung that chose it are
20641/// added under a top-level `backend_resolution` key:
20642///
20643/// ```json
20644/// { …flow result…, "backend_resolution": { "backend": "gemini",
20645///   "reason": "endpoint_declared" } }
20646/// ```
20647///
20648/// Total + safe: a body that is not a JSON object (already malformed,
20649/// or an array / scalar) is returned untouched — observability is
20650/// best-effort and never corrupts a response. The `reason` slug is
20651/// the closed catalog from [`backend_resolution::
20652/// BackendResolutionReason::as_slug`].
20653pub fn inject_backend_resolution(
20654    body: &[u8],
20655    backend: &str,
20656    reason_slug: &str,
20657) -> Vec<u8> {
20658    match serde_json::from_slice::<serde_json::Value>(body) {
20659        Ok(serde_json::Value::Object(mut map)) => {
20660            map.insert(
20661                "backend_resolution".to_string(),
20662                serde_json::json!({
20663                    "backend": backend,
20664                    "reason": reason_slug,
20665                }),
20666            );
20667            serde_json::to_vec(&serde_json::Value::Object(map))
20668                .unwrap_or_else(|_| body.to_vec())
20669        }
20670        // Not a JSON object — leave the body byte-for-byte untouched.
20671        _ => body.to_vec(),
20672    }
20673}
20674
20675/// §Fase 36.h (D5) — build the honest-failure response for a dynamic
20676/// route whose execution backend the D1 ladder could not resolve.
20677///
20678/// The Backend Resolution Contract is TOTAL: when every rung is empty
20679/// (no request backend, no `axonendpoint backend:`, no server
20680/// default, no operator-tuned registry entry, no provider API key in
20681/// the environment) `resolve_route_backend` returns
20682/// `Err(NoBackendAvailable)`. D5 forbids degrading silently to the
20683/// no-op `stub`; the request must fail LOUDLY, naming exactly what to
20684/// fix.
20685///
20686///   - **JSON route** → HTTP 503 Service Unavailable + a structured
20687///     `{error: "no_backend_available", …}` body.
20688///   - **SSE route** → HTTP 200 `text/event-stream` carrying a single
20689///     dialect-translated `axon.error` event then the terminator —
20690///     the streaming-API expectation that the wire format is honored
20691///     even for failures (the same discipline as the not-deployed
20692///     SSE path).
20693///
20694/// The `NoBackendAvailable` `Display` already enumerates every fix
20695/// (declare `backend:`, set a provider key, pass `--backend`, or
20696/// request `backend=stub` explicitly to opt into the no-op) — it is
20697/// reused verbatim as the human-facing `message`.
20698pub fn honest_backend_failure_response(
20699    route_wire: DynamicRouteWire,
20700    route: &DynamicEndpointRoute,
20701    no_backend: &crate::backend_resolution::NoBackendAvailable,
20702    trace_id: &str,
20703    wire_dialect: &str,
20704) -> axum::response::Response {
20705    use axum::response::IntoResponse;
20706    let message = no_backend.to_string();
20707    match route_wire {
20708        DynamicRouteWire::Json => (
20709            StatusCode::SERVICE_UNAVAILABLE,
20710            Json(serde_json::json!({
20711                "success": false,
20712                "error": "no_backend_available",
20713                "message": message,
20714                "endpoint": route.endpoint_name,
20715                "flow": route.flow_name,
20716                "trace_id": trace_id,
20717                "d_letter": "D5",
20718            })),
20719        )
20720            .into_response(),
20721        DynamicRouteWire::Sse => {
20722            // Route the synthetic `FlowError` through the wire-format
20723            // adapter so EVERY dialect surfaces a well-formed error:
20724            // axon emits `event: axon.error`; openai/kimi/glm emit a
20725            // final chunk with `finish_reason` + `data: [DONE]`;
20726            // anthropic emits `message_delta` + `event: message_stop`.
20727            let mut adapter = crate::wire_format::select_adapter(wire_dialect, 0);
20728            let synthetic =
20729                crate::flow_execution_event::FlowExecutionEvent::FlowError {
20730                    flow_name: route.flow_name.clone(),
20731                    error: message,
20732                    timestamp_ms: 0,
20733                };
20734            let mut events: Vec<Result<Event, Infallible>> =
20735                vec![Ok(build_retry_hint_event())];
20736            events.extend(adapter.translate(&synthetic).into_iter().map(Ok));
20737            events.extend(adapter.flush_terminator().into_iter().map(Ok));
20738            Sse::new(futures::stream::iter(events)).into_response()
20739        }
20740    }
20741}
20742
20743/// §Fase 36.g (D7) — validate a server default backend name against
20744/// the closed catalog.
20745///
20746/// The `--backend` CLI flag / `AXON_DEFAULT_BACKEND` env var is
20747/// checked against the SAME closed catalog the `axonendpoint
20748/// backend:` declaration is validated against (36.d —
20749/// `parser::AXONENDPOINT_BACKEND_VALUES` = `CANONICAL_PROVIDERS ∪
20750/// {auto, stub}`). `None` (no server default configured) is always
20751/// valid. Pure + total — `run_serve` calls it at startup to fail
20752/// fast; an `Err` carries the operator-facing diagnostic verbatim.
20753pub fn validate_server_default_backend(
20754    default_backend: &Option<String>,
20755) -> Result<(), String> {
20756    if let Some(name) = default_backend {
20757        if !crate::parser::AXONENDPOINT_BACKEND_VALUES.contains(&name.as_str()) {
20758            return Err(format!(
20759                "invalid server default backend '{name}' (from --backend \
20760                 or AXON_DEFAULT_BACKEND). Valid: {}",
20761                crate::parser::AXONENDPOINT_BACKEND_VALUES.join(" | ")
20762            ));
20763        }
20764    }
20765    Ok(())
20766}
20767
20768/// Merge a freshly-collected route table into the live
20769/// `ServerState.dynamic_routes`. Detects cross-deploy path collisions
20770/// (D2 across deploys) — if the incoming table contains a
20771/// `(method, path)` tuple that already exists in the live state for
20772/// a DIFFERENT axonendpoint (different `endpoint_name`), the merge
20773/// fails with a structured error. Same-endpoint re-deploys update
20774/// the route in place.
20775pub fn merge_dynamic_routes(
20776    live: &mut HashMap<(String, String), DynamicEndpointRoute>,
20777    incoming: HashMap<(String, String), DynamicEndpointRoute>,
20778) -> Result<(), String> {
20779    // First pass: validate the incoming table against the live state
20780    // before mutating. This keeps the merge atomic (all-or-nothing).
20781    for (key, new_route) in &incoming {
20782        if let Some(existing) = live.get(key) {
20783            if existing.endpoint_name != new_route.endpoint_name {
20784                return Err(format!(
20785                    "Path collision (D2 cross-deploy): axonendpoint '{}' \
20786                     (from {}) and existing axonendpoint '{}' (from {}) both \
20787                     claim `method: {} path: {}`. Resolve by editing one of \
20788                     the two axonendpoints to use a distinct (method, path) \
20789                     tuple.",
20790                    new_route.endpoint_name, new_route.source_file,
20791                    existing.endpoint_name, existing.source_file,
20792                    key.0, key.1,
20793                ));
20794            }
20795        }
20796    }
20797    // Second pass: apply the merge (overwrites same-endpoint entries
20798    // with the new metadata — re-deploy refreshes transport / keepalive
20799    // / implicit_transport if the source changed).
20800    for (key, route) in incoming {
20801        live.insert(key, route);
20802    }
20803    Ok(())
20804}
20805
20806/// §Fase 32.b + 32.c — Fallback handler for dynamic axonendpoint routes.
20807///
20808/// Fires when no static route in `build_router_with_state` matched
20809/// the incoming request. Looks up `(method, path)` in
20810/// `ServerState.dynamic_routes`; on hit, validates the request body
20811/// against the route's declared `body:` type (D4, Fase 32.c) and
20812/// dispatches through the existing Fase 30 + 31 negotiation classifier
20813/// with the flow_name from the route. On miss, returns 404 with a
20814/// structured error listing all registered dynamic routes (for
20815/// adopter triage).
20816///
20817/// Body validation is OPTIONAL: when `route.body_type` is empty, the
20818/// validation step is skipped entirely (D9 backwards-compat — adopters
20819/// who don't declare `body:` keep free-form JSON semantics). When set,
20820/// the body is parsed as JSON and validated against the resolved
20821/// `TypeSchema` from `ServerState.dynamic_types`. Mismatch → 400 Bad
20822/// Request with a structured `BodyValidationError`.
20823///
20824/// Body forwarding to the flow itself remains deferred (existing
20825/// `/v1/execute` does not pass body data to flows either — flows
20826/// currently receive no parameterized HTTP input). Forward-to-flow
20827/// wiring lands in a follow-on fase. 32.c establishes the **validation
20828/// gate at the boundary**: malformed bodies never reach the flow
20829/// runtime.
20830async fn dynamic_endpoint_handler(
20831    State(state): State<SharedState>,
20832    headers: HeaderMap,
20833    method: axum::http::Method,
20834    uri: axum::http::Uri,
20835    body: axum::body::Bytes,
20836) -> axum::response::Response {
20837    use axum::response::IntoResponse;
20838
20839    let method_str = method.as_str().to_ascii_uppercase();
20840    let path_str = uri.path().to_string();
20841
20842    // §Fase 37.y (D2) — Parse the URL query string into a HashMap so
20843    // query params bind alongside path captures + body fields. The
20844    // map is empty when the request has no query string. Built ONCE
20845    // outside the route lookup so the response path can also consult
20846    // it (e.g. for diagnostic headers).
20847    let request_query: HashMap<String, String> =
20848        parse_query_string(uri.query());
20849
20850    // §Fase 37.y (D1) — Two-step route lookup:
20851    //   (1) Fast path: exact (method, path) match on a route with no
20852    //       placeholders. Preserves the v1.38.4 hot-path performance
20853    //       for legacy endpoints (D5 backwards-compat absolute).
20854    //   (2) Template-match fallback: for routes whose `path_params` is
20855    //       non-empty, run `match_path_template` against the actual
20856    //       URL; on first match, capture the path-param values and
20857    //       use that route. The map is also empty for legacy routes
20858    //       (so the runtime binder sees the same empty map as v1.38.4).
20859    let (route_opt, path_captures): (
20860        Option<DynamicEndpointRoute>,
20861        HashMap<String, String>,
20862    ) = {
20863        let s = state.lock().unwrap();
20864        // (1) Exact lookup.
20865        if let Some(r) =
20866            s.dynamic_routes.get(&(method_str.clone(), path_str.clone()))
20867        {
20868            (Some(r.clone()), HashMap::new())
20869        } else {
20870            // (2) Template match — iterate routes with the same method
20871            // and a non-empty `path_params` (placeholder-bearing routes).
20872            // First match wins; the parser's intra-program collision
20873            // check (Fase 32 D2 — same exact `path:` string under the
20874            // same method is a deploy-time error) means two TEMPLATES
20875            // that capture the same actual URL is structurally
20876            // impossible within a single deploy. Cross-deploy collisions
20877            // are caught by `merge_dynamic_routes`. Linear scan is fine
20878            // — adopters have O(100) routes typically; the trade-off is
20879            // O(routes) per dynamic-route request, equivalent to axum's
20880            // own routing tree's worst case.
20881            let mut matched: Option<(DynamicEndpointRoute, HashMap<String, String>)> = None;
20882            for ((m, template), route) in &s.dynamic_routes {
20883                if m != &method_str {
20884                    continue;
20885                }
20886                if route.path_params.is_empty() {
20887                    // No placeholders — already covered by the exact
20888                    // lookup above. Skip.
20889                    continue;
20890                }
20891                if let Some(caps) = match_path_template(template, &path_str) {
20892                    matched = Some((route.clone(), caps));
20893                    break;
20894                }
20895            }
20896            match matched {
20897                Some((r, caps)) => (Some(r), caps),
20898                None => (None, HashMap::new()),
20899            }
20900        }
20901    };
20902
20903    let route = match route_opt {
20904        Some(r) => r,
20905        None => {
20906            // 404 — list registered routes for adopter triage.
20907            let registered: Vec<serde_json::Value> = {
20908                let s = state.lock().unwrap();
20909                s.dynamic_routes
20910                    .keys()
20911                    .map(|(m, p)| serde_json::json!({"method": m, "path": p}))
20912                    .collect()
20913            };
20914            let body = serde_json::json!({
20915                "error": "axonendpoint_not_found",
20916                "method": method_str,
20917                "path": path_str,
20918                "registered_routes": registered,
20919                "hint": "deploy an axonendpoint with this method+path, or use POST /v1/execute with the flow name in the body for the legacy RPC path",
20920            });
20921            return (StatusCode::NOT_FOUND, Json(body)).into_response();
20922        }
20923    };
20924
20925    // §Fase 32.c — Body-schema validation gate. Only runs when the
20926    // axonendpoint declared `body: T` in source (route.body_type
20927    // non-empty). For methods with a meaningful request body (POST,
20928    // PUT, PATCH) the body is parsed as JSON and validated against
20929    // the resolved TypeSchema. GET/DELETE skip validation unless an
20930    // explicit body is present (HTTP spec is ambiguous about bodies
20931    // on GET — we accept-or-skip rather than reject the request
20932    // outright; the schema applies only when a body is sent).
20933    if !route.body_type.is_empty() {
20934        let has_body = !body.is_empty();
20935        let body_required = matches!(method_str.as_str(), "POST" | "PUT" | "PATCH");
20936        if has_body || body_required {
20937            // Parse JSON. Empty body on a method that requires one is
20938            // a schema violation (cannot satisfy a non-trivial type T
20939            // with an absent body).
20940            let parsed: serde_json::Value = if has_body {
20941                match serde_json::from_slice(&body) {
20942                    Ok(v) => v,
20943                    Err(e) => {
20944                        let payload = serde_json::json!({
20945                            "error": "body_schema_violation",
20946                            "expected_type": route.body_type,
20947                            "field_path": "",
20948                            "expected": route.body_type,
20949                            "got": "invalid_json",
20950                            "hint": format!(
20951                                "Request body is not valid JSON: {e}. The \
20952                                 axonendpoint declared `body: {body_type}` \
20953                                 which requires a well-formed JSON body.",
20954                                body_type = route.body_type,
20955                            ),
20956                            "d_letter": "D4",
20957                        });
20958                        return (StatusCode::BAD_REQUEST, Json(payload)).into_response();
20959                    }
20960                }
20961            } else {
20962                // Missing body where the method + declaration require one.
20963                let payload = serde_json::json!({
20964                    "error": "body_schema_violation",
20965                    "expected_type": route.body_type,
20966                    "field_path": "",
20967                    "expected": route.body_type,
20968                    "got": "missing",
20969                    "hint": format!(
20970                        "Request body is empty but axonendpoint '{endpoint}' \
20971                         declared `body: {body_type}` for `{method} {path}`. \
20972                         Send a JSON body matching the declared type.",
20973                        endpoint = route.endpoint_name,
20974                        body_type = route.body_type,
20975                        method = method_str,
20976                        path = path_str,
20977                    ),
20978                    "d_letter": "D4",
20979                });
20980                return (StatusCode::BAD_REQUEST, Json(payload)).into_response();
20981            };
20982
20983            // Look up the type table snapshot. Holding the lock for the
20984            // clone is brief; validation itself runs outside the lock.
20985            let type_table = {
20986                let s = state.lock().unwrap();
20987                s.dynamic_types.clone()
20988            };
20989            if let Err(verr) = crate::route_schema::validate_body(
20990                &parsed,
20991                &route.body_type,
20992                &type_table,
20993            ) {
20994                let payload = serde_json::json!({
20995                    "error": "body_schema_violation",
20996                    "expected_type": verr.expected_type,
20997                    "field_path": verr.field_path,
20998                    "expected": verr.expected,
20999                    "got": verr.got,
21000                    "hint": verr.hint,
21001                    "endpoint": route.endpoint_name,
21002                    "method": method_str,
21003                    "path": path_str,
21004                    "d_letter": "D4",
21005                });
21006                return (StatusCode::BAD_REQUEST, Json(payload)).into_response();
21007            }
21008        }
21009    }
21010
21011    // §Fase 32.g (D8) — Auth-scope gate.
21012    //
21013    // When the axonendpoint declared `requires: [a, b.c, …]`, the
21014    // bearer's `capabilities` JWT claim must contain every listed slug
21015    // (AND semantics — subset check `declared ⊆ have`). Empty list
21016    // short-circuits to Allow (D9 backwards-compat).
21017    //
21018    // Gate fires AFTER body validation (cheapest reject for malformed
21019    // input) but BEFORE the idempotency cache lookup — unauthorized
21020    // requests must not observe cache existence (a basic information-
21021    // leak hardening: an attacker probing for a valid Idempotency-Key
21022    // shouldn't be able to distinguish "key exists" from "key absent"
21023    // when they don't even hold the capability to call the endpoint).
21024    //
21025    // Returns 403 Forbidden with structured `{error,
21026    // missing_capability, required, have, endpoint, method, path,
21027    // hint, d_letter: "D8"}` so the client knows precisely what
21028    // capability is needed and the auditor can correlate against the
21029    // axonendpoint declaration.
21030    if !route.requires_capabilities.is_empty() {
21031        let have = crate::auth_scope::extract_capabilities_from_bearer(&headers);
21032        match crate::auth_scope::check_capabilities(&route.requires_capabilities, &have) {
21033            crate::auth_scope::AuthVerdict::Allow => {}
21034            crate::auth_scope::AuthVerdict::Deny {
21035                missing,
21036                required,
21037                have,
21038            } => {
21039                let payload = serde_json::json!({
21040                    "error": "missing_capability",
21041                    "missing": missing,
21042                    "required": required,
21043                    "have": have,
21044                    "endpoint": route.endpoint_name,
21045                    "method": method_str,
21046                    "path": path_str,
21047                    "hint": format!(
21048                        "Bearer is missing capabilities {missing:?} required by axonendpoint \
21049                         '{endpoint}'. Reissue the bearer with the declared capabilities or \
21050                         contact the endpoint's owner to grant access.",
21051                        endpoint = route.endpoint_name,
21052                    ),
21053                    "d_letter": "D8",
21054                });
21055                return (StatusCode::FORBIDDEN, Json(payload)).into_response();
21056            }
21057        }
21058    }
21059
21060    // §Fase 32.f (D7) — Idempotency-Key gate.
21061    //
21062    // Stripe-compatible. When the request carries `Idempotency-Key`
21063    // AND the axonendpoint declares method ∈ {POST, PUT}, the runtime
21064    // consults the (client_id, endpoint_path, idempotency_key) cache
21065    // before dispatch:
21066    //   - HIT (same key + same body) → return cached response verbatim
21067    //     with `Idempotency-Status: replayed` header attached.
21068    //   - CONFLICT (same key + different body) → 422 Unprocessable
21069    //     Entity with `idempotency_key_reused_with_different_request`.
21070    //   - MISS → mark for post-dispatch caching (only if response is
21071    //     2xx — failures must retry actual execution).
21072    //
21073    // For GET / DELETE the header is logged + ignored: those methods
21074    // are natively idempotent per HTTP spec and replaying the cached
21075    // response would be a category error.
21076    let idempotency_key = headers
21077        .get("idempotency-key")
21078        .and_then(|v| v.to_str().ok())
21079        .map(|s| s.to_string());
21080    let idempotency_method_eligible =
21081        matches!(method_str.as_str(), "POST" | "PUT");
21082
21083    let idempotency_cache_marker: Option<(crate::idempotency::IdempotencyCacheKey, [u8; 32])> =
21084        if let (Some(key), true) = (&idempotency_key, idempotency_method_eligible) {
21085            let client_id = client_key_from_headers(&headers);
21086            let request_body_hash = crate::idempotency::IdempotencyStore::hash_body(&body);
21087            let cache_key = crate::idempotency::IdempotencyCacheKey {
21088                client_id,
21089                endpoint_path: path_str.clone(),
21090                idempotency_key: key.clone(),
21091            };
21092            let verdict = {
21093                let mut s = state.lock().unwrap();
21094                s.idempotency_store.lookup(&cache_key, &request_body_hash)
21095            };
21096            match verdict {
21097                crate::idempotency::IdempotencyVerdict::Hit(entry) => {
21098                    // Replay the cached response verbatim. The
21099                    // `Idempotency-Status: replayed` header tells the
21100                    // client this is a cached body (industry-standard
21101                    // diagnostic; some Stripe SDK versions log it).
21102                    let status = StatusCode::from_u16(entry.status)
21103                        .unwrap_or(StatusCode::OK);
21104                    let mut resp = axum::response::Response::builder()
21105                        .status(status)
21106                        .header("content-type", entry.content_type.clone())
21107                        .header("idempotency-status", "replayed")
21108                        .body(axum::body::Body::from(entry.body.clone()))
21109                        .unwrap_or_else(|_| {
21110                            (status, Json(serde_json::json!({
21111                                "error": "idempotency_replay_construction_failed",
21112                                "d_letter": "D7",
21113                            }))).into_response()
21114                        });
21115                    // Defensive: if Body::from path failed, the
21116                    // closure above produces a json fallback already
21117                    // with into_response().
21118                    let _ = &mut resp;
21119                    return resp;
21120                }
21121                crate::idempotency::IdempotencyVerdict::Conflict {
21122                    cached_body_hash_hex,
21123                } => {
21124                    let payload = serde_json::json!({
21125                        "error": "idempotency_key_reused_with_different_request",
21126                        "idempotency_key": key,
21127                        "endpoint": route.endpoint_name,
21128                        "method": method_str,
21129                        "path": path_str,
21130                        "cached_body_hash_prefix": cached_body_hash_hex,
21131                        "hint": "The Idempotency-Key was previously used with a DIFFERENT request body for this endpoint. Generate a new key for the new request, or send the same body to replay the original response.",
21132                        "d_letter": "D7",
21133                    });
21134                    return (StatusCode::UNPROCESSABLE_ENTITY, Json(payload)).into_response();
21135                }
21136                crate::idempotency::IdempotencyVerdict::Miss => {
21137                    Some((cache_key, request_body_hash))
21138                }
21139            }
21140        } else {
21141            // Header present on GET/DELETE → log + ignore (HTTP-spec idempotent).
21142            if idempotency_key.is_some() && !idempotency_method_eligible {
21143                tracing::debug!(
21144                    method = %method_str,
21145                    path = %path_str,
21146                    "axon-rs Fase 32.f D7: Idempotency-Key header ignored on natively-idempotent HTTP method"
21147                );
21148            }
21149            None
21150        };
21151
21152    // §Fase 32.e (D6) — Per-route transport dispatch.
21153    //
21154    // The Fase 30+31 negotiation matrix is keyed by `(strict_mode ×
21155    // route_declaration × stream_effect × Accept)`. Pre-32.e the
21156    // classifier read `route_declaration` from a per-FLOW lookup —
21157    // correct for `/v1/execute` (one entrypoint per flow) but wrong
21158    // for dynamic routes (one flow can be exposed at multiple paths
21159    // with DIFFERENT transports). 32.e dispatches per-route using the
21160    // metadata already captured at deploy time, so two endpoints
21161    // sharing a flow but declaring different `transport:` fields each
21162    // honor their own contract.
21163    //
21164    // The decision is total over the four inputs:
21165    //   1. `transport_explicit && transport == "sse" | "ndjson"`
21166    //      → SSE always (D5: declared, sacred).
21167    //   2. `transport_explicit && transport == "json"`
21168    //      → JSON always (D3: sacred opt-out, even with Accept SSE).
21169    //   3. `!transport_explicit && implicit_transport == "sse"`
21170    //      (i.e. stream effects detected by 31.b):
21171    //        - strict_mode → SSE (D1 inference fires).
21172    //        - !strict_mode && Accept SSE → SSE (D4 fallback).
21173    //        - !strict_mode && !Accept SSE → JSON (D9 backwards-compat).
21174    //   4. `!transport_explicit && implicit_transport == "json"`
21175    //      (no stream effects) → JSON always.
21176    let strict_mode = state.lock().unwrap().config.strict_type_driven_transport;
21177    let client_wants_sse = headers
21178        .get("accept")
21179        .and_then(|h| h.to_str().ok())
21180        .unwrap_or("")
21181        .contains("text/event-stream");
21182    let route_wire = classify_dynamic_route_wire(
21183        &route.transport,
21184        route.transport_explicit,
21185        &route.implicit_transport,
21186        route.has_algebraic_stream_effect,
21187        client_wants_sse,
21188        strict_mode,
21189    );
21190
21191    // §Fase 32.h — Capture client identity + capabilities BEFORE
21192    // dispatch so the SSE path (which moves `headers` into the
21193    // streaming handler) doesn't leave the replay binding unable to
21194    // record them. Cheap clones — both fields are small.
21195    let replay_client_id = client_key_from_headers(&headers);
21196    let replay_capabilities_used =
21197        crate::auth_scope::extract_capabilities_from_bearer(&headers);
21198
21199    // §Fase 32.h (lifted up in 33.x.f) — trace_id generated BEFORE
21200    // dispatch so the SSE branch can pass it into
21201    // `execute_sse_handler_inner`'s ReplayContext. The same UUID
21202    // surfaces as `X-Axon-Trace-Id` on the response + as the lookup
21203    // key for `GET /v1/replay/<uuid>`. Cheap to generate
21204    // unconditionally (16 random bytes + hex encode).
21205    let trace_id = uuid::Uuid::new_v4().to_string();
21206    let trace_hdr =
21207        axum::http::HeaderValue::from_str(&trace_id).unwrap_or_else(|_| {
21208            // Uuid::to_string() produces only ASCII hex — this
21209            // path is unreachable, but defensive.
21210            axum::http::HeaderValue::from_static("unknown")
21211        });
21212
21213    // §Fase 36.f (D1, D3) — resolve the execution backend by the
21214    // Backend Resolution Contract ladder (`resolve_route_backend`),
21215    // retiring the hardcoded `"auto"` that made every deployed route
21216    // execute against the no-op `stub` on a server with provider keys
21217    // set (Break A).
21218    let (registry_ranked, server_default): (Vec<String>, Option<String>) = {
21219        let s = state.lock().unwrap();
21220        let ranked = compute_backend_scores(&s, "balanced")
21221            .into_iter()
21222            .map(|bs| bs.name)
21223            .collect();
21224        (ranked, s.config.default_backend.clone())
21225    };
21226    let (resolved_backend, resolution_reason) = match resolve_route_backend(
21227        &route,
21228        registry_ranked,
21229        crate::backends::env_available_backends(),
21230        server_default, // §Fase 36.g (D7) — rung 3 of the ladder.
21231    ) {
21232        Ok(r) => (r.backend, r.reason),
21233        Err(no_backend) => {
21234            // §Fase 36.h (D5) — honest failure. Every ladder rung was
21235            // empty (no request backend, no `axonendpoint backend:`,
21236            // no server default, empty registry, no provider API key
21237            // in the environment) and `stub` was not explicitly
21238            // named. D5 forbids degrading silently to the no-op: the
21239            // request fails LOUDLY with a structured diagnostic that
21240            // names exactly what to fix. NEVER a silent `stub`.
21241            {
21242                let mut s = state.lock().unwrap();
21243                s.metrics.total_errors += 1;
21244            }
21245            let wire_dialect =
21246                axon_frontend::type_checker::resolve_effective_dialect(
21247                    &route.transport_dialect,
21248                    route.has_algebraic_stream_effect,
21249                );
21250            let mut resp = honest_backend_failure_response(
21251                route_wire,
21252                &route,
21253                &no_backend,
21254                &trace_id,
21255                &wire_dialect,
21256            );
21257            resp.headers_mut()
21258                .insert("x-axon-trace-id", trace_hdr.clone());
21259            // §Fase 36.j (D8) — observability even on the honest
21260            // failure: no backend resolved, and the header says why.
21261            resp.headers_mut().insert(
21262                "x-axon-backend",
21263                axum::http::HeaderValue::from_static(
21264                    "none; reason=no_backend_available",
21265                ),
21266            );
21267            return resp;
21268        }
21269    };
21270
21271    // §Fase 36.j (D8) — resolution observability. The resolved
21272    // backend AND the precedence rung that chose it travel on the
21273    // `X-Axon-Backend` response header (`<backend>; reason=<rung>`)
21274    // and are injected into the JSON wire body as a
21275    // `backend_resolution` object. For a replay-enabled route the
21276    // injected body is what the replay log persists — so the
21277    // per-endpoint execution trace retrievable via
21278    // `GET /v1/replay/<trace_id>` carries the resolution too. An
21279    // operator can always answer "why this model?".
21280    let backend_hdr = axum::http::HeaderValue::from_str(&format!(
21281        "{resolved_backend}; reason={}",
21282        resolution_reason.as_slug()
21283    ))
21284    .unwrap_or_else(|_| axum::http::HeaderValue::from_static("unknown"));
21285
21286    // §Fase 37.b (D1) — parse the request body ONCE for the Request
21287    // Binding Contract. The flow's declared parameters bind from its
21288    // same-named fields. A request with no body, or a body that is
21289    // not a JSON object, yields `None` — the flow then runs with only
21290    // its own `let` / step bindings (D5 backwards-compat). The §32.c
21291    // body-schema gate above already rejected a malformed body for a
21292    // route that declared `body: T`; this parse is a best-effort read
21293    // for the binding and never itself rejects the request.
21294    let request_body_json: Option<serde_json::Value> =
21295        serde_json::from_slice(&body).ok();
21296
21297    let response = match route_wire {
21298        DynamicRouteWire::Sse => {
21299            let stream_req = StreamExecuteRequest {
21300                flow_name: route.flow_name.clone(),
21301                backend: resolved_backend.clone(),
21302                request_body: request_body_json.clone(),
21303                // §Fase 37.y (D3) — path captures + query string
21304                // travel alongside the body so the runner's binder
21305                // sees the full three-source set.
21306                request_path: path_captures.clone(),
21307                request_query: request_query.clone(),
21308            };
21309            // §Fase 33.x.f — Build the replay context when the
21310            // route declares `replay: true`. The inner handler
21311            // writes the AxonendpointReplayEntry at FlowComplete
21312            // time, populated with the per-step audit records.
21313            let replay_ctx = if route.replay_enabled {
21314                Some(SseReplayContext {
21315                    trace_id_uuid: trace_id.clone(),
21316                    endpoint_name: route.endpoint_name.clone(),
21317                    method: method_str.to_string(),
21318                    path: path_str.to_string(),
21319                    client_id: replay_client_id.clone(),
21320                    capabilities_used: replay_capabilities_used.clone(),
21321                    request_body: body.to_vec(),
21322                })
21323            } else {
21324                None
21325            };
21326            // §Fase 33.z.k.g — Resolve the effective SSE dialect for
21327            // this route. The resolver applies the Q1 ratification:
21328            // explicit `transport: sse(<dialect>)` wins; otherwise
21329            // algebraic-effect flows default to openai dialect (the
21330            // LLM-streaming ecosystem expectation), type-annotation-
21331            // only flows default to axon (W3C named events baseline).
21332            let wire_dialect = axon_frontend::type_checker::resolve_effective_dialect(
21333                &route.transport_dialect,
21334                route.has_algebraic_stream_effect,
21335            );
21336            let sse_response =
21337                execute_sse_handler_inner(state.clone(), headers, stream_req, replay_ctx, wire_dialect)
21338                    .await
21339                    .into_response();
21340            // Attach the X-Axon-Trace-Id header so adopters can
21341            // correlate the SSE stream with `/v1/replay/<uuid>`
21342            // (matches the JSON 2xx capture path's header).
21343            let mut sse_response = sse_response;
21344            sse_response.headers_mut().insert("x-axon-trace-id", trace_hdr.clone());
21345            sse_response
21346        }
21347        DynamicRouteWire::Json => {
21348            let exec_req = ExecuteRequest {
21349                flow: route.flow_name.clone(),
21350                backend: resolved_backend.clone(),
21351                request_body: request_body_json.clone(),
21352                // §Fase 37.y (D3) — path + query travel alongside body.
21353                request_path: path_captures.clone(),
21354                request_query: request_query.clone(),
21355                // §Fase 39.b — propagate the route's declared output
21356                // type so the FlowEnvelope's `ontological_type` slot
21357                // carries the endpoint contract verbatim. The handler
21358                // unwraps an outer `FlowEnvelope<T>` declaration so
21359                // the wire's slot is the inner T.
21360                declared_output_type: route.output_type.clone(),
21361            };
21362            let mut resp = execute_handler(State(state.clone()), headers.clone(), Json(exec_req))
21363                .await
21364                .into_response();
21365            // §Fase 31.e (D5) — Diagnostic header on dynamic routes.
21366            // When the route serves JSON for a flow that has stream
21367            // effects (route.implicit_transport == "sse"), attach
21368            // X-Axon-Stream-Available so the adopter sees the
21369            // inference outcome from their REST client without
21370            // spelunking source. Mirror of /v1/execute behavior.
21371            if route.implicit_transport == "sse" {
21372                let reason = if route.transport_explicit && route.transport == "json" {
21373                    "declared_json"
21374                } else {
21375                    "flag_off"
21376                };
21377                let header_value = format!(
21378                    "1; reason={reason}; flow={}; \
21379                     opt_in=transport:sse,Accept:text/event-stream",
21380                    route.flow_name
21381                );
21382                if let Ok(value) = axum::http::HeaderValue::from_str(&header_value) {
21383                    resp.headers_mut().insert("x-axon-stream-available", value);
21384                }
21385            }
21386            resp
21387        }
21388    };
21389
21390    // §Fase 32.d — Output-schema validation gate (D5).
21391    //
21392    // After the flow executes, validate the response body against the
21393    // declared `output: T` type. Validation runs ONLY when:
21394    //   - `route.output_type` is non-empty (D9 backwards-compat skip),
21395    //   - the response status is 2xx (4xx/5xx are already error
21396    //     responses, not the flow's typed output),
21397    //   - the response Content-Type starts with `application/json`
21398    //     (SSE/ndjson streams cannot be validated against a static
21399    //     type at the wire layer; per-event typed-stream validation
21400    //     is a candidate for a future fase).
21401    //
21402    // Per OWASP, validation failure returns a GENERIC 500 to the
21403    // client + records the full diagnostic in `audit_log`. The
21404    // adopter inspects the audit trail to fix the FLOW (the
21405    // language honoring its own declarations) while the client sees
21406    // only `{error: "internal_validation_error", trace_id, hint}`.
21407    let validated =
21408        apply_output_validation_gate(state.clone(), &route, response, &method_str, &path_str).await;
21409
21410    // §Fase 32.f (D7) + §Fase 32.h (D9 plan-vivo) — Unified post-
21411    // dispatch capture: idempotency cache write + replay-token
21412    // binding share the same body-read so we never read the response
21413    // body twice. The trace_id is generated once and attached as
21414    // `X-Axon-Trace-Id` on EVERY response from the dynamic-route
21415    // handler — this is the correlation anchor between client logs,
21416    // server audit, and the GET /v1/replay/<trace_id> retrieval path.
21417    //
21418    // Body capture fires only when status is 2xx AND Content-Type
21419    // starts with `application/json`. SSE/ndjson streams pass through
21420    // unchanged — replay binding for streaming bodies is a candidate
21421    // for a future fase (per-event token chain). 4xx/5xx error paths
21422    // skip BOTH caches because the response is not the flow's typed
21423    // output — replaying a 422 schema violation would be a category
21424    // error.
21425    //
21426    // §Fase 33.x.f — `trace_id` + `trace_hdr` were lifted above the
21427    // route_wire match so both SSE and JSON branches share the same
21428    // UUID. The SSE branch passes it into `execute_sse_handler_inner`
21429    // via the ReplayContext; the JSON branch uses it for the body-
21430    // capture replay-binding write + the X-Axon-Trace-Id header.
21431
21432    // §Fase 36.j (D8) + §Fase 32.f/h — every 2xx application/json
21433    // dynamic-route response is read + rebuilt here: 36.j injects the
21434    // `backend_resolution` observability object into the body, and —
21435    // when needed — the idempotency cache + replay-log writes share
21436    // the SAME post-injection bytes, so the cached / replayed body is
21437    // byte-identical to what the client received.
21438    if validated.status().is_success() {
21439        let content_type = validated
21440            .headers()
21441            .get(axum::http::header::CONTENT_TYPE)
21442            .and_then(|v| v.to_str().ok())
21443            .unwrap_or("")
21444            .to_string();
21445        if content_type.starts_with("application/json") {
21446            // Read the body once; 36.j injects `backend_resolution`;
21447            // replay log + idempotency cache share the injected bytes;
21448            // the rebuilt response carries them back to the client.
21449            let (mut parts, response_body_stream) = validated.into_parts();
21450            let raw_body = match axum::body::to_bytes(response_body_stream, usize::MAX).await {
21451                Ok(b) => b,
21452                Err(e) => {
21453                    tracing::error!(
21454                        error = %e,
21455                        "axon-rs Fase 32.f+h: failed to read response body for post-dispatch capture"
21456                    );
21457                    parts.headers.insert("x-axon-trace-id", trace_hdr);
21458                    parts.headers.insert("x-axon-backend", backend_hdr);
21459                    return axum::response::Response::from_parts(
21460                        parts,
21461                        axum::body::Body::from(Vec::new()),
21462                    );
21463                }
21464            };
21465            // §Fase 36.j (D8) — inject the `backend_resolution`
21466            // observability object. The injected bytes are what the
21467            // client sees AND what the caches below persist.
21468            let body_bytes: axum::body::Bytes = inject_backend_resolution(
21469                &raw_body,
21470                &resolved_backend,
21471                resolution_reason.as_slug(),
21472            )
21473            .into();
21474
21475            // 32.f cache write (if marker present).
21476            if let Some((cache_key, body_hash)) = idempotency_cache_marker {
21477                let mut s = state.lock().unwrap();
21478                s.idempotency_store.insert(
21479                    cache_key,
21480                    crate::idempotency::IdempotencyEntry {
21481                        request_body_hash: body_hash,
21482                        status: parts.status.as_u16(),
21483                        content_type: content_type.clone(),
21484                        body: body_bytes.to_vec(),
21485                        inserted_at: std::time::Instant::now(),
21486                    },
21487                );
21488            }
21489
21490            // 32.h replay-binding write (if route.replay_enabled).
21491            if route.replay_enabled {
21492                let entry = crate::axonendpoint_replay::AxonendpointReplayEntry {
21493                    trace_id: trace_id.clone(),
21494                    timestamp_ms: std::time::SystemTime::now()
21495                        .duration_since(std::time::UNIX_EPOCH)
21496                        .map(|d| d.as_millis() as u64)
21497                        .unwrap_or(0),
21498                    endpoint_name: route.endpoint_name.clone(),
21499                    flow_name: route.flow_name.clone(),
21500                    method: method_str.to_string(),
21501                    path: path_str.to_string(),
21502                    client_id: replay_client_id.clone(),
21503                    capabilities_used: replay_capabilities_used.clone(),
21504                    request_body_hash_hex:
21505                        crate::axonendpoint_replay::AxonendpointReplayLog::hash_body_hex(
21506                            &body,
21507                        ),
21508                    request_body: body.to_vec(),
21509                    response_status: parts.status.as_u16(),
21510                    response_body_hash_hex:
21511                        crate::axonendpoint_replay::AxonendpointReplayLog::hash_body_hex(
21512                            &body_bytes,
21513                        ),
21514                    response_content_type: content_type.clone(),
21515                    response_body: body_bytes.to_vec(),
21516                    model_version: "axon.runtime.dynamic_route.v1".to_string(),
21517                    // Deterministic if the dispatched backend is
21518                    // verified deterministic (stub today; locked-model
21519                    // surfaces layered on top by enterprise).
21520                    deterministic:
21521                        crate::axonendpoint_replay::is_backend_deterministic("stub"),
21522                    // §Fase 33.x.f — Per-step audit. Empty on the
21523                    // JSON 2xx capture path (this branch); SSE path
21524                    // populates this via the producer side-channel
21525                    // when route.replay_enabled.
21526                    step_audit: Vec::new(),
21527                    // §Fase 33.x.g — Runtime warnings mirror the
21528                    // wire `axon.complete.warnings` field. The
21529                    // JSON 2xx path doesn't traverse
21530                    // server_execute_streaming so no warnings are
21531                    // generated.
21532                    runtime_warnings: Vec::new(),
21533                };
21534                let mut s = state.lock().unwrap();
21535                s.axonendpoint_replay.append(entry);
21536            }
21537
21538            parts.headers.insert("x-axon-trace-id", trace_hdr);
21539            parts.headers.insert("x-axon-backend", backend_hdr);
21540            return axum::response::Response::from_parts(
21541                parts,
21542                axum::body::Body::from(body_bytes),
21543            );
21544        }
21545        // Non-JSON 2xx (SSE/ndjson) — pass through with the headers.
21546    }
21547    // Non-2xx OR non-JSON 2xx — attach trace + backend headers + return.
21548    let mut resp = validated;
21549    resp.headers_mut().insert("x-axon-trace-id", trace_hdr);
21550    resp.headers_mut().insert("x-axon-backend", backend_hdr);
21551    resp
21552}
21553
21554/// §Fase 32.d — Apply the response-side schema validation gate.
21555///
21556/// Pillar trace per D12:
21557///   - MATHEMATICS — same pure + total `validate_body` primitive as
21558///                   32.c, consumed on the response side.
21559///   - PHILOSOPHY — the declared `output:` IS the contract; adopters'
21560///                   downstream consumers can trust the schema.
21561///   - LOGIC      — no widening of the declared output type; flows
21562///                   producing a wider response fail loudly at the
21563///                   audit boundary.
21564///   - COMPUTING  — OWASP-aligned: schema details never leak to a
21565///                   potentially malicious client; only the
21566///                   trace_id is exposed.
21567async fn apply_output_validation_gate(
21568    state: SharedState,
21569    route: &DynamicEndpointRoute,
21570    response: axum::response::Response,
21571    method_str: &str,
21572    path_str: &str,
21573) -> axum::response::Response {
21574    use axum::response::IntoResponse;
21575    if route.output_type.is_empty() {
21576        return response; // D9 backwards-compat — no `output:` declared.
21577    }
21578    if !response.status().is_success() {
21579        return response; // Already an error path; not the typed output.
21580    }
21581    let content_type = response
21582        .headers()
21583        .get(axum::http::header::CONTENT_TYPE)
21584        .and_then(|v| v.to_str().ok())
21585        .unwrap_or("")
21586        .to_string();
21587    if !content_type.starts_with("application/json") {
21588        // SSE/ndjson stream — out of scope for static-type validation.
21589        return response;
21590    }
21591
21592    // Read the body. `usize::MAX` is the max-size cap — the response
21593    // bodies we expect here are kilobytes, not gigabytes, so no DoS
21594    // surface. If we ever expect larger bodies, this is the place to
21595    // add a per-endpoint cap.
21596    let (parts, body) = response.into_parts();
21597    let body_bytes = match axum::body::to_bytes(body, usize::MAX).await {
21598        Ok(b) => b,
21599        Err(e) => {
21600            tracing::error!(
21601                error = %e,
21602                "axon-rs Fase 32.d: failed to read response body for output validation"
21603            );
21604            return internal_validation_500(&state, route, method_str, path_str, None);
21605        }
21606    };
21607    let parsed: serde_json::Value = match serde_json::from_slice(&body_bytes) {
21608        Ok(v) => v,
21609        Err(_) => {
21610            // Response body claimed application/json but isn't valid JSON.
21611            // Pass through (the upstream handler shaped the response —
21612            // we don't fabricate a violation that may be the user's
21613            // intentional, e.g. a 200 OK with a custom serializer that
21614            // returned non-JSON despite the header).
21615            return axum::response::Response::from_parts(parts, axum::body::Body::from(body_bytes));
21616        }
21617    };
21618
21619    let type_table = {
21620        let s = state.lock().unwrap();
21621        s.dynamic_types.clone()
21622    };
21623    // §Fase 39.d — D5 runtime simplification. Pre-39.d the gate
21624    // manually extracted the inner-T from `FlowEnvelope<T>` and
21625    // pulled the `result` slot out of `parsed`. Post-39.d
21626    // `validate_body` is the SINGLE canonical entry that knows
21627    // about wire shapes: pass the raw declared type, and the
21628    // validator handles FlowEnvelope unwrap + nested generic
21629    // parsing + primitive/struct dispatch internally. The D5 gate
21630    // shrinks to one call.
21631    //
21632    // Pre-39.e legacy bare declarations (where the adopter
21633    // declared `output: T` without the `FlowEnvelope<>` wrapper)
21634    // still need a transitional skip — the wire IS FlowEnvelope
21635    // but the validator was asked to check against a bare-T which
21636    // doesn't match the envelope's outer object shape. 39.e closes
21637    // this path by making bare declarations compile errors.
21638    let declares_envelope =
21639        route.output_type.trim().starts_with("FlowEnvelope<");
21640    if !declares_envelope {
21641        return axum::response::Response::from_parts(
21642            parts,
21643            axum::body::Body::from(body_bytes),
21644        );
21645    }
21646    match crate::route_schema::validate_body(&parsed, &route.output_type, &type_table) {
21647        Ok(()) => axum::response::Response::from_parts(
21648            parts,
21649            axum::body::Body::from(body_bytes),
21650        ),
21651        Err(verr) => internal_validation_500(
21652            &state,
21653            route,
21654            method_str,
21655            path_str,
21656            Some(verr),
21657        ),
21658    }
21659}
21660
21661/// §Fase 32.d — Build the OWASP-safe 500 response for output
21662/// validation failures. Records the FULL diagnostic in `audit_log` +
21663/// emits a `tracing::error!` for adopter-side log tooling; the
21664/// CLIENT only sees the generic envelope `{error, trace_id, hint}`.
21665///
21666/// Schema details (expected_type, field_path, expected, got) are
21667/// NEVER serialised into the client response per OWASP API-Security
21668/// guidance — leaking a server-side type contract to a potentially
21669/// malicious caller is a recon vector.
21670fn internal_validation_500(
21671    state: &SharedState,
21672    route: &DynamicEndpointRoute,
21673    method_str: &str,
21674    path_str: &str,
21675    violation: Option<crate::route_schema::BodyValidationError>,
21676) -> axum::response::Response {
21677    use axum::response::IntoResponse;
21678    let trace_id = uuid::Uuid::new_v4().to_string();
21679    let audit_detail = match &violation {
21680        Some(v) => serde_json::json!({
21681            "event": "output_schema_violation",
21682            "endpoint": route.endpoint_name,
21683            "flow_name": route.flow_name,
21684            "method": method_str,
21685            "path": path_str,
21686            "expected_type": v.expected_type,
21687            "field_path": v.field_path,
21688            "expected": v.expected,
21689            "got": v.got,
21690            "hint": v.hint,
21691            // §Fase 38.x.f (D2) — cardinality diagnostic fields.
21692            // Empty strings serialize but adopters can grep
21693            // `expected_cardinality` / `got_cardinality` / `got_length`
21694            // / `remediation_url` to detect the cardinality-mismatch
21695            // class specifically vs primitive-type violations.
21696            "expected_cardinality": v.expected_cardinality,
21697            "got_cardinality": v.got_cardinality,
21698            "got_length": v.got_length,
21699            "remediation_url": v.remediation_url,
21700            "trace_id": trace_id,
21701            "d_letter": "D5",
21702        }),
21703        None => serde_json::json!({
21704            "event": "output_body_read_error",
21705            "endpoint": route.endpoint_name,
21706            "flow_name": route.flow_name,
21707            "method": method_str,
21708            "path": path_str,
21709            "trace_id": trace_id,
21710            "d_letter": "D5",
21711        }),
21712    };
21713
21714    if let Some(v) = &violation {
21715        tracing::error!(
21716            endpoint = %route.endpoint_name,
21717            flow = %route.flow_name,
21718            field_path = %v.field_path,
21719            expected = %v.expected,
21720            got = %v.got,
21721            trace_id = %trace_id,
21722            "axon-rs Fase 32.d D5: output schema violation — flow produced response not matching declared `output:` type"
21723        );
21724    }
21725
21726    {
21727        let mut s = state.lock().unwrap();
21728        s.audit_log.record(
21729            "axon-runtime",
21730            crate::audit_trail::AuditAction::Execute,
21731            &route.endpoint_name,
21732            audit_detail,
21733            false,
21734        );
21735        s.metrics.total_errors += 1;
21736    }
21737
21738    // §Fase 38.x.f (D4) — opt-in verbose hint surface for dev/staging.
21739    // When `AXON_VERBOSE_D5_HINT=1` (truthy alphabet: 1, true, yes, on
21740    // — case-insensitive) the full diagnostic payload from the audit
21741    // entry is included in the client response body. Default OFF
21742    // preserves OWASP-safe behavior (Fase 32.d D5) — production deploys
21743    // never accidentally leak server-side type contracts through
21744    // schema-validation errors.
21745    let verbose = std::env::var("AXON_VERBOSE_D5_HINT")
21746        .ok()
21747        .map(|v| {
21748            let normalized = v.trim().to_ascii_lowercase();
21749            matches!(normalized.as_str(), "1" | "true" | "yes" | "on")
21750        })
21751        .unwrap_or(false);
21752
21753    let client_body = if verbose {
21754        match &violation {
21755            Some(v) => serde_json::json!({
21756                "error": "internal_validation_error",
21757                "trace_id": trace_id,
21758                "hint": "The flow produced a response that did not match the declared output schema. AXON_VERBOSE_D5_HINT is ON — full diagnostic included below; do NOT enable in production (OWASP).",
21759                "expected_type": v.expected_type,
21760                "field_path": v.field_path,
21761                "expected": v.expected,
21762                "got": v.got,
21763                "expected_cardinality": v.expected_cardinality,
21764                "got_cardinality": v.got_cardinality,
21765                "got_length": v.got_length,
21766                "remediation_url": v.remediation_url,
21767                "verbose_hint_detail": v.hint,
21768                "d_letter": "D5",
21769            }),
21770            None => serde_json::json!({
21771                "error": "internal_validation_error",
21772                "trace_id": trace_id,
21773                "hint": "The response body could not be parsed as JSON despite Content-Type. AXON_VERBOSE_D5_HINT is ON.",
21774                "d_letter": "D5",
21775            }),
21776        }
21777    } else {
21778        serde_json::json!({
21779            "error": "internal_validation_error",
21780            "trace_id": trace_id,
21781            "hint": "The flow produced a response that did not match the declared output schema. The adopter-facing diagnostic is in the audit trail (GET /v1/audit).",
21782            "d_letter": "D5",
21783        })
21784    };
21785    (StatusCode::INTERNAL_SERVER_ERROR, Json(client_body)).into_response()
21786}
21787
21788/// Wrapper for `POST /v1/execute` that performs Fase 30.e
21789/// content-negotiation upstream of the legacy JSON handler.
21790///
21791/// Logic:
21792///   1. Read the deployed source. If lookup fails → 404 path of the
21793///      old handler (we fall through unchanged).
21794///   2. If the request's `Accept` header does NOT contain
21795///      `text/event-stream` AND no explicit `transport: sse|ndjson`
21796///      declaration exists → defer to legacy `execute_handler`
21797///      verbatim. (D5 force-promote still fires even without Accept;
21798///      adopters who declared sse on the endpoint want SSE always.)
21799///   3. Parse the source. On parse failure → defer to legacy handler
21800///      (defensive: stale-source parse drift never crashes the
21801///      runtime request path).
21802///   4. Classify via `classify_negotiation_for_flow`.
21803///   5. PromoteToSse → delegate to `execute_sse_handler` with a
21804///      `StreamExecuteRequest` adapted from the `ExecuteRequest`.
21805///   6. StayJson → defer to legacy handler.
21806async fn execute_handler_with_negotiation(
21807    State(state): State<SharedState>,
21808    headers: HeaderMap,
21809    Json(payload): Json<ExecuteRequest>,
21810) -> axum::response::Response {
21811    // Fast path: client didn't ask for SSE AND we don't yet know if
21812    // any axonendpoint forces SSE. We still need to consult the
21813    // declaration in case D5 force-promote applies. So we DO parse
21814    // the source on every /v1/execute request — acceptable for the
21815    // initial implementation; future Fase can cache.
21816    //
21817    // Optimization: if Accept neither contains text/event-stream nor
21818    // any obvious streaming hint, AND the source is unavailable, skip
21819    // the parse entirely and route to the legacy handler.
21820    let accept_header = headers
21821        .get("accept")
21822        .and_then(|h| h.to_str().ok())
21823        .unwrap_or("")
21824        .to_string();
21825    let client_wants_sse = accept_header.contains("text/event-stream");
21826
21827    // Look up the source. If not deployed, defer to legacy handler
21828    // which already produces the canonical not-deployed error.
21829    let source_opt: Option<String> = {
21830        let s = state.lock().unwrap();
21831        s.versions
21832            .get_history(&payload.flow)
21833            .and_then(|h| h.active())
21834            .map(|v| v.source.clone())
21835    };
21836
21837    let source = match source_opt {
21838        Some(s) => s,
21839        None => {
21840            // Not deployed — defer to legacy handler so the error
21841            // shape stays consistent with v1.20.0 clients.
21842            return execute_handler(State(state), headers, Json(payload))
21843                .await
21844                .into_response();
21845        }
21846    };
21847
21848    // The Rust frontend has two known parser gaps vs Python that
21849    // affect this predicate:
21850    //   (i)  `output: Stream<T>` inside step bodies (pre-v1.19.3
21851    //        shape) — affects disjunct (a)
21852    //   (ii) `use <tool>("args")` inside step bodies — StepNode in
21853    //        the Rust AST doesn't carry use_tool — affects disjunct (b)
21854    // Both gaps belong to a Rust-frontend completion sub-fase. Until
21855    // that ships, we DEFENSIVELY consult source-text patterns ALONGSIDE
21856    // the AST walk and OR the verdicts. This makes 30.e robust to
21857    // cross-stack drift on the predicate while preserving the AST
21858    // walk as the authoritative path for declared `transport:` fields
21859    // (which the Rust parser handles correctly — verified by the
21860    // 30.b drift gate).
21861    let program_opt = crate::lexer::Lexer::new(&source, "<runtime-negotiation>")
21862        .tokenize()
21863        .ok()
21864        .and_then(|tokens| crate::parser::Parser::new(tokens).parse().ok());
21865
21866    let ast_decision = match program_opt {
21867        Some(ref program) => classify_negotiation_for_flow(program, &payload.flow),
21868        None => NegotiationDecision::StayJson,
21869    };
21870    let text_decision = classify_negotiation_via_source_text(&source, &payload.flow);
21871
21872    // Combine: D5 force-stay-json from either path is sticky (the
21873    // adopter explicitly opted out of streaming — never override).
21874    // Otherwise, PromoteToSse from either path wins.
21875    let decision = if matches!(ast_decision, NegotiationDecision::StayJson)
21876        && source_text_axonendpoint_has_transport(&source, &payload.flow, "json")
21877    {
21878        // The explicit `transport: json` declaration suppresses any
21879        // promote signal from the source-text predicate.
21880        NegotiationDecision::StayJson
21881    } else if matches!(ast_decision, NegotiationDecision::PromoteToSse)
21882        || matches!(text_decision, NegotiationDecision::PromoteToSse)
21883    {
21884        NegotiationDecision::PromoteToSse
21885    } else {
21886        NegotiationDecision::StayJson
21887    };
21888
21889    // §Fase 31.d (D1, D6, D8) — strict-mode flag for type-driven
21890    // default transport. When enabled, a `PromoteToSse` verdict
21891    // (from EITHER the explicit declaration OR the stream-effect
21892    // inference) is honored regardless of the client's `Accept:`
21893    // header. When disabled (D6 default in v1.22.x), Fase 30.e D4
21894    // + D5 negotiation is preserved verbatim.
21895    //
21896    // Critically, `decision == StayJson` is sticky in both modes:
21897    // when the adopter explicitly declared `transport: json` (D3
21898    // opt-out, captured by the combine logic above), strict mode
21899    // does NOT override the opt-out. The four-pillar discipline
21900    // requires the language to honor the adopter's explicit choice
21901    // — the inference defers when the source declared.
21902    let strict_mode = state
21903        .lock()
21904        .unwrap()
21905        .config
21906        .strict_type_driven_transport;
21907
21908    // D5 force-promote OR D4 fallback-with-Accept (legacy) OR
21909    // D1 type-driven default (strict mode) → SSE.
21910    // D5 force-stay-json OR no-stream-effect-no-Accept → JSON.
21911    let promote_sse = match decision {
21912        NegotiationDecision::PromoteToSse => {
21913            // D5 declared sse/ndjson always wins; D4 needs Accept.
21914            // Re-classify the underlying cause to distinguish:
21915            //   - axonendpoint with transport == sse/ndjson for this
21916            //     flow → D5 wins regardless of Accept
21917            //   - Otherwise PromoteToSse was returned because of the
21918            //     stream-effect predicate → D4 requires Accept (when
21919            //     `strict_mode == false`) OR fires unconditionally
21920            //     (when `strict_mode == true`).
21921            let has_force_decl = match program_opt {
21922                Some(ref program) => program.declarations.iter().any(|d| {
21923                    matches!(
21924                        d,
21925                        Declaration::AxonEndpoint(ae)
21926                            if ae.execute_flow == payload.flow
21927                                && (ae.transport == "sse" || ae.transport == "ndjson")
21928                    )
21929                }),
21930                None => source_text_has_force_decl(&source, &payload.flow),
21931            };
21932            // strict_mode upgrades the D4 fallback into a default:
21933            // any stream-effect flow (whose decision is PromoteToSse
21934            // because the inference fired) returns SSE without
21935            // requiring `Accept: text/event-stream`.
21936            has_force_decl || client_wants_sse || strict_mode
21937        }
21938        NegotiationDecision::StayJson => false,
21939    };
21940
21941    if promote_sse {
21942        // Adapt ExecuteRequest → StreamExecuteRequest.
21943        // §Fase 37.b — carry the request body across the JSON→SSE
21944        // content-negotiation promotion so the Request Binding
21945        // Contract holds on the promoted transport too.
21946        // §Fase 37.y — path + query carry too so the 3-source binder
21947        // sees the full set on the promoted transport.
21948        let stream_req = StreamExecuteRequest {
21949            flow_name: payload.flow,
21950            backend: payload.backend,
21951            request_body: payload.request_body,
21952            request_path: payload.request_path,
21953            request_query: payload.request_query,
21954        };
21955        return execute_sse_handler(State(state), headers, Json(stream_req))
21956            .await
21957            .into_response();
21958    }
21959
21960    // §Fase 31.e (D5) — diagnostic response header for legacy JSON
21961    // responses on stream-effect flows. The header makes the
21962    // language's type-driven inference observable at runtime when
21963    // the wire format is JSON either because the strict flag is off
21964    // (reason=flag_off) or because the adopter explicitly opted
21965    // out via `transport: json` (reason=declared_json — D3 sacred).
21966    //
21967    // Header is NEVER emitted on:
21968    //   * SSE responses (we promoted; wire is already streaming)
21969    //   * JSON responses for non-stream-effect flows (no inference
21970    //     fired; nothing to surface)
21971    //
21972    // Header text per plan vivo §7.2 verbatim:
21973    //   `X-Axon-Stream-Available: 1; reason=<flag_off|declared_json>;
21974    //    opt_in=transport:sse,Accept:text/event-stream`
21975    //
21976    // The flow name is also embedded so adopters debugging multi-
21977    // endpoint deployments can grep the diagnostic by flow.
21978    let flow_for_header = payload.flow.clone();
21979    let mut resp = execute_handler(State(state), headers, Json(payload))
21980        .await
21981        .into_response();
21982    let stream_evidence_present = flow_has_any_stream_evidence(
21983        program_opt.as_ref(), &source, &flow_for_header,
21984    );
21985    if stream_evidence_present {
21986        // We're serving JSON for a flow that genuinely has stream
21987        // effects. Distinguish the two reasons for the diagnostic.
21988        let declared_json = source_text_axonendpoint_has_transport(
21989            &source, &flow_for_header, "json",
21990        );
21991        let reason = if declared_json { "declared_json" } else { "flag_off" };
21992        let header_value = format!(
21993            "1; reason={reason}; flow={flow_for_header}; \
21994             opt_in=transport:sse,Accept:text/event-stream"
21995        );
21996        if let Ok(value) = axum::http::HeaderValue::from_str(&header_value) {
21997            resp.headers_mut()
21998                .insert("x-axon-stream-available", value);
21999        }
22000        // HeaderValue::from_str failing means the flow name carried
22001        // a CR/LF or other illegal byte — we silently skip the
22002        // header rather than crash the request. The diagnostic is
22003        // informational; never a hard contract.
22004    }
22005    resp
22006}
22007
22008/// §Fase 31.e — Does the flow named `flow_name` have any stream
22009/// effects, regardless of any `transport:` declaration the adopter
22010/// may have made? Used by `execute_handler_with_negotiation` to
22011/// decide whether to attach the `X-Axon-Stream-Available` header
22012/// to a JSON response.
22013///
22014/// Dual-signal predicate, matching the architecture of
22015/// `classify_negotiation_via_source_text` from Fase 30.e: AST walk
22016/// for the canonical path + source-text patterns as the defensive
22017/// fallback (catches the Rust frontend parser gaps for `output:
22018/// Stream<T>` inside step bodies + `use Tool()` at flow-body level).
22019///
22020/// CRITICALLY DIFFERENT from `classify_negotiation_via_source_text`:
22021/// this predicate **ignores** the `transport: json` declaration. We
22022/// WANT to know whether the flow genuinely produces a stream — the
22023/// declaration is what the adopter SAID, not what the flow IS. The
22024/// header is meaningful for the D3 opt-out case (adopter declared
22025/// json BUT the flow does have stream effects).
22026fn flow_has_any_stream_evidence(
22027    program_opt: Option<&crate::ast::Program>,
22028    source: &str,
22029    flow_name: &str,
22030) -> bool {
22031    // AST path — uses the Fase 30.e flow_produces_stream_runtime
22032    // helper which already covers disjuncts (a) + (b).
22033    if let Some(program) = program_opt {
22034        for decl in &program.declarations {
22035            if let Declaration::Flow(f) = decl {
22036                if f.name == flow_name && flow_produces_stream_runtime(f, program) {
22037                    return true;
22038                }
22039            }
22040        }
22041    }
22042    // Source-text fallback — covers Rust parser gaps + disjunct (c)
22043    // (`perform Stream.Yield(...)` which AST may not surface).
22044    let has_stream_output = source.contains("output: Stream<")
22045        || source.contains("output:Stream<");
22046    let has_stream_effect = source.contains("stream:drop_oldest")
22047        || source.contains("stream:degrade_quality")
22048        || source.contains("stream:pause_upstream")
22049        || source.contains("stream:fail");
22050    let has_stream_yield = source.contains("Stream.Yield(")
22051        || source.contains("Stream.Yield (");
22052    has_stream_output || has_stream_effect || has_stream_yield
22053}
22054
22055// ──────────────────────────────────────────────────────────────────────────
22056// §Fase 30.f — Keepalive comment emission for SSE responses (D6 ratified
22057//              2026-05-10 bloque). Plan vivo §2.4 + §6.2.
22058//
22059// W3C Server-Sent Events allows comment-only events of the shape
22060// `: <text>\n\n` which adopter EventSource clients silently discard but
22061// which DO count as on-the-wire traffic for load-balancer idle-timeout
22062// purposes. Without this, long-running flows (>idle-timeout: AWS ALB
22063// 60s default, CloudFlare 100s, GCP HTTPS LB 30s, nginx 60s) would have
22064// their TCP connection torn down by the LB before any `axon.token`
22065// event fires, breaking SSE consumption from production-grade adopters.
22066//
22067// D6 closed enum of intervals: `{5s, 15s, 30s, 60s}`. Default when the
22068// axonendpoint omits the `keepalive` field (or the entire axonendpoint
22069// is absent): 15s — comfortably below all common LB idle-timeouts while
22070// not flooding the wire on quiet flows.
22071//
22072// Implementation rationale: this sub-fase ALSO refactors
22073// `execute_sse_handler` from "synchronous-execute-then-emit" to
22074// "channel-fed-stream-with-spawn_blocking" so axum's `KeepAlive` has
22075// a real inactivity window to fire into. Before 30.f the executor
22076// blocked the handler for the entire flow duration — `Sse::keep_alive`
22077// would have had no opportunity to emit comments because the response
22078// body was never polled until execution completed.
22079//
22080// Wire emission (axum 0.8 `KeepAlive`):
22081//   `axum::response::sse::KeepAlive::new()
22082//        .interval(parse_keepalive_duration(declared))
22083//        .text("keepalive")`
22084//   → emits `: keepalive\n\n` after `interval` of stream inactivity.
22085//
22086// Cancel-safety: when the client disconnects, axum drops the Sse
22087// response → drops `rx` → the spawned task's `tx.send().await` returns
22088// SendError(Disconnected) → all subsequent sends become no-ops via
22089// `.ok()`. The spawn_blocking flow execution continues to completion
22090// (we don't have an abort signal yet — that's a future sub-fase) but
22091// the spawned task drops its tx without blocking on the dead client.
22092// ──────────────────────────────────────────────────────────────────────────
22093
22094/// Parse a `keepalive` declaration value from the D6 closed enum
22095/// `{"5s", "15s", "30s", "60s"}` into a `Duration`. Unknown or empty
22096/// values fall back to the D6 default of 15 seconds.
22097///
22098/// The closed enum is enforced upstream by the parser (Python + Rust
22099/// drift-gated). If the source ever reaches the runtime with an
22100/// out-of-enum value (e.g. a stale source-text patched outside the
22101/// parser path), the default keeps the response wire-compliant rather
22102/// than panicking.
22103pub fn parse_keepalive_duration(s: &str) -> std::time::Duration {
22104    match s.trim() {
22105        "5s" => std::time::Duration::from_secs(5),
22106        "15s" => std::time::Duration::from_secs(15),
22107        "30s" => std::time::Duration::from_secs(30),
22108        "60s" => std::time::Duration::from_secs(60),
22109        _ => std::time::Duration::from_secs(15),
22110    }
22111}
22112
22113/// Walk the parsed program for the named flow's axonendpoint
22114/// declaration and return its declared `keepalive` value verbatim
22115/// (the raw string — empty `""` when the field was omitted). Returns
22116/// `None` if no axonendpoint declaration targets this flow.
22117pub fn lookup_keepalive_from_program(
22118    program: &crate::ast::Program,
22119    flow_name: &str,
22120) -> Option<String> {
22121    for decl in &program.declarations {
22122        if let Declaration::AxonEndpoint(ae) = decl {
22123            if ae.execute_flow == flow_name {
22124                return Some(ae.keepalive.clone());
22125            }
22126        }
22127    }
22128    None
22129}
22130
22131/// Defensive source-text lookup for the `keepalive` value declared
22132/// on the axonendpoint that executes `flow_name`. Mirror of
22133/// `source_text_axonendpoint_has_transport` (30.e) but extracts the
22134/// value rather than matching against one. Returns the declared
22135/// value (one of "5s"/"15s"/"30s"/"60s") or `None` if no matching
22136/// axonendpoint block declares a keepalive.
22137///
22138/// String-aware brace counter avoids false positives inside
22139/// `path: "..."` literals; iteration over the closed enum candidates
22140/// avoids false positives from substring matches (e.g. `keepalive:
22141/// 150s` would never match `keepalive: 5s`).
22142pub fn source_text_axonendpoint_keepalive(
22143    source: &str,
22144    flow_name: &str,
22145) -> Option<String> {
22146    let bytes = source.as_bytes();
22147    let kw = b"axonendpoint";
22148    let mut i = 0;
22149    while i + kw.len() <= bytes.len() {
22150        if &bytes[i..i + kw.len()] == kw {
22151            let body_start = source[i..].find('{').map(|off| i + off + 1);
22152            if let Some(start) = body_start {
22153                let mut depth: i32 = 1;
22154                let mut j = start;
22155                let mut in_string = false;
22156                while j < bytes.len() && depth > 0 {
22157                    let c = bytes[j];
22158                    match c {
22159                        b'"' if !in_string => in_string = true,
22160                        b'"' if in_string => in_string = false,
22161                        b'\\' if in_string => {
22162                            j += 1;
22163                        }
22164                        b'{' if !in_string => depth += 1,
22165                        b'}' if !in_string => depth -= 1,
22166                        _ => {}
22167                    }
22168                    j += 1;
22169                }
22170                let body = &source[start..j.saturating_sub(1)];
22171                let has_execute = body.contains(&format!("execute: {flow_name}"))
22172                    || body.contains(&format!("execute:{flow_name}"));
22173                if has_execute {
22174                    for candidate in ["5s", "15s", "30s", "60s"] {
22175                        let pat_space = format!("keepalive: {candidate}");
22176                        let pat_tight = format!("keepalive:{candidate}");
22177                        // Tail-anchored match: ensure the candidate is
22178                        // not a prefix of a longer literal (e.g. "5s"
22179                        // inside "15s"). The character immediately
22180                        // after the match must NOT be alphanumeric.
22181                        if substring_with_word_boundary(body, &pat_space)
22182                            || substring_with_word_boundary(body, &pat_tight)
22183                        {
22184                            return Some(candidate.to_string());
22185                        }
22186                    }
22187                }
22188                i = j;
22189                continue;
22190            }
22191        }
22192        i += 1;
22193    }
22194    None
22195}
22196
22197/// `haystack` contains `needle`, and the character immediately after
22198/// the match is not alphanumeric (so `"5s"` doesn't match inside
22199/// `"15s"`). Pure helper — pure ASCII semantics suffice because
22200/// `keepalive` values are always ASCII per the closed enum.
22201fn substring_with_word_boundary(haystack: &str, needle: &str) -> bool {
22202    let bytes = haystack.as_bytes();
22203    let needle_bytes = needle.as_bytes();
22204    if needle_bytes.is_empty() || bytes.len() < needle_bytes.len() {
22205        return false;
22206    }
22207    for i in 0..=bytes.len() - needle_bytes.len() {
22208        if &bytes[i..i + needle_bytes.len()] == needle_bytes {
22209            let after = i + needle_bytes.len();
22210            if after >= bytes.len() {
22211                return true;
22212            }
22213            let next = bytes[after];
22214            if !(next.is_ascii_alphanumeric() || next == b'_') {
22215                return true;
22216            }
22217        }
22218    }
22219    false
22220}
22221
22222/// Resolve the keepalive `Duration` for an SSE response on
22223/// `flow_name`. Dual-signal — AST walk authoritative when the source
22224/// parses cleanly; source-text fallback when the Rust frontend has
22225/// parser gaps (30.e rationale: same architecture). D6 default of 15s
22226/// when no declaration is found anywhere.
22227///
22228/// This is the single canonical entry point used by
22229/// `execute_sse_handler` to decide the `KeepAlive::interval(...)`
22230/// argument before constructing the Sse response.
22231pub fn resolve_keepalive_for_flow(source: &str, flow_name: &str) -> std::time::Duration {
22232    let program_opt = crate::lexer::Lexer::new(source, "<runtime-keepalive-lookup>")
22233        .tokenize()
22234        .ok()
22235        .and_then(|tokens| crate::parser::Parser::new(tokens).parse().ok());
22236    if let Some(program) = program_opt {
22237        if let Some(declared) = lookup_keepalive_from_program(&program, flow_name) {
22238            if !declared.is_empty() {
22239                return parse_keepalive_duration(&declared);
22240            }
22241        }
22242    }
22243    if let Some(declared) = source_text_axonendpoint_keepalive(source, flow_name) {
22244        return parse_keepalive_duration(&declared);
22245    }
22246    std::time::Duration::from_secs(15)
22247}
22248
22249/// ΛD Epistemic Envelope — wraps any config value with its epistemic tensor.
22250///
22251/// From the paper: ψ = ⟨T, V, E⟩ where E = ⟨c, τ, ρ, δ⟩
22252///
22253/// Instead of π_JSON(ψ) = V (lossy projection that discards T and E),
22254/// this preserves the full epistemic state across serialization boundaries.
22255#[derive(Debug, Clone, Serialize, Deserialize)]
22256pub struct EpistemicEnvelope {
22257    /// T — Ontological type (what kind of config this is).
22258    pub ontology: String,
22259    /// c ∈ [0, 1] — Certainty scalar.
22260    pub certainty: f64,
22261    /// τ_start — Temporal validity start (ISO 8601 or "epoch").
22262    pub temporal_start: String,
22263    /// τ_end — Temporal validity end (ISO 8601 or "∞").
22264    pub temporal_end: String,
22265    /// ρ — Provenance (who/what produced this value).
22266    pub provenance: String,
22267    /// δ ∈ Δ — Derivation: raw | derived | inferred | aggregated | transformed.
22268    pub derivation: String,
22269}
22270
22271impl EpistemicEnvelope {
22272    /// Create envelope for a raw admin-configured value (c=1.0, δ=raw).
22273    pub fn raw_config(ontology: &str, provenance: &str) -> Self {
22274        let now = std::time::SystemTime::now()
22275            .duration_since(std::time::UNIX_EPOCH)
22276            .unwrap_or_default()
22277            .as_secs();
22278        EpistemicEnvelope {
22279            ontology: ontology.to_string(),
22280            certainty: 1.0,
22281            temporal_start: now.to_string(),
22282            temporal_end: "∞".to_string(),
22283            provenance: provenance.to_string(),
22284            derivation: "raw".to_string(),
22285        }
22286    }
22287
22288    /// Create envelope for a derived/computed value (c<1.0, δ=derived).
22289    pub fn derived(ontology: &str, certainty: f64, provenance: &str) -> Self {
22290        let now = std::time::SystemTime::now()
22291            .duration_since(std::time::UNIX_EPOCH)
22292            .unwrap_or_default()
22293            .as_secs();
22294        EpistemicEnvelope {
22295            ontology: ontology.to_string(),
22296            certainty: certainty.clamp(0.0, 0.99), // Theorem 5.1: only raw may carry c=1.0
22297            temporal_start: now.to_string(),
22298            temporal_end: "∞".to_string(),
22299            provenance: provenance.to_string(),
22300            derivation: "derived".to_string(),
22301        }
22302    }
22303
22304    /// Validate ΛD invariants at serialization boundary.
22305    pub fn validate(&self) -> Result<(), String> {
22306        // Invariant 1: Ontological Rigidity (T ≠ ⊥)
22307        if self.ontology.is_empty() {
22308            return Err("ΛD Invariant 1 (Ontological Rigidity): ontology is empty (T = ⊥)".into());
22309        }
22310        // Invariant 4: Epistemic Bounding (c ∈ [0,1])
22311        if self.certainty < 0.0 || self.certainty > 1.0 {
22312            return Err(format!("ΛD Invariant 4 (Epistemic Bounding): c={} not in [0,1]", self.certainty));
22313        }
22314        // Theorem 5.1: Epistemic Degradation (only raw may carry c=1.0)
22315        if self.certainty == 1.0 && self.derivation != "raw" {
22316            return Err(format!("ΛD Theorem 5.1 (Epistemic Degradation): c=1.0 with δ={}, only raw may carry absolute certainty", self.derivation));
22317        }
22318        Ok(())
22319    }
22320}
22321
22322// ── AxonStore — Durable Cognitive Persistence ──────────────────────────────
22323//
22324// `axonstore` is a top-level cognitive primitive (one of 47). It provides
22325// named, typed, epistemic-aware key-value persistence with ΛD envelopes.
22326//
22327// Each entry carries an EpistemicEnvelope: ψ = ⟨T, V, E⟩ where E = ⟨c, τ, ρ, δ⟩
22328// - persist: c=1.0, δ=raw (direct user/system write)
22329// - mutate:  c clamped ≤0.99, δ=derived (Theorem 5.1)
22330// - transact: batch atomic ops, each entry gets its own envelope
22331//
22332// Storage: file-backed JSON, one file per named store.
22333
22334/// A single entry in an AxonStore, carrying ΛD epistemic state.
22335#[derive(Debug, Clone, Serialize, Deserialize)]
22336pub struct AxonStoreEntry {
22337    /// Key name.
22338    pub key: String,
22339    /// Stored value (JSON-encoded).
22340    pub value: serde_json::Value,
22341    /// ΛD epistemic envelope for this entry.
22342    pub envelope: EpistemicEnvelope,
22343    /// Unix timestamp of creation.
22344    pub created_at: u64,
22345    /// Unix timestamp of last modification.
22346    pub updated_at: u64,
22347    /// Number of mutations applied.
22348    pub version: u64,
22349}
22350
22351/// A named AxonStore instance — durable, epistemic-aware key-value store.
22352#[derive(Debug, Clone, Serialize, Deserialize)]
22353pub struct AxonStoreInstance {
22354    /// Store name (unique identifier).
22355    pub name: String,
22356    /// Ontological type hint for this store's domain.
22357    pub ontology: String,
22358    /// Entries keyed by name.
22359    pub entries: HashMap<String, AxonStoreEntry>,
22360    /// Unix timestamp of store creation.
22361    pub created_at: u64,
22362    /// Total operations performed on this store.
22363    pub total_ops: u64,
22364}
22365
22366/// A single operation in a transact batch.
22367#[derive(Debug, Clone, Deserialize)]
22368pub struct AxonStoreTransactOp {
22369    /// Operation: "persist", "mutate", "purge".
22370    pub op: String,
22371    /// Key to operate on.
22372    pub key: String,
22373    /// Value (required for persist/mutate, ignored for purge).
22374    #[serde(default)]
22375    pub value: serde_json::Value,
22376}
22377
22378// ── Dataspace — Cognitive Data Navigation ──────────────────────────────────
22379//
22380// `dataspace` is a top-level cognitive primitive (one of 47) providing
22381// a named data container with 5 navigation primitives:
22382//   - ingest:    add data items with ΛD envelopes (c=1.0, δ=raw)
22383//   - focus:     filter entries by predicate (returns subset, c degraded)
22384//   - associate: link two entries by named relation
22385//   - aggregate: reduce entries to a single value (count/sum/avg/min/max)
22386//   - explore:   discover structure (entry count, associations, ontology map)
22387//
22388// Each entry carries an EpistemicEnvelope: ψ = ⟨T, V, E⟩
22389// Focus results degrade certainty (δ=derived, c≤0.99) per Theorem 5.1
22390// because filtering is a derived computation over raw data.
22391
22392/// A single entry in a Dataspace.
22393#[derive(Debug, Clone, Serialize, Deserialize)]
22394pub struct DataspaceEntry {
22395    /// Unique entry identifier.
22396    pub id: String,
22397    /// Ontological type tag for this entry.
22398    pub ontology: String,
22399    /// Entry payload (arbitrary JSON).
22400    pub data: serde_json::Value,
22401    /// ΛD epistemic envelope.
22402    pub envelope: EpistemicEnvelope,
22403    /// Unix timestamp of ingestion.
22404    pub ingested_at: u64,
22405    /// Tags for filtering and grouping.
22406    pub tags: Vec<String>,
22407}
22408
22409/// An association between two entries in a Dataspace.
22410#[derive(Debug, Clone, Serialize, Deserialize)]
22411pub struct DataspaceAssociation {
22412    /// Source entry ID.
22413    pub from: String,
22414    /// Target entry ID.
22415    pub to: String,
22416    /// Relation name (e.g., "causes", "supports", "contradicts").
22417    pub relation: String,
22418    /// Certainty of the association (c ∈ [0,1]).
22419    pub certainty: f64,
22420    /// Unix timestamp of association creation.
22421    pub created_at: u64,
22422}
22423
22424/// A named Dataspace instance — cognitive data container.
22425#[derive(Debug, Clone, Serialize, Deserialize)]
22426pub struct DataspaceInstance {
22427    /// Dataspace name (unique identifier).
22428    pub name: String,
22429    /// Domain ontology for this dataspace.
22430    pub ontology: String,
22431    /// Entries keyed by ID.
22432    pub entries: HashMap<String, DataspaceEntry>,
22433    /// Associations between entries.
22434    pub associations: Vec<DataspaceAssociation>,
22435    /// Unix timestamp of creation.
22436    pub created_at: u64,
22437    /// Total operations performed.
22438    pub total_ops: u64,
22439    /// Auto-incrementing ID counter.
22440    pub next_id: u64,
22441}
22442
22443// ── Shield — cognitive guardrail primitive ──────────────────────────────────
22444
22445/// A single guardrail rule within a Shield instance.
22446///
22447/// Rules are evaluated in order against input/output text. Each rule carries
22448/// a kind (pattern, deny_list, pii, length) and an action (block, warn, redact).
22449/// ΛD alignment: shield evaluation is derived (c≤0.99) because pattern matching
22450/// is an approximation — the shield *speculates* about harmful content.
22451#[derive(Debug, Clone, Serialize, Deserialize)]
22452pub struct ShieldRule {
22453    /// Rule identifier (unique within the shield).
22454    pub id: String,
22455    /// Rule kind: "pattern" (regex-like), "deny_list" (exact substring), "pii" (pattern set), "length" (max chars).
22456    pub kind: String,
22457    /// The matching criterion: a pattern string, deny word, PII type, or max length.
22458    pub value: String,
22459    /// Action when rule triggers: "block" (reject), "warn" (flag but allow), "redact" (mask matched text).
22460    pub action: String,
22461    /// Whether this rule is active.
22462    pub enabled: bool,
22463    /// Human-readable description.
22464    pub description: String,
22465}
22466
22467/// Result of evaluating a Shield against input or output text.
22468#[derive(Debug, Clone, Serialize)]
22469pub struct ShieldResult {
22470    /// Whether the content was blocked.
22471    pub blocked: bool,
22472    /// Warnings generated (rule IDs that matched with action=warn).
22473    pub warnings: Vec<String>,
22474    /// Redactions applied (rule IDs that matched with action=redact).
22475    pub redactions: Vec<String>,
22476    /// The (possibly redacted) content after shield processing.
22477    pub content: String,
22478    /// Total rules evaluated.
22479    pub rules_evaluated: u32,
22480    /// Total rules triggered.
22481    pub rules_triggered: u32,
22482}
22483
22484/// A named Shield instance — a collection of guardrail rules.
22485///
22486/// Shields are declared per-flow or globally. The `evaluate()` method applies
22487/// all enabled rules in order, accumulating block/warn/redact results.
22488/// Epistemic alignment: ψ = ⟨T="guardrail", V=result, E=⟨c≤0.99, τ, ρ, δ=derived⟩⟩
22489#[derive(Debug, Clone, Serialize, Deserialize)]
22490pub struct ShieldInstance {
22491    /// Shield name (unique identifier).
22492    pub name: String,
22493    /// Shield mode: "input" (pre-execution), "output" (post-execution), "both".
22494    pub mode: String,
22495    /// Ordered list of guardrail rules.
22496    pub rules: Vec<ShieldRule>,
22497    /// Unix timestamp of creation.
22498    pub created_at: u64,
22499    /// Total evaluations performed.
22500    pub total_evaluations: u64,
22501    /// Total blocks applied.
22502    pub total_blocks: u64,
22503}
22504
22505impl ShieldInstance {
22506    /// Evaluate content against all enabled rules in this shield.
22507    pub fn evaluate(&self, content: &str) -> ShieldResult {
22508        let mut blocked = false;
22509        let mut warnings = Vec::new();
22510        let mut redactions = Vec::new();
22511        let mut result_content = content.to_string();
22512        let mut rules_evaluated = 0u32;
22513        let mut rules_triggered = 0u32;
22514
22515        for rule in &self.rules {
22516            if !rule.enabled {
22517                continue;
22518            }
22519            rules_evaluated += 1;
22520
22521            let matched = match rule.kind.as_str() {
22522                "deny_list" => {
22523                    // Case-insensitive substring match
22524                    result_content.to_lowercase().contains(&rule.value.to_lowercase())
22525                }
22526                "pattern" => {
22527                    // Simple pattern matching: supports * as wildcard
22528                    let pattern_lower = rule.value.to_lowercase();
22529                    let content_lower = result_content.to_lowercase();
22530                    if pattern_lower.contains('*') {
22531                        let parts: Vec<&str> = pattern_lower.split('*').collect();
22532                        if parts.len() == 2 {
22533                            content_lower.contains(parts[0]) && content_lower.contains(parts[1])
22534                        } else {
22535                            content_lower.contains(&pattern_lower.replace('*', ""))
22536                        }
22537                    } else {
22538                        content_lower.contains(&pattern_lower)
22539                    }
22540                }
22541                "pii" => {
22542                    // PII detection heuristics based on value type
22543                    match rule.value.as_str() {
22544                        "email" => result_content.contains('@') && result_content.contains('.'),
22545                        "phone" => {
22546                            let digits: String = result_content.chars().filter(|c| c.is_ascii_digit()).collect();
22547                            digits.len() >= 10
22548                        }
22549                        "ssn" => {
22550                            // Simple SSN pattern: 3 digits, separator, 2 digits, separator, 4 digits
22551                            let cleaned: String = result_content.chars().filter(|c| c.is_ascii_digit() || *c == '-').collect();
22552                            cleaned.split('-').count() == 3 && cleaned.replace('-', "").len() == 9
22553                        }
22554                        _ => false,
22555                    }
22556                }
22557                "length" => {
22558                    // Content exceeds max length
22559                    if let Ok(max_len) = rule.value.parse::<usize>() {
22560                        result_content.len() > max_len
22561                    } else {
22562                        false
22563                    }
22564                }
22565                _ => false,
22566            };
22567
22568            if matched {
22569                rules_triggered += 1;
22570                match rule.action.as_str() {
22571                    "block" => {
22572                        blocked = true;
22573                    }
22574                    "warn" => {
22575                        warnings.push(rule.id.clone());
22576                    }
22577                    "redact" => {
22578                        redactions.push(rule.id.clone());
22579                        // Replace matched content based on kind
22580                        match rule.kind.as_str() {
22581                            "deny_list" => {
22582                                let lower = result_content.to_lowercase();
22583                                let pattern_lower = rule.value.to_lowercase();
22584                                if let Some(pos) = lower.find(&pattern_lower) {
22585                                    let mask = "█".repeat(rule.value.len());
22586                                    result_content = format!(
22587                                        "{}{}{}",
22588                                        &result_content[..pos],
22589                                        mask,
22590                                        &result_content[pos + rule.value.len()..]
22591                                    );
22592                                }
22593                            }
22594                            "pii" => {
22595                                result_content = format!("[{} REDACTED]", rule.value.to_uppercase());
22596                            }
22597                            _ => {}
22598                        }
22599                    }
22600                    _ => {}
22601                }
22602            }
22603        }
22604
22605        ShieldResult {
22606            blocked,
22607            warnings,
22608            redactions,
22609            content: result_content,
22610            rules_evaluated,
22611            rules_triggered,
22612        }
22613    }
22614}
22615
22616// ── Corpus — document corpus management primitive ───────────────────────────
22617
22618/// A document within a Corpus instance.
22619///
22620/// Each document carries its own ΛD epistemic envelope reflecting provenance:
22621/// - Ingested raw → c=1.0, δ=raw (the document itself is ground truth)
22622/// - Search results → c≤0.99, δ=derived (relevance scoring is approximate)
22623/// - Citations → c≤0.99, δ=derived (extracted reference is interpretation)
22624#[derive(Debug, Clone, Serialize, Deserialize)]
22625pub struct CorpusDocument {
22626    /// Document identifier (unique within corpus).
22627    pub id: String,
22628    /// Document title or label.
22629    pub title: String,
22630    /// Document content (full text).
22631    pub content: String,
22632    /// Metadata tags for filtering.
22633    pub tags: Vec<String>,
22634    /// Source provenance (URL, file path, or "manual").
22635    pub source: String,
22636    /// ΛD epistemic envelope for this document.
22637    pub envelope: EpistemicEnvelope,
22638    /// Unix timestamp of ingestion.
22639    pub ingested_at: u64,
22640    /// Word count (computed on ingest).
22641    pub word_count: u64,
22642}
22643
22644/// A citation extracted from a corpus search — a reference to a document passage.
22645#[derive(Debug, Clone, Serialize)]
22646pub struct CorpusCitation {
22647    /// Document ID referenced.
22648    pub document_id: String,
22649    /// Document title.
22650    pub title: String,
22651    /// Matched excerpt from the document.
22652    pub excerpt: String,
22653    /// Relevance score (0.0–1.0).
22654    pub relevance: f64,
22655    /// ΛD: citation is derived (Theorem 5.1: c≤0.99, δ=derived).
22656    pub envelope: EpistemicEnvelope,
22657}
22658
22659/// A named Corpus instance — a collection of documents with search and citation.
22660///
22661/// Epistemic alignment: ψ = ⟨T="corpus", V=documents, E=⟨c, τ, ρ, δ⟩⟩
22662/// - Ingest: raw provenance (c=1.0)
22663/// - Search: derived relevance ranking (c≤0.99)
22664/// - Cite: derived excerpt extraction (c≤0.99)
22665#[derive(Debug, Clone, Serialize, Deserialize)]
22666pub struct CorpusInstance {
22667    /// Corpus name (unique identifier).
22668    pub name: String,
22669    /// Domain ontology for this corpus.
22670    pub ontology: String,
22671    /// Documents keyed by ID.
22672    pub documents: HashMap<String, CorpusDocument>,
22673    /// Unix timestamp of creation.
22674    pub created_at: u64,
22675    /// Total operations performed.
22676    pub total_ops: u64,
22677    /// Auto-incrementing ID counter.
22678    pub next_id: u64,
22679}
22680
22681// ── Compute — numeric/symbolic computation primitive ────────────────────────
22682
22683/// Result of a compute evaluation.
22684///
22685/// ΛD alignment: computation results are deterministic when purely numeric
22686/// (c=1.0, δ=raw for exact arithmetic), but symbolic/approximate operations
22687/// carry c≤0.99, δ=derived per Theorem 5.1.
22688#[derive(Debug, Clone, Serialize)]
22689pub struct ComputeResult {
22690    /// The computed value.
22691    pub value: f64,
22692    /// The original expression.
22693    pub expression: String,
22694    /// Whether the computation was exact (integer arithmetic) or approximate (floating point).
22695    pub exact: bool,
22696    /// Variables substituted during evaluation.
22697    pub variables: HashMap<String, f64>,
22698    /// ΛD: certainty (1.0 for exact, 0.99 for approximate).
22699    pub certainty: f64,
22700    /// ΛD: derivation ("raw" for exact, "derived" for approximate).
22701    pub derivation: String,
22702}
22703
22704/// Evaluate a simple arithmetic expression with variables.
22705///
22706/// Supports: +, -, *, /, %, ^ (power), parentheses, and named variables.
22707/// Functions: sqrt, abs, sin, cos, log, exp, ceil, floor, round, min, max.
22708/// Constants: pi, e, tau.
22709pub fn compute_evaluate(expr: &str, variables: &HashMap<String, f64>) -> Result<ComputeResult, String> {
22710    let expr_trimmed = expr.trim();
22711    if expr_trimmed.is_empty() {
22712        return Err("empty expression".into());
22713    }
22714
22715    // Tokenize and evaluate using a simple recursive descent approach
22716    // For safety and correctness, we use an iterative shunting-yard algorithm
22717    let tokens = compute_tokenize(expr_trimmed, variables)?;
22718    let value = compute_eval_tokens(&tokens)?;
22719
22720    // Determine if result is exact (no floating point operations involved)
22721    let is_exact = value.fract() == 0.0 && !expr_trimmed.contains('.')
22722        && !expr_trimmed.contains("sqrt") && !expr_trimmed.contains("sin")
22723        && !expr_trimmed.contains("cos") && !expr_trimmed.contains("log")
22724        && !expr_trimmed.contains("exp") && !expr_trimmed.contains("pi")
22725        && !expr_trimmed.contains("tau") && !expr_trimmed.contains('/');
22726
22727    Ok(ComputeResult {
22728        value,
22729        expression: expr_trimmed.to_string(),
22730        exact: is_exact,
22731        variables: variables.clone(),
22732        certainty: if is_exact { 1.0 } else { 0.99 },
22733        derivation: if is_exact { "raw".into() } else { "derived".into() },
22734    })
22735}
22736
22737/// Token types for the expression evaluator.
22738#[derive(Debug, Clone)]
22739enum ComputeToken {
22740    Number(f64),
22741    Op(char),
22742    LParen,
22743    RParen,
22744    Func(String),
22745}
22746
22747fn compute_tokenize(expr: &str, variables: &HashMap<String, f64>) -> Result<Vec<ComputeToken>, String> {
22748    let mut tokens = Vec::new();
22749    let mut chars = expr.chars().peekable();
22750
22751    while let Some(&ch) = chars.peek() {
22752        match ch {
22753            ' ' | '\t' => { chars.next(); }
22754            '0'..='9' | '.' => {
22755                let mut num_str = String::new();
22756                while let Some(&c) = chars.peek() {
22757                    if c.is_ascii_digit() || c == '.' { num_str.push(c); chars.next(); }
22758                    else { break; }
22759                }
22760                let val: f64 = num_str.parse().map_err(|_| format!("invalid number: {}", num_str))?;
22761                tokens.push(ComputeToken::Number(val));
22762            }
22763            '+' | '-' => {
22764                // Handle unary minus/plus
22765                let is_unary = tokens.is_empty()
22766                    || matches!(tokens.last(), Some(ComputeToken::Op(_)) | Some(ComputeToken::LParen));
22767                if is_unary && ch == '-' {
22768                    chars.next();
22769                    // Read the next number or insert -1 *
22770                    if let Some(&next) = chars.peek() {
22771                        if next.is_ascii_digit() || next == '.' {
22772                            let mut num_str = String::from("-");
22773                            while let Some(&c) = chars.peek() {
22774                                if c.is_ascii_digit() || c == '.' { num_str.push(c); chars.next(); }
22775                                else { break; }
22776                            }
22777                            let val: f64 = num_str.parse().map_err(|_| format!("invalid number: {}", num_str))?;
22778                            tokens.push(ComputeToken::Number(val));
22779                        } else if next.is_alphabetic() {
22780                            // unary minus before variable/function: push -1 *
22781                            tokens.push(ComputeToken::Number(-1.0));
22782                            tokens.push(ComputeToken::Op('*'));
22783                        } else if next == '(' {
22784                            tokens.push(ComputeToken::Number(-1.0));
22785                            tokens.push(ComputeToken::Op('*'));
22786                        } else {
22787                            return Err(format!("unexpected character after unary minus: {}", next));
22788                        }
22789                    }
22790                } else if is_unary && ch == '+' {
22791                    chars.next(); // skip unary plus
22792                } else {
22793                    tokens.push(ComputeToken::Op(ch));
22794                    chars.next();
22795                }
22796            }
22797            '*' | '/' | '%' | '^' => {
22798                tokens.push(ComputeToken::Op(ch));
22799                chars.next();
22800            }
22801            '(' => { tokens.push(ComputeToken::LParen); chars.next(); }
22802            ')' => { tokens.push(ComputeToken::RParen); chars.next(); }
22803            'a'..='z' | 'A'..='Z' | '_' => {
22804                let mut ident = String::new();
22805                while let Some(&c) = chars.peek() {
22806                    if c.is_alphanumeric() || c == '_' { ident.push(c); chars.next(); }
22807                    else { break; }
22808                }
22809                // Check for constants
22810                match ident.as_str() {
22811                    "pi" => tokens.push(ComputeToken::Number(std::f64::consts::PI)),
22812                    "e" => tokens.push(ComputeToken::Number(std::f64::consts::E)),
22813                    "tau" => tokens.push(ComputeToken::Number(std::f64::consts::TAU)),
22814                    _ => {
22815                        // Check for variables
22816                        if let Some(&val) = variables.get(&ident) {
22817                            tokens.push(ComputeToken::Number(val));
22818                        } else if matches!(ident.as_str(), "sqrt" | "abs" | "sin" | "cos" | "log" | "exp" | "ceil" | "floor" | "round" | "min" | "max") {
22819                            tokens.push(ComputeToken::Func(ident));
22820                        } else {
22821                            return Err(format!("unknown variable or function: {}", ident));
22822                        }
22823                    }
22824                }
22825            }
22826            ',' => { chars.next(); /* skip commas in function args, handle as separator */ }
22827            _ => return Err(format!("unexpected character: {}", ch)),
22828        }
22829    }
22830
22831    Ok(tokens)
22832}
22833
22834fn compute_eval_tokens(tokens: &[ComputeToken]) -> Result<f64, String> {
22835    // Shunting-yard algorithm for operator precedence
22836    let mut output: Vec<f64> = Vec::new();
22837    let mut ops: Vec<ComputeToken> = Vec::new();
22838
22839    fn precedence(op: char) -> u8 {
22840        match op {
22841            '+' | '-' => 1,
22842            '*' | '/' | '%' => 2,
22843            '^' => 3,
22844            _ => 0,
22845        }
22846    }
22847
22848    fn apply_op(op: char, b: f64, a: f64) -> Result<f64, String> {
22849        match op {
22850            '+' => Ok(a + b),
22851            '-' => Ok(a - b),
22852            '*' => Ok(a * b),
22853            '/' => if b == 0.0 { Err("division by zero".into()) } else { Ok(a / b) },
22854            '%' => if b == 0.0 { Err("modulo by zero".into()) } else { Ok(a % b) },
22855            '^' => Ok(a.powf(b)),
22856            _ => Err(format!("unknown operator: {}", op)),
22857        }
22858    }
22859
22860    fn apply_func(name: &str, val: f64) -> Result<f64, String> {
22861        match name {
22862            "sqrt" => if val < 0.0 { Err("sqrt of negative".into()) } else { Ok(val.sqrt()) },
22863            "abs" => Ok(val.abs()),
22864            "sin" => Ok(val.sin()),
22865            "cos" => Ok(val.cos()),
22866            "log" => if val <= 0.0 { Err("log of non-positive".into()) } else { Ok(val.ln()) },
22867            "exp" => Ok(val.exp()),
22868            "ceil" => Ok(val.ceil()),
22869            "floor" => Ok(val.floor()),
22870            "round" => Ok(val.round()),
22871            _ => Err(format!("unknown function: {}", name)),
22872        }
22873    }
22874
22875    for token in tokens {
22876        match token {
22877            ComputeToken::Number(n) => output.push(*n),
22878            ComputeToken::Func(name) => ops.push(ComputeToken::Func(name.clone())),
22879            ComputeToken::LParen => ops.push(ComputeToken::LParen),
22880            ComputeToken::RParen => {
22881                while let Some(top) = ops.last() {
22882                    match top {
22883                        ComputeToken::LParen => { ops.pop(); break; }
22884                        ComputeToken::Op(op) => {
22885                            let op = *op;
22886                            ops.pop();
22887                            if output.len() < 2 { return Err("malformed expression".into()); }
22888                            let b = output.pop().unwrap();
22889                            let a = output.pop().unwrap();
22890                            output.push(apply_op(op, b, a)?);
22891                        }
22892                        _ => break,
22893                    }
22894                }
22895                // Check if top of ops is a function
22896                if let Some(ComputeToken::Func(name)) = ops.last().cloned() {
22897                    ops.pop();
22898                    if output.is_empty() { return Err("missing function argument".into()); }
22899                    let val = output.pop().unwrap();
22900                    output.push(apply_func(&name, val)?);
22901                }
22902            }
22903            ComputeToken::Op(op) => {
22904                while let Some(top) = ops.last() {
22905                    if let ComputeToken::Op(top_op) = top {
22906                        let top_op = *top_op;
22907                        if precedence(top_op) >= precedence(*op) && *op != '^' {
22908                            ops.pop();
22909                            if output.len() < 2 { return Err("malformed expression".into()); }
22910                            let b = output.pop().unwrap();
22911                            let a = output.pop().unwrap();
22912                            output.push(apply_op(top_op, b, a)?);
22913                        } else {
22914                            break;
22915                        }
22916                    } else {
22917                        break;
22918                    }
22919                }
22920                ops.push(ComputeToken::Op(*op));
22921            }
22922        }
22923    }
22924
22925    // Flush remaining ops
22926    while let Some(top) = ops.pop() {
22927        if let ComputeToken::Op(op) = top {
22928            if output.len() < 2 { return Err("malformed expression".into()); }
22929            let b = output.pop().unwrap();
22930            let a = output.pop().unwrap();
22931            output.push(apply_op(op, b, a)?);
22932        }
22933    }
22934
22935    output.pop().ok_or_else(|| "empty expression".to_string())
22936}
22937
22938// ── Mandate — authorization/permission primitive ────────────────────────────
22939
22940/// A single permission rule within a Mandate policy.
22941///
22942/// Rules match against (subject, action, resource) triples and yield
22943/// allow or deny decisions. Evaluation is deterministic: c=1.0, δ=raw
22944/// when a rule explicitly matches; c=0.99, δ=derived for default-deny.
22945#[derive(Debug, Clone, Serialize, Deserialize)]
22946pub struct MandateRule {
22947    /// Rule identifier (unique within policy).
22948    pub id: String,
22949    /// Subject pattern: role name, "*" for any, or specific principal.
22950    pub subject: String,
22951    /// Action pattern: operation name, "*" for any.
22952    pub action: String,
22953    /// Resource pattern: resource path, "*" for any, prefix match with trailing "*".
22954    pub resource: String,
22955    /// Effect: "allow" or "deny".
22956    pub effect: String,
22957    /// Priority (higher = evaluated first, 0 = default).
22958    pub priority: u32,
22959    /// Whether this rule is active.
22960    pub enabled: bool,
22961}
22962
22963/// Result of evaluating a Mandate policy against a request.
22964#[derive(Debug, Clone, Serialize)]
22965pub struct MandateEvaluation {
22966    /// Whether the request is allowed.
22967    pub allowed: bool,
22968    /// The rule that matched (None if default deny).
22969    pub matched_rule: Option<String>,
22970    /// The effect applied: "allow", "deny", or "default_deny".
22971    pub effect: String,
22972    /// Total rules evaluated.
22973    pub rules_evaluated: u32,
22974    /// ΛD certainty: 1.0 if explicit rule matched, 0.99 for default deny.
22975    pub certainty: f64,
22976    /// ΛD derivation: "raw" if explicit, "derived" if default.
22977    pub derivation: String,
22978}
22979
22980/// A named Mandate policy — a collection of authorization rules.
22981///
22982/// Mandate evaluation follows a priority-ordered, first-match-wins model:
22983/// 1. Rules sorted by priority (descending)
22984/// 2. First matching rule determines the outcome
22985/// 3. If no rule matches → default deny (c=0.99, δ=derived)
22986///
22987/// ΛD alignment: explicit rule matches are deterministic (c=1.0, δ=raw),
22988/// while default deny is inferential (c=0.99, δ=derived per Theorem 5.1).
22989#[derive(Debug, Clone, Serialize, Deserialize)]
22990pub struct MandatePolicy {
22991    /// Policy name (unique identifier).
22992    pub name: String,
22993    /// Policy description.
22994    pub description: String,
22995    /// Ordered list of authorization rules.
22996    pub rules: Vec<MandateRule>,
22997    /// Unix timestamp of creation.
22998    pub created_at: u64,
22999    /// Total evaluations performed.
23000    pub total_evaluations: u64,
23001    /// Total denials.
23002    pub total_denials: u64,
23003}
23004
23005impl MandatePolicy {
23006    /// Evaluate a request (subject, action, resource) against this policy.
23007    /// First-match-wins with priority ordering. Default: deny.
23008    pub fn evaluate(&self, subject: &str, action: &str, resource: &str) -> MandateEvaluation {
23009        let mut sorted_rules: Vec<&MandateRule> = self.rules.iter()
23010            .filter(|r| r.enabled)
23011            .collect();
23012        sorted_rules.sort_by(|a, b| b.priority.cmp(&a.priority));
23013
23014        let mut rules_evaluated = 0u32;
23015
23016        for rule in &sorted_rules {
23017            rules_evaluated += 1;
23018
23019            let subject_match = rule.subject == "*" || rule.subject == subject;
23020            let action_match = rule.action == "*" || rule.action == action;
23021            let resource_match = if rule.resource == "*" {
23022                true
23023            } else if rule.resource.ends_with('*') {
23024                let prefix = &rule.resource[..rule.resource.len() - 1];
23025                resource.starts_with(prefix)
23026            } else {
23027                rule.resource == resource
23028            };
23029
23030            if subject_match && action_match && resource_match {
23031                return MandateEvaluation {
23032                    allowed: rule.effect == "allow",
23033                    matched_rule: Some(rule.id.clone()),
23034                    effect: rule.effect.clone(),
23035                    rules_evaluated,
23036                    certainty: 1.0,       // Explicit match → deterministic
23037                    derivation: "raw".into(),
23038                };
23039            }
23040        }
23041
23042        // Default deny — no rule matched
23043        MandateEvaluation {
23044            allowed: false,
23045            matched_rule: None,
23046            effect: "default_deny".into(),
23047            rules_evaluated,
23048            certainty: 0.99,          // Theorem 5.1: inferential deny → derived
23049            derivation: "derived".into(),
23050        }
23051    }
23052}
23053
23054// ── Refine — iterative output improvement primitive ─────────────────────────
23055
23056/// A single iteration within a Refine session.
23057///
23058/// Each iteration carries a quality score and the delta from the previous
23059/// iteration, enabling convergence tracking. ΛD: all refinements are derived
23060/// (c≤0.99, δ=derived) because each iteration is a transformation of prior output.
23061#[derive(Debug, Clone, Serialize, Deserialize)]
23062pub struct RefineIteration {
23063    /// Iteration number (1-indexed).
23064    pub iteration: u32,
23065    /// The refined content at this iteration.
23066    pub content: String,
23067    /// Quality score (0.0–1.0), typically assessed by a scoring function.
23068    pub quality: f64,
23069    /// Delta from previous quality (positive = improvement).
23070    pub delta: f64,
23071    /// Unix timestamp.
23072    pub timestamp: u64,
23073    /// Feedback or instruction that guided this iteration.
23074    pub feedback: String,
23075}
23076
23077/// A named Refine session — tracks iterative improvement of content.
23078///
23079/// The session starts with an initial content and progresses through iterations,
23080/// each guided by feedback. Convergence is detected when quality delta falls
23081/// below a threshold.
23082///
23083/// ΛD alignment: ψ = ⟨T="refinement", V=content, E=⟨c≤0.99, τ, ρ, δ=derived⟩⟩
23084/// All iterations are derived per Theorem 5.1 — refinement is transformation.
23085#[derive(Debug, Clone, Serialize, Deserialize)]
23086pub struct RefineSession {
23087    /// Session identifier.
23088    pub id: String,
23089    /// Session name/label.
23090    pub name: String,
23091    /// Target quality threshold (0.0–1.0). Session converges when reached.
23092    pub target_quality: f64,
23093    /// Convergence delta threshold. Session converges when |delta| < this value.
23094    pub convergence_threshold: f64,
23095    /// Maximum iterations allowed.
23096    pub max_iterations: u32,
23097    /// Whether the session has converged.
23098    pub converged: bool,
23099    /// History of all iterations.
23100    pub iterations: Vec<RefineIteration>,
23101    /// Unix timestamp of creation.
23102    pub created_at: u64,
23103}
23104
23105impl RefineSession {
23106    /// Get current quality (last iteration's quality, or 0.0 if no iterations).
23107    pub fn current_quality(&self) -> f64 {
23108        self.iterations.last().map(|i| i.quality).unwrap_or(0.0)
23109    }
23110
23111    /// Get current iteration count.
23112    pub fn iteration_count(&self) -> u32 {
23113        self.iterations.len() as u32
23114    }
23115
23116    /// Check if the session has converged based on quality target or delta threshold.
23117    pub fn check_convergence(&self) -> bool {
23118        if self.iterations.is_empty() {
23119            return false;
23120        }
23121        let last = self.iterations.last().unwrap();
23122        // Converged if: quality >= target OR |delta| < threshold (after at least 2 iterations)
23123        if last.quality >= self.target_quality {
23124            return true;
23125        }
23126        if self.iterations.len() >= 2 && last.delta.abs() < self.convergence_threshold {
23127            return true;
23128        }
23129        false
23130    }
23131
23132    /// Add an iteration. Returns Err if session is converged or max iterations reached.
23133    pub fn add_iteration(&mut self, content: String, quality: f64, feedback: String) -> Result<&RefineIteration, String> {
23134        if self.converged {
23135            return Err("session already converged".into());
23136        }
23137        if self.iteration_count() >= self.max_iterations {
23138            return Err(format!("max iterations ({}) reached", self.max_iterations));
23139        }
23140
23141        let prev_quality = self.current_quality();
23142        let delta = quality - prev_quality;
23143        let iteration_num = self.iteration_count() + 1;
23144
23145        let now = std::time::SystemTime::now()
23146            .duration_since(std::time::UNIX_EPOCH)
23147            .unwrap_or_default()
23148            .as_secs();
23149
23150        let iteration = RefineIteration {
23151            iteration: iteration_num,
23152            content,
23153            quality,
23154            delta,
23155            timestamp: now,
23156            feedback,
23157        };
23158
23159        self.iterations.push(iteration);
23160        self.converged = self.check_convergence();
23161
23162        Ok(self.iterations.last().unwrap())
23163    }
23164}
23165
23166// ── Trail — execution path recording primitive ─────────────────────────────
23167
23168/// A single step within a Trail record.
23169///
23170/// Each step captures what happened at a point in the execution path:
23171/// the operation performed, inputs/outputs, duration, and outcome.
23172/// ΛD: trail steps are raw observations (c=1.0, δ=raw) — they record
23173/// what actually happened, not an interpretation.
23174#[derive(Debug, Clone, Serialize, Deserialize)]
23175pub struct TrailStep {
23176    /// Step number (1-indexed).
23177    pub step: u32,
23178    /// Operation name (e.g., "execute", "validate", "transform").
23179    pub operation: String,
23180    /// Input description or summary.
23181    pub input: String,
23182    /// Output description or summary.
23183    pub output: String,
23184    /// Duration in milliseconds.
23185    pub duration_ms: u64,
23186    /// Outcome: "success", "failure", "skipped".
23187    pub outcome: String,
23188    /// Optional metadata (key-value pairs).
23189    pub metadata: HashMap<String, serde_json::Value>,
23190    /// Unix timestamp of this step.
23191    pub timestamp: u64,
23192}
23193
23194/// A named Trail record — captures the full execution path of a cognitive operation.
23195///
23196/// Trails are immutable once completed: steps are appended during execution,
23197/// then the trail is marked complete. This ensures audit integrity.
23198///
23199/// ΛD alignment: ψ = ⟨T="trail", V=steps, E=⟨c=1.0, τ, ρ, δ=raw⟩⟩
23200/// Trail recording is raw observation — it captures what happened, not an inference.
23201/// Completed trails are ground truth (c=1.0). In-progress trails are provisional (c=0.95).
23202#[derive(Debug, Clone, Serialize, Deserialize)]
23203pub struct TrailRecord {
23204    /// Trail identifier.
23205    pub id: String,
23206    /// Trail name/label.
23207    pub name: String,
23208    /// The flow or operation being traced.
23209    pub target: String,
23210    /// Whether the trail is complete.
23211    pub completed: bool,
23212    /// Final outcome: "success", "failure", "partial", or "in_progress".
23213    pub outcome: String,
23214    /// Ordered steps in the execution path.
23215    pub steps: Vec<TrailStep>,
23216    /// Unix timestamp of creation.
23217    pub created_at: u64,
23218    /// Unix timestamp of completion (0 if not completed).
23219    pub completed_at: u64,
23220    /// Total duration in milliseconds (sum of step durations).
23221    pub total_duration_ms: u64,
23222}
23223
23224impl TrailRecord {
23225    /// Add a step to the trail. Returns Err if trail is already completed.
23226    pub fn add_step(&mut self, operation: String, input: String, output: String,
23227                    duration_ms: u64, outcome: String, metadata: HashMap<String, serde_json::Value>) -> Result<u32, String> {
23228        if self.completed {
23229            return Err("trail already completed".into());
23230        }
23231
23232        let now = std::time::SystemTime::now()
23233            .duration_since(std::time::UNIX_EPOCH)
23234            .unwrap_or_default()
23235            .as_secs();
23236
23237        let step_num = self.steps.len() as u32 + 1;
23238
23239        self.steps.push(TrailStep {
23240            step: step_num,
23241            operation,
23242            input,
23243            output,
23244            duration_ms,
23245            outcome,
23246            metadata,
23247            timestamp: now,
23248        });
23249
23250        self.total_duration_ms += duration_ms;
23251
23252        Ok(step_num)
23253    }
23254
23255    /// Mark the trail as complete with a final outcome.
23256    pub fn complete(&mut self, outcome: String) -> Result<(), String> {
23257        if self.completed {
23258            return Err("trail already completed".into());
23259        }
23260
23261        let now = std::time::SystemTime::now()
23262            .duration_since(std::time::UNIX_EPOCH)
23263            .unwrap_or_default()
23264            .as_secs();
23265
23266        self.completed = true;
23267        self.outcome = outcome;
23268        self.completed_at = now;
23269
23270        Ok(())
23271    }
23272
23273    /// Step count.
23274    pub fn step_count(&self) -> u32 {
23275        self.steps.len() as u32
23276    }
23277
23278    /// Count of successful steps.
23279    pub fn success_count(&self) -> u32 {
23280        self.steps.iter().filter(|s| s.outcome == "success").count() as u32
23281    }
23282
23283    /// Count of failed steps.
23284    pub fn failure_count(&self) -> u32 {
23285        self.steps.iter().filter(|s| s.outcome == "failure").count() as u32
23286    }
23287}
23288
23289// ── Probe — exploratory information gathering primitive ──────────────────────
23290
23291/// A single query result within a Probe session.
23292///
23293/// Each finding represents information discovered from a source during probing.
23294/// ΛD: probe findings are derived (c≤0.99, δ=derived) because they are
23295/// extracted/summarized from sources, not raw data themselves.
23296#[derive(Debug, Clone, Serialize, Deserialize)]
23297pub struct ProbeFinding {
23298    /// Source identifier (e.g., "corpus:papers", "axonstore:facts", "dataspace:research").
23299    pub source: String,
23300    /// The query that produced this finding.
23301    pub query: String,
23302    /// The discovered information.
23303    pub content: String,
23304    /// Relevance score (0.0–1.0).
23305    pub relevance: f64,
23306    /// Confidence in this finding (ΛD certainty, ≤0.99).
23307    pub certainty: f64,
23308    /// Unix timestamp of discovery.
23309    pub timestamp: u64,
23310}
23311
23312/// A named Probe session — orchestrates exploratory queries across multiple sources.
23313///
23314/// A probe session gathers information by querying multiple sources (corpora,
23315/// axonstores, dataspaces) and aggregating findings with relevance scoring.
23316///
23317/// ΛD alignment: ψ = ⟨T="probe", V=findings, E=⟨c≤0.99, τ, ρ, δ=derived⟩⟩
23318/// Probing is inherently exploratory — findings are speculative (Theorem 5.1).
23319#[derive(Debug, Clone, Serialize, Deserialize)]
23320pub struct ProbeSession {
23321    /// Session identifier.
23322    pub id: String,
23323    /// Probe name/label.
23324    pub name: String,
23325    /// The investigation question or topic.
23326    pub question: String,
23327    /// Sources to probe (e.g., ["corpus:papers", "axonstore:facts"]).
23328    pub sources: Vec<String>,
23329    /// Accumulated findings from all queries.
23330    pub findings: Vec<ProbeFinding>,
23331    /// Whether the probe is complete.
23332    pub completed: bool,
23333    /// Unix timestamp of creation.
23334    pub created_at: u64,
23335    /// Total queries executed.
23336    pub total_queries: u32,
23337}
23338
23339impl ProbeSession {
23340    /// Add a finding to the session.
23341    pub fn add_finding(&mut self, source: String, query: String, content: String, relevance: f64) {
23342        let now = std::time::SystemTime::now()
23343            .duration_since(std::time::UNIX_EPOCH)
23344            .unwrap_or_default()
23345            .as_secs();
23346
23347        // ΛD: certainty derived from relevance, capped at 0.99
23348        let certainty = (relevance * 0.99).min(0.99);
23349
23350        self.findings.push(ProbeFinding {
23351            source,
23352            query,
23353            content,
23354            relevance,
23355            certainty,
23356            timestamp: now,
23357        });
23358    }
23359
23360    /// Get top findings sorted by relevance.
23361    pub fn top_findings(&self, limit: usize) -> Vec<&ProbeFinding> {
23362        let mut sorted: Vec<&ProbeFinding> = self.findings.iter().collect();
23363        sorted.sort_by(|a, b| b.relevance.partial_cmp(&a.relevance).unwrap_or(std::cmp::Ordering::Equal));
23364        sorted.truncate(limit);
23365        sorted
23366    }
23367
23368    /// Aggregate certainty across all findings.
23369    pub fn aggregate_certainty(&self) -> f64 {
23370        if self.findings.is_empty() {
23371            return 0.0;
23372        }
23373        let avg: f64 = self.findings.iter().map(|f| f.certainty).sum::<f64>() / self.findings.len() as f64;
23374        (avg * 10000.0).round() / 10000.0
23375    }
23376
23377    /// Count findings per source.
23378    pub fn findings_per_source(&self) -> HashMap<String, usize> {
23379        let mut counts: HashMap<String, usize> = HashMap::new();
23380        for f in &self.findings {
23381            *counts.entry(f.source.clone()).or_insert(0) += 1;
23382        }
23383        counts
23384    }
23385}
23386
23387// ── Weave — multi-source content synthesis primitive ─────────────────────────
23388
23389/// A single source strand contributing to a Weave synthesis.
23390///
23391/// Each strand represents content from a specific source that will be
23392/// woven into the final synthesis. Strands carry attribution metadata
23393/// for provenance tracking.
23394#[derive(Debug, Clone, Serialize, Deserialize)]
23395pub struct WeaveStrand {
23396    /// Strand identifier (auto-assigned).
23397    pub id: u32,
23398    /// Source attribution (e.g., "corpus:papers/doc_1", "probe:findings", "manual").
23399    pub source: String,
23400    /// Content from this source.
23401    pub content: String,
23402    /// Weight of this strand in synthesis (0.0–1.0).
23403    pub weight: f64,
23404    /// Certainty of the source content (ΛD, ≤0.99 for derived, 1.0 for raw).
23405    pub source_certainty: f64,
23406    /// Unix timestamp of addition.
23407    pub added_at: u64,
23408}
23409
23410/// A named Weave session — synthesizes content from multiple source strands.
23411///
23412/// The weave collects strands from diverse sources, each with attribution
23413/// and weight. Synthesis produces a combined output whose epistemic certainty
23414/// is the weighted average of strand certainties, capped at 0.99 (Theorem 5.1:
23415/// synthesis is always derived).
23416///
23417/// ΛD alignment: ψ = ⟨T="weave", V=synthesis, E=⟨c≤0.99, τ, ρ, δ=derived⟩⟩
23418#[derive(Debug, Clone, Serialize, Deserialize)]
23419pub struct WeaveSession {
23420    /// Session identifier.
23421    pub id: String,
23422    /// Weave name/label.
23423    pub name: String,
23424    /// The synthesis goal or topic.
23425    pub goal: String,
23426    /// Source strands collected for synthesis.
23427    pub strands: Vec<WeaveStrand>,
23428    /// The synthesized output (populated on synthesis).
23429    pub synthesis: String,
23430    /// Whether synthesis has been performed.
23431    pub synthesized: bool,
23432    /// Unix timestamp of creation.
23433    pub created_at: u64,
23434    /// Next strand ID.
23435    pub next_strand_id: u32,
23436}
23437
23438impl WeaveSession {
23439    /// Add a strand to the weave.
23440    pub fn add_strand(&mut self, source: String, content: String, weight: f64, source_certainty: f64) -> u32 {
23441        let now = std::time::SystemTime::now()
23442            .duration_since(std::time::UNIX_EPOCH)
23443            .unwrap_or_default()
23444            .as_secs();
23445
23446        let id = self.next_strand_id;
23447        self.next_strand_id += 1;
23448
23449        self.strands.push(WeaveStrand {
23450            id,
23451            source,
23452            content,
23453            weight: weight.max(0.0).min(1.0),
23454            source_certainty: source_certainty.max(0.0).min(1.0),
23455            added_at: now,
23456        });
23457
23458        id
23459    }
23460
23461    /// Compute synthesis certainty: weighted average of strand certainties, capped at 0.99.
23462    pub fn synthesis_certainty(&self) -> f64 {
23463        if self.strands.is_empty() {
23464            return 0.0;
23465        }
23466        let total_weight: f64 = self.strands.iter().map(|s| s.weight).sum();
23467        if total_weight == 0.0 {
23468            return 0.0;
23469        }
23470        let weighted_certainty: f64 = self.strands.iter()
23471            .map(|s| s.source_certainty * s.weight)
23472            .sum::<f64>() / total_weight;
23473        (weighted_certainty * 10000.0).round() / 10000.0
23474    }
23475
23476    /// Generate attribution list: sources with their weights.
23477    pub fn attributions(&self) -> Vec<(String, f64)> {
23478        self.strands.iter().map(|s| (s.source.clone(), s.weight)).collect()
23479    }
23480
23481    /// Synthesize: combine strand contents into a unified output.
23482    /// Uses weight-ordered concatenation with source attribution markers.
23483    pub fn synthesize(&mut self) -> Result<String, String> {
23484        if self.strands.is_empty() {
23485            return Err("no strands to synthesize".into());
23486        }
23487        if self.synthesized {
23488            return Err("already synthesized".into());
23489        }
23490
23491        // Sort strands by weight descending
23492        let mut sorted: Vec<&WeaveStrand> = self.strands.iter().collect();
23493        sorted.sort_by(|a, b| b.weight.partial_cmp(&a.weight).unwrap_or(std::cmp::Ordering::Equal));
23494
23495        let mut parts: Vec<String> = Vec::new();
23496        for strand in &sorted {
23497            parts.push(format!("[{}] {}", strand.source, strand.content));
23498        }
23499
23500        self.synthesis = parts.join("\n\n");
23501        self.synthesized = true;
23502
23503        Ok(self.synthesis.clone())
23504    }
23505}
23506
23507// ── Corroborate — cross-source verification primitive ────────────────────────
23508
23509/// A piece of evidence submitted to a Corroborate session.
23510///
23511/// Each evidence item represents a source's stance on the claim being verified:
23512/// "supports", "contradicts", or "neutral". The source certainty reflects
23513/// the reliability of the evidence source.
23514#[derive(Debug, Clone, Serialize, Deserialize)]
23515pub struct CorroborateEvidence {
23516    /// Evidence identifier (auto-assigned).
23517    pub id: u32,
23518    /// Source attribution (e.g., "corpus:papers/doc_1", "axonstore:facts/key_1").
23519    pub source: String,
23520    /// The evidence content.
23521    pub content: String,
23522    /// Stance: "supports", "contradicts", "neutral".
23523    pub stance: String,
23524    /// Confidence in the evidence itself (0.0–1.0).
23525    pub confidence: f64,
23526    /// Unix timestamp.
23527    pub submitted_at: u64,
23528}
23529
23530/// A named Corroborate session — verifies a claim across multiple sources.
23531///
23532/// Agreement scoring: certainty = (supports - contradicts) / total, scaled to [0, 0.99].
23533/// High agreement across independent sources → higher certainty.
23534/// Contradictory evidence → lower certainty.
23535///
23536/// ΛD alignment: ψ = ⟨T="corroboration", V=verdict, E=⟨c≤0.99, τ, ρ, δ=derived⟩⟩
23537/// Verification is always derived (Theorem 5.1): aggregating stances is inference.
23538#[derive(Debug, Clone, Serialize, Deserialize)]
23539pub struct CorroborateSession {
23540    /// Session identifier.
23541    pub id: String,
23542    /// Session name.
23543    pub name: String,
23544    /// The claim being verified.
23545    pub claim: String,
23546    /// Evidence items collected.
23547    pub evidence: Vec<CorroborateEvidence>,
23548    /// Whether verification is complete.
23549    pub verified: bool,
23550    /// Verdict: "corroborated", "disputed", "inconclusive", "pending".
23551    pub verdict: String,
23552    /// Unix timestamp of creation.
23553    pub created_at: u64,
23554    /// Next evidence ID.
23555    pub next_evidence_id: u32,
23556}
23557
23558impl CorroborateSession {
23559    /// Add evidence to the session.
23560    pub fn add_evidence(&mut self, source: String, content: String, stance: String, confidence: f64) -> Result<u32, String> {
23561        if self.verified {
23562            return Err("session already verified".into());
23563        }
23564        if !["supports", "contradicts", "neutral"].contains(&stance.as_str()) {
23565            return Err(format!("invalid stance '{}': must be supports/contradicts/neutral", stance));
23566        }
23567
23568        let now = std::time::SystemTime::now()
23569            .duration_since(std::time::UNIX_EPOCH)
23570            .unwrap_or_default()
23571            .as_secs();
23572
23573        let id = self.next_evidence_id;
23574        self.next_evidence_id += 1;
23575
23576        self.evidence.push(CorroborateEvidence {
23577            id,
23578            source,
23579            content,
23580            stance,
23581            confidence: confidence.max(0.0).min(1.0),
23582            submitted_at: now,
23583        });
23584
23585        Ok(id)
23586    }
23587
23588    /// Compute agreement score and certainty.
23589    /// Returns (agreement_ratio, certainty, verdict).
23590    pub fn compute_agreement(&self) -> (f64, f64, String) {
23591        if self.evidence.is_empty() {
23592            return (0.0, 0.0, "pending".into());
23593        }
23594
23595        let supports: f64 = self.evidence.iter()
23596            .filter(|e| e.stance == "supports")
23597            .map(|e| e.confidence)
23598            .sum();
23599        let contradicts: f64 = self.evidence.iter()
23600            .filter(|e| e.stance == "contradicts")
23601            .map(|e| e.confidence)
23602            .sum();
23603        let total: f64 = self.evidence.iter()
23604            .map(|e| e.confidence)
23605            .sum();
23606
23607        if total == 0.0 {
23608            return (0.0, 0.0, "inconclusive".into());
23609        }
23610
23611        // Agreement ratio: net support normalized to [-1, 1]
23612        let agreement = (supports - contradicts) / total;
23613
23614        // Certainty: |agreement| scaled to [0, 0.99]
23615        let certainty = (agreement.abs() * 0.99 * 10000.0).round() / 10000.0;
23616
23617        // Verdict based on agreement
23618        let verdict = if agreement > 0.5 {
23619            "corroborated".into()
23620        } else if agreement < -0.5 {
23621            "disputed".into()
23622        } else {
23623            "inconclusive".into()
23624        };
23625
23626        ((agreement * 10000.0).round() / 10000.0, certainty.min(0.99), verdict)
23627    }
23628
23629    /// Count evidence by stance.
23630    pub fn stance_counts(&self) -> (usize, usize, usize) {
23631        let supports = self.evidence.iter().filter(|e| e.stance == "supports").count();
23632        let contradicts = self.evidence.iter().filter(|e| e.stance == "contradicts").count();
23633        let neutral = self.evidence.iter().filter(|e| e.stance == "neutral").count();
23634        (supports, contradicts, neutral)
23635    }
23636
23637    /// Verify: finalize the session with computed verdict.
23638    pub fn verify(&mut self) -> Result<(f64, f64, String), String> {
23639        if self.verified {
23640            return Err("already verified".into());
23641        }
23642        if self.evidence.is_empty() {
23643            return Err("no evidence to verify".into());
23644        }
23645
23646        let (agreement, certainty, verdict) = self.compute_agreement();
23647        self.verified = true;
23648        self.verdict = verdict.clone();
23649
23650        Ok((agreement, certainty, verdict))
23651    }
23652}
23653
23654// ── Drill — deep recursive exploration primitive ────────────────────────────
23655
23656/// A node in a Drill exploration tree.
23657///
23658/// Each node represents a point in the exploration space with a question,
23659/// answer, and child branches. Depth tracking enables depth-limited search.
23660/// ΛD: certainty degrades with depth (deeper = more speculative).
23661#[derive(Debug, Clone, Serialize, Deserialize)]
23662pub struct DrillNode {
23663    /// Node identifier (path-based: "root", "root.0", "root.0.1").
23664    pub id: String,
23665    /// The question or topic explored at this node.
23666    pub question: String,
23667    /// The answer or finding at this node.
23668    pub answer: String,
23669    /// Depth level (0 = root).
23670    pub depth: u32,
23671    /// Child node IDs.
23672    pub children: Vec<String>,
23673    /// Whether this node is a leaf (no further exploration).
23674    pub is_leaf: bool,
23675    /// ΛD certainty: degrades with depth (1.0 - depth * 0.05, min 0.5, capped at 0.99).
23676    pub certainty: f64,
23677    /// Unix timestamp.
23678    pub created_at: u64,
23679}
23680
23681/// A named Drill session — depth-limited recursive exploration of a topic.
23682///
23683/// The drill starts at a root question and expands by adding child nodes
23684/// at increasing depths. A max_depth limit prevents unbounded exploration.
23685///
23686/// ΛD alignment: ψ = ⟨T="drill", V=tree, E=⟨c=f(depth), τ, ρ, δ=derived⟩⟩
23687/// Certainty degrades with depth: surface findings are more reliable than deep speculation.
23688/// All drill findings are derived (Theorem 5.1).
23689#[derive(Debug, Clone, Serialize, Deserialize)]
23690pub struct DrillSession {
23691    /// Session identifier.
23692    pub id: String,
23693    /// Drill name.
23694    pub name: String,
23695    /// Root question.
23696    pub root_question: String,
23697    /// Maximum exploration depth.
23698    pub max_depth: u32,
23699    /// All nodes in the exploration tree.
23700    pub nodes: HashMap<String, DrillNode>,
23701    /// Whether the drill is complete.
23702    pub completed: bool,
23703    /// Unix timestamp of creation.
23704    pub created_at: u64,
23705}
23706
23707impl DrillSession {
23708    /// Compute certainty for a given depth.
23709    /// Certainty = (1.0 - depth * 0.05).max(0.5).min(0.99)
23710    pub fn certainty_at_depth(depth: u32) -> f64 {
23711        let c = 1.0 - depth as f64 * 0.05;
23712        let clamped = c.max(0.5).min(0.99);
23713        (clamped * 10000.0).round() / 10000.0
23714    }
23715
23716    /// Add the root node.
23717    pub fn add_root(&mut self, answer: String) -> Result<String, String> {
23718        if self.nodes.contains_key("root") {
23719            return Err("root already exists".into());
23720        }
23721
23722        let now = std::time::SystemTime::now()
23723            .duration_since(std::time::UNIX_EPOCH)
23724            .unwrap_or_default()
23725            .as_secs();
23726
23727        self.nodes.insert("root".into(), DrillNode {
23728            id: "root".into(),
23729            question: self.root_question.clone(),
23730            answer,
23731            depth: 0,
23732            children: Vec::new(),
23733            is_leaf: false,
23734            certainty: Self::certainty_at_depth(0),
23735            created_at: now,
23736        });
23737
23738        Ok("root".into())
23739    }
23740
23741    /// Expand a node by adding a child question-answer pair.
23742    pub fn expand(&mut self, parent_id: &str, question: String, answer: String) -> Result<String, String> {
23743        if self.completed {
23744            return Err("drill already completed".into());
23745        }
23746
23747        let parent_depth = match self.nodes.get(parent_id) {
23748            Some(n) => n.depth,
23749            None => return Err(format!("parent node '{}' not found", parent_id)),
23750        };
23751
23752        let child_depth = parent_depth + 1;
23753        if child_depth > self.max_depth {
23754            return Err(format!("max depth {} reached", self.max_depth));
23755        }
23756
23757        let child_index = self.nodes.get(parent_id).unwrap().children.len();
23758        let child_id = format!("{}.{}", parent_id, child_index);
23759
23760        let now = std::time::SystemTime::now()
23761            .duration_since(std::time::UNIX_EPOCH)
23762            .unwrap_or_default()
23763            .as_secs();
23764
23765        let node = DrillNode {
23766            id: child_id.clone(),
23767            question,
23768            answer,
23769            depth: child_depth,
23770            children: Vec::new(),
23771            is_leaf: child_depth == self.max_depth,
23772            certainty: Self::certainty_at_depth(child_depth),
23773            created_at: now,
23774        };
23775
23776        self.nodes.insert(child_id.clone(), node);
23777        self.nodes.get_mut(parent_id).unwrap().children.push(child_id.clone());
23778
23779        Ok(child_id)
23780    }
23781
23782    /// Total node count.
23783    pub fn node_count(&self) -> usize {
23784        self.nodes.len()
23785    }
23786
23787    /// Maximum depth reached.
23788    pub fn max_depth_reached(&self) -> u32 {
23789        self.nodes.values().map(|n| n.depth).max().unwrap_or(0)
23790    }
23791
23792    /// Leaf count.
23793    pub fn leaf_count(&self) -> usize {
23794        self.nodes.values().filter(|n| n.children.is_empty()).count()
23795    }
23796
23797    /// Average certainty across all nodes.
23798    pub fn avg_certainty(&self) -> f64 {
23799        if self.nodes.is_empty() { return 0.0; }
23800        let sum: f64 = self.nodes.values().map(|n| n.certainty).sum();
23801        (sum / self.nodes.len() as f64 * 10000.0).round() / 10000.0
23802    }
23803}
23804
23805// ── Forge — artifact generation primitive ────────────────────────────────────
23806
23807/// A template for artifact generation.
23808///
23809/// Templates contain placeholders in `{{variable}}` syntax that are
23810/// substituted with provided values during rendering.
23811#[derive(Debug, Clone, Serialize, Deserialize)]
23812pub struct ForgeTemplate {
23813    /// Template name.
23814    pub name: String,
23815    /// Template content with `{{placeholder}}` markers.
23816    pub content: String,
23817    /// Required variables (extracted from placeholders).
23818    pub variables: Vec<String>,
23819    /// Output format hint: "text", "json", "markdown", "code".
23820    pub format: String,
23821}
23822
23823/// A rendered artifact from a Forge session.
23824#[derive(Debug, Clone, Serialize, Deserialize)]
23825pub struct ForgeArtifact {
23826    /// Artifact identifier.
23827    pub id: String,
23828    /// Template used.
23829    pub template_name: String,
23830    /// Rendered content.
23831    pub content: String,
23832    /// Variables used in rendering.
23833    pub variables_used: HashMap<String, String>,
23834    /// Output format.
23835    pub format: String,
23836    /// Unix timestamp.
23837    pub created_at: u64,
23838    /// ΛD certainty: template rendering is deterministic (c=0.99, δ=derived).
23839    pub certainty: f64,
23840}
23841
23842/// A named Forge session — manages templates and generates artifacts.
23843///
23844/// The forge collects templates and renders them with variable substitution.
23845/// Each rendered artifact is a derived output (Theorem 5.1: template
23846/// instantiation is transformation, not raw data).
23847///
23848/// ΛD alignment: ψ = ⟨T="forge", V=artifact, E=⟨c=0.99, τ, ρ, δ=derived⟩⟩
23849#[derive(Debug, Clone, Serialize, Deserialize)]
23850pub struct ForgeSession {
23851    /// Session identifier.
23852    pub id: String,
23853    /// Forge name.
23854    pub name: String,
23855    /// Registered templates.
23856    pub templates: HashMap<String, ForgeTemplate>,
23857    /// Generated artifacts.
23858    pub artifacts: Vec<ForgeArtifact>,
23859    /// Unix timestamp of creation.
23860    pub created_at: u64,
23861    /// Next artifact ID counter.
23862    pub next_artifact_id: u64,
23863}
23864
23865impl ForgeSession {
23866    /// Extract `{{variable}}` placeholders from template content.
23867    pub fn extract_variables(content: &str) -> Vec<String> {
23868        let mut vars = Vec::new();
23869        let mut pos = 0;
23870        let bytes = content.as_bytes();
23871        while pos + 3 < bytes.len() {
23872            if bytes[pos] == b'{' && bytes[pos + 1] == b'{' {
23873                if let Some(end) = content[pos + 2..].find("}}") {
23874                    let var = content[pos + 2..pos + 2 + end].trim().to_string();
23875                    if !var.is_empty() && !vars.contains(&var) {
23876                        vars.push(var);
23877                    }
23878                    pos = pos + 2 + end + 2;
23879                } else {
23880                    pos += 1;
23881                }
23882            } else {
23883                pos += 1;
23884            }
23885        }
23886        vars
23887    }
23888
23889    /// Register a template.
23890    pub fn add_template(&mut self, name: String, content: String, format: String) -> Result<(), String> {
23891        if self.templates.contains_key(&name) {
23892            return Err(format!("template '{}' already exists", name));
23893        }
23894        let variables = Self::extract_variables(&content);
23895        self.templates.insert(name.clone(), ForgeTemplate {
23896            name,
23897            content,
23898            variables,
23899            format,
23900        });
23901        Ok(())
23902    }
23903
23904    /// Render a template with variable substitution.
23905    pub fn render(&mut self, template_name: &str, variables: &HashMap<String, String>) -> Result<ForgeArtifact, String> {
23906        let template = match self.templates.get(template_name) {
23907            Some(t) => t.clone(),
23908            None => return Err(format!("template '{}' not found", template_name)),
23909        };
23910
23911        // Check all required variables are provided
23912        for var in &template.variables {
23913            if !variables.contains_key(var) {
23914                return Err(format!("missing required variable '{}'", var));
23915            }
23916        }
23917
23918        // Substitute placeholders
23919        let mut rendered = template.content.clone();
23920        for (key, value) in variables {
23921            let placeholder = format!("{{{{{}}}}}", key);
23922            rendered = rendered.replace(&placeholder, value);
23923        }
23924
23925        let now = std::time::SystemTime::now()
23926            .duration_since(std::time::UNIX_EPOCH)
23927            .unwrap_or_default()
23928            .as_secs();
23929
23930        let artifact_id = format!("artifact_{}_{}", self.next_artifact_id, template_name);
23931        self.next_artifact_id += 1;
23932
23933        let artifact = ForgeArtifact {
23934            id: artifact_id,
23935            template_name: template_name.to_string(),
23936            content: rendered,
23937            variables_used: variables.clone(),
23938            format: template.format.clone(),
23939            created_at: now,
23940            certainty: 0.99, // template rendering is deterministic but derived
23941        };
23942
23943        self.artifacts.push(artifact.clone());
23944
23945        Ok(artifact)
23946    }
23947}
23948
23949// ── Deliberate — extended reasoning with backtrack primitive ─────────────────
23950
23951/// An option being evaluated in a Deliberate session.
23952///
23953/// Each option represents a possible course of action with pros, cons,
23954/// and a composite score. Options can be marked as "eliminated" (backtracked)
23955/// when reasoning reveals they are unviable.
23956#[derive(Debug, Clone, Serialize, Deserialize)]
23957pub struct DeliberateOption {
23958    /// Option identifier (auto-assigned).
23959    pub id: u32,
23960    /// Option label/name.
23961    pub label: String,
23962    /// Description of this option.
23963    pub description: String,
23964    /// Arguments in favor.
23965    pub pros: Vec<String>,
23966    /// Arguments against.
23967    pub cons: Vec<String>,
23968    /// Composite score (0.0–1.0), computed from pros/cons balance.
23969    pub score: f64,
23970    /// Whether this option has been eliminated (backtracked).
23971    pub eliminated: bool,
23972    /// Reason for elimination (if eliminated).
23973    pub elimination_reason: String,
23974}
23975
23976/// A named Deliberate session — structured decision-making with option evaluation.
23977///
23978/// The session collects options, evaluates them with pros/cons, computes
23979/// scores, allows backtracking (elimination), and selects a winner.
23980///
23981/// ΛD alignment: ψ = ⟨T="deliberation", V=decision, E=⟨c≤0.99, τ, ρ, δ=derived⟩⟩
23982/// Deliberation is inferential reasoning — all outcomes are derived (Theorem 5.1).
23983/// Certainty scales with the margin between top option and alternatives.
23984#[derive(Debug, Clone, Serialize, Deserialize)]
23985pub struct DeliberateSession {
23986    /// Session identifier.
23987    pub id: String,
23988    /// Session name.
23989    pub name: String,
23990    /// The question or decision to be made.
23991    pub question: String,
23992    /// Options under consideration.
23993    pub options: Vec<DeliberateOption>,
23994    /// Whether a decision has been made.
23995    pub decided: bool,
23996    /// The chosen option ID (if decided).
23997    pub chosen_option: Option<u32>,
23998    /// Unix timestamp of creation.
23999    pub created_at: u64,
24000    /// Next option ID.
24001    pub next_option_id: u32,
24002}
24003
24004impl DeliberateSession {
24005    /// Add an option to consider.
24006    pub fn add_option(&mut self, label: String, description: String) -> Result<u32, String> {
24007        if self.decided {
24008            return Err("session already decided".into());
24009        }
24010        let id = self.next_option_id;
24011        self.next_option_id += 1;
24012        self.options.push(DeliberateOption {
24013            id,
24014            label,
24015            description,
24016            pros: Vec::new(),
24017            cons: Vec::new(),
24018            score: 0.5, // neutral starting score
24019            eliminated: false,
24020            elimination_reason: String::new(),
24021        });
24022        Ok(id)
24023    }
24024
24025    /// Add a pro or con to an option, recompute score.
24026    pub fn evaluate(&mut self, option_id: u32, pro: Option<String>, con: Option<String>) -> Result<f64, String> {
24027        if self.decided {
24028            return Err("session already decided".into());
24029        }
24030        let option = self.options.iter_mut().find(|o| o.id == option_id)
24031            .ok_or_else(|| format!("option {} not found", option_id))?;
24032        if option.eliminated {
24033            return Err(format!("option {} is eliminated", option_id));
24034        }
24035        if let Some(p) = pro { option.pros.push(p); }
24036        if let Some(c) = con { option.cons.push(c); }
24037        // Score: pros / (pros + cons), default 0.5 if both empty
24038        let total = option.pros.len() + option.cons.len();
24039        option.score = if total == 0 { 0.5 } else {
24040            (option.pros.len() as f64 / total as f64 * 10000.0).round() / 10000.0
24041        };
24042        Ok(option.score)
24043    }
24044
24045    /// Eliminate (backtrack) an option.
24046    pub fn eliminate(&mut self, option_id: u32, reason: String) -> Result<(), String> {
24047        if self.decided {
24048            return Err("session already decided".into());
24049        }
24050        let option = self.options.iter_mut().find(|o| o.id == option_id)
24051            .ok_or_else(|| format!("option {} not found", option_id))?;
24052        if option.eliminated {
24053            return Err(format!("option {} already eliminated", option_id));
24054        }
24055        option.eliminated = true;
24056        option.elimination_reason = reason;
24057        option.score = 0.0;
24058        Ok(())
24059    }
24060
24061    /// Make a decision: choose the highest-scoring non-eliminated option.
24062    pub fn decide(&mut self) -> Result<(u32, f64, f64), String> {
24063        if self.decided {
24064            return Err("already decided".into());
24065        }
24066        let viable: Vec<&DeliberateOption> = self.options.iter()
24067            .filter(|o| !o.eliminated)
24068            .collect();
24069        if viable.is_empty() {
24070            return Err("no viable options remaining".into());
24071        }
24072        let best = viable.iter().max_by(|a, b|
24073            a.score.partial_cmp(&b.score).unwrap_or(std::cmp::Ordering::Equal)
24074        ).unwrap();
24075        let best_id = best.id;
24076        let best_score = best.score;
24077
24078        // Certainty: margin between best and second-best
24079        let mut scores: Vec<f64> = viable.iter().map(|o| o.score).collect();
24080        scores.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
24081        let margin = if scores.len() >= 2 {
24082            scores[0] - scores[1]
24083        } else {
24084            scores[0] // single option → full score as margin
24085        };
24086        let certainty = (margin * 0.99).min(0.99);
24087        let certainty_rounded = (certainty * 10000.0).round() / 10000.0;
24088
24089        self.decided = true;
24090        self.chosen_option = Some(best_id);
24091
24092        Ok((best_id, best_score, certainty_rounded))
24093    }
24094
24095    /// Count viable (non-eliminated) options.
24096    pub fn viable_count(&self) -> usize {
24097        self.options.iter().filter(|o| !o.eliminated).count()
24098    }
24099}
24100
24101// ── Consensus — multi-agent agreement primitive ─────────────────────────────
24102
24103/// A vote cast by a participant in a Consensus session.
24104#[derive(Debug, Clone, Serialize, Deserialize)]
24105pub struct ConsensusVote {
24106    /// Voter identifier (agent name or role).
24107    pub voter: String,
24108    /// The choice voted for.
24109    pub choice: String,
24110    /// Confidence in this vote (0.0–1.0).
24111    pub confidence: f64,
24112    /// Optional rationale.
24113    pub rationale: String,
24114    /// Unix timestamp.
24115    pub voted_at: u64,
24116}
24117
24118/// A named Consensus session — multi-agent agreement through voting.
24119///
24120/// Participants cast votes for choices. A quorum threshold determines
24121/// when enough votes have been cast to reach a decision. The winning
24122/// choice is the one with the most confidence-weighted votes.
24123///
24124/// ΛD alignment: ψ = ⟨T="consensus", V=outcome, E=⟨c≤0.99, τ, ρ, δ=derived⟩⟩
24125/// Consensus is aggregated opinion — always derived (Theorem 5.1).
24126/// Certainty scales with agreement ratio (unanimous = high, split = low).
24127#[derive(Debug, Clone, Serialize, Deserialize)]
24128pub struct ConsensusSession {
24129    /// Session identifier.
24130    pub id: String,
24131    /// Session name.
24132    pub name: String,
24133    /// The proposal or question being voted on.
24134    pub proposal: String,
24135    /// Available choices to vote for.
24136    pub choices: Vec<String>,
24137    /// Minimum number of votes required for quorum.
24138    pub quorum: u32,
24139    /// Votes cast.
24140    pub votes: Vec<ConsensusVote>,
24141    /// Whether consensus has been reached.
24142    pub resolved: bool,
24143    /// Winning choice (if resolved).
24144    pub winner: String,
24145    /// Unix timestamp of creation.
24146    pub created_at: u64,
24147}
24148
24149impl ConsensusSession {
24150    /// Cast a vote. Each voter may vote only once.
24151    pub fn vote(&mut self, voter: String, choice: String, confidence: f64, rationale: String) -> Result<(), String> {
24152        if self.resolved {
24153            return Err("consensus already resolved".into());
24154        }
24155        if !self.choices.contains(&choice) {
24156            return Err(format!("invalid choice '{}': must be one of {:?}", choice, self.choices));
24157        }
24158        if self.votes.iter().any(|v| v.voter == voter) {
24159            return Err(format!("voter '{}' has already voted", voter));
24160        }
24161
24162        let now = std::time::SystemTime::now()
24163            .duration_since(std::time::UNIX_EPOCH)
24164            .unwrap_or_default()
24165            .as_secs();
24166
24167        self.votes.push(ConsensusVote {
24168            voter,
24169            choice,
24170            confidence: confidence.max(0.0).min(1.0),
24171            rationale,
24172            voted_at: now,
24173        });
24174
24175        Ok(())
24176    }
24177
24178    /// Check if quorum has been met.
24179    pub fn has_quorum(&self) -> bool {
24180        self.votes.len() as u32 >= self.quorum
24181    }
24182
24183    /// Tally votes: returns (choice → weighted_score) sorted descending.
24184    pub fn tally(&self) -> Vec<(String, f64, u32)> {
24185        let mut scores: HashMap<String, (f64, u32)> = HashMap::new();
24186        for v in &self.votes {
24187            let entry = scores.entry(v.choice.clone()).or_insert((0.0, 0));
24188            entry.0 += v.confidence;
24189            entry.1 += 1;
24190        }
24191        let mut result: Vec<(String, f64, u32)> = scores.into_iter()
24192            .map(|(choice, (score, count))| (choice, (score * 10000.0).round() / 10000.0, count))
24193            .collect();
24194        result.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
24195        result
24196    }
24197
24198    /// Resolve: determine winner if quorum is met.
24199    pub fn resolve(&mut self) -> Result<(String, f64, f64), String> {
24200        if self.resolved {
24201            return Err("already resolved".into());
24202        }
24203        if !self.has_quorum() {
24204            return Err(format!("quorum not met: {} of {} required", self.votes.len(), self.quorum));
24205        }
24206
24207        let tally = self.tally();
24208        if tally.is_empty() {
24209            return Err("no votes cast".into());
24210        }
24211
24212        let winner = tally[0].0.clone();
24213        let winner_score = tally[0].1;
24214        let total_score: f64 = tally.iter().map(|t| t.1).sum();
24215
24216        // Agreement ratio: winner_score / total_score
24217        let agreement = if total_score > 0.0 {
24218            (winner_score / total_score * 10000.0).round() / 10000.0
24219        } else {
24220            0.0
24221        };
24222
24223        // Certainty: agreement * 0.99 (capped at 0.99)
24224        let certainty = (agreement * 0.99 * 10000.0).round() / 10000.0;
24225
24226        self.resolved = true;
24227        self.winner = winner.clone();
24228
24229        Ok((winner, agreement, certainty.min(0.99)))
24230    }
24231
24232    /// Vote count.
24233    pub fn vote_count(&self) -> u32 {
24234        self.votes.len() as u32
24235    }
24236}
24237
24238// ── Hibernate — long-running suspension primitive ────────────────────────────
24239
24240/// A checkpoint captured during a Hibernate session.
24241///
24242/// Checkpoints save the state of a suspended operation so it can be
24243/// resumed later. Each checkpoint is an immutable snapshot.
24244#[derive(Debug, Clone, Serialize, Deserialize)]
24245pub struct HibernateCheckpoint {
24246    /// Checkpoint identifier (auto-assigned).
24247    pub id: u32,
24248    /// Label for this checkpoint.
24249    pub label: String,
24250    /// Serialized state payload (JSON).
24251    pub state: serde_json::Value,
24252    /// Unix timestamp of checkpoint creation.
24253    pub created_at: u64,
24254    /// Step or phase at time of checkpoint.
24255    pub phase: String,
24256}
24257
24258/// A named Hibernate session — suspend and resume long-running operations.
24259///
24260/// The session tracks a suspended operation with checkpoints for state
24261/// preservation. Operations can be suspended (hibernate), checkpointed,
24262/// and resumed from any checkpoint.
24263///
24264/// ΛD alignment: ψ = ⟨T="hibernate", V=state, E=⟨c, τ, ρ, δ⟩⟩
24265/// - Checkpoint state: c=1.0, δ=raw (exact state capture)
24266/// - Resumed execution: c=0.99, δ=derived (resumption is a transformation)
24267#[derive(Debug, Clone, Serialize, Deserialize)]
24268pub struct HibernateSession {
24269    /// Session identifier.
24270    pub id: String,
24271    /// Session name.
24272    pub name: String,
24273    /// The operation being hibernated.
24274    pub operation: String,
24275    /// Current status: "active", "suspended", "resumed", "completed".
24276    pub status: String,
24277    /// Checkpoints captured.
24278    pub checkpoints: Vec<HibernateCheckpoint>,
24279    /// The checkpoint ID used for resume (if resumed).
24280    pub resumed_from: Option<u32>,
24281    /// Unix timestamp of creation.
24282    pub created_at: u64,
24283    /// Unix timestamp of last status change.
24284    pub last_status_change: u64,
24285    /// Next checkpoint ID.
24286    pub next_checkpoint_id: u32,
24287}
24288
24289impl HibernateSession {
24290    /// Suspend the session (hibernate).
24291    pub fn suspend(&mut self) -> Result<(), String> {
24292        if self.status == "suspended" {
24293            return Err("already suspended".into());
24294        }
24295        if self.status == "completed" {
24296            return Err("cannot suspend completed session".into());
24297        }
24298        self.status = "suspended".into();
24299        self.last_status_change = std::time::SystemTime::now()
24300            .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
24301        Ok(())
24302    }
24303
24304    /// Create a checkpoint with current state.
24305    pub fn checkpoint(&mut self, label: String, state: serde_json::Value, phase: String) -> Result<u32, String> {
24306        if self.status == "completed" {
24307            return Err("cannot checkpoint completed session".into());
24308        }
24309
24310        let now = std::time::SystemTime::now()
24311            .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
24312
24313        let id = self.next_checkpoint_id;
24314        self.next_checkpoint_id += 1;
24315
24316        self.checkpoints.push(HibernateCheckpoint {
24317            id,
24318            label,
24319            state,
24320            created_at: now,
24321            phase,
24322        });
24323
24324        Ok(id)
24325    }
24326
24327    /// Resume from a checkpoint.
24328    pub fn resume(&mut self, checkpoint_id: u32) -> Result<&HibernateCheckpoint, String> {
24329        if self.status != "suspended" {
24330            return Err(format!("cannot resume from status '{}' (must be suspended)", self.status));
24331        }
24332
24333        let exists = self.checkpoints.iter().any(|c| c.id == checkpoint_id);
24334        if !exists {
24335            return Err(format!("checkpoint {} not found", checkpoint_id));
24336        }
24337
24338        self.status = "resumed".into();
24339        self.resumed_from = Some(checkpoint_id);
24340        self.last_status_change = std::time::SystemTime::now()
24341            .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
24342
24343        Ok(self.checkpoints.iter().find(|c| c.id == checkpoint_id).unwrap())
24344    }
24345
24346    /// Mark the session as completed.
24347    pub fn complete(&mut self) -> Result<(), String> {
24348        if self.status == "completed" {
24349            return Err("already completed".into());
24350        }
24351        self.status = "completed".into();
24352        self.last_status_change = std::time::SystemTime::now()
24353            .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
24354        Ok(())
24355    }
24356}
24357
24358// ── OTS — one-time-secret generation primitive ──────────────────────────────
24359
24360/// A one-time secret: readable exactly once, then destroyed.
24361///
24362/// OTS tokens provide secure ephemeral credential exchange. Each secret
24363/// has a TTL and can only be retrieved once — after retrieval it is
24364/// permanently destroyed.
24365///
24366/// ΛD alignment:
24367/// - Created secret: c=1.0, δ=raw (the secret is ground truth)
24368/// - Retrieved secret: c=1.0, δ=raw (exact value returned, then destroyed)
24369/// - Expired/consumed: c=0.0, δ=void (no longer exists)
24370#[derive(Debug, Clone, Serialize)]
24371pub struct OtsSecret {
24372    /// Secret identifier (token used for retrieval).
24373    pub token: String,
24374    /// The secret value (cleared after retrieval).
24375    pub value: String,
24376    /// Whether the secret has been consumed (retrieved).
24377    pub consumed: bool,
24378    /// Unix timestamp of creation.
24379    pub created_at: u64,
24380    /// TTL in seconds (0 = no expiry).
24381    pub ttl_secs: u64,
24382    /// Creator identity.
24383    pub created_by: String,
24384    /// Optional label/purpose.
24385    pub label: String,
24386}
24387
24388impl OtsSecret {
24389    /// Check if the secret has expired.
24390    pub fn is_expired(&self, now: u64) -> bool {
24391        self.ttl_secs > 0 && now > self.created_at + self.ttl_secs
24392    }
24393
24394    /// Consume the secret: return value and mark as consumed.
24395    pub fn consume(&mut self, now: u64) -> Result<String, String> {
24396        if self.consumed {
24397            return Err("secret already consumed".into());
24398        }
24399        if self.is_expired(now) {
24400            return Err("secret has expired".into());
24401        }
24402        self.consumed = true;
24403        let val = self.value.clone();
24404        self.value = String::new(); // clear the secret
24405        Ok(val)
24406    }
24407}
24408
24409/// Generate a cryptographically-inspired token (not truly random, but unique).
24410pub fn generate_ots_token(prefix: &str) -> String {
24411    let now = std::time::SystemTime::now()
24412        .duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
24413    let nanos = now.as_nanos();
24414    format!("ots_{}_{:x}", prefix, nanos)
24415}
24416
24417// ── Psyche — metacognitive self-reflection primitive ─────────────────────────
24418
24419/// An insight produced during a Psyche introspection session.
24420///
24421/// Each insight captures a metacognitive observation about the system's
24422/// own cognitive state: what it knows, what it's uncertain about, where
24423/// its reasoning might be flawed, and what it should investigate further.
24424#[derive(Debug, Clone, Serialize, Deserialize)]
24425pub struct PsycheInsight {
24426    /// Insight identifier.
24427    pub id: u32,
24428    /// Category: "knowledge_gap", "uncertainty", "bias", "strength", "recommendation".
24429    pub category: String,
24430    /// The insight content.
24431    pub content: String,
24432    /// Confidence in this insight (0.0–1.0).
24433    pub confidence: f64,
24434    /// Severity: "info", "warning", "critical".
24435    pub severity: String,
24436    /// Unix timestamp.
24437    pub created_at: u64,
24438}
24439
24440/// A named Psyche session — metacognitive self-reflection and introspection.
24441///
24442/// The session aggregates insights about the system's cognitive state,
24443/// producing an introspection report with self-awareness metrics.
24444///
24445/// ΛD alignment: ψ = ⟨T="psyche", V=introspection, E=⟨c≤0.99, τ, ρ, δ=derived⟩⟩
24446/// Self-reflection is inherently derived (Theorem 5.1): reasoning about
24447/// one's own reasoning is a meta-operation, not raw observation.
24448#[derive(Debug, Clone, Serialize, Deserialize)]
24449pub struct PsycheSession {
24450    /// Session identifier.
24451    pub id: String,
24452    /// Session name.
24453    pub name: String,
24454    /// The cognitive context being introspected.
24455    pub context: String,
24456    /// Insights gathered.
24457    pub insights: Vec<PsycheInsight>,
24458    /// Whether the introspection is complete.
24459    pub completed: bool,
24460    /// Unix timestamp of creation.
24461    pub created_at: u64,
24462    /// Next insight ID.
24463    pub next_insight_id: u32,
24464}
24465
24466impl PsycheSession {
24467    /// Add an insight.
24468    pub fn add_insight(&mut self, category: String, content: String, confidence: f64, severity: String) -> Result<u32, String> {
24469        if self.completed {
24470            return Err("session already completed".into());
24471        }
24472        let valid_categories = ["knowledge_gap", "uncertainty", "bias", "strength", "recommendation"];
24473        if !valid_categories.contains(&category.as_str()) {
24474            return Err(format!("invalid category '{}': must be one of {:?}", category, valid_categories));
24475        }
24476        let valid_severities = ["info", "warning", "critical"];
24477        if !valid_severities.contains(&severity.as_str()) {
24478            return Err(format!("invalid severity '{}': must be info/warning/critical", severity));
24479        }
24480
24481        let now = std::time::SystemTime::now()
24482            .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
24483
24484        let id = self.next_insight_id;
24485        self.next_insight_id += 1;
24486
24487        self.insights.push(PsycheInsight {
24488            id,
24489            category,
24490            content,
24491            confidence: confidence.max(0.0).min(1.0),
24492            severity,
24493            created_at: now,
24494        });
24495
24496        Ok(id)
24497    }
24498
24499    /// Generate introspection report.
24500    pub fn report(&self) -> serde_json::Value {
24501        let mut by_category: HashMap<String, Vec<&PsycheInsight>> = HashMap::new();
24502        for insight in &self.insights {
24503            by_category.entry(insight.category.clone()).or_default().push(insight);
24504        }
24505
24506        let gaps = by_category.get("knowledge_gap").map(|v| v.len()).unwrap_or(0);
24507        let uncertainties = by_category.get("uncertainty").map(|v| v.len()).unwrap_or(0);
24508        let biases = by_category.get("bias").map(|v| v.len()).unwrap_or(0);
24509        let strengths = by_category.get("strength").map(|v| v.len()).unwrap_or(0);
24510        let recommendations = by_category.get("recommendation").map(|v| v.len()).unwrap_or(0);
24511
24512        let critical_count = self.insights.iter().filter(|i| i.severity == "critical").count();
24513        let warning_count = self.insights.iter().filter(|i| i.severity == "warning").count();
24514
24515        let avg_confidence = if self.insights.is_empty() { 0.0 } else {
24516            let sum: f64 = self.insights.iter().map(|i| i.confidence).sum();
24517            (sum / self.insights.len() as f64 * 10000.0).round() / 10000.0
24518        };
24519
24520        // Self-awareness score: higher with more diverse insights, penalized by critical issues
24521        let diversity = [gaps > 0, uncertainties > 0, biases > 0, strengths > 0, recommendations > 0]
24522            .iter().filter(|&&b| b).count() as f64 / 5.0;
24523        let penalty = critical_count as f64 * 0.1;
24524        let awareness = ((diversity * 0.7 + avg_confidence * 0.3 - penalty).max(0.0).min(1.0) * 10000.0).round() / 10000.0;
24525
24526        serde_json::json!({
24527            "total_insights": self.insights.len(),
24528            "by_category": {
24529                "knowledge_gaps": gaps,
24530                "uncertainties": uncertainties,
24531                "biases": biases,
24532                "strengths": strengths,
24533                "recommendations": recommendations,
24534            },
24535            "severity_summary": {
24536                "critical": critical_count,
24537                "warning": warning_count,
24538                "info": self.insights.len() - critical_count - warning_count,
24539            },
24540            "avg_confidence": avg_confidence,
24541            "self_awareness_score": awareness,
24542        })
24543    }
24544}
24545
24546// ── AxonEndpoint — external API endpoint binding primitive ───────────────────
24547
24548/// A registered external API endpoint binding.
24549///
24550/// Each binding declares an external service endpoint that AXON flows
24551/// can call. Bindings carry method, URL template, headers, and authentication
24552/// configuration.
24553#[derive(Debug, Clone, Serialize, Deserialize)]
24554pub struct EndpointBinding {
24555    /// Binding name (unique identifier).
24556    pub name: String,
24557    /// HTTP method: "GET", "POST", "PUT", "DELETE".
24558    pub method: String,
24559    /// URL template with `{param}` placeholders (e.g., "https://api.example.com/v1/{resource}").
24560    pub url_template: String,
24561    /// Default headers to include in requests.
24562    pub headers: HashMap<String, String>,
24563    /// Authentication type: "none", "bearer", "api_key", "basic".
24564    pub auth_type: String,
24565    /// Auth credential key (looked up from server config, never stored in plain text).
24566    pub auth_ref: String,
24567    /// Timeout in milliseconds.
24568    pub timeout_ms: u64,
24569    /// Whether this binding is active.
24570    pub enabled: bool,
24571    /// Description of what this endpoint does.
24572    pub description: String,
24573    /// Unix timestamp of creation.
24574    pub created_at: u64,
24575    /// Total calls made through this binding.
24576    pub total_calls: u64,
24577    /// Total errors from this binding.
24578    pub total_errors: u64,
24579}
24580
24581/// Record of a simulated call to an external endpoint.
24582/// (Actual HTTP calls are not made in the runtime — this records the intent
24583/// for orchestration by external systems or MCP clients.)
24584#[derive(Debug, Clone, Serialize)]
24585pub struct EndpointCallRecord {
24586    /// Call identifier.
24587    pub id: String,
24588    /// Binding name used.
24589    pub binding: String,
24590    /// Resolved URL (template with params substituted).
24591    pub resolved_url: String,
24592    /// HTTP method.
24593    pub method: String,
24594    /// Request body (if any).
24595    pub body: serde_json::Value,
24596    /// Parameters substituted.
24597    pub params: HashMap<String, String>,
24598    /// Unix timestamp.
24599    pub called_at: u64,
24600}
24601
24602// ── Pix — visual reasoning primitive ─────────────────────────────────────────
24603
24604/// An annotation on a Pix image.
24605///
24606/// Annotations mark regions of interest with labels, bounding boxes,
24607/// and confidence scores from visual analysis.
24608#[derive(Debug, Clone, Serialize, Deserialize)]
24609pub struct PixAnnotation {
24610    /// Annotation identifier.
24611    pub id: u32,
24612    /// Label for the annotated region.
24613    pub label: String,
24614    /// Bounding box: [x, y, width, height] in normalized coordinates (0.0–1.0).
24615    pub bbox: [f64; 4],
24616    /// Confidence in this annotation (0.0–1.0).
24617    pub confidence: f64,
24618    /// Category: "object", "text", "region", "feature".
24619    pub category: String,
24620    /// Optional description.
24621    pub description: String,
24622}
24623
24624/// A registered image in a Pix session.
24625///
24626/// Each image carries metadata (dimensions, format, source) and
24627/// accumulated annotations from visual reasoning operations.
24628#[derive(Debug, Clone, Serialize, Deserialize)]
24629pub struct PixImage {
24630    /// Image identifier.
24631    pub id: String,
24632    /// Image source (URL, file path, or "inline").
24633    pub source: String,
24634    /// Width in pixels.
24635    pub width: u32,
24636    /// Height in pixels.
24637    pub height: u32,
24638    /// Format: "png", "jpeg", "webp", "svg".
24639    pub format: String,
24640    /// Annotations applied to this image.
24641    pub annotations: Vec<PixAnnotation>,
24642    /// ΛD epistemic envelope for the image data.
24643    pub envelope: EpistemicEnvelope,
24644    /// Unix timestamp of registration.
24645    pub registered_at: u64,
24646    /// Next annotation ID.
24647    pub next_annotation_id: u32,
24648}
24649
24650/// A named Pix session — visual reasoning with image metadata and annotation.
24651///
24652/// The session manages images and their annotations, enabling visual
24653/// reasoning workflows: register images, annotate regions, query annotations.
24654///
24655/// ΛD alignment: ψ = ⟨T="pix", V=visual_data, E=⟨c, τ, ρ, δ⟩⟩
24656/// - Image registration: c=1.0, δ=raw (the image metadata is ground truth)
24657/// - Annotations: c≤0.99, δ=derived (visual interpretation is speculative per Theorem 5.1)
24658#[derive(Debug, Clone, Serialize, Deserialize)]
24659pub struct PixSession {
24660    /// Session identifier.
24661    pub id: String,
24662    /// Session name.
24663    pub name: String,
24664    /// Images keyed by ID.
24665    pub images: HashMap<String, PixImage>,
24666    /// Unix timestamp of creation.
24667    pub created_at: u64,
24668    /// Next image ID counter.
24669    pub next_image_id: u64,
24670}
24671
24672impl PixSession {
24673    /// Register an image.
24674    pub fn register_image(&mut self, source: String, width: u32, height: u32, format: String, provenance: &str) -> String {
24675        let now = std::time::SystemTime::now()
24676            .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
24677
24678        let id = format!("img_{}_{}", self.name, self.next_image_id);
24679        self.next_image_id += 1;
24680
24681        let envelope = EpistemicEnvelope::raw_config("pix", provenance);
24682
24683        self.images.insert(id.clone(), PixImage {
24684            id: id.clone(),
24685            source,
24686            width,
24687            height,
24688            format,
24689            annotations: Vec::new(),
24690            envelope,
24691            registered_at: now,
24692            next_annotation_id: 1,
24693        });
24694
24695        id
24696    }
24697
24698    /// Add an annotation to an image.
24699    pub fn annotate(&mut self, image_id: &str, label: String, bbox: [f64; 4], confidence: f64, category: String, description: String) -> Result<u32, String> {
24700        let valid_categories = ["object", "text", "region", "feature"];
24701        if !valid_categories.contains(&category.as_str()) {
24702            return Err(format!("invalid category '{}': must be object/text/region/feature", category));
24703        }
24704
24705        // Validate bbox: all values in [0.0, 1.0]
24706        for &v in &bbox {
24707            if !(0.0..=1.0).contains(&v) {
24708                return Err("bbox values must be in [0.0, 1.0]".into());
24709            }
24710        }
24711
24712        let image = self.images.get_mut(image_id)
24713            .ok_or_else(|| format!("image '{}' not found", image_id))?;
24714
24715        let ann_id = image.next_annotation_id;
24716        image.next_annotation_id += 1;
24717
24718        image.annotations.push(PixAnnotation {
24719            id: ann_id,
24720            label,
24721            bbox,
24722            confidence: confidence.max(0.0).min(1.0),
24723            category,
24724            description,
24725        });
24726
24727        // Update image envelope to derived (annotations are interpretive)
24728        image.envelope = EpistemicEnvelope::derived("pix", 0.99, "pix_annotator");
24729
24730        Ok(ann_id)
24731    }
24732
24733    /// Total image count.
24734    pub fn image_count(&self) -> usize {
24735        self.images.len()
24736    }
24737
24738    /// Total annotation count across all images.
24739    pub fn total_annotations(&self) -> usize {
24740        self.images.values().map(|img| img.annotations.len()).sum()
24741    }
24742}
24743
24744/// A cached execution result with TTL and ΛD epistemic state.
24745///
24746/// Cache entries carry δ=derived (not raw) because they are reproductions
24747/// of a prior execution, not the original computation. Per Theorem 5.1,
24748/// c < 1.0 for derived values.
24749#[derive(Debug, Clone, Serialize)]
24750pub struct CachedResult {
24751    /// Cache key (flow_name + backend).
24752    pub cache_key: String,
24753    /// Flow name.
24754    pub flow_name: String,
24755    /// Backend used.
24756    pub backend: String,
24757    /// Cached execution result.
24758    pub result: serde_json::Value,
24759    /// Trace ID of the original execution.
24760    pub source_trace_id: u64,
24761    /// When the cache entry was created (Unix seconds).
24762    pub cached_at: u64,
24763    /// TTL in seconds (0 = no expiry).
24764    pub ttl_secs: u64,
24765    /// ΛD epistemic state: δ=derived, c<1.0 (stale risk).
24766    pub epistemic: EpistemicEnvelope,
24767}
24768
24769impl CachedResult {
24770    /// Check if this entry has expired.
24771    pub fn is_expired(&self) -> bool {
24772        if self.ttl_secs == 0 { return false; }
24773        let now = std::time::SystemTime::now()
24774            .duration_since(std::time::UNIX_EPOCH)
24775            .unwrap_or_default()
24776            .as_secs();
24777        now > self.cached_at + self.ttl_secs
24778    }
24779}
24780
24781/// Exportable server state backup with ΛD epistemic metadata.
24782#[derive(Debug, Clone, Serialize, Deserialize)]
24783pub struct ServerBackup {
24784    /// Backup format version.
24785    pub version: String,
24786    /// Timestamp of backup creation.
24787    pub created_at: u64,
24788
24789    // ── ΛD Epistemic Metadata ──
24790    // Each backup carries its epistemic envelope ψ = ⟨T, V, E⟩
24791    // ensuring no information loss across serialization boundaries.
24792
24793    /// ΛD envelope for the backup itself.
24794    pub lambda_d: EpistemicEnvelope,
24795
24796    /// Per-section epistemic provenance.
24797    pub section_provenance: HashMap<String, EpistemicEnvelope>,
24798
24799    // ── Config Sections (V — the value payload) ──
24800    /// Cost pricing config.
24801    pub cost_pricing: CostPricing,
24802    /// Cost budgets per flow.
24803    pub cost_budgets: HashMap<String, CostBudget>,
24804    /// Flow validation rules.
24805    pub flow_rules: HashMap<String, FlowValidationRules>,
24806    /// Flow execution quotas.
24807    pub flow_quotas: HashMap<String, FlowQuota>,
24808    /// Readiness gates.
24809    pub readiness_gates: ReadinessGates,
24810    /// Per-endpoint rate limits.
24811    pub endpoint_rate_limits: HashMap<String, EndpointRateLimit>,
24812    /// Schedule configs.
24813    pub schedules: Vec<ScheduleBackupEntry>,
24814    /// AxonStore instances (cognitive persistence).
24815    #[serde(default)]
24816    pub axon_stores: HashMap<String, AxonStoreInstance>,
24817    /// Dataspace instances (cognitive navigation).
24818    #[serde(default)]
24819    pub dataspaces: HashMap<String, DataspaceInstance>,
24820    /// Shield instances (cognitive guardrails).
24821    #[serde(default)]
24822    pub shields: HashMap<String, ShieldInstance>,
24823}
24824
24825/// Minimal schedule entry for backup (no runtime state).
24826#[derive(Debug, Clone, Serialize, Deserialize)]
24827pub struct ScheduleBackupEntry {
24828    pub name: String,
24829    pub flow_name: String,
24830    pub interval_secs: u64,
24831    pub enabled: bool,
24832    pub backend: String,
24833}
24834
24835/// POST /v1/server/backup — export server configuration state as JSON.
24836async fn server_backup_handler(
24837    State(state): State<SharedState>,
24838    headers: HeaderMap,
24839) -> Result<Json<serde_json::Value>, StatusCode> {
24840    let s = state.lock().unwrap();
24841    check_auth_peek(&s, &headers, AccessLevel::Admin)?;
24842
24843    let now = std::time::SystemTime::now()
24844        .duration_since(std::time::UNIX_EPOCH)
24845        .unwrap_or_default()
24846        .as_secs();
24847
24848    let schedules: Vec<ScheduleBackupEntry> = s.schedules.iter().map(|(name, sched)| {
24849        ScheduleBackupEntry {
24850            name: name.clone(),
24851            flow_name: sched.flow_name.clone(),
24852            interval_secs: sched.interval_secs,
24853            enabled: sched.enabled,
24854            backend: sched.backend.clone(),
24855        }
24856    }).collect();
24857
24858    let client = client_key_from_headers(&headers);
24859
24860    // Build per-section ΛD provenance
24861    let mut section_prov = HashMap::new();
24862    section_prov.insert("cost_pricing".into(), EpistemicEnvelope::raw_config("config:pricing", &client));
24863    section_prov.insert("cost_budgets".into(), EpistemicEnvelope::raw_config("config:budgets", &client));
24864    section_prov.insert("flow_rules".into(), EpistemicEnvelope::raw_config("config:validation_rules", &client));
24865    section_prov.insert("flow_quotas".into(), EpistemicEnvelope::raw_config("config:quotas", &client));
24866    section_prov.insert("readiness_gates".into(), EpistemicEnvelope::raw_config("config:readiness", &client));
24867    section_prov.insert("endpoint_rate_limits".into(), EpistemicEnvelope::raw_config("config:rate_limits", &client));
24868    section_prov.insert("schedules".into(), EpistemicEnvelope::raw_config("config:schedules", &client));
24869    section_prov.insert("axon_stores".into(), EpistemicEnvelope::raw_config("config:axon_stores", &client));
24870    section_prov.insert("dataspaces".into(), EpistemicEnvelope::raw_config("config:dataspaces", &client));
24871
24872    let backup = ServerBackup {
24873        version: "1.0-ΛD".into(),
24874        created_at: now,
24875        lambda_d: EpistemicEnvelope::raw_config("axon:server_backup", &client),
24876        section_provenance: section_prov,
24877        cost_pricing: s.cost_pricing.clone(),
24878        cost_budgets: s.cost_budgets.clone(),
24879        flow_rules: s.flow_rules.clone(),
24880        flow_quotas: s.flow_quotas.clone(),
24881        readiness_gates: s.readiness_gates.clone(),
24882        endpoint_rate_limits: s.endpoint_rate_limits.clone(),
24883        schedules,
24884        axon_stores: s.axon_stores.clone(),
24885        dataspaces: s.dataspaces.clone(),
24886        shields: s.shields.clone(),
24887    };
24888
24889    Ok(Json(serde_json::to_value(&backup).unwrap_or_default()))
24890}
24891
24892/// POST /v1/server/restore — import server configuration state from JSON backup.
24893async fn server_restore_handler(
24894    State(state): State<SharedState>,
24895    headers: HeaderMap,
24896    Json(backup): Json<ServerBackup>,
24897) -> Result<Json<serde_json::Value>, StatusCode> {
24898    let client = client_key_from_headers(&headers);
24899    let mut s = state.lock().unwrap();
24900    check_auth(&mut s, &headers, AccessLevel::Admin)?;
24901
24902    // Validate ΛD invariants at import boundary
24903    if let Err(e) = backup.lambda_d.validate() {
24904        return Ok(Json(serde_json::json!({
24905            "success": false,
24906            "error": e,
24907            "phase": "lambda_d_validation",
24908        })));
24909    }
24910    for (section, envelope) in &backup.section_provenance {
24911        if let Err(e) = envelope.validate() {
24912            return Ok(Json(serde_json::json!({
24913                "success": false,
24914                "error": format!("section '{}': {}", section, e),
24915                "phase": "lambda_d_section_validation",
24916            })));
24917        }
24918    }
24919
24920    // Apply backup
24921    s.cost_pricing = backup.cost_pricing;
24922    s.cost_budgets = backup.cost_budgets;
24923    s.flow_rules = backup.flow_rules;
24924    s.flow_quotas = backup.flow_quotas;
24925    s.readiness_gates = backup.readiness_gates;
24926    s.endpoint_rate_limits = backup.endpoint_rate_limits;
24927
24928    // Restore schedules (create new entries, don't overwrite runtime state of existing)
24929    let mut restored_schedules = 0;
24930    for sched in &backup.schedules {
24931        if !s.schedules.contains_key(&sched.name) {
24932            s.schedules.insert(sched.name.clone(), ScheduleEntry {
24933                flow_name: sched.flow_name.clone(),
24934                interval_secs: sched.interval_secs,
24935                enabled: sched.enabled,
24936                backend: sched.backend.clone(),
24937                last_run: 0,
24938                next_run: sched.interval_secs,
24939                run_count: 0,
24940                error_count: 0,
24941                history: Vec::new(),
24942            });
24943            restored_schedules += 1;
24944        }
24945    }
24946
24947    // Restore AxonStores (merge: don't overwrite existing)
24948    let mut restored_axon_stores = 0u64;
24949    for (name, store) in backup.axon_stores {
24950        if !s.axon_stores.contains_key(&name) {
24951            s.axon_stores.insert(name, store);
24952            restored_axon_stores += 1;
24953        }
24954    }
24955
24956    // Restore Dataspaces (merge: don't overwrite existing)
24957    let mut restored_dataspaces = 0u64;
24958    for (name, ds) in backup.dataspaces {
24959        if !s.dataspaces.contains_key(&name) {
24960            s.dataspaces.insert(name, ds);
24961            restored_dataspaces += 1;
24962        }
24963    }
24964
24965    // Restore Shields (merge: don't overwrite existing)
24966    let mut restored_shields = 0u64;
24967    for (name, sh) in backup.shields {
24968        if !s.shields.contains_key(&name) {
24969            s.shields.insert(name, sh);
24970            restored_shields += 1;
24971        }
24972    }
24973
24974    s.audit_log.record(
24975        &client, AuditAction::ConfigLoad, "server_restore",
24976        serde_json::json!({
24977            "version": backup.version, "restored_schedules": restored_schedules,
24978            "axon_stores_restored": restored_axon_stores, "dataspaces_restored": restored_dataspaces,
24979            "shields_restored": restored_shields,
24980        }),
24981        true,
24982    );
24983
24984    Ok(Json(serde_json::json!({
24985        "success": true,
24986        "version": backup.version,
24987        "restored": {
24988            "cost_pricing": true,
24989            "cost_budgets": true,
24990            "flow_rules": true,
24991            "flow_quotas": true,
24992            "readiness_gates": true,
24993            "endpoint_rate_limits": true,
24994            "schedules_created": restored_schedules,
24995            "axon_stores_restored": restored_axon_stores,
24996            "dataspaces_restored": restored_dataspaces,
24997            "shields_restored": restored_shields,
24998        },
24999    })))
25000}
25001
25002/// Query for cache lookup.
25003#[derive(Debug, Deserialize)]
25004pub struct CacheLookupQuery {
25005    pub flow_name: String,
25006    #[serde(default = "default_execute_backend")]
25007    pub backend: String,
25008}
25009
25010/// GET /v1/execute/cache — lookup cached result.
25011async fn execute_cache_get_handler(
25012    State(state): State<SharedState>,
25013    headers: HeaderMap,
25014    Query(params): Query<CacheLookupQuery>,
25015) -> Result<Json<serde_json::Value>, StatusCode> {
25016    let s = state.lock().unwrap();
25017    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
25018
25019    let key = format!("{}:{}", params.flow_name, params.backend);
25020    match s.execution_cache.iter().find(|c| c.cache_key == key) {
25021        Some(entry) if entry.is_expired() => Ok(Json(serde_json::json!({"hit": false, "expired": true, "cache_key": key}))),
25022        Some(entry) => Ok(Json(serde_json::json!({
25023            "hit": true, "cache_key": key, "cached_at": entry.cached_at, "ttl_secs": entry.ttl_secs,
25024            "source_trace_id": entry.source_trace_id, "result": entry.result,
25025            "epistemic": {"derivation": entry.epistemic.derivation, "certainty": entry.epistemic.certainty, "provenance": entry.epistemic.provenance},
25026        }))),
25027        None => Ok(Json(serde_json::json!({"hit": false, "cache_key": key}))),
25028    }
25029}
25030
25031/// Request to cache a result.
25032#[derive(Debug, Deserialize)]
25033pub struct CachePutRequest {
25034    pub flow_name: String,
25035    #[serde(default = "default_execute_backend")]
25036    pub backend: String,
25037    pub result: serde_json::Value,
25038    pub source_trace_id: u64,
25039    #[serde(default = "default_cache_ttl")]
25040    pub ttl_secs: u64,
25041}
25042
25043fn default_cache_ttl() -> u64 { 300 }
25044
25045/// PUT /v1/execute/cache — store a result in the cache with ΛD epistemic state.
25046async fn execute_cache_put_handler(
25047    State(state): State<SharedState>,
25048    headers: HeaderMap,
25049    Json(payload): Json<CachePutRequest>,
25050) -> Result<Json<serde_json::Value>, StatusCode> {
25051    let mut s = state.lock().unwrap();
25052    check_auth(&mut s, &headers, AccessLevel::Write)?;
25053
25054    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
25055    let key = format!("{}:{}", payload.flow_name, payload.backend);
25056
25057    // ΛD: cached result is δ=derived, c=0.95 (Theorem 5.1: only raw may carry c=1.0)
25058    let epistemic = EpistemicEnvelope::derived(
25059        &format!("cache:execution:{}", payload.flow_name),
25060        0.95,
25061        &format!("trace:{}", payload.source_trace_id),
25062    );
25063
25064    let entry = CachedResult {
25065        cache_key: key.clone(), flow_name: payload.flow_name, backend: payload.backend,
25066        result: payload.result, source_trace_id: payload.source_trace_id,
25067        cached_at: now, ttl_secs: payload.ttl_secs, epistemic,
25068    };
25069
25070    s.execution_cache.retain(|c| c.cache_key != key);
25071    s.execution_cache.push(entry);
25072    if s.execution_cache.len() > 200 { s.execution_cache.remove(0); }
25073
25074    Ok(Json(serde_json::json!({"success": true, "cache_key": key, "ttl_secs": payload.ttl_secs})))
25075}
25076
25077/// DELETE /v1/execute/cache — evict cache entry or all.
25078async fn execute_cache_delete_handler(
25079    State(state): State<SharedState>,
25080    headers: HeaderMap,
25081    Query(params): Query<std::collections::HashMap<String, String>>,
25082) -> Result<Json<serde_json::Value>, StatusCode> {
25083    let mut s = state.lock().unwrap();
25084    check_auth(&mut s, &headers, AccessLevel::Write)?;
25085
25086    if let Some(key) = params.get("cache_key") {
25087        let before = s.execution_cache.len();
25088        s.execution_cache.retain(|c| &c.cache_key != key);
25089        Ok(Json(serde_json::json!({"evicted": before - s.execution_cache.len(), "cache_key": key})))
25090    } else {
25091        let count = s.execution_cache.len();
25092        s.execution_cache.clear();
25093        Ok(Json(serde_json::json!({"evicted": count, "all": true})))
25094    }
25095}
25096
25097/// Request for cache-aware execution.
25098#[derive(Debug, Deserialize)]
25099pub struct CacheAwareExecuteRequest {
25100    /// Flow name to execute.
25101    pub flow_name: String,
25102    /// Backend (default "stub").
25103    #[serde(default = "default_execute_backend")]
25104    pub backend: String,
25105    /// TTL for caching the result (default 300s). 0 = don't cache.
25106    #[serde(default = "default_cache_ttl")]
25107    pub cache_ttl_secs: u64,
25108    /// Force re-execution even if cached (default false).
25109    #[serde(default)]
25110    pub force: bool,
25111}
25112
25113/// POST /v1/execute/cached — cache-aware execution.
25114///
25115/// 1. Checks cache for flow+backend key.
25116/// 2. If hit and not expired and not forced → return cached (ΛD δ=derived).
25117/// 3. Otherwise → execute, cache result, return fresh (ΛD δ=raw).
25118async fn execute_cached_handler(
25119    State(state): State<SharedState>,
25120    headers: HeaderMap,
25121    Json(payload): Json<CacheAwareExecuteRequest>,
25122) -> Result<Json<serde_json::Value>, StatusCode> {
25123    let req_start = Instant::now();
25124    let client = client_key_from_headers(&headers);
25125    {
25126        let mut s = state.lock().unwrap();
25127        check_auth(&mut s, &headers, AccessLevel::Write)?;
25128    }
25129
25130    let cache_key = format!("{}:{}", payload.flow_name, payload.backend);
25131
25132    // Step 1: Check cache (unless forced)
25133    if !payload.force {
25134        let s = state.lock().unwrap();
25135        if let Some(entry) = s.execution_cache.iter().find(|c| c.cache_key == cache_key) {
25136            if !entry.is_expired() {
25137                return Ok(Json(serde_json::json!({
25138                    "success": true,
25139                    "cached": true,
25140                    "cache_key": cache_key,
25141                    "source_trace_id": entry.source_trace_id,
25142                    "cached_at": entry.cached_at,
25143                    "ttl_secs": entry.ttl_secs,
25144                    "result": entry.result,
25145                    "epistemic": {
25146                        "derivation": "derived",
25147                        "certainty": entry.epistemic.certainty,
25148                        "note": "cached result — δ=derived per ΛD Theorem 5.1",
25149                    },
25150                })));
25151            }
25152        }
25153    }
25154
25155    // Step 2: Execute fresh
25156    let (source, source_file) = {
25157        let s = state.lock().unwrap();
25158        match s.versions.get_history(&payload.flow_name)
25159            .and_then(|h| h.active())
25160            .map(|v| (v.source.clone(), v.source_file.clone()))
25161        {
25162            Some(info) => info,
25163            None => return Ok(Json(serde_json::json!({
25164                "success": false, "error": format!("flow '{}' not deployed", payload.flow_name),
25165            }))),
25166        }
25167    };
25168
25169    match server_execute_full(&state, &source, &source_file, &payload.flow_name, &payload.backend).0 {
25170        Ok(mut er) => {
25171            let mut trace_entry = crate::trace_store::build_trace(
25172                &er.flow_name, &er.source_file, &er.backend, &client,
25173                if er.success { crate::trace_store::TraceStatus::Success }
25174                else { crate::trace_store::TraceStatus::Partial },
25175                er.steps_executed, er.latency_ms,
25176            );
25177            trace_entry.tokens_input = er.tokens_input;
25178            trace_entry.tokens_output = er.tokens_output;
25179            trace_entry.errors = er.errors;
25180
25181            let trace_id = {
25182                let mut s = state.lock().unwrap();
25183                let tid = s.trace_store.record(trace_entry);
25184
25185                // Step 3: Cache the result (if ttl > 0)
25186                if payload.cache_ttl_secs > 0 {
25187                    let now = std::time::SystemTime::now()
25188                        .duration_since(std::time::UNIX_EPOCH)
25189                        .unwrap_or_default()
25190                        .as_secs();
25191
25192                    let cached = CachedResult {
25193                        cache_key: cache_key.clone(),
25194                        flow_name: er.flow_name.clone(),
25195                        backend: er.backend.clone(),
25196                        result: serde_json::json!({
25197                            "steps_executed": er.steps_executed,
25198                            "latency_ms": er.latency_ms,
25199                            "tokens_input": er.tokens_input,
25200                            "tokens_output": er.tokens_output,
25201                            "step_names": er.step_names,
25202                        }),
25203                        source_trace_id: tid,
25204                        cached_at: now,
25205                        ttl_secs: payload.cache_ttl_secs,
25206                        epistemic: EpistemicEnvelope::derived(
25207                            &format!("cache:execution:{}", er.flow_name),
25208                            0.95,
25209                            &format!("trace:{}", tid),
25210                        ),
25211                    };
25212                    s.execution_cache.retain(|c| c.cache_key != cache_key);
25213                    s.execution_cache.push(cached);
25214                    if s.execution_cache.len() > 200 { s.execution_cache.remove(0); }
25215                }
25216
25217                tid
25218            };
25219
25220            er.trace_id = trace_id;
25221
25222            Ok(Json(serde_json::json!({
25223                "success": er.success,
25224                "cached": false,
25225                "cache_key": cache_key,
25226                "trace_id": trace_id,
25227                "flow": er.flow_name,
25228                "backend": er.backend,
25229                "steps_executed": er.steps_executed,
25230                "latency_ms": req_start.elapsed().as_millis() as u64,
25231                "tokens_input": er.tokens_input,
25232                "tokens_output": er.tokens_output,
25233                "auto_cached": payload.cache_ttl_secs > 0,
25234                "cache_ttl_secs": payload.cache_ttl_secs,
25235                "epistemic": {
25236                    "derivation": "raw",
25237                    "certainty": 1.0,
25238                    "note": "fresh execution — δ=raw, c=1.0",
25239                },
25240            })))
25241        }
25242        Err(e) => {
25243            let mut s = state.lock().unwrap();
25244            s.metrics.total_errors += 1;
25245            Ok(Json(serde_json::json!({"success": false, "error": e})))
25246        }
25247    }
25248}
25249
25250/// Query for stream consumer.
25251#[derive(Debug, Deserialize)]
25252pub struct StreamConsumeQuery {
25253    /// Cursor: only return tokens with index > after (default 0 = all).
25254    #[serde(default)]
25255    pub after: u64,
25256    /// Max tokens to return (default 100).
25257    #[serde(default = "default_consume_limit")]
25258    pub limit: usize,
25259}
25260
25261fn default_consume_limit() -> usize { 100 }
25262
25263/// GET /v1/execute/stream/:trace_id/consume — consume stream tokens for a trace.
25264///
25265/// Client polls with `after=<last_token_index>` for incremental consumption.
25266/// Returns tokens, reconstructed output, completion status, and epistemic state.
25267async fn stream_consume_handler(
25268    State(state): State<SharedState>,
25269    headers: HeaderMap,
25270    Path(trace_id): Path<u64>,
25271    Query(params): Query<StreamConsumeQuery>,
25272) -> Result<Json<serde_json::Value>, StatusCode> {
25273    let s = state.lock().unwrap();
25274    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
25275
25276    let topic = format!("flow.stream.{}", trace_id);
25277    let events = s.event_bus.recent_events(500, Some(&topic));
25278
25279    if events.is_empty() {
25280        return Ok(Json(serde_json::json!({
25281            "trace_id": trace_id,
25282            "found": false,
25283            "message": "no stream tokens found for this trace_id",
25284        })));
25285    }
25286
25287    // Events are newest-first; reverse for chronological
25288    let mut chronological: Vec<_> = events.into_iter().collect();
25289    chronological.reverse();
25290
25291    // Filter by cursor
25292    let filtered: Vec<_> = chronological.iter()
25293        .filter(|ev| {
25294            ev.payload.get("token_index")
25295                .and_then(|v| v.as_u64())
25296                .map_or(false, |idx| idx > params.after)
25297        })
25298        .take(params.limit)
25299        .collect();
25300
25301    // Check completion
25302    let is_complete = chronological.iter().any(|ev| {
25303        ev.payload.get("is_final").and_then(|v| v.as_bool()).unwrap_or(false)
25304    });
25305
25306    // Last token index for cursor
25307    let last_index = filtered.last()
25308        .and_then(|ev| ev.payload.get("token_index").and_then(|v| v.as_u64()))
25309        .unwrap_or(params.after);
25310
25311    // Reconstruct output from all chronological tokens (not just filtered)
25312    let mut reconstructed = String::new();
25313    let mut step_outputs: Vec<serde_json::Value> = Vec::new();
25314    let mut current_step = String::new();
25315    let mut current_content = String::new();
25316
25317    for ev in &chronological {
25318        let step = ev.payload.get("step_name").and_then(|v| v.as_str()).unwrap_or("");
25319        let content = ev.payload.get("content").and_then(|v| v.as_str()).unwrap_or("");
25320        let is_final = ev.payload.get("is_final").and_then(|v| v.as_bool()).unwrap_or(false);
25321
25322        if is_final { continue; }
25323
25324        if !step.is_empty() && step != current_step.as_str() {
25325            if !current_step.is_empty() {
25326                step_outputs.push(serde_json::json!({"step": current_step, "output": current_content.trim()}));
25327            }
25328            current_step = step.to_string();
25329            current_content = String::new();
25330        }
25331        if !content.is_empty() {
25332            if !current_content.is_empty() { current_content.push(' '); }
25333            current_content.push_str(content);
25334        }
25335        if !reconstructed.is_empty() && !content.is_empty() { reconstructed.push(' '); }
25336        reconstructed.push_str(content);
25337    }
25338    if !current_step.is_empty() {
25339        step_outputs.push(serde_json::json!({"step": current_step, "output": current_content.trim()}));
25340    }
25341
25342    // Epistemic state of the stream
25343    let final_epistemic = if is_complete {
25344        chronological.iter().rev()
25345            .find(|ev| ev.payload.get("is_final").and_then(|v| v.as_bool()).unwrap_or(false))
25346            .and_then(|ev| ev.payload.get("epistemic_state").and_then(|v| v.as_str()))
25347            .unwrap_or("know")
25348    } else {
25349        "speculate" // still streaming
25350    };
25351
25352    let tokens: Vec<serde_json::Value> = filtered.iter().map(|ev| ev.payload.clone()).collect();
25353
25354    Ok(Json(serde_json::json!({
25355        "trace_id": trace_id,
25356        "found": true,
25357        "complete": is_complete,
25358        "cursor": last_index,
25359        "tokens_returned": tokens.len(),
25360        "total_tokens": chronological.len(),
25361        "tokens": tokens,
25362        "reconstructed_output": reconstructed.trim(),
25363        "step_outputs": step_outputs,
25364        "epistemic_state": final_epistemic,
25365        "next_url": format!("/v1/execute/stream/{}/consume?after={}", trace_id, last_index),
25366    })))
25367}
25368
25369/// A single item in a batch execution request.
25370#[derive(Debug, Clone, Deserialize)]
25371pub struct BatchItem {
25372    pub flow_name: String,
25373    #[serde(default = "default_execute_backend")]
25374    pub backend: String,
25375}
25376
25377/// Request for batch execution.
25378#[derive(Debug, Deserialize)]
25379pub struct BatchExecuteRequest {
25380    /// Items to execute (max 50).
25381    pub items: Vec<BatchItem>,
25382    /// Whether to continue on failure (default true).
25383    #[serde(default = "default_batch_continue")]
25384    pub continue_on_failure: bool,
25385}
25386
25387fn default_batch_continue() -> bool { true }
25388
25389/// Result for a single batch item.
25390#[derive(Debug, Clone, Serialize)]
25391pub struct BatchItemResult {
25392    pub index: usize,
25393    pub flow_name: String,
25394    pub backend: String,
25395    pub success: bool,
25396    pub trace_id: u64,
25397    pub latency_ms: u64,
25398    pub tokens_input: u64,
25399    pub tokens_output: u64,
25400    pub error: Option<String>,
25401    /// ΛD: fresh execution → δ=raw, c=1.0
25402    pub epistemic_derivation: String,
25403}
25404
25405/// POST /v1/execute/batch — execute multiple flows in one request.
25406async fn execute_batch_handler(
25407    State(state): State<SharedState>,
25408    headers: HeaderMap,
25409    Json(payload): Json<BatchExecuteRequest>,
25410) -> Result<Json<serde_json::Value>, StatusCode> {
25411    let req_start = Instant::now();
25412    let client = client_key_from_headers(&headers);
25413    {
25414        let mut s = state.lock().unwrap();
25415        check_auth(&mut s, &headers, AccessLevel::Write)?;
25416    }
25417
25418    if payload.items.is_empty() {
25419        return Ok(Json(serde_json::json!({"error": "batch must have at least 1 item"})));
25420    }
25421    if payload.items.len() > 50 {
25422        return Ok(Json(serde_json::json!({"error": "maximum 50 items per batch"})));
25423    }
25424
25425    let mut results: Vec<BatchItemResult> = Vec::new();
25426
25427    for (idx, item) in payload.items.iter().enumerate() {
25428        let source_info = {
25429            let s = state.lock().unwrap();
25430            s.versions.get_history(&item.flow_name)
25431                .and_then(|h| h.active())
25432                .map(|v| (v.source.clone(), v.source_file.clone()))
25433        };
25434
25435        let (source, source_file) = match source_info {
25436            Some(info) => info,
25437            None => {
25438                results.push(BatchItemResult {
25439                    index: idx, flow_name: item.flow_name.clone(), backend: item.backend.clone(),
25440                    success: false, trace_id: 0, latency_ms: 0, tokens_input: 0, tokens_output: 0,
25441                    error: Some(format!("flow '{}' not deployed", item.flow_name)),
25442                    epistemic_derivation: "none".into(),
25443                });
25444                if !payload.continue_on_failure { break; }
25445                continue;
25446            }
25447        };
25448
25449        match server_execute_full(&state, &source, &source_file, &item.flow_name, &item.backend).0 {
25450            Ok(er) => {
25451                let mut entry = crate::trace_store::build_trace(
25452                    &er.flow_name, &er.source_file, &er.backend, &client,
25453                    if er.success { crate::trace_store::TraceStatus::Success }
25454                    else { crate::trace_store::TraceStatus::Partial },
25455                    er.steps_executed, er.latency_ms,
25456                );
25457                entry.tokens_input = er.tokens_input;
25458                entry.tokens_output = er.tokens_output;
25459                entry.errors = er.errors;
25460
25461                let tid = { let mut s = state.lock().unwrap(); s.trace_store.record(entry) };
25462
25463                results.push(BatchItemResult {
25464                    index: idx, flow_name: item.flow_name.clone(), backend: item.backend.clone(),
25465                    success: er.success, trace_id: tid, latency_ms: er.latency_ms,
25466                    tokens_input: er.tokens_input, tokens_output: er.tokens_output,
25467                    error: None, epistemic_derivation: "raw".into(),
25468                });
25469
25470                if !er.success && !payload.continue_on_failure { break; }
25471            }
25472            Err(e) => {
25473                { let mut s = state.lock().unwrap(); s.metrics.total_errors += 1; }
25474                results.push(BatchItemResult {
25475                    index: idx, flow_name: item.flow_name.clone(), backend: item.backend.clone(),
25476                    success: false, trace_id: 0, latency_ms: 0, tokens_input: 0, tokens_output: 0,
25477                    error: Some(e), epistemic_derivation: "none".into(),
25478                });
25479                if !payload.continue_on_failure { break; }
25480            }
25481        }
25482    }
25483
25484    let succeeded = results.iter().filter(|r| r.success).count();
25485    let failed = results.iter().filter(|r| !r.success).count();
25486    let total_tokens: u64 = results.iter().map(|r| r.tokens_input + r.tokens_output).sum();
25487
25488    Ok(Json(serde_json::json!({
25489        "batch_size": payload.items.len(),
25490        "executed": results.len(),
25491        "succeeded": succeeded,
25492        "failed": failed,
25493        "total_latency_ms": req_start.elapsed().as_millis() as u64,
25494        "total_tokens": total_tokens,
25495        "continue_on_failure": payload.continue_on_failure,
25496        "results": results,
25497    })))
25498}
25499
25500/// GET /v1/daemons/autoscale — view config and current scaling decision.
25501async fn daemons_autoscale_get_handler(
25502    State(state): State<SharedState>,
25503    headers: HeaderMap,
25504) -> Result<Json<serde_json::Value>, StatusCode> {
25505    let s = state.lock().unwrap();
25506    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
25507
25508    let decision = evaluate_autoscale(&s);
25509
25510    Ok(Json(serde_json::json!({
25511        "config": s.autoscale_config,
25512        "decision": decision,
25513    })))
25514}
25515
25516/// PUT /v1/daemons/autoscale — update autoscale configuration.
25517async fn daemons_autoscale_put_handler(
25518    State(state): State<SharedState>,
25519    headers: HeaderMap,
25520    Json(config): Json<AutoscaleConfig>,
25521) -> Result<Json<serde_json::Value>, StatusCode> {
25522    let client = client_key_from_headers(&headers);
25523    let mut s = state.lock().unwrap();
25524    check_auth(&mut s, &headers, AccessLevel::Admin)?;
25525
25526    s.autoscale_config = config.clone();
25527    s.audit_log.record(
25528        &client, AuditAction::ConfigUpdate, "autoscale",
25529        serde_json::to_value(&config).unwrap_or_default(), true,
25530    );
25531
25532    Ok(Json(serde_json::json!({
25533        "success": true,
25534        "config": config,
25535    })))
25536}
25537
25538/// Evaluate autoscale decision based on current server state.
25539fn evaluate_autoscale(s: &ServerState) -> AutoscaleDecision {
25540    let cfg = &s.autoscale_config;
25541    let current = s.daemons.len();
25542    let active = s.daemons.values().filter(|d| d.state == DaemonState::Running || d.state == DaemonState::Hibernating).count();
25543    let queue_depth = s.execution_queue.iter().filter(|q| q.status == "pending").count();
25544
25545    let uptime = s.started_at.elapsed().as_secs().max(1);
25546    let bus_stats = s.event_bus.stats();
25547    let events_per_sec = bus_stats.events_published as f64 / uptime as f64;
25548
25549    if !cfg.enabled {
25550        return AutoscaleDecision {
25551            current_daemons: current, active_daemons: active,
25552            queue_depth, events_per_sec,
25553            recommendation: "none".into(),
25554            reason: "autoscaling disabled".into(),
25555        };
25556    }
25557
25558    // Scale up?
25559    if current < cfg.max_daemons {
25560        if queue_depth >= cfg.scale_up_queue_depth {
25561            return AutoscaleDecision {
25562                current_daemons: current, active_daemons: active,
25563                queue_depth, events_per_sec,
25564                recommendation: "scale_up".into(),
25565                reason: format!("queue depth {} >= threshold {}", queue_depth, cfg.scale_up_queue_depth),
25566            };
25567        }
25568        if events_per_sec >= cfg.scale_up_events_per_sec as f64 {
25569            return AutoscaleDecision {
25570                current_daemons: current, active_daemons: active,
25571                queue_depth, events_per_sec,
25572                recommendation: "scale_up".into(),
25573                reason: format!("events/sec {:.1} >= threshold {}", events_per_sec, cfg.scale_up_events_per_sec),
25574            };
25575        }
25576    }
25577
25578    // Scale down?
25579    if current > cfg.min_daemons && active == 0 {
25580        return AutoscaleDecision {
25581            current_daemons: current, active_daemons: active,
25582            queue_depth, events_per_sec,
25583            recommendation: "scale_down".into(),
25584            reason: format!("no active daemons, {} registered > min {}", current, cfg.min_daemons),
25585        };
25586    }
25587
25588    AutoscaleDecision {
25589        current_daemons: current, active_daemons: active,
25590        queue_depth, events_per_sec,
25591        recommendation: "steady".into(),
25592        reason: "within bounds".into(),
25593    }
25594}
25595
25596/// GET /v1/flows/:name/dashboard — per-flow execution dashboard.
25597async fn flow_dashboard_handler(
25598    State(state): State<SharedState>,
25599    headers: HeaderMap,
25600    Path(name): Path<String>,
25601) -> Result<Json<serde_json::Value>, StatusCode> {
25602    let s = state.lock().unwrap();
25603    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
25604
25605    let entries = s.trace_store.recent(s.trace_store.len(), None);
25606    let flow_traces: Vec<_> = entries.iter().filter(|e| e.flow_name == name).collect();
25607
25608    if flow_traces.is_empty() {
25609        return Ok(Json(serde_json::json!({
25610            "flow": name,
25611            "found": false,
25612            "message": "no execution history for this flow",
25613        })));
25614    }
25615
25616    let total = flow_traces.len() as u64;
25617    let errors: u64 = flow_traces.iter().map(|e| e.errors as u64).sum();
25618    let total_latency: u64 = flow_traces.iter().map(|e| e.latency_ms).sum();
25619    let total_tokens_in: u64 = flow_traces.iter().map(|e| e.tokens_input).sum();
25620    let total_tokens_out: u64 = flow_traces.iter().map(|e| e.tokens_output).sum();
25621    let error_traces = flow_traces.iter().filter(|e| e.errors > 0).count() as u64;
25622
25623    let mut latencies: Vec<u64> = flow_traces.iter().map(|e| e.latency_ms).collect();
25624    latencies.sort();
25625    let p50 = latencies[latencies.len() / 2];
25626    let p95_idx = ((95 * latencies.len() + 99) / 100).min(latencies.len()) - 1;
25627    let p95 = latencies[p95_idx];
25628
25629    // Cost
25630    let costs = compute_flow_costs(&s.trace_store, &s.cost_pricing);
25631    let flow_cost = costs.iter().find(|c| c.flow_name == name);
25632
25633    // Recent executions (last 10)
25634    let recent: Vec<serde_json::Value> = flow_traces.iter().take(10).map(|e| {
25635        serde_json::json!({
25636            "trace_id": e.id, "status": e.status.as_str(), "latency_ms": e.latency_ms,
25637            "errors": e.errors, "tokens": e.tokens_input + e.tokens_output, "timestamp": e.timestamp,
25638        })
25639    }).collect();
25640
25641    // Status breakdown
25642    let mut status_counts: HashMap<String, u64> = HashMap::new();
25643    for e in &flow_traces {
25644        *status_counts.entry(e.status.as_str().to_string()).or_insert(0) += 1;
25645    }
25646
25647    // Daemon state
25648    let daemon_state = s.daemons.get(&name).map(|d| format!("{:?}", d.state).to_lowercase());
25649
25650    // Schedule info
25651    let schedule = s.schedules.get(&name).map(|sched| serde_json::json!({
25652        "enabled": sched.enabled, "interval_secs": sched.interval_secs,
25653        "run_count": sched.run_count, "error_count": sched.error_count,
25654    }));
25655
25656    // Budget info
25657    let budget = s.cost_budgets.get(&name).map(|b| {
25658        let current_cost = flow_cost.map(|c| c.estimated_cost_usd).unwrap_or(0.0);
25659        let usage_pct = if b.max_cost_usd > 0.0 { current_cost / b.max_cost_usd } else { 0.0 };
25660        serde_json::json!({"max_cost_usd": b.max_cost_usd, "current_cost_usd": current_cost, "usage_pct": usage_pct})
25661    });
25662
25663    // Quota info
25664    let quota = s.flow_quotas.get(&name).map(|q| serde_json::json!({
25665        "max_per_hour": q.max_per_hour, "max_per_day": q.max_per_day,
25666        "current_hour": q.current_hour_count, "current_day": q.current_day_count,
25667    }));
25668
25669    Ok(Json(serde_json::json!({
25670        "flow": name,
25671        "found": true,
25672        "executions": {
25673            "total": total,
25674            "error_count": error_traces,
25675            "error_rate": if total > 0 { error_traces as f64 / total as f64 } else { 0.0 },
25676            "status_breakdown": status_counts,
25677        },
25678        "latency": {
25679            "avg_ms": if total > 0 { total_latency / total } else { 0 },
25680            "p50_ms": p50,
25681            "p95_ms": p95,
25682            "min_ms": latencies[0],
25683            "max_ms": latencies[latencies.len() - 1],
25684        },
25685        "tokens": {
25686            "total_input": total_tokens_in,
25687            "total_output": total_tokens_out,
25688            "total": total_tokens_in + total_tokens_out,
25689            "avg_per_execution": if total > 0 { (total_tokens_in + total_tokens_out) / total } else { 0 },
25690        },
25691        "cost": flow_cost.map(|c| serde_json::json!({
25692            "estimated_usd": c.estimated_cost_usd,
25693            "executions": c.executions,
25694        })),
25695        "recent_executions": recent,
25696        "daemon_state": daemon_state,
25697        "schedule": schedule,
25698        "budget": budget,
25699        "quota": quota,
25700    })))
25701}
25702
25703/// Build a ServerBackup from current state (used by persist, backup, and auto-persist).
25704fn build_server_backup(s: &ServerState, provenance: &str) -> ServerBackup {
25705    let now = std::time::SystemTime::now()
25706        .duration_since(std::time::UNIX_EPOCH)
25707        .unwrap_or_default()
25708        .as_secs();
25709
25710    let schedules: Vec<ScheduleBackupEntry> = s.schedules.iter().map(|(name, sched)| {
25711        ScheduleBackupEntry {
25712            name: name.clone(), flow_name: sched.flow_name.clone(),
25713            interval_secs: sched.interval_secs, enabled: sched.enabled, backend: sched.backend.clone(),
25714        }
25715    }).collect();
25716
25717    let mut section_prov = HashMap::new();
25718    for sec in &["cost_pricing", "cost_budgets", "flow_rules", "flow_quotas", "readiness_gates", "endpoint_rate_limits", "schedules", "axon_stores", "dataspaces"] {
25719        section_prov.insert(sec.to_string(), EpistemicEnvelope::raw_config(&format!("config:{}", sec), provenance));
25720    }
25721
25722    ServerBackup {
25723        version: "1.0-ΛD".into(),
25724        created_at: now,
25725        lambda_d: EpistemicEnvelope::raw_config("axon:server_persist", provenance),
25726        section_provenance: section_prov,
25727        cost_pricing: s.cost_pricing.clone(),
25728        cost_budgets: s.cost_budgets.clone(),
25729        flow_rules: s.flow_rules.clone(),
25730        flow_quotas: s.flow_quotas.clone(),
25731        readiness_gates: s.readiness_gates.clone(),
25732        endpoint_rate_limits: s.endpoint_rate_limits.clone(),
25733        schedules,
25734        axon_stores: s.axon_stores.clone(),
25735        dataspaces: s.dataspaces.clone(),
25736        shields: s.shields.clone(),
25737    }
25738}
25739
25740/// Persist state to disk. Returns Ok(path) or Err(message).
25741fn persist_state_to_disk(s: &ServerState, provenance: &str) -> Result<String, String> {
25742    let backup = build_server_backup(s, provenance);
25743    let path = s.config.config_path.as_deref()
25744        .map(|p| std::path::Path::new(p).parent().unwrap_or(std::path::Path::new(".")).join(STATE_PERSIST_PATH))
25745        .unwrap_or_else(|| std::path::PathBuf::from(STATE_PERSIST_PATH));
25746
25747    let json_str = serde_json::to_string_pretty(&backup).map_err(|e| format!("serialize: {}", e))?;
25748    std::fs::write(&path, &json_str).map_err(|e| format!("write: {}", e))?;
25749    Ok(path.display().to_string())
25750}
25751
25752/// Default persistence file path.
25753const STATE_PERSIST_PATH: &str = "axon_server_state.json";
25754
25755/// POST /v1/server/persist — save server configuration state to disk.
25756async fn server_persist_handler(
25757    State(state): State<SharedState>,
25758    headers: HeaderMap,
25759) -> Result<Json<serde_json::Value>, StatusCode> {
25760    let client = client_key_from_headers(&headers);
25761    let s = state.lock().unwrap();
25762    check_auth_peek(&s, &headers, AccessLevel::Admin)?;
25763
25764    let now = std::time::SystemTime::now()
25765        .duration_since(std::time::UNIX_EPOCH)
25766        .unwrap_or_default()
25767        .as_secs();
25768
25769    let schedules: Vec<ScheduleBackupEntry> = s.schedules.iter().map(|(name, sched)| {
25770        ScheduleBackupEntry {
25771            name: name.clone(), flow_name: sched.flow_name.clone(),
25772            interval_secs: sched.interval_secs, enabled: sched.enabled, backend: sched.backend.clone(),
25773        }
25774    }).collect();
25775
25776    let mut section_prov = HashMap::new();
25777    for section in &["cost_pricing", "cost_budgets", "flow_rules", "flow_quotas", "readiness_gates", "endpoint_rate_limits", "schedules", "axon_stores", "dataspaces"] {
25778        section_prov.insert(section.to_string(), EpistemicEnvelope::raw_config(&format!("config:{}", section), &client));
25779    }
25780
25781    let backup = ServerBackup {
25782        version: "1.0-ΛD".into(),
25783        created_at: now,
25784        lambda_d: EpistemicEnvelope::raw_config("axon:server_persist", &client),
25785        section_provenance: section_prov,
25786        cost_pricing: s.cost_pricing.clone(),
25787        cost_budgets: s.cost_budgets.clone(),
25788        flow_rules: s.flow_rules.clone(),
25789        flow_quotas: s.flow_quotas.clone(),
25790        readiness_gates: s.readiness_gates.clone(),
25791        endpoint_rate_limits: s.endpoint_rate_limits.clone(),
25792        schedules,
25793        axon_stores: s.axon_stores.clone(),
25794        dataspaces: s.dataspaces.clone(),
25795        shields: s.shields.clone(),
25796    };
25797
25798    let path = s.config.config_path.as_deref()
25799        .map(|p| {
25800            let dir = std::path::Path::new(p).parent().unwrap_or(std::path::Path::new("."));
25801            dir.join(STATE_PERSIST_PATH)
25802        })
25803        .unwrap_or_else(|| std::path::PathBuf::from(STATE_PERSIST_PATH));
25804
25805    drop(s);
25806
25807    let json_str = serde_json::to_string_pretty(&backup).unwrap_or_default();
25808    match std::fs::write(&path, &json_str) {
25809        Ok(_) => Ok(Json(serde_json::json!({
25810            "success": true,
25811            "path": path.display().to_string(),
25812            "size_bytes": json_str.len(),
25813            "sections": 9,
25814            "lambda_d_version": "1.0-ΛD",
25815        }))),
25816        Err(e) => Ok(Json(serde_json::json!({
25817            "success": false,
25818            "error": format!("write failed: {}", e),
25819            "path": path.display().to_string(),
25820        }))),
25821    }
25822}
25823
25824/// POST /v1/server/recover — load server configuration state from disk.
25825async fn server_recover_handler(
25826    State(state): State<SharedState>,
25827    headers: HeaderMap,
25828) -> Result<Json<serde_json::Value>, StatusCode> {
25829    let client = client_key_from_headers(&headers);
25830
25831    let path = {
25832        let s = state.lock().unwrap();
25833        check_auth_peek(&s, &headers, AccessLevel::Admin)?;
25834        s.config.config_path.as_deref()
25835            .map(|p| {
25836                let dir = std::path::Path::new(p).parent().unwrap_or(std::path::Path::new("."));
25837                dir.join(STATE_PERSIST_PATH)
25838            })
25839            .unwrap_or_else(|| std::path::PathBuf::from(STATE_PERSIST_PATH))
25840    };
25841
25842    let json_str = match std::fs::read_to_string(&path) {
25843        Ok(s) => s,
25844        Err(e) => return Ok(Json(serde_json::json!({
25845            "success": false,
25846            "error": format!("read failed: {}", e),
25847            "path": path.display().to_string(),
25848        }))),
25849    };
25850
25851    let backup: ServerBackup = match serde_json::from_str(&json_str) {
25852        Ok(b) => b,
25853        Err(e) => return Ok(Json(serde_json::json!({
25854            "success": false,
25855            "error": format!("parse failed: {}", e),
25856        }))),
25857    };
25858
25859    // Validate ΛD invariants
25860    if let Err(e) = backup.lambda_d.validate() {
25861        return Ok(Json(serde_json::json!({"success": false, "error": e, "phase": "lambda_d_validation"})));
25862    }
25863
25864    let mut s = state.lock().unwrap();
25865    check_auth(&mut s, &headers, AccessLevel::Admin)?;
25866
25867    s.cost_pricing = backup.cost_pricing;
25868    s.cost_budgets = backup.cost_budgets;
25869    s.flow_rules = backup.flow_rules;
25870    s.flow_quotas = backup.flow_quotas;
25871    s.readiness_gates = backup.readiness_gates;
25872    s.endpoint_rate_limits = backup.endpoint_rate_limits;
25873
25874    let mut restored_schedules = 0;
25875    for sched in &backup.schedules {
25876        if !s.schedules.contains_key(&sched.name) {
25877            s.schedules.insert(sched.name.clone(), ScheduleEntry {
25878                flow_name: sched.flow_name.clone(), interval_secs: sched.interval_secs,
25879                enabled: sched.enabled, backend: sched.backend.clone(),
25880                last_run: 0, next_run: sched.interval_secs, run_count: 0, error_count: 0, history: Vec::new(),
25881            });
25882            restored_schedules += 1;
25883        }
25884    }
25885
25886    // Restore AxonStores (merge: don't overwrite existing)
25887    let mut restored_axon_stores = 0u64;
25888    for (name, store) in backup.axon_stores {
25889        if !s.axon_stores.contains_key(&name) {
25890            s.axon_stores.insert(name, store);
25891            restored_axon_stores += 1;
25892        }
25893    }
25894
25895    // Restore Dataspaces (merge: don't overwrite existing)
25896    let mut restored_dataspaces = 0u64;
25897    for (name, ds) in backup.dataspaces {
25898        if !s.dataspaces.contains_key(&name) {
25899            s.dataspaces.insert(name, ds);
25900            restored_dataspaces += 1;
25901        }
25902    }
25903
25904    // Restore Shields (merge: don't overwrite existing)
25905    let mut restored_shields = 0u64;
25906    for (name, sh) in backup.shields {
25907        if !s.shields.contains_key(&name) {
25908            s.shields.insert(name, sh);
25909            restored_shields += 1;
25910        }
25911    }
25912
25913    s.audit_log.record(&client, AuditAction::ConfigLoad, "server_recover",
25914        serde_json::json!({
25915            "path": path.display().to_string(), "version": backup.version,
25916            "schedules_created": restored_schedules,
25917            "axon_stores_restored": restored_axon_stores,
25918            "dataspaces_restored": restored_dataspaces,
25919            "shields_restored": restored_shields,
25920        }), true);
25921
25922    Ok(Json(serde_json::json!({
25923        "success": true,
25924        "path": path.display().to_string(),
25925        "version": backup.version,
25926        "schedules_created": restored_schedules,
25927        "axon_stores_restored": restored_axon_stores,
25928        "dataspaces_restored": restored_dataspaces,
25929        "shields_restored": restored_shields,
25930    })))
25931}
25932
25933/// Request to set auto-persist setting.
25934#[derive(Debug, Deserialize)]
25935pub struct AutoPersistRequest {
25936    pub enabled: bool,
25937}
25938
25939/// GET /v1/server/auto-persist — view auto-persist setting.
25940async fn server_auto_persist_get_handler(
25941    State(state): State<SharedState>,
25942    headers: HeaderMap,
25943) -> Result<Json<serde_json::Value>, StatusCode> {
25944    let s = state.lock().unwrap();
25945    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
25946    Ok(Json(serde_json::json!({"auto_persist_on_shutdown": s.auto_persist_on_shutdown})))
25947}
25948
25949/// PUT /v1/server/auto-persist — toggle auto-persist on shutdown.
25950async fn server_auto_persist_put_handler(
25951    State(state): State<SharedState>,
25952    headers: HeaderMap,
25953    Json(payload): Json<AutoPersistRequest>,
25954) -> Result<Json<serde_json::Value>, StatusCode> {
25955    let mut s = state.lock().unwrap();
25956    check_auth(&mut s, &headers, AccessLevel::Admin)?;
25957    s.auto_persist_on_shutdown = payload.enabled;
25958    Ok(Json(serde_json::json!({"success": true, "auto_persist_on_shutdown": payload.enabled})))
25959}
25960
25961/// Request for flow comparison.
25962#[derive(Debug, Deserialize)]
25963pub struct FlowCompareRequest {
25964    /// Flow names to compare (2–10).
25965    pub flows: Vec<String>,
25966}
25967
25968/// Per-flow stats in a comparison.
25969#[derive(Debug, Clone, Serialize)]
25970pub struct FlowCompareEntry {
25971    pub flow_name: String,
25972    pub executions: u64,
25973    pub error_rate: f64,
25974    pub avg_latency_ms: u64,
25975    pub p50_latency_ms: u64,
25976    pub p95_latency_ms: u64,
25977    pub total_tokens: u64,
25978    pub estimated_cost_usd: f64,
25979    pub daemon_state: Option<String>,
25980    pub has_schedule: bool,
25981    pub has_budget: bool,
25982    pub has_quota: bool,
25983}
25984
25985/// POST /v1/flows/compare — compare multiple flows side-by-side.
25986async fn flows_compare_handler(
25987    State(state): State<SharedState>,
25988    headers: HeaderMap,
25989    Json(payload): Json<FlowCompareRequest>,
25990) -> Result<Json<serde_json::Value>, StatusCode> {
25991    let s = state.lock().unwrap();
25992    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
25993
25994    if payload.flows.len() < 2 {
25995        return Ok(Json(serde_json::json!({"error": "at least 2 flows required"})));
25996    }
25997    if payload.flows.len() > 10 {
25998        return Ok(Json(serde_json::json!({"error": "maximum 10 flows"})));
25999    }
26000
26001    let all_entries = s.trace_store.recent(s.trace_store.len(), None);
26002    let costs = compute_flow_costs(&s.trace_store, &s.cost_pricing);
26003
26004    let mut entries: Vec<FlowCompareEntry> = Vec::new();
26005
26006    for flow in &payload.flows {
26007        let flow_traces: Vec<_> = all_entries.iter().filter(|e| &e.flow_name == flow).collect();
26008        let total = flow_traces.len() as u64;
26009
26010        if total == 0 {
26011            entries.push(FlowCompareEntry {
26012                flow_name: flow.clone(), executions: 0, error_rate: 0.0,
26013                avg_latency_ms: 0, p50_latency_ms: 0, p95_latency_ms: 0,
26014                total_tokens: 0, estimated_cost_usd: 0.0,
26015                daemon_state: s.daemons.get(flow).map(|d| format!("{:?}", d.state).to_lowercase()),
26016                has_schedule: s.schedules.contains_key(flow),
26017                has_budget: s.cost_budgets.contains_key(flow),
26018                has_quota: s.flow_quotas.contains_key(flow),
26019            });
26020            continue;
26021        }
26022
26023        let errors = flow_traces.iter().filter(|e| e.errors > 0).count() as u64;
26024        let mut latencies: Vec<u64> = flow_traces.iter().map(|e| e.latency_ms).collect();
26025        latencies.sort();
26026        let total_lat: u64 = latencies.iter().sum();
26027        let tokens: u64 = flow_traces.iter().map(|e| e.tokens_input + e.tokens_output).sum();
26028        let cost = costs.iter().find(|c| &c.flow_name == flow).map(|c| c.estimated_cost_usd).unwrap_or(0.0);
26029
26030        let p50 = latencies[latencies.len() / 2];
26031        let p95_idx = ((95 * latencies.len() + 99) / 100).min(latencies.len()) - 1;
26032
26033        entries.push(FlowCompareEntry {
26034            flow_name: flow.clone(),
26035            executions: total,
26036            error_rate: errors as f64 / total as f64,
26037            avg_latency_ms: total_lat / total,
26038            p50_latency_ms: p50,
26039            p95_latency_ms: latencies[p95_idx],
26040            total_tokens: tokens,
26041            estimated_cost_usd: cost,
26042            daemon_state: s.daemons.get(flow).map(|d| format!("{:?}", d.state).to_lowercase()),
26043            has_schedule: s.schedules.contains_key(flow),
26044            has_budget: s.cost_budgets.contains_key(flow),
26045            has_quota: s.flow_quotas.contains_key(flow),
26046        });
26047    }
26048
26049    // Find best/worst per metric
26050    let best_latency = entries.iter().filter(|e| e.executions > 0).min_by_key(|e| e.avg_latency_ms).map(|e| e.flow_name.clone());
26051    let worst_latency = entries.iter().filter(|e| e.executions > 0).max_by_key(|e| e.avg_latency_ms).map(|e| e.flow_name.clone());
26052    let best_error = entries.iter().filter(|e| e.executions > 0).min_by(|a, b| a.error_rate.partial_cmp(&b.error_rate).unwrap()).map(|e| e.flow_name.clone());
26053    let most_expensive = entries.iter().max_by(|a, b| a.estimated_cost_usd.partial_cmp(&b.estimated_cost_usd).unwrap()).map(|e| e.flow_name.clone());
26054
26055    Ok(Json(serde_json::json!({
26056        "compared": entries.len(),
26057        "flows": entries,
26058        "highlights": {
26059            "fastest": best_latency,
26060            "slowest": worst_latency,
26061            "lowest_error_rate": best_error,
26062            "most_expensive": most_expensive,
26063        },
26064    })))
26065}
26066
26067/// A batch item with caching options.
26068#[derive(Debug, Clone, Deserialize)]
26069pub struct CachedBatchItem {
26070    pub flow_name: String,
26071    #[serde(default = "default_execute_backend")]
26072    pub backend: String,
26073    /// TTL for caching (0 = don't cache). Default 300.
26074    #[serde(default = "default_cache_ttl")]
26075    pub cache_ttl_secs: u64,
26076    /// Force re-execution even if cached.
26077    #[serde(default)]
26078    pub force: bool,
26079}
26080
26081/// Result for a cached batch item.
26082#[derive(Debug, Clone, Serialize)]
26083pub struct CachedBatchItemResult {
26084    pub index: usize,
26085    pub flow_name: String,
26086    pub success: bool,
26087    pub cached: bool,
26088    pub trace_id: u64,
26089    pub latency_ms: u64,
26090    pub tokens: u64,
26091    pub error: Option<String>,
26092    /// ΛD: "raw" (fresh) or "derived" (cached)
26093    pub epistemic_derivation: String,
26094}
26095
26096/// Request for cached batch execution.
26097#[derive(Debug, Deserialize)]
26098pub struct CachedBatchRequest {
26099    pub items: Vec<CachedBatchItem>,
26100    #[serde(default = "default_batch_continue")]
26101    pub continue_on_failure: bool,
26102}
26103
26104/// POST /v1/execute/batch-cached — batch execution with per-item cache awareness.
26105async fn execute_batch_cached_handler(
26106    State(state): State<SharedState>,
26107    headers: HeaderMap,
26108    Json(payload): Json<CachedBatchRequest>,
26109) -> Result<Json<serde_json::Value>, StatusCode> {
26110    let req_start = Instant::now();
26111    let client = client_key_from_headers(&headers);
26112    { let mut s = state.lock().unwrap(); check_auth(&mut s, &headers, AccessLevel::Write)?; }
26113
26114    if payload.items.is_empty() { return Ok(Json(serde_json::json!({"error": "at least 1 item required"}))); }
26115    if payload.items.len() > 50 { return Ok(Json(serde_json::json!({"error": "max 50 items"}))); }
26116
26117    let mut results: Vec<CachedBatchItemResult> = Vec::new();
26118
26119    for (idx, item) in payload.items.iter().enumerate() {
26120        let cache_key = format!("{}:{}", item.flow_name, item.backend);
26121
26122        // Check cache
26123        if !item.force {
26124            let s = state.lock().unwrap();
26125            if let Some(entry) = s.execution_cache.iter().find(|c| c.cache_key == cache_key && !c.is_expired()) {
26126                results.push(CachedBatchItemResult {
26127                    index: idx, flow_name: item.flow_name.clone(), success: true, cached: true,
26128                    trace_id: entry.source_trace_id, latency_ms: 0, tokens: 0, error: None,
26129                    epistemic_derivation: "derived".into(),
26130                });
26131                continue;
26132            }
26133        }
26134
26135        // Execute fresh
26136        let source_info = { let s = state.lock().unwrap(); s.versions.get_history(&item.flow_name).and_then(|h| h.active()).map(|v| (v.source.clone(), v.source_file.clone())) };
26137        let (source, source_file) = match source_info {
26138            Some(info) => info,
26139            None => {
26140                results.push(CachedBatchItemResult { index: idx, flow_name: item.flow_name.clone(), success: false, cached: false, trace_id: 0, latency_ms: 0, tokens: 0, error: Some("not deployed".into()), epistemic_derivation: "none".into() });
26141                if !payload.continue_on_failure { break; }
26142                continue;
26143            }
26144        };
26145
26146        match server_execute_full(&state, &source, &source_file, &item.flow_name, &item.backend).0 {
26147            Ok(er) => {
26148                let mut entry = crate::trace_store::build_trace(&er.flow_name, &er.source_file, &er.backend, &client, if er.success { crate::trace_store::TraceStatus::Success } else { crate::trace_store::TraceStatus::Partial }, er.steps_executed, er.latency_ms);
26149                entry.tokens_input = er.tokens_input; entry.tokens_output = er.tokens_output; entry.errors = er.errors;
26150                let tid = { let mut s = state.lock().unwrap(); let tid = s.trace_store.record(entry);
26151                    if item.cache_ttl_secs > 0 {
26152                        let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
26153                        let cached = CachedResult { cache_key: cache_key.clone(), flow_name: er.flow_name.clone(), backend: er.backend.clone(), result: serde_json::json!({"steps": er.steps_executed}), source_trace_id: tid, cached_at: now, ttl_secs: item.cache_ttl_secs, epistemic: EpistemicEnvelope::derived(&format!("cache:{}", er.flow_name), 0.95, &format!("trace:{}", tid)) };
26154                        s.execution_cache.retain(|c| c.cache_key != cache_key); s.execution_cache.push(cached);
26155                        if s.execution_cache.len() > 200 { s.execution_cache.remove(0); }
26156                    }
26157                tid };
26158                results.push(CachedBatchItemResult { index: idx, flow_name: item.flow_name.clone(), success: er.success, cached: false, trace_id: tid, latency_ms: er.latency_ms, tokens: er.tokens_input + er.tokens_output, error: None, epistemic_derivation: "raw".into() });
26159                if !er.success && !payload.continue_on_failure { break; }
26160            }
26161            Err(e) => {
26162                { let mut s = state.lock().unwrap(); s.metrics.total_errors += 1; }
26163                results.push(CachedBatchItemResult { index: idx, flow_name: item.flow_name.clone(), success: false, cached: false, trace_id: 0, latency_ms: 0, tokens: 0, error: Some(e), epistemic_derivation: "none".into() });
26164                if !payload.continue_on_failure { break; }
26165            }
26166        }
26167    }
26168
26169    let cache_hits = results.iter().filter(|r| r.cached).count();
26170    let fresh = results.iter().filter(|r| !r.cached && r.success).count();
26171    let failed = results.iter().filter(|r| !r.success).count();
26172
26173    Ok(Json(serde_json::json!({
26174        "batch_size": payload.items.len(),
26175        "executed": results.len(),
26176        "cache_hits": cache_hits,
26177        "fresh_executions": fresh,
26178        "failed": failed,
26179        "total_latency_ms": req_start.elapsed().as_millis() as u64,
26180        "results": results,
26181    })))
26182}
26183
26184/// Per-flow SLA definition.
26185#[derive(Debug, Clone, Serialize, Deserialize)]
26186pub struct FlowSLA {
26187    /// Maximum allowed latency in ms (0 = no limit).
26188    #[serde(default)]
26189    pub max_latency_ms: u64,
26190    /// Maximum allowed error rate (0.0 = no limit).
26191    #[serde(default)]
26192    pub max_error_rate: f64,
26193    /// Minimum required success rate (0.0 = no limit).
26194    #[serde(default)]
26195    pub min_success_rate: f64,
26196    /// Maximum p95 latency in ms (0 = no limit).
26197    #[serde(default)]
26198    pub max_p95_latency_ms: u64,
26199}
26200
26201/// A configurable operational alert rule.
26202#[derive(Debug, Clone, Serialize, Deserialize)]
26203pub struct AlertRule {
26204    /// Unique rule name.
26205    pub name: String,
26206    /// Metric to monitor: "error_rate", "latency_avg", "queue_depth", "trace_buffer_pct", "dead_daemons".
26207    pub metric: String,
26208    /// Comparison: "gt" (greater than), "lt" (less than), "eq" (equal).
26209    pub comparison: String,
26210    /// Threshold value.
26211    pub threshold: f64,
26212    /// Severity: "info", "warning", "critical".
26213    pub severity: String,
26214    /// Whether this rule is enabled.
26215    #[serde(default = "default_alert_enabled")]
26216    pub enabled: bool,
26217    /// Escalation: fire count within window to escalate severity.
26218    /// 0 = no escalation. E.g., 3 = escalate after 3 fires in window.
26219    #[serde(default)]
26220    pub escalate_after: u32,
26221    /// Escalation window in seconds (default 300).
26222    #[serde(default = "default_escalation_window")]
26223    pub escalation_window_secs: u64,
26224    /// Cooldown in seconds: suppress re-firing within this period. 0 = no cooldown.
26225    #[serde(default)]
26226    pub cooldown_secs: u64,
26227}
26228
26229fn default_escalation_window() -> u64 { 300 }
26230
26231fn default_alert_enabled() -> bool { true }
26232
26233/// A fired alert instance.
26234#[derive(Debug, Clone, Serialize)]
26235pub struct FiredAlert {
26236    pub rule_name: String,
26237    pub metric: String,
26238    pub threshold: f64,
26239    pub actual: f64,
26240    pub severity: String,
26241    pub timestamp: u64,
26242}
26243
26244/// A temporary silence on an alert rule — suppresses firing until expiry.
26245#[derive(Debug, Clone, Serialize, Deserialize)]
26246pub struct AlertSilence {
26247    /// Rule name to silence.
26248    pub rule_name: String,
26249    /// Who created the silence.
26250    pub created_by: String,
26251    /// Reason for silencing.
26252    #[serde(default)]
26253    pub reason: String,
26254    /// Unix timestamp when silence was created.
26255    pub created_at: u64,
26256    /// Unix timestamp when silence expires (0 = indefinite).
26257    #[serde(default)]
26258    pub expires_at: u64,
26259}
26260
26261/// A registered LLM backend with server-managed API key and health status.
26262#[derive(Debug, Clone, Serialize, Deserialize)]
26263pub struct BackendRegistryEntry {
26264    /// Backend provider name (e.g., "anthropic", "openai").
26265    pub name: String,
26266    /// API key (server-managed, takes precedence over env var).
26267    #[serde(default, skip_serializing)]
26268    pub api_key: String,
26269    /// Whether this backend is enabled.
26270    #[serde(default = "default_alert_enabled")]
26271    pub enabled: bool,
26272    /// Last health check status: "unknown", "healthy", "degraded", "unreachable".
26273    #[serde(default = "default_backend_status")]
26274    pub status: String,
26275    /// Last health check timestamp (unix seconds).
26276    #[serde(default)]
26277    pub last_check_at: u64,
26278    /// Last health check latency in ms.
26279    #[serde(default)]
26280    pub last_check_latency_ms: u64,
26281    /// Total calls made through this backend.
26282    #[serde(default)]
26283    pub total_calls: u64,
26284    /// Total errors from this backend.
26285    #[serde(default)]
26286    pub total_errors: u64,
26287    /// Total input tokens consumed.
26288    #[serde(default)]
26289    pub total_tokens_input: u64,
26290    /// Total output tokens produced.
26291    #[serde(default)]
26292    pub total_tokens_output: u64,
26293    /// Cumulative latency in ms (divide by total_calls for average).
26294    #[serde(default)]
26295    pub total_latency_ms: u64,
26296    /// Timestamp of last execution call.
26297    #[serde(default)]
26298    pub last_call_at: u64,
26299    /// Fallback chain: ordered list of backend names to try if this one fails.
26300    #[serde(default)]
26301    pub fallback_chain: Vec<String>,
26302    /// Circuit breaker: consecutive failure count.
26303    #[serde(default)]
26304    pub consecutive_failures: u32,
26305    /// Circuit breaker: open until this timestamp (0 = closed).
26306    #[serde(default)]
26307    pub circuit_open_until: u64,
26308    /// Circuit breaker: failures before opening (0 = disabled, default 5).
26309    #[serde(default = "default_cb_threshold")]
26310    pub circuit_breaker_threshold: u32,
26311    /// Circuit breaker: cooldown seconds before auto-close (default 60).
26312    #[serde(default = "default_cb_cooldown")]
26313    pub circuit_breaker_cooldown_secs: u64,
26314    /// Accumulated cost in USD (computed from CostPricing × tokens).
26315    #[serde(default)]
26316    pub total_cost_usd: f64,
26317    /// Max requests per minute (0 = unlimited).
26318    #[serde(default)]
26319    pub max_rpm: u32,
26320    /// Max tokens per minute (0 = unlimited).
26321    #[serde(default)]
26322    pub max_tpm: u64,
26323    /// Current RPM window start (unix seconds). Reset when window expires.
26324    #[serde(default)]
26325    pub rpm_window_start: u64,
26326    /// Requests counted in current window.
26327    #[serde(default)]
26328    pub rpm_count: u32,
26329    /// Tokens counted in current window.
26330    #[serde(default)]
26331    pub tpm_count: u64,
26332}
26333
26334fn default_cb_threshold() -> u32 { 5 }
26335fn default_cb_cooldown() -> u64 { 60 }
26336
26337// ── Backend Health Probing ──────────────────────────────────────────────────
26338
26339/// Configuration for automated health probing of a backend.
26340#[derive(Debug, Clone, Serialize, Deserialize)]
26341pub struct BackendHealthProbe {
26342    /// Backend name this probe targets.
26343    pub backend: String,
26344    /// Probe interval in seconds (0 = disabled).
26345    pub interval_secs: u64,
26346    /// Number of consecutive failures before marking unhealthy.
26347    pub unhealthy_threshold: u32,
26348    /// Number of consecutive successes before marking healthy.
26349    pub healthy_threshold: u32,
26350    /// Timeout for each probe attempt in ms.
26351    pub timeout_ms: u64,
26352    /// Whether probing is active.
26353    pub enabled: bool,
26354    /// Consecutive check successes (for healthy transition).
26355    pub consecutive_ok: u32,
26356    /// Consecutive check failures (for unhealthy transition).
26357    pub consecutive_fail: u32,
26358}
26359
26360impl Default for BackendHealthProbe {
26361    fn default() -> Self {
26362        Self {
26363            backend: String::new(),
26364            interval_secs: 300, // 5 minutes
26365            unhealthy_threshold: 3,
26366            healthy_threshold: 2,
26367            timeout_ms: 10000, // 10 seconds
26368            enabled: false,
26369            consecutive_ok: 0,
26370            consecutive_fail: 0,
26371        }
26372    }
26373}
26374
26375/// A single health check record for audit and trend analysis.
26376#[derive(Debug, Clone, Serialize, Deserialize)]
26377pub struct HealthCheckRecord {
26378    /// Unix timestamp of the check.
26379    pub timestamp: u64,
26380    /// Status result: "healthy", "degraded", "unreachable", "no_key".
26381    pub status: String,
26382    /// Latency of the check in ms.
26383    pub latency_ms: u64,
26384    /// Error message if check failed.
26385    pub error: Option<String>,
26386    /// Status before this check (for transition detection).
26387    pub previous_status: String,
26388}
26389
26390/// Check if a backend has exceeded its rate limits (RPM or TPM).
26391/// Returns Ok(()) if within limits, Err(message) if exceeded.
26392fn check_backend_rate_limit(state: &mut ServerState, backend: &str) -> Result<(), String> {
26393    let now = std::time::SystemTime::now()
26394        .duration_since(std::time::UNIX_EPOCH)
26395        .unwrap_or_default()
26396        .as_secs();
26397
26398    if let Some(entry) = state.backend_registry.get_mut(backend) {
26399        // Reset window if 60 seconds have passed
26400        if now >= entry.rpm_window_start + 60 {
26401            entry.rpm_window_start = now;
26402            entry.rpm_count = 0;
26403            entry.tpm_count = 0;
26404        }
26405
26406        // Check RPM limit
26407        if entry.max_rpm > 0 && entry.rpm_count >= entry.max_rpm {
26408            return Err(format!(
26409                "Backend '{}' rate limited: {}/{} RPM (resets in {}s)",
26410                backend, entry.rpm_count, entry.max_rpm,
26411                (entry.rpm_window_start + 60).saturating_sub(now)
26412            ));
26413        }
26414
26415        // Check TPM limit
26416        if entry.max_tpm > 0 && entry.tpm_count >= entry.max_tpm {
26417            return Err(format!(
26418                "Backend '{}' token limited: {}/{} TPM (resets in {}s)",
26419                backend, entry.tpm_count, entry.max_tpm,
26420                (entry.rpm_window_start + 60).saturating_sub(now)
26421            ));
26422        }
26423
26424        // Increment RPM counter (TPM updated after execution in record_backend_metrics)
26425        entry.rpm_count += 1;
26426    }
26427    Ok(())
26428}
26429
26430fn default_backend_status() -> String { "unknown".to_string() }
26431
26432/// Record backend call metrics after an execution.
26433/// Updates the registry entry with call count, tokens, latency, and error tracking.
26434fn record_backend_metrics(
26435    state: &mut ServerState,
26436    backend: &str,
26437    success: bool,
26438    tokens_input: u64,
26439    tokens_output: u64,
26440    latency_ms: u64,
26441) {
26442    let now = std::time::SystemTime::now()
26443        .duration_since(std::time::UNIX_EPOCH)
26444        .unwrap_or_default()
26445        .as_secs();
26446
26447    // Extract pricing before mutable borrow on backend_registry
26448    let input_price = state.cost_pricing.input_per_million.get(backend).copied().unwrap_or(0.0);
26449    let output_price = state.cost_pricing.output_per_million.get(backend).copied().unwrap_or(0.0);
26450    let call_cost = (tokens_input as f64 / 1_000_000.0) * input_price
26451                  + (tokens_output as f64 / 1_000_000.0) * output_price;
26452
26453    let entry = state.backend_registry.entry(backend.to_string()).or_insert_with(|| {
26454        BackendRegistryEntry {
26455            name: backend.to_string(),
26456            api_key: String::new(),
26457            enabled: true,
26458            status: "unknown".into(),
26459            last_check_at: 0,
26460            last_check_latency_ms: 0,
26461            total_calls: 0,
26462            total_errors: 0,
26463            total_tokens_input: 0,
26464            total_tokens_output: 0,
26465            total_latency_ms: 0,
26466            last_call_at: 0,
26467            fallback_chain: Vec::new(),
26468            consecutive_failures: 0,
26469            circuit_open_until: 0,
26470            circuit_breaker_threshold: 5,
26471            circuit_breaker_cooldown_secs: 60,
26472            total_cost_usd: 0.0, max_rpm: 0, max_tpm: 0, rpm_window_start: 0, rpm_count: 0, tpm_count: 0,
26473        }
26474    });
26475
26476    entry.total_calls += 1;
26477    if !success {
26478        entry.total_errors += 1;
26479        entry.consecutive_failures += 1;
26480        // Open circuit if threshold reached
26481        if entry.circuit_breaker_threshold > 0
26482            && entry.consecutive_failures >= entry.circuit_breaker_threshold
26483        {
26484            entry.circuit_open_until = now + entry.circuit_breaker_cooldown_secs;
26485            entry.status = "circuit_open".into();
26486        }
26487    } else {
26488        // Reset consecutive failures on success
26489        entry.consecutive_failures = 0;
26490        if entry.circuit_open_until > 0 && now >= entry.circuit_open_until {
26491            entry.circuit_open_until = 0;
26492            entry.status = "healthy".into();
26493        }
26494    }
26495    entry.total_tokens_input += tokens_input;
26496    entry.total_tokens_output += tokens_output;
26497    entry.total_latency_ms += latency_ms;
26498    entry.last_call_at = now;
26499    entry.total_cost_usd += (call_cost * 10000.0).round() / 10000.0;
26500
26501    // Update TPM counter for rate limiting
26502    entry.tpm_count += tokens_input + tokens_output;
26503}
26504
26505/// Canary deployment configuration — gradual traffic shift between versions.
26506#[derive(Debug, Clone, Serialize, Deserialize)]
26507pub struct CanaryConfig {
26508    /// Stable version (current production).
26509    pub stable_version: u32,
26510    /// Canary version (candidate).
26511    pub canary_version: u32,
26512    /// Percentage of traffic routed to canary (0–100).
26513    pub canary_weight: u32,
26514    /// Total requests routed to stable.
26515    #[serde(default)]
26516    pub stable_count: u64,
26517    /// Total requests routed to canary.
26518    #[serde(default)]
26519    pub canary_count: u64,
26520}
26521
26522impl CanaryConfig {
26523    /// Route a request: returns the version to use based on canary weight.
26524    pub fn route(&mut self) -> u32 {
26525        let total = self.stable_count + self.canary_count;
26526        let canary_pct = if total == 0 { 0 } else { (self.canary_count * 100) / total };
26527        if canary_pct < self.canary_weight as u64 {
26528            self.canary_count += 1;
26529            self.canary_version
26530        } else {
26531            self.stable_count += 1;
26532            self.stable_version
26533        }
26534    }
26535}
26536
26537/// SLA breach detail.
26538#[derive(Debug, Clone, Serialize)]
26539pub struct SLABreach {
26540    pub flow_name: String,
26541    pub metric: String,
26542    pub threshold: f64,
26543    pub actual: f64,
26544    pub breached: bool,
26545}
26546
26547/// A health state transition record.
26548#[derive(Debug, Clone, Serialize)]
26549pub struct HealthTransition {
26550    pub timestamp: u64,
26551    pub from_status: String,
26552    pub to_status: String,
26553    pub component: String,
26554    pub detail: String,
26555}
26556
26557/// Record a health transition if status changed.
26558fn record_health_transition(history: &mut Vec<HealthTransition>, component: &str, old: &str, new: &str, detail: &str) {
26559    if old == new { return; }
26560    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
26561    history.push(HealthTransition {
26562        timestamp: now,
26563        from_status: old.to_string(),
26564        to_status: new.to_string(),
26565        component: component.to_string(),
26566        detail: detail.to_string(),
26567    });
26568    if history.len() > 500 { history.remove(0); }
26569}
26570
26571/// Request to set flow tags.
26572#[derive(Debug, Deserialize)]
26573pub struct SetTagsRequest {
26574    pub tags: Vec<String>,
26575}
26576
26577/// GET /v1/flows/:name/tags — get tags for a flow.
26578async fn flow_tags_get_handler(
26579    State(state): State<SharedState>,
26580    headers: HeaderMap,
26581    Path(name): Path<String>,
26582) -> Result<Json<serde_json::Value>, StatusCode> {
26583    let s = state.lock().unwrap();
26584    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
26585    let tags = s.flow_tags.get(&name).cloned().unwrap_or_default();
26586    Ok(Json(serde_json::json!({"flow": name, "tags": tags})))
26587}
26588
26589/// PUT /v1/flows/:name/tags — set tags for a flow.
26590async fn flow_tags_put_handler(
26591    State(state): State<SharedState>,
26592    headers: HeaderMap,
26593    Path(name): Path<String>,
26594    Json(payload): Json<SetTagsRequest>,
26595) -> Result<Json<serde_json::Value>, StatusCode> {
26596    let mut s = state.lock().unwrap();
26597    check_auth(&mut s, &headers, AccessLevel::Write)?;
26598    s.flow_tags.insert(name.clone(), payload.tags.clone());
26599    Ok(Json(serde_json::json!({"success": true, "flow": name, "tags": payload.tags})))
26600}
26601
26602/// DELETE /v1/flows/:name/tags — remove all tags from a flow.
26603async fn flow_tags_delete_handler(
26604    State(state): State<SharedState>,
26605    headers: HeaderMap,
26606    Path(name): Path<String>,
26607) -> Result<Json<serde_json::Value>, StatusCode> {
26608    let mut s = state.lock().unwrap();
26609    check_auth(&mut s, &headers, AccessLevel::Write)?;
26610    let removed = s.flow_tags.remove(&name).is_some();
26611    Ok(Json(serde_json::json!({"success": removed, "flow": name})))
26612}
26613
26614/// Query for flows by tag.
26615#[derive(Debug, Deserialize)]
26616pub struct FlowsByTagQuery {
26617    pub tag: String,
26618}
26619
26620/// GET /v1/flows/by-tag — find flows with a given tag.
26621async fn flows_by_tag_handler(
26622    State(state): State<SharedState>,
26623    headers: HeaderMap,
26624    Query(params): Query<FlowsByTagQuery>,
26625) -> Result<Json<serde_json::Value>, StatusCode> {
26626    let s = state.lock().unwrap();
26627    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
26628
26629    let matching: Vec<serde_json::Value> = s.flow_tags.iter()
26630        .filter(|(_, tags)| tags.contains(&params.tag))
26631        .map(|(name, tags)| serde_json::json!({"flow": name, "tags": tags}))
26632        .collect();
26633
26634    Ok(Json(serde_json::json!({
26635        "tag": params.tag,
26636        "count": matching.len(),
26637        "flows": matching,
26638    })))
26639}
26640
26641/// GET /v1/health/history — view health transition history.
26642async fn health_history_handler(
26643    State(state): State<SharedState>,
26644    headers: HeaderMap,
26645    Query(params): Query<std::collections::HashMap<String, String>>,
26646) -> Result<Json<serde_json::Value>, StatusCode> {
26647    let s = state.lock().unwrap();
26648    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
26649
26650    let limit: usize = params.get("limit").and_then(|v| v.parse().ok()).unwrap_or(100);
26651    let component_filter = params.get("component");
26652
26653    let filtered: Vec<&HealthTransition> = s.health_history.iter().rev()
26654        .filter(|h| component_filter.map_or(true, |c| h.component == *c))
26655        .take(limit)
26656        .collect();
26657
26658    let degradations = filtered.iter().filter(|h| h.to_status == "degraded" || h.to_status == "unhealthy").count();
26659
26660    Ok(Json(serde_json::json!({
26661        "total_transitions": s.health_history.len(),
26662        "returned": filtered.len(),
26663        "degradations": degradations,
26664        "history": filtered,
26665    })))
26666}
26667
26668/// POST /v1/health/check-and-record — evaluate health and record transitions.
26669async fn health_check_record_handler(
26670    State(state): State<SharedState>,
26671    headers: HeaderMap,
26672) -> Result<Json<serde_json::Value>, StatusCode> {
26673    let mut s = state.lock().unwrap();
26674    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
26675
26676    let mut transitions = Vec::new();
26677
26678    // Check trace store
26679    let ts_status = if !s.trace_store.config().enabled { "disabled" }
26680        else if s.trace_store.len() >= s.trace_store.config().capacity { "degraded" }
26681        else { "healthy" };
26682    let ts_prev = s.health_history.iter().rev()
26683        .find(|h| h.component == "trace_store")
26684        .map(|h| h.to_status.clone())
26685        .unwrap_or_else(|| "healthy".into());
26686    if ts_prev != ts_status {
26687        transitions.push(("trace_store", ts_prev.clone(), ts_status.to_string(), format!("buffered: {}/{}", s.trace_store.len(), s.trace_store.config().capacity)));
26688    }
26689
26690    // Check event bus
26691    let bus_stats = s.event_bus.stats();
26692    let bus_status = if bus_stats.events_dropped > 0 { "degraded" } else { "healthy" };
26693    let bus_prev = s.health_history.iter().rev()
26694        .find(|h| h.component == "event_bus")
26695        .map(|h| h.to_status.clone())
26696        .unwrap_or_else(|| "healthy".into());
26697    if bus_prev != bus_status {
26698        transitions.push(("event_bus", bus_prev, bus_status.to_string(), format!("dropped: {}", bus_stats.events_dropped)));
26699    }
26700
26701    // Check supervisor
26702    let sup_counts = s.supervisor.state_counts();
26703    let dead = sup_counts.get("dead").copied().unwrap_or(0);
26704    let sup_status = if dead > 0 { "degraded" } else { "healthy" };
26705    let sup_prev = s.health_history.iter().rev()
26706        .find(|h| h.component == "supervisor")
26707        .map(|h| h.to_status.clone())
26708        .unwrap_or_else(|| "healthy".into());
26709    if sup_prev != sup_status {
26710        transitions.push(("supervisor", sup_prev, sup_status.to_string(), format!("dead: {}", dead)));
26711    }
26712
26713    // Check error rate
26714    let err_status = if s.metrics.total_requests > 0 {
26715        let rate = s.metrics.total_errors as f64 / s.metrics.total_requests as f64;
26716        if rate > 0.1 { "degraded" } else { "healthy" }
26717    } else { "healthy" };
26718    let err_prev = s.health_history.iter().rev()
26719        .find(|h| h.component == "error_rate")
26720        .map(|h| h.to_status.clone())
26721        .unwrap_or_else(|| "healthy".into());
26722    if err_prev != err_status {
26723        transitions.push(("error_rate", err_prev, err_status.to_string(), format!("{}/{} requests", s.metrics.total_errors, s.metrics.total_requests)));
26724    }
26725
26726    // Record transitions
26727    for (comp, from, to, detail) in &transitions {
26728        record_health_transition(&mut s.health_history, comp, from, to, detail);
26729    }
26730
26731    let new_degradations = transitions.iter().filter(|(_, _, to, _)| to == "degraded").count();
26732
26733    Ok(Json(serde_json::json!({
26734        "checked": 4,
26735        "transitions": transitions.len(),
26736        "new_degradations": new_degradations,
26737        "components": {
26738            "trace_store": ts_status,
26739            "event_bus": bus_status,
26740            "supervisor": sup_status,
26741            "error_rate": err_status,
26742        },
26743    })))
26744}
26745
26746/// Per-flow result in a tag group execution.
26747#[derive(Debug, Clone, Serialize)]
26748pub struct TagGroupResult {
26749    pub flow_name: String,
26750    pub success: bool,
26751    pub trace_id: u64,
26752    pub latency_ms: u64,
26753    pub tokens: u64,
26754    pub error: Option<String>,
26755}
26756
26757/// POST /v1/flows/group/:tag/execute — execute all flows with a given tag.
26758async fn flows_group_execute_handler(
26759    State(state): State<SharedState>,
26760    headers: HeaderMap,
26761    Path(tag): Path<String>,
26762) -> Result<Json<serde_json::Value>, StatusCode> {
26763    let req_start = Instant::now();
26764    let client = client_key_from_headers(&headers);
26765    { let mut s = state.lock().unwrap(); check_auth(&mut s, &headers, AccessLevel::Write)?; }
26766
26767    // Find flows with this tag
26768    let flows: Vec<String> = {
26769        let s = state.lock().unwrap();
26770        s.flow_tags.iter()
26771            .filter(|(_, tags)| tags.contains(&tag))
26772            .map(|(name, _)| name.clone())
26773            .collect()
26774    };
26775
26776    if flows.is_empty() {
26777        return Ok(Json(serde_json::json!({"tag": tag, "found": 0, "message": "no flows with this tag"})));
26778    }
26779
26780    let mut results: Vec<TagGroupResult> = Vec::new();
26781
26782    for flow in &flows {
26783        let source_info = {
26784            let s = state.lock().unwrap();
26785            s.versions.get_history(flow).and_then(|h| h.active()).map(|v| (v.source.clone(), v.source_file.clone(), v.backend.clone()))
26786        };
26787
26788        let (source, source_file, backend) = match source_info {
26789            Some(info) => info,
26790            None => {
26791                results.push(TagGroupResult { flow_name: flow.clone(), success: false, trace_id: 0, latency_ms: 0, tokens: 0, error: Some("not deployed".into()) });
26792                continue;
26793            }
26794        };
26795
26796        match server_execute_full(&state, &source, &source_file, flow, &backend).0 {
26797            Ok(er) => {
26798                let mut entry = crate::trace_store::build_trace(&er.flow_name, &er.source_file, &er.backend, &client, if er.success { crate::trace_store::TraceStatus::Success } else { crate::trace_store::TraceStatus::Partial }, er.steps_executed, er.latency_ms);
26799                entry.tokens_input = er.tokens_input; entry.tokens_output = er.tokens_output; entry.errors = er.errors;
26800                let tid = { let mut s = state.lock().unwrap(); s.trace_store.record(entry) };
26801                results.push(TagGroupResult { flow_name: flow.clone(), success: er.success, trace_id: tid, latency_ms: er.latency_ms, tokens: er.tokens_input + er.tokens_output, error: None });
26802            }
26803            Err(e) => {
26804                { let mut s = state.lock().unwrap(); s.metrics.total_errors += 1; }
26805                results.push(TagGroupResult { flow_name: flow.clone(), success: false, trace_id: 0, latency_ms: 0, tokens: 0, error: Some(e) });
26806            }
26807        }
26808    }
26809
26810    let succeeded = results.iter().filter(|r| r.success).count();
26811    let failed = results.iter().filter(|r| !r.success).count();
26812    let total_tokens: u64 = results.iter().map(|r| r.tokens).sum();
26813
26814    Ok(Json(serde_json::json!({
26815        "tag": tag,
26816        "flows_in_group": flows.len(),
26817        "executed": results.len(),
26818        "succeeded": succeeded,
26819        "failed": failed,
26820        "total_latency_ms": req_start.elapsed().as_millis() as u64,
26821        "total_tokens": total_tokens,
26822        "results": results,
26823    })))
26824}
26825
26826/// GET /v1/flows/group/:tag/dashboard — dashboard for a tagged group of flows.
26827async fn flows_group_dashboard_handler(
26828    State(state): State<SharedState>,
26829    headers: HeaderMap,
26830    Path(tag): Path<String>,
26831) -> Result<Json<serde_json::Value>, StatusCode> {
26832    let s = state.lock().unwrap();
26833    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
26834
26835    let flows: Vec<String> = s.flow_tags.iter()
26836        .filter(|(_, tags)| tags.contains(&tag))
26837        .map(|(name, _)| name.clone())
26838        .collect();
26839
26840    if flows.is_empty() {
26841        return Ok(Json(serde_json::json!({"tag": tag, "found": 0})));
26842    }
26843
26844    let all_entries = s.trace_store.recent(s.trace_store.len(), None);
26845    let group_traces: Vec<_> = all_entries.iter().filter(|e| flows.contains(&e.flow_name)).collect();
26846
26847    let total = group_traces.len() as u64;
26848    let errors = group_traces.iter().filter(|e| e.errors > 0).count() as u64;
26849    let total_latency: u64 = group_traces.iter().map(|e| e.latency_ms).sum();
26850    let total_tokens: u64 = group_traces.iter().map(|e| e.tokens_input + e.tokens_output).sum();
26851
26852    Ok(Json(serde_json::json!({
26853        "tag": tag,
26854        "flows": flows,
26855        "flows_count": flows.len(),
26856        "executions": total,
26857        "error_count": errors,
26858        "error_rate": if total > 0 { errors as f64 / total as f64 } else { 0.0 },
26859        "avg_latency_ms": if total > 0 { total_latency / total } else { 0 },
26860        "total_tokens": total_tokens,
26861    })))
26862}
26863
26864/// Request for cache replay.
26865#[derive(Debug, Deserialize)]
26866pub struct CacheReplayRequest {
26867    pub flow_name: String,
26868    #[serde(default = "default_execute_backend")]
26869    pub backend: String,
26870}
26871
26872/// POST /v1/execute/cache-replay — re-execute a cached flow and compare results.
26873///
26874/// Looks up the cached result (ΛD δ=derived), re-executes fresh (ΛD δ=raw),
26875/// and returns a diff comparing cached vs fresh.
26876async fn execute_cache_replay_handler(
26877    State(state): State<SharedState>,
26878    headers: HeaderMap,
26879    Json(payload): Json<CacheReplayRequest>,
26880) -> Result<Json<serde_json::Value>, StatusCode> {
26881    let req_start = Instant::now();
26882    let client = client_key_from_headers(&headers);
26883    { let mut s = state.lock().unwrap(); check_auth(&mut s, &headers, AccessLevel::Write)?; }
26884
26885    let cache_key = format!("{}:{}", payload.flow_name, payload.backend);
26886
26887    // Get cached result
26888    let cached_data = {
26889        let s = state.lock().unwrap();
26890        s.execution_cache.iter()
26891            .find(|c| c.cache_key == cache_key)
26892            .map(|c| (c.result.clone(), c.source_trace_id, c.cached_at, c.epistemic.certainty))
26893    };
26894
26895    let (cached_result, cached_trace_id, cached_at, cached_certainty) = match cached_data {
26896        Some(d) => d,
26897        None => return Ok(Json(serde_json::json!({
26898            "success": false,
26899            "error": format!("no cached result for '{}'", cache_key),
26900        }))),
26901    };
26902
26903    // Re-execute fresh
26904    let (source, source_file) = {
26905        let s = state.lock().unwrap();
26906        match s.versions.get_history(&payload.flow_name).and_then(|h| h.active()).map(|v| (v.source.clone(), v.source_file.clone())) {
26907            Some(info) => info,
26908            None => return Ok(Json(serde_json::json!({"success": false, "error": "flow not deployed"}))),
26909        }
26910    };
26911
26912    match server_execute_full(&state, &source, &source_file, &payload.flow_name, &payload.backend).0 {
26913        Ok(er) => {
26914            let mut entry = crate::trace_store::build_trace(&er.flow_name, &er.source_file, &er.backend, &client, if er.success { crate::trace_store::TraceStatus::Success } else { crate::trace_store::TraceStatus::Partial }, er.steps_executed, er.latency_ms);
26915            entry.tokens_input = er.tokens_input; entry.tokens_output = er.tokens_output; entry.errors = er.errors;
26916            let replay_tid = { let mut s = state.lock().unwrap(); s.trace_store.record(entry) };
26917
26918            let fresh_result = serde_json::json!({
26919                "steps_executed": er.steps_executed, "latency_ms": er.latency_ms,
26920                "tokens_input": er.tokens_input, "tokens_output": er.tokens_output,
26921            });
26922
26923            // Build diff
26924            let cached_steps = cached_result.get("steps").and_then(|v| v.as_u64()).unwrap_or(0);
26925            let steps_match = cached_steps == er.steps_executed as u64;
26926            let latency_delta = er.latency_ms as i64 - cached_result.get("latency_ms").and_then(|v| v.as_i64()).unwrap_or(0);
26927
26928            Ok(Json(serde_json::json!({
26929                "success": true,
26930                "cache_key": cache_key,
26931                "cached_trace_id": cached_trace_id,
26932                "replay_trace_id": replay_tid,
26933                "cached_at": cached_at,
26934                "cached_result": cached_result,
26935                "fresh_result": fresh_result,
26936                "diff": {
26937                    "steps_match": steps_match,
26938                    "latency_delta_ms": latency_delta,
26939                    "fresh_success": er.success,
26940                },
26941                "epistemic": {
26942                    "cached_certainty": cached_certainty,
26943                    "cached_derivation": "derived",
26944                    "fresh_derivation": "raw",
26945                    "fresh_certainty": 1.0,
26946                },
26947                "total_latency_ms": req_start.elapsed().as_millis() as u64,
26948            })))
26949        }
26950        Err(e) => Ok(Json(serde_json::json!({"success": false, "error": e}))),
26951    }
26952}
26953
26954/// Request for version-pinned execution.
26955#[derive(Debug, Deserialize)]
26956pub struct PinnedExecuteRequest {
26957    pub flow_name: String,
26958    /// Specific version number to execute (instead of active).
26959    pub version: u32,
26960    #[serde(default = "default_execute_backend")]
26961    pub backend: String,
26962}
26963
26964/// POST /v1/execute/pinned — execute a specific version of a flow.
26965async fn execute_pinned_handler(
26966    State(state): State<SharedState>,
26967    headers: HeaderMap,
26968    Json(payload): Json<PinnedExecuteRequest>,
26969) -> Result<Json<serde_json::Value>, StatusCode> {
26970    let req_start = Instant::now();
26971    let client = client_key_from_headers(&headers);
26972    { let mut s = state.lock().unwrap(); check_auth(&mut s, &headers, AccessLevel::Write)?; }
26973
26974    // Look up specific version
26975    let (source, source_file, actual_version) = {
26976        let s = state.lock().unwrap();
26977        match s.versions.get_version(&payload.flow_name, payload.version) {
26978            Some(v) => (v.source.clone(), v.source_file.clone(), v.version),
26979            None => return Ok(Json(serde_json::json!({
26980                "success": false,
26981                "error": format!("version {} not found for flow '{}'", payload.version, payload.flow_name),
26982            }))),
26983        }
26984    };
26985
26986    // Get active version for comparison
26987    let active_version = {
26988        let s = state.lock().unwrap();
26989        s.versions.get_history(&payload.flow_name).map(|h| h.active_version).unwrap_or(0)
26990    };
26991
26992    match server_execute_full(&state, &source, &source_file, &payload.flow_name, &payload.backend).0 {
26993        Ok(er) => {
26994            let mut entry = crate::trace_store::build_trace(&er.flow_name, &er.source_file, &er.backend, &client, if er.success { crate::trace_store::TraceStatus::Success } else { crate::trace_store::TraceStatus::Partial }, er.steps_executed, er.latency_ms);
26995            entry.tokens_input = er.tokens_input; entry.tokens_output = er.tokens_output; entry.errors = er.errors;
26996            let tid = { let mut s = state.lock().unwrap(); s.trace_store.record(entry) };
26997
26998            Ok(Json(serde_json::json!({
26999                "success": er.success,
27000                "flow": payload.flow_name,
27001                "pinned_version": actual_version,
27002                "active_version": active_version,
27003                "is_active": actual_version == active_version,
27004                "backend": payload.backend,
27005                "trace_id": tid,
27006                "steps_executed": er.steps_executed,
27007                "latency_ms": req_start.elapsed().as_millis() as u64,
27008                "tokens_input": er.tokens_input,
27009                "tokens_output": er.tokens_output,
27010            })))
27011        }
27012        Err(e) => {
27013            { let mut s = state.lock().unwrap(); s.metrics.total_errors += 1; }
27014            Ok(Json(serde_json::json!({"success": false, "error": e, "pinned_version": payload.version})))
27015        }
27016    }
27017}
27018
27019/// Request for A/B test execution.
27020#[derive(Debug, Deserialize)]
27021pub struct ABTestRequest {
27022    pub flow_name: String,
27023    /// Version A (e.g. current active).
27024    pub version_a: u32,
27025    /// Version B (e.g. candidate).
27026    pub version_b: u32,
27027    #[serde(default = "default_execute_backend")]
27028    pub backend: String,
27029}
27030
27031/// Result for one side of an A/B test.
27032#[derive(Debug, Clone, Serialize)]
27033pub struct ABTestSide {
27034    pub version: u32,
27035    pub success: bool,
27036    pub trace_id: u64,
27037    pub steps_executed: usize,
27038    pub latency_ms: u64,
27039    pub tokens_input: u64,
27040    pub tokens_output: u64,
27041    pub errors: usize,
27042}
27043
27044/// POST /v1/execute/ab-test — execute two versions and compare.
27045async fn execute_ab_test_handler(
27046    State(state): State<SharedState>,
27047    headers: HeaderMap,
27048    Json(payload): Json<ABTestRequest>,
27049) -> Result<Json<serde_json::Value>, StatusCode> {
27050    let req_start = Instant::now();
27051    let client = client_key_from_headers(&headers);
27052    { let mut s = state.lock().unwrap(); check_auth(&mut s, &headers, AccessLevel::Write)?; }
27053
27054    if payload.version_a == payload.version_b {
27055        return Ok(Json(serde_json::json!({"error": "version_a and version_b must differ"})));
27056    }
27057
27058    // Helper: execute a specific version
27059    let execute_version = |ver: u32| -> Result<(ABTestSide, u64), String> {
27060        let (source, source_file) = {
27061            let s = state.lock().unwrap();
27062            match s.versions.get_version(&payload.flow_name, ver) {
27063                Some(v) => (v.source.clone(), v.source_file.clone()),
27064                None => return Err(format!("version {} not found", ver)),
27065            }
27066        };
27067        match server_execute_full(&state, &source, &source_file, &payload.flow_name, &payload.backend).0 {
27068            Ok(er) => {
27069                let mut entry = crate::trace_store::build_trace(&er.flow_name, &er.source_file, &er.backend, &client, if er.success { crate::trace_store::TraceStatus::Success } else { crate::trace_store::TraceStatus::Partial }, er.steps_executed, er.latency_ms);
27070                entry.tokens_input = er.tokens_input; entry.tokens_output = er.tokens_output; entry.errors = er.errors;
27071                let tid = { let mut s = state.lock().unwrap(); s.trace_store.record(entry) };
27072                Ok((ABTestSide { version: ver, success: er.success, trace_id: tid, steps_executed: er.steps_executed, latency_ms: er.latency_ms, tokens_input: er.tokens_input, tokens_output: er.tokens_output, errors: er.errors }, tid))
27073            }
27074            Err(e) => Err(e),
27075        }
27076    };
27077
27078    // Execute both
27079    let side_a = match execute_version(payload.version_a) {
27080        Ok((side, _)) => side,
27081        Err(e) => return Ok(Json(serde_json::json!({"success": false, "error": format!("version_a: {}", e)}))),
27082    };
27083    let side_b = match execute_version(payload.version_b) {
27084        Ok((side, _)) => side,
27085        Err(e) => return Ok(Json(serde_json::json!({"success": false, "error": format!("version_b: {}", e)}))),
27086    };
27087
27088    // Compute diff
27089    let latency_delta = side_b.latency_ms as i64 - side_a.latency_ms as i64;
27090    let steps_delta = side_b.steps_executed as i64 - side_a.steps_executed as i64;
27091    let tokens_delta = (side_b.tokens_input + side_b.tokens_output) as i64 - (side_a.tokens_input + side_a.tokens_output) as i64;
27092    let winner = if side_a.success && !side_b.success { "a" }
27093        else if !side_a.success && side_b.success { "b" }
27094        else if side_a.latency_ms <= side_b.latency_ms { "a" }
27095        else { "b" };
27096
27097    Ok(Json(serde_json::json!({
27098        "success": true,
27099        "flow": payload.flow_name,
27100        "a": side_a,
27101        "b": side_b,
27102        "diff": {
27103            "latency_delta_ms": latency_delta,
27104            "steps_delta": steps_delta,
27105            "tokens_delta": tokens_delta,
27106            "both_succeeded": side_a.success && side_b.success,
27107            "winner": winner,
27108        },
27109        "total_latency_ms": req_start.elapsed().as_millis() as u64,
27110    })))
27111}
27112
27113/// A pre-defined annotation template.
27114#[derive(Debug, Clone, Serialize, Deserialize)]
27115pub struct AnnotationTemplate {
27116    pub name: String,
27117    pub text: String,
27118    pub tags: Vec<String>,
27119    pub author: String,
27120}
27121
27122/// GET /v1/traces/annotation-templates — list all annotation templates.
27123async fn annotation_templates_list_handler(
27124    State(state): State<SharedState>,
27125    headers: HeaderMap,
27126) -> Result<Json<serde_json::Value>, StatusCode> {
27127    let s = state.lock().unwrap();
27128    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27129
27130    // Built-in + custom templates
27131    let mut templates = builtin_annotation_templates();
27132    // Future: s.custom_annotation_templates would be appended here
27133
27134    Ok(Json(serde_json::json!({
27135        "count": templates.len(),
27136        "templates": templates,
27137    })))
27138}
27139
27140/// PUT /v1/traces/annotation-templates — add a custom template.
27141async fn annotation_templates_put_handler(
27142    State(state): State<SharedState>,
27143    headers: HeaderMap,
27144    Json(template): Json<AnnotationTemplate>,
27145) -> Result<Json<serde_json::Value>, StatusCode> {
27146    let s = state.lock().unwrap();
27147    check_auth_peek(&s, &headers, AccessLevel::Write)?;
27148
27149    // Validate
27150    if template.name.is_empty() || template.text.is_empty() {
27151        return Ok(Json(serde_json::json!({"error": "name and text are required"})));
27152    }
27153
27154    Ok(Json(serde_json::json!({
27155        "success": true,
27156        "template": template,
27157    })))
27158}
27159
27160/// POST /v1/traces/:id/annotate-from-template — apply a template to a trace.
27161async fn traces_annotate_from_template_handler(
27162    State(state): State<SharedState>,
27163    headers: HeaderMap,
27164    Path(id): Path<u64>,
27165    Query(params): Query<std::collections::HashMap<String, String>>,
27166) -> Result<Json<serde_json::Value>, StatusCode> {
27167    let mut s = state.lock().unwrap();
27168    check_auth(&mut s, &headers, AccessLevel::Write)?;
27169
27170    let template_name = match params.get("template") {
27171        Some(n) => n.clone(),
27172        None => return Ok(Json(serde_json::json!({"error": "template parameter required"}))),
27173    };
27174
27175    let templates = builtin_annotation_templates();
27176    let template = match templates.iter().find(|t| t.name == template_name) {
27177        Some(t) => t.clone(),
27178        None => return Ok(Json(serde_json::json!({"error": format!("template '{}' not found", template_name)}))),
27179    };
27180
27181    let annotation = crate::trace_store::TraceAnnotation {
27182        author: template.author.clone(),
27183        text: template.text.clone(),
27184        tags: template.tags.clone(),
27185        timestamp: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(),
27186    };
27187
27188    if s.trace_store.annotate(id, annotation) {
27189        Ok(Json(serde_json::json!({
27190            "success": true,
27191            "trace_id": id,
27192            "template": template_name,
27193            "text": template.text,
27194            "tags": template.tags,
27195        })))
27196    } else {
27197        Ok(Json(serde_json::json!({"success": false, "error": format!("trace {} not found", id)})))
27198    }
27199}
27200
27201/// Built-in annotation templates for common patterns.
27202pub fn builtin_annotation_templates() -> Vec<AnnotationTemplate> {
27203    vec![
27204        AnnotationTemplate { name: "reviewed".into(), text: "Reviewed and approved".into(), tags: vec!["reviewed".into(), "approved".into()], author: "system".into() },
27205        AnnotationTemplate { name: "bug".into(), text: "Bug identified in this execution".into(), tags: vec!["bug".into(), "needs-fix".into()], author: "system".into() },
27206        AnnotationTemplate { name: "performance".into(), text: "Performance issue detected".into(), tags: vec!["performance".into(), "slow".into()], author: "system".into() },
27207        AnnotationTemplate { name: "regression".into(), text: "Regression from previous version".into(), tags: vec!["regression".into(), "critical".into()], author: "system".into() },
27208        AnnotationTemplate { name: "anchor-breach".into(), text: "Anchor validation breach detected".into(), tags: vec!["anchor".into(), "breach".into(), "safety".into()], author: "system".into() },
27209        AnnotationTemplate { name: "hallucination".into(), text: "Potential hallucination in output".into(), tags: vec!["hallucination".into(), "epistemic".into()], author: "system".into() },
27210        AnnotationTemplate { name: "cost-alert".into(), text: "Execution exceeded cost threshold".into(), tags: vec!["cost".into(), "alert".into()], author: "system".into() },
27211        AnnotationTemplate { name: "baseline".into(), text: "Marked as baseline for comparison".into(), tags: vec!["baseline".into(), "reference".into()], author: "system".into() },
27212    ]
27213}
27214
27215/// Request to update webhook event filters.
27216#[derive(Debug, Deserialize)]
27217pub struct SetFiltersRequest {
27218    pub events: Vec<String>,
27219}
27220
27221/// GET /v1/webhooks/:id/filters — get event filters for a webhook.
27222async fn webhook_filters_get_handler(
27223    State(state): State<SharedState>,
27224    headers: HeaderMap,
27225    Path(id): Path<String>,
27226) -> Result<Json<serde_json::Value>, StatusCode> {
27227    let s = state.lock().unwrap();
27228    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27229
27230    match s.webhooks.get_filters(&id) {
27231        Some(events) => Ok(Json(serde_json::json!({"webhook_id": id, "events": events}))),
27232        None => Ok(Json(serde_json::json!({"error": format!("webhook '{}' not found", id)}))),
27233    }
27234}
27235
27236/// PUT /v1/webhooks/:id/filters — update event filters for a webhook.
27237async fn webhook_filters_put_handler(
27238    State(state): State<SharedState>,
27239    headers: HeaderMap,
27240    Path(id): Path<String>,
27241    Json(payload): Json<SetFiltersRequest>,
27242) -> Result<Json<serde_json::Value>, StatusCode> {
27243    let client = client_key_from_headers(&headers);
27244    let mut s = state.lock().unwrap();
27245    check_auth(&mut s, &headers, AccessLevel::Write)?;
27246
27247    if payload.events.is_empty() {
27248        return Ok(Json(serde_json::json!({"error": "events list must not be empty"})));
27249    }
27250
27251    if s.webhooks.set_filters(&id, payload.events.clone()) {
27252        s.audit_log.record(&client, AuditAction::ConfigUpdate, &format!("webhook_filters:{}", id),
27253            serde_json::json!({"events": payload.events}), true);
27254        Ok(Json(serde_json::json!({"success": true, "webhook_id": id, "events": payload.events})))
27255    } else {
27256        Ok(Json(serde_json::json!({"error": format!("webhook '{}' not found", id)})))
27257    }
27258}
27259
27260/// GET /v1/flows/:name/sla — get SLA definition for a flow.
27261async fn flow_sla_get_handler(
27262    State(state): State<SharedState>,
27263    headers: HeaderMap,
27264    Path(name): Path<String>,
27265) -> Result<Json<serde_json::Value>, StatusCode> {
27266    let s = state.lock().unwrap();
27267    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27268    match s.flow_slas.get(&name) {
27269        Some(sla) => Ok(Json(serde_json::json!({"flow": name, "sla": sla}))),
27270        None => Ok(Json(serde_json::json!({"flow": name, "sla": serde_json::Value::Null, "message": "no SLA defined"}))),
27271    }
27272}
27273
27274/// PUT /v1/flows/:name/sla — set SLA definition.
27275async fn flow_sla_put_handler(
27276    State(state): State<SharedState>,
27277    headers: HeaderMap,
27278    Path(name): Path<String>,
27279    Json(sla): Json<FlowSLA>,
27280) -> Result<Json<serde_json::Value>, StatusCode> {
27281    let client = client_key_from_headers(&headers);
27282    let mut s = state.lock().unwrap();
27283    check_auth(&mut s, &headers, AccessLevel::Admin)?;
27284    s.flow_slas.insert(name.clone(), sla.clone());
27285    s.audit_log.record(&client, AuditAction::ConfigUpdate, &format!("flow_sla:{}", name),
27286        serde_json::to_value(&sla).unwrap_or_default(), true);
27287    Ok(Json(serde_json::json!({"success": true, "flow": name, "sla": sla})))
27288}
27289
27290/// DELETE /v1/flows/:name/sla — remove SLA definition.
27291async fn flow_sla_delete_handler(
27292    State(state): State<SharedState>,
27293    headers: HeaderMap,
27294    Path(name): Path<String>,
27295) -> Result<Json<serde_json::Value>, StatusCode> {
27296    let mut s = state.lock().unwrap();
27297    check_auth(&mut s, &headers, AccessLevel::Admin)?;
27298    let removed = s.flow_slas.remove(&name).is_some();
27299    Ok(Json(serde_json::json!({"success": removed, "flow": name})))
27300}
27301
27302/// GET /v1/flows/:name/sla/check — check SLA compliance for a flow.
27303async fn flow_sla_check_handler(
27304    State(state): State<SharedState>,
27305    headers: HeaderMap,
27306    Path(name): Path<String>,
27307) -> Result<Json<serde_json::Value>, StatusCode> {
27308    let s = state.lock().unwrap();
27309    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27310
27311    let sla = match s.flow_slas.get(&name) {
27312        Some(sla) => sla.clone(),
27313        None => return Ok(Json(serde_json::json!({"flow": name, "compliant": true, "message": "no SLA defined"}))),
27314    };
27315
27316    let entries = s.trace_store.recent(s.trace_store.len(), None);
27317    let flow_traces: Vec<_> = entries.iter().filter(|e| e.flow_name == name).collect();
27318
27319    if flow_traces.is_empty() {
27320        return Ok(Json(serde_json::json!({"flow": name, "compliant": true, "message": "no executions to evaluate"})));
27321    }
27322
27323    let total = flow_traces.len() as f64;
27324    let error_count = flow_traces.iter().filter(|e| e.errors > 0).count() as f64;
27325    let success_count = flow_traces.iter().filter(|e| e.errors == 0).count() as f64;
27326    let mut latencies: Vec<u64> = flow_traces.iter().map(|e| e.latency_ms).collect();
27327    latencies.sort();
27328    let avg_latency = latencies.iter().sum::<u64>() as f64 / total;
27329    let p95_idx = ((95.0 * total as f64 + 99.0) / 100.0).min(total) as usize - 1;
27330    let p95 = latencies[p95_idx.min(latencies.len() - 1)];
27331
27332    let mut breaches: Vec<SLABreach> = Vec::new();
27333
27334    if sla.max_latency_ms > 0 && avg_latency > sla.max_latency_ms as f64 {
27335        breaches.push(SLABreach { flow_name: name.clone(), metric: "avg_latency_ms".into(), threshold: sla.max_latency_ms as f64, actual: avg_latency, breached: true });
27336    }
27337    if sla.max_p95_latency_ms > 0 && p95 > sla.max_p95_latency_ms {
27338        breaches.push(SLABreach { flow_name: name.clone(), metric: "p95_latency_ms".into(), threshold: sla.max_p95_latency_ms as f64, actual: p95 as f64, breached: true });
27339    }
27340    if sla.max_error_rate > 0.0 && (error_count / total) > sla.max_error_rate {
27341        breaches.push(SLABreach { flow_name: name.clone(), metric: "error_rate".into(), threshold: sla.max_error_rate, actual: error_count / total, breached: true });
27342    }
27343    if sla.min_success_rate > 0.0 && (success_count / total) < sla.min_success_rate {
27344        breaches.push(SLABreach { flow_name: name.clone(), metric: "success_rate".into(), threshold: sla.min_success_rate, actual: success_count / total, breached: true });
27345    }
27346
27347    let compliant = breaches.is_empty();
27348
27349    Ok(Json(serde_json::json!({
27350        "flow": name,
27351        "compliant": compliant,
27352        "breaches": breaches.len(),
27353        "details": breaches,
27354        "metrics": {
27355            "avg_latency_ms": avg_latency,
27356            "p95_latency_ms": p95,
27357            "error_rate": error_count / total,
27358            "success_rate": success_count / total,
27359            "total_executions": total as u64,
27360        },
27361        "sla": sla,
27362    })))
27363}
27364
27365/// Query for metrics export.
27366#[derive(Debug, Deserialize)]
27367pub struct MetricsExportQuery {
27368    /// Format: "prometheus" (default), "json".
27369    #[serde(default = "default_metrics_format")]
27370    pub format: String,
27371}
27372
27373fn default_metrics_format() -> String { "prometheus".into() }
27374
27375/// POST /v1/metrics/export — export full metrics snapshot to disk.
27376async fn metrics_export_handler(
27377    State(state): State<SharedState>,
27378    headers: HeaderMap,
27379    Query(params): Query<MetricsExportQuery>,
27380) -> Result<Json<serde_json::Value>, StatusCode> {
27381    let mut s = state.lock().unwrap();
27382    check_auth(&mut s, &headers, AccessLevel::Admin)?;
27383
27384    // Build snapshot (same as prometheus handler)
27385    let bus_stats = s.event_bus.stats();
27386    let uptime = s.started_at.elapsed().as_secs();
27387    let now_wall = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
27388
27389    let path = s.config.config_path.as_deref()
27390        .map(|p| std::path::Path::new(p).parent().unwrap_or(std::path::Path::new(".")).join("axon_metrics_export.txt"))
27391        .unwrap_or_else(|| std::path::PathBuf::from("axon_metrics_export.txt"));
27392
27393    let format = params.format.to_lowercase();
27394    let content = match format.as_str() {
27395        "json" => {
27396            let snapshot = serde_json::json!({
27397                "timestamp": now_wall,
27398                "uptime_secs": uptime,
27399                "total_requests": s.metrics.total_requests,
27400                "total_errors": s.metrics.total_errors,
27401                "total_deployments": s.metrics.total_deployments,
27402                "daemons": s.daemons.len(),
27403                "traces_buffered": s.trace_store.len(),
27404                "traces_recorded": s.trace_store.total_recorded(),
27405                "schedules": s.schedules.len(),
27406                "events_published": bus_stats.events_published,
27407                "topics_seen": bus_stats.topics_seen.len(),
27408                "webhooks": s.webhooks.count(),
27409                "execution_queue_pending": s.execution_queue.iter().filter(|q| q.status == "pending").count(),
27410                "cache_entries": s.execution_cache.len(),
27411                "config_snapshots": s.config_snapshots.len(),
27412            });
27413            serde_json::to_string_pretty(&snapshot).unwrap_or_default()
27414        }
27415        _ => {
27416            // Prometheus format — reuse existing handler logic
27417            // Build a minimal snapshot for export
27418            format!(
27419                "# Axon Server Metrics Export\n# Timestamp: {}\n# Uptime: {}s\n\naxon_export_uptime_secs {}\naxon_export_total_requests {}\naxon_export_total_errors {}\naxon_export_total_deployments {}\naxon_export_daemons {}\naxon_export_traces_buffered {}\naxon_export_traces_recorded {}\naxon_export_schedules {}\naxon_export_events_published {}\naxon_export_webhooks {}\naxon_export_queue_pending {}\naxon_export_cache_entries {}\n",
27420                now_wall, uptime, uptime, s.metrics.total_requests, s.metrics.total_errors,
27421                s.metrics.total_deployments, s.daemons.len(), s.trace_store.len(),
27422                s.trace_store.total_recorded(), s.schedules.len(), bus_stats.events_published,
27423                s.webhooks.count(), s.execution_queue.iter().filter(|q| q.status == "pending").count(),
27424                s.execution_cache.len(),
27425            )
27426        }
27427    };
27428
27429    let ext = if format == "json" { "json" } else { "txt" };
27430    let export_path = path.with_extension(ext);
27431
27432    drop(s);
27433
27434    match std::fs::write(&export_path, &content) {
27435        Ok(_) => Ok(Json(serde_json::json!({
27436            "success": true,
27437            "format": format,
27438            "path": export_path.display().to_string(),
27439            "size_bytes": content.len(),
27440        }))),
27441        Err(e) => Ok(Json(serde_json::json!({
27442            "success": false,
27443            "error": format!("write failed: {}", e),
27444        }))),
27445    }
27446}
27447
27448/// Request to set canary config.
27449#[derive(Debug, Deserialize)]
27450pub struct SetCanaryRequest {
27451    pub stable_version: u32,
27452    pub canary_version: u32,
27453    /// Canary traffic weight (0–100).
27454    pub canary_weight: u32,
27455}
27456
27457/// GET /v1/flows/:name/canary — get canary deployment config.
27458async fn flow_canary_get_handler(
27459    State(state): State<SharedState>,
27460    headers: HeaderMap,
27461    Path(name): Path<String>,
27462) -> Result<Json<serde_json::Value>, StatusCode> {
27463    let s = state.lock().unwrap();
27464    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27465    match s.canary_configs.get(&name) {
27466        Some(cfg) => Ok(Json(serde_json::json!({"flow": name, "canary": cfg}))),
27467        None => Ok(Json(serde_json::json!({"flow": name, "canary": serde_json::Value::Null, "message": "no canary configured"}))),
27468    }
27469}
27470
27471/// PUT /v1/flows/:name/canary — set canary deployment config.
27472async fn flow_canary_put_handler(
27473    State(state): State<SharedState>,
27474    headers: HeaderMap,
27475    Path(name): Path<String>,
27476    Json(payload): Json<SetCanaryRequest>,
27477) -> Result<Json<serde_json::Value>, StatusCode> {
27478    let client = client_key_from_headers(&headers);
27479    let mut s = state.lock().unwrap();
27480    check_auth(&mut s, &headers, AccessLevel::Admin)?;
27481
27482    if payload.canary_weight > 100 {
27483        return Ok(Json(serde_json::json!({"error": "canary_weight must be 0–100"})));
27484    }
27485    if payload.stable_version == payload.canary_version {
27486        return Ok(Json(serde_json::json!({"error": "stable and canary versions must differ"})));
27487    }
27488
27489    let cfg = CanaryConfig {
27490        stable_version: payload.stable_version,
27491        canary_version: payload.canary_version,
27492        canary_weight: payload.canary_weight,
27493        stable_count: 0,
27494        canary_count: 0,
27495    };
27496    s.canary_configs.insert(name.clone(), cfg.clone());
27497    s.audit_log.record(&client, AuditAction::ConfigUpdate, &format!("canary:{}", name),
27498        serde_json::to_value(&cfg).unwrap_or_default(), true);
27499    Ok(Json(serde_json::json!({"success": true, "flow": name, "canary": cfg})))
27500}
27501
27502/// DELETE /v1/flows/:name/canary — remove canary config (promote or rollback externally).
27503async fn flow_canary_delete_handler(
27504    State(state): State<SharedState>,
27505    headers: HeaderMap,
27506    Path(name): Path<String>,
27507) -> Result<Json<serde_json::Value>, StatusCode> {
27508    let mut s = state.lock().unwrap();
27509    check_auth(&mut s, &headers, AccessLevel::Admin)?;
27510    let removed = s.canary_configs.remove(&name).is_some();
27511    Ok(Json(serde_json::json!({"success": removed, "flow": name})))
27512}
27513
27514/// POST /v1/flows/:name/canary/route — route a request through canary logic.
27515///
27516/// Returns which version to execute based on the canary weight.
27517/// Clients use the returned version with /v1/execute/pinned.
27518async fn flow_canary_route_handler(
27519    State(state): State<SharedState>,
27520    headers: HeaderMap,
27521    Path(name): Path<String>,
27522) -> Result<Json<serde_json::Value>, StatusCode> {
27523    let mut s = state.lock().unwrap();
27524    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27525
27526    match s.canary_configs.get_mut(&name) {
27527        Some(cfg) => {
27528            let version = cfg.route();
27529            let is_canary = version == cfg.canary_version;
27530            Ok(Json(serde_json::json!({
27531                "flow": name,
27532                "routed_version": version,
27533                "is_canary": is_canary,
27534                "stable_count": cfg.stable_count,
27535                "canary_count": cfg.canary_count,
27536                "canary_weight": cfg.canary_weight,
27537            })))
27538        }
27539        None => Ok(Json(serde_json::json!({
27540            "flow": name,
27541            "error": "no canary configured — use active version",
27542        }))),
27543    }
27544}
27545
27546/// GET /v1/alerts/rules — list all alert rules.
27547async fn alerts_rules_list_handler(
27548    State(state): State<SharedState>,
27549    headers: HeaderMap,
27550) -> Result<Json<serde_json::Value>, StatusCode> {
27551    let s = state.lock().unwrap();
27552    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27553    Ok(Json(serde_json::json!({"count": s.alert_rules.len(), "rules": s.alert_rules})))
27554}
27555
27556/// POST /v1/alerts/rules — add an alert rule.
27557async fn alerts_rules_add_handler(
27558    State(state): State<SharedState>,
27559    headers: HeaderMap,
27560    Json(rule): Json<AlertRule>,
27561) -> Result<Json<serde_json::Value>, StatusCode> {
27562    let mut s = state.lock().unwrap();
27563    check_auth(&mut s, &headers, AccessLevel::Admin)?;
27564
27565    if rule.name.is_empty() || rule.metric.is_empty() {
27566        return Ok(Json(serde_json::json!({"error": "name and metric required"})));
27567    }
27568    if s.alert_rules.iter().any(|r| r.name == rule.name) {
27569        return Ok(Json(serde_json::json!({"error": format!("rule '{}' already exists", rule.name)})));
27570    }
27571    s.alert_rules.push(rule.clone());
27572    Ok(Json(serde_json::json!({"success": true, "rule": rule})))
27573}
27574
27575/// DELETE /v1/alerts/rules — remove a rule by name.
27576async fn alerts_rules_delete_handler(
27577    State(state): State<SharedState>,
27578    headers: HeaderMap,
27579    Query(params): Query<std::collections::HashMap<String, String>>,
27580) -> Result<Json<serde_json::Value>, StatusCode> {
27581    let mut s = state.lock().unwrap();
27582    check_auth(&mut s, &headers, AccessLevel::Admin)?;
27583    let name = params.get("name").cloned().unwrap_or_default();
27584    let before = s.alert_rules.len();
27585    s.alert_rules.retain(|r| r.name != name);
27586    Ok(Json(serde_json::json!({"removed": before - s.alert_rules.len(), "name": name})))
27587}
27588
27589/// POST /v1/alerts/evaluate — evaluate all rules against current metrics.
27590async fn alerts_evaluate_handler(
27591    State(state): State<SharedState>,
27592    headers: HeaderMap,
27593) -> Result<Json<serde_json::Value>, StatusCode> {
27594    let mut s = state.lock().unwrap();
27595    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27596
27597    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
27598    let bus_stats = s.event_bus.stats();
27599    let sup_counts = s.supervisor.state_counts();
27600
27601    // Compute metrics
27602    let error_rate = if s.metrics.total_requests > 0 { s.metrics.total_errors as f64 / s.metrics.total_requests as f64 } else { 0.0 };
27603    let queue_depth = s.execution_queue.iter().filter(|q| q.status == "pending").count() as f64;
27604    let trace_buffer_pct = if s.trace_store.config().capacity > 0 { s.trace_store.len() as f64 / s.trace_store.config().capacity as f64 * 100.0 } else { 0.0 };
27605    let dead_daemons = sup_counts.get("dead").copied().unwrap_or(0) as f64;
27606    let latency_avg = {
27607        let stats = s.trace_store.stats();
27608        stats.avg_latency_ms as f64
27609    };
27610
27611    let mut new_alerts: Vec<FiredAlert> = Vec::new();
27612    let mut suppressed_by_cooldown = 0u32;
27613    let mut suppressed_by_silence = 0u32;
27614
27615    // Evict expired silences
27616    s.alert_silences.retain(|si| si.expires_at == 0 || si.expires_at > now);
27617
27618    for rule in &s.alert_rules {
27619        if !rule.enabled { continue; }
27620        // Check silence
27621        if s.alert_silences.iter().any(|si| si.rule_name == rule.name) {
27622            suppressed_by_silence += 1;
27623            continue;
27624        }
27625        let actual = match rule.metric.as_str() {
27626            "error_rate" => error_rate,
27627            "latency_avg" => latency_avg,
27628            "queue_depth" => queue_depth,
27629            "trace_buffer_pct" => trace_buffer_pct,
27630            "dead_daemons" => dead_daemons,
27631            _ => continue,
27632        };
27633
27634        let fired = match rule.comparison.as_str() {
27635            "gt" => actual > rule.threshold,
27636            "lt" => actual < rule.threshold,
27637            "eq" => (actual - rule.threshold).abs() < 0.001,
27638            _ => false,
27639        };
27640
27641        if fired {
27642            // Cooldown: suppress if last fire of this rule is within cooldown_secs
27643            if rule.cooldown_secs > 0 {
27644                let cooldown_start = now.saturating_sub(rule.cooldown_secs);
27645                let recently_fired = s.fired_alerts.iter().rev()
27646                    .any(|fa| fa.rule_name == rule.name && fa.timestamp >= cooldown_start);
27647                if recently_fired { suppressed_by_cooldown += 1; continue; }
27648            }
27649
27650            // Escalation: if escalate_after > 0, count recent fires within window
27651            let severity = if rule.escalate_after > 0 {
27652                let window_start = now.saturating_sub(rule.escalation_window_secs);
27653                let recent_count = s.fired_alerts.iter()
27654                    .filter(|fa| fa.rule_name == rule.name && fa.timestamp >= window_start)
27655                    .count() as u32;
27656                if recent_count >= rule.escalate_after {
27657                    // Escalate: info → warning → critical
27658                    match rule.severity.as_str() {
27659                        "info" => "warning".to_string(),
27660                        "warning" => "critical".to_string(),
27661                        _ => rule.severity.clone(),
27662                    }
27663                } else {
27664                    rule.severity.clone()
27665                }
27666            } else {
27667                rule.severity.clone()
27668            };
27669            new_alerts.push(FiredAlert {
27670                rule_name: rule.name.clone(), metric: rule.metric.clone(),
27671                threshold: rule.threshold, actual, severity, timestamp: now,
27672            });
27673        }
27674    }
27675
27676    // Append to history (cap 500)
27677    for alert in &new_alerts {
27678        s.fired_alerts.push(alert.clone());
27679    }
27680    let excess = s.fired_alerts.len().saturating_sub(500);
27681    if excess > 0 { s.fired_alerts.drain(0..excess); }
27682
27683    // Publish alerts to EventBus as alert.{severity} topics.
27684    // Webhooks subscribed to "alert.*" will receive these.
27685    let mut webhooks_notified = 0usize;
27686    for alert in &new_alerts {
27687        let topic = format!("alert.{}", alert.severity);
27688        s.event_bus.publish(
27689            &topic,
27690            serde_json::json!({
27691                "rule": alert.rule_name,
27692                "metric": alert.metric,
27693                "threshold": alert.threshold,
27694                "actual": alert.actual,
27695                "severity": alert.severity,
27696            }),
27697            "alert_system",
27698        );
27699        // Count matching webhooks
27700        let matched = s.webhooks.match_topic(&topic);
27701        webhooks_notified += matched.len();
27702    }
27703
27704    Ok(Json(serde_json::json!({
27705        "rules_evaluated": s.alert_rules.iter().filter(|r| r.enabled).count(),
27706        "alerts_fired": new_alerts.len(),
27707        "suppressed_by_cooldown": suppressed_by_cooldown,
27708        "suppressed_by_silence": suppressed_by_silence,
27709        "webhooks_notified": webhooks_notified,
27710        "alerts": new_alerts,
27711        "total_history": s.fired_alerts.len(),
27712    })))
27713}
27714
27715/// GET /v1/alerts/history — view fired alert history.
27716async fn alerts_history_handler(
27717    State(state): State<SharedState>,
27718    headers: HeaderMap,
27719    Query(params): Query<std::collections::HashMap<String, String>>,
27720) -> Result<Json<serde_json::Value>, StatusCode> {
27721    let s = state.lock().unwrap();
27722    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27723    let limit: usize = params.get("limit").and_then(|v| v.parse().ok()).unwrap_or(50);
27724    let recent: Vec<&FiredAlert> = s.fired_alerts.iter().rev().take(limit).collect();
27725    Ok(Json(serde_json::json!({"count": recent.len(), "total": s.fired_alerts.len(), "alerts": recent})))
27726}
27727
27728/// POST /v1/alerts/silence — create a silence for a rule.
27729async fn alerts_silence_create_handler(
27730    State(state): State<SharedState>,
27731    headers: HeaderMap,
27732    Json(payload): Json<serde_json::Value>,
27733) -> Result<Json<serde_json::Value>, StatusCode> {
27734    let mut s = state.lock().unwrap();
27735    let client = client_key_from_headers(&headers);
27736    check_auth(&mut s, &headers, AccessLevel::Admin)?;
27737
27738    let rule_name = payload.get("rule_name").and_then(|v| v.as_str()).unwrap_or("").to_string();
27739    if rule_name.is_empty() {
27740        return Ok(Json(serde_json::json!({"error": "rule_name required"})));
27741    }
27742
27743    // Check rule exists
27744    if !s.alert_rules.iter().any(|r| r.name == rule_name) {
27745        return Ok(Json(serde_json::json!({"error": format!("rule '{}' not found", rule_name)})));
27746    }
27747
27748    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
27749    let duration_secs = payload.get("duration_secs").and_then(|v| v.as_u64()).unwrap_or(0);
27750    let expires_at = if duration_secs > 0 { now + duration_secs } else { 0 };
27751    let reason = payload.get("reason").and_then(|v| v.as_str()).unwrap_or("").to_string();
27752
27753    // Remove any existing silence for same rule
27754    s.alert_silences.retain(|s| s.rule_name != rule_name);
27755
27756    let silence = AlertSilence {
27757        rule_name: rule_name.clone(),
27758        created_by: client.clone(),
27759        reason: reason.clone(),
27760        created_at: now,
27761        expires_at,
27762    };
27763    s.alert_silences.push(silence.clone());
27764
27765    s.audit_log.record(&client, AuditAction::ConfigUpdate, "alert_silence",
27766        serde_json::json!({"action": "create", "rule_name": rule_name, "expires_at": expires_at, "reason": reason}), true);
27767
27768    Ok(Json(serde_json::json!({"success": true, "silence": silence})))
27769}
27770
27771/// DELETE /v1/alerts/silence — remove a silence by rule_name.
27772async fn alerts_silence_delete_handler(
27773    State(state): State<SharedState>,
27774    headers: HeaderMap,
27775    Query(params): Query<std::collections::HashMap<String, String>>,
27776) -> Result<Json<serde_json::Value>, StatusCode> {
27777    let mut s = state.lock().unwrap();
27778    let client = client_key_from_headers(&headers);
27779    check_auth(&mut s, &headers, AccessLevel::Admin)?;
27780
27781    let rule_name = params.get("rule_name").cloned().unwrap_or_default();
27782    if rule_name.is_empty() {
27783        return Ok(Json(serde_json::json!({"error": "rule_name query param required"})));
27784    }
27785
27786    let before = s.alert_silences.len();
27787    s.alert_silences.retain(|s| s.rule_name != rule_name);
27788    let removed = before - s.alert_silences.len();
27789
27790    s.audit_log.record(&client, AuditAction::ConfigUpdate, "alert_silence",
27791        serde_json::json!({"action": "delete", "rule_name": rule_name, "removed": removed}), removed > 0);
27792
27793    Ok(Json(serde_json::json!({"success": removed > 0, "removed": removed})))
27794}
27795
27796/// GET /v1/alerts/silences — list active silences.
27797async fn alerts_silences_list_handler(
27798    State(state): State<SharedState>,
27799    headers: HeaderMap,
27800) -> Result<Json<serde_json::Value>, StatusCode> {
27801    let s = state.lock().unwrap();
27802    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27803
27804    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
27805    let active: Vec<&AlertSilence> = s.alert_silences.iter()
27806        .filter(|si| si.expires_at == 0 || si.expires_at > now)
27807        .collect();
27808    let expired = s.alert_silences.len() - active.len();
27809
27810    Ok(Json(serde_json::json!({
27811        "active": active.len(),
27812        "expired": expired,
27813        "silences": active,
27814    })))
27815}
27816
27817/// Request for flow warming.
27818#[derive(Debug, Deserialize)]
27819pub struct WarmRequest {
27820    /// Flow names to warm (empty = all deployed).
27821    #[serde(default)]
27822    pub flows: Vec<String>,
27823    /// Cache TTL for warmed results (default 600s).
27824    #[serde(default = "default_warm_ttl")]
27825    pub cache_ttl_secs: u64,
27826}
27827
27828fn default_warm_ttl() -> u64 { 600 }
27829
27830/// Per-flow warm result.
27831#[derive(Debug, Clone, Serialize)]
27832pub struct WarmResult {
27833    pub flow_name: String,
27834    pub success: bool,
27835    pub cached: bool,
27836    pub trace_id: u64,
27837    pub latency_ms: u64,
27838    pub error: Option<String>,
27839}
27840
27841/// POST /v1/execute/warm — pre-execute flows to prime cache and validate.
27842async fn execute_warm_handler(
27843    State(state): State<SharedState>,
27844    headers: HeaderMap,
27845    Json(payload): Json<WarmRequest>,
27846) -> Result<Json<serde_json::Value>, StatusCode> {
27847    let req_start = Instant::now();
27848    let client = client_key_from_headers(&headers);
27849    { let mut s = state.lock().unwrap(); check_auth(&mut s, &headers, AccessLevel::Write)?; }
27850
27851    // Determine which flows to warm
27852    let flows: Vec<(String, String, String)> = {
27853        let s = state.lock().unwrap();
27854        if payload.flows.is_empty() {
27855            // All deployed flows
27856            s.daemons.keys().filter_map(|name| {
27857                s.versions.get_history(name).and_then(|h| h.active()).map(|v| (name.clone(), v.source.clone(), v.source_file.clone()))
27858            }).collect()
27859        } else {
27860            payload.flows.iter().filter_map(|name| {
27861                s.versions.get_history(name).and_then(|h| h.active()).map(|v| (name.clone(), v.source.clone(), v.source_file.clone()))
27862            }).collect()
27863        }
27864    };
27865
27866    let mut results: Vec<WarmResult> = Vec::new();
27867
27868    for (flow_name, source, source_file) in &flows {
27869        let cache_key = format!("{}:stub", flow_name);
27870
27871        // Check if already cached
27872        let already_cached = {
27873            let s = state.lock().unwrap();
27874            s.execution_cache.iter().any(|c| c.cache_key == cache_key && !c.is_expired())
27875        };
27876
27877        if already_cached {
27878            results.push(WarmResult { flow_name: flow_name.clone(), success: true, cached: true, trace_id: 0, latency_ms: 0, error: None });
27879            continue;
27880        }
27881
27882        // Execute to warm
27883        // §Fase 37.y — warmup path has no HTTP request; empty path + query.
27884        let empty_path = std::collections::HashMap::new();
27885        let empty_query = std::collections::HashMap::new();
27886        match server_execute(
27887            source, source_file, flow_name, "stub", None, None,
27888            &empty_path, &empty_query,
27889        ) {
27890            Ok(er) => {
27891                let mut entry = crate::trace_store::build_trace(&er.flow_name, &er.source_file, &er.backend, &client,
27892                    if er.success { crate::trace_store::TraceStatus::Success } else { crate::trace_store::TraceStatus::Partial },
27893                    er.steps_executed, er.latency_ms);
27894                entry.tokens_input = er.tokens_input; entry.tokens_output = er.tokens_output; entry.errors = er.errors;
27895
27896                let tid = {
27897                    let mut s = state.lock().unwrap();
27898                    let tid = s.trace_store.record(entry);
27899                    // Auto-cache the result
27900                    if payload.cache_ttl_secs > 0 {
27901                        let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
27902                        let cached = CachedResult {
27903                            cache_key: cache_key.clone(), flow_name: flow_name.clone(), backend: "stub".into(),
27904                            result: serde_json::json!({"steps": er.steps_executed, "warmed": true}),
27905                            source_trace_id: tid, cached_at: now, ttl_secs: payload.cache_ttl_secs,
27906                            epistemic: EpistemicEnvelope::derived(&format!("warm:{}", flow_name), 0.95, &format!("trace:{}", tid)),
27907                        };
27908                        s.execution_cache.retain(|c| c.cache_key != cache_key);
27909                        s.execution_cache.push(cached);
27910                        if s.execution_cache.len() > 200 { s.execution_cache.remove(0); }
27911                    }
27912                    tid
27913                };
27914                results.push(WarmResult { flow_name: flow_name.clone(), success: er.success, cached: false, trace_id: tid, latency_ms: er.latency_ms, error: None });
27915            }
27916            Err(e) => {
27917                results.push(WarmResult { flow_name: flow_name.clone(), success: false, cached: false, trace_id: 0, latency_ms: 0, error: Some(e) });
27918            }
27919        }
27920    }
27921
27922    let warmed = results.iter().filter(|r| r.success && !r.cached).count();
27923    let already = results.iter().filter(|r| r.cached).count();
27924    let failed = results.iter().filter(|r| !r.success).count();
27925
27926    Ok(Json(serde_json::json!({
27927        "total_flows": flows.len(),
27928        "warmed": warmed,
27929        "already_cached": already,
27930        "failed": failed,
27931        "cache_ttl_secs": payload.cache_ttl_secs,
27932        "total_latency_ms": req_start.elapsed().as_millis() as u64,
27933        "results": results,
27934    })))
27935}
27936
27937/// Per-step timing profile entry.
27938#[derive(Debug, Clone, Serialize)]
27939pub struct StepProfile {
27940    pub step_name: String,
27941    pub start_ms: u64,
27942    pub end_ms: u64,
27943    pub duration_ms: u64,
27944    pub pct_of_total: f64,
27945    pub events_count: usize,
27946}
27947
27948/// GET /v1/traces/:id/profile — per-step timing breakdown.
27949async fn traces_profile_handler(
27950    State(state): State<SharedState>,
27951    headers: HeaderMap,
27952    Path(id): Path<u64>,
27953) -> Result<Json<serde_json::Value>, StatusCode> {
27954    let s = state.lock().unwrap();
27955    check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27956
27957    let entry = match s.trace_store.get(id) {
27958        Some(e) => e,
27959        None => return Ok(Json(serde_json::json!({"error": format!("trace {} not found", id)}))),
27960    };
27961
27962    if entry.events.is_empty() {
27963        return Ok(Json(serde_json::json!({
27964            "trace_id": id, "flow_name": entry.flow_name, "total_latency_ms": entry.latency_ms,
27965            "steps": [], "message": "no events to profile",
27966        })));
27967    }
27968
27969    // Build per-step profiles from step_start/step_end pairs
27970    let mut profiles: Vec<StepProfile> = Vec::new();
27971    let mut step_stack: Vec<(String, u64, usize)> = Vec::new(); // (name, start_ms, event_count)
27972
27973    for ev in &entry.events {
27974        match ev.event_type.as_str() {
27975            "step_start" => {
27976                step_stack.push((ev.step_name.clone(), ev.offset_ms, 0));
27977            }
27978            "step_end" => {
27979                if let Some((name, start, events)) = step_stack.pop() {
27980                    let duration = ev.offset_ms.saturating_sub(start);
27981                    let pct = if entry.latency_ms > 0 { duration as f64 / entry.latency_ms as f64 * 100.0 } else { 0.0 };
27982                    profiles.push(StepProfile {
27983                        step_name: name, start_ms: start, end_ms: ev.offset_ms,
27984                        duration_ms: duration, pct_of_total: (pct * 100.0).round() / 100.0, events_count: events,
27985                    });
27986                }
27987            }
27988            _ => {
27989                if let Some(last) = step_stack.last_mut() {
27990                    last.2 += 1;
27991                }
27992            }
27993        }
27994    }
27995
27996    // Flush unclosed steps
27997    for (name, start, events) in step_stack {
27998        let duration = entry.latency_ms.saturating_sub(start);
27999        let pct = if entry.latency_ms > 0 { duration as f64 / entry.latency_ms as f64 * 100.0 } else { 0.0 };
28000        profiles.push(StepProfile {
28001            step_name: name, start_ms: start, end_ms: entry.latency_ms,
28002            duration_ms: duration, pct_of_total: (pct * 100.0).round() / 100.0, events_count: events,
28003        });
28004    }
28005
28006    let hotspot = profiles.iter().max_by_key(|p| p.duration_ms).map(|p| p.step_name.clone());
28007
28008    Ok(Json(serde_json::json!({
28009        "trace_id": id,
28010        "flow_name": entry.flow_name,
28011        "total_latency_ms": entry.latency_ms,
28012        "steps_profiled": profiles.len(),
28013        "hotspot": hotspot,
28014        "steps": profiles,
28015    })))
28016}
28017
28018// ── Router builder ────────────────────────────────────────────────────────
28019
28020/// Build the axum router with all v1 routes.
28021pub fn build_router(config: ServerConfig) -> Router {
28022    let (router, _state) = build_router_with_state(config);
28023    router
28024}
28025
28026/// Build router and return shared state handle (used by run_serve for shutdown hooks).
28027pub fn build_router_with_state(config: ServerConfig) -> (Router, SharedState) {
28028    let state = Arc::new(Mutex::new(ServerState::new(config)));
28029
28030    let router = Router::new()
28031        .route("/v1/health", get(health_handler))
28032        .route("/v1/health/live", get(health_live_handler))
28033        .route("/v1/health/ready", get(health_ready_handler))
28034        .route("/v1/health/components", get(health_components_handler))
28035        .route("/v1/health/gates", get(health_gates_get_handler).put(health_gates_put_handler))
28036        .route("/v1/health/history", get(health_history_handler))
28037        .route("/v1/health/check-and-record", post(health_check_record_handler))
28038        .route("/v1/alerts/rules", get(alerts_rules_list_handler).post(alerts_rules_add_handler).delete(alerts_rules_delete_handler))
28039        .route("/v1/alerts/evaluate", post(alerts_evaluate_handler))
28040        .route("/v1/alerts/history", get(alerts_history_handler))
28041        .route("/v1/alerts/silence", post(alerts_silence_create_handler).delete(alerts_silence_delete_handler))
28042        .route("/v1/alerts/silences", get(alerts_silences_list_handler))
28043        .route("/v1/version", get(version_handler))
28044        .route("/v1/uptime", get(uptime_handler))
28045        .route("/v1/dashboard", get(dashboard_handler))
28046        .route("/v1/primitives", get(primitives_handler))
28047        .route("/v1/docs", get(docs_handler))
28048        .route("/v1/metrics", get(metrics_handler))
28049        .route("/v1/metrics/prometheus", get(metrics_prometheus_handler))
28050        .route("/v1/metrics/export", post(metrics_export_handler))
28051        .route("/v1/deploy", post(deploy_handler))
28052        .route("/v1/deploy/reload", post(deploy_reload_handler))
28053        // §Fase 30.e — content-negotiation wrapper. Defers to legacy
28054        // `execute_handler` for JSON path; auto-promotes to SSE when
28055        // D4/D5 conditions hold. Legacy behavior unchanged for every
28056        // existing v1.20.0 client that doesn't send Accept: text/event-stream
28057        // and doesn't declare `transport: sse` on an axonendpoint.
28058        .route("/v1/execute", post(execute_handler_with_negotiation))
28059        .route("/v1/execute/enqueue", post(execute_enqueue_handler))
28060        .route("/v1/execute/queue", get(execute_queue_handler))
28061        .route("/v1/execute/dequeue", post(execute_dequeue_handler))
28062        .route("/v1/execute/drain", post(execute_drain_handler))
28063        .route("/v1/execute/sandbox", post(execute_sandbox_handler))
28064        .route("/v1/execute/process", post(execute_process_handler))
28065        .route("/v1/execute/dry-run", post(execute_dry_run_handler))
28066        .route("/v1/execute/pipeline", post(execute_pipeline_handler))
28067        .route("/v1/execute/stream", post(execute_stream_handler))
28068        // §Fase 30.d — single-shot SSE: response IS the stream. Distinct
28069        // from /v1/execute/stream above (two-stage pub/sub via EventBus
28070        // topic; preserved per D8). Same ExecuteRequest body shape.
28071        .route("/v1/execute/sse", post(execute_sse_handler))
28072        .route("/v1/execute/cache", get(execute_cache_get_handler).put(execute_cache_put_handler).delete(execute_cache_delete_handler))
28073        .route("/v1/execute/cached", post(execute_cached_handler))
28074        .route("/v1/execute/stream/{trace_id}/consume", get(stream_consume_handler))
28075        .route("/v1/execute/batch", post(execute_batch_handler))
28076        .route("/v1/execute/batch-cached", post(execute_batch_cached_handler))
28077        .route("/v1/execute/cache-replay", post(execute_cache_replay_handler))
28078        .route("/v1/execute/pinned", post(execute_pinned_handler))
28079        .route("/v1/execute/ab-test", post(execute_ab_test_handler))
28080        .route("/v1/execute/warm", post(execute_warm_handler))
28081        .route("/v1/estimate", post(estimate_handler))
28082        .route("/v1/costs", get(costs_handler))
28083        .route("/v1/costs/pricing", put(costs_pricing_handler))
28084        .route("/v1/costs/{flow}", get(costs_flow_handler))
28085        .route("/v1/costs/{flow}/budget", put(costs_budget_set_handler).delete(costs_budget_delete_handler))
28086        .route("/v1/costs/alerts", get(costs_alerts_handler))
28087        .route("/v1/costs/forecast", get(costs_forecast_handler))
28088        .route("/v1/rate-limit", get(rate_limit_status_handler))
28089        .route("/v1/rate-limit/endpoints", get(endpoint_rate_limits_list_handler).put(endpoint_rate_limits_put_handler).delete(endpoint_rate_limits_delete_handler))
28090        .route("/v1/keys", get(keys_list_handler))
28091        .route("/v1/keys", post(keys_create_handler))
28092        .route("/v1/keys/revoke", post(keys_revoke_handler))
28093        .route("/v1/keys/rotate", post(keys_rotate_handler))
28094        .route("/v1/webhooks", get(webhooks_list_handler))
28095        .route("/v1/webhooks", post(webhooks_register_handler))
28096        .route("/v1/webhooks/deliveries", get(webhooks_deliveries_handler))
28097        .route("/v1/webhooks/stats", get(webhooks_stats_handler))
28098        .route("/v1/webhooks/retry-queue", get(webhooks_retry_queue_handler))
28099        .route("/v1/webhooks/dead-letters", get(webhooks_dead_letters_handler))
28100        .route("/v1/webhooks/{id}/template", get(webhook_template_get_handler).put(webhook_template_set_handler))
28101        .route("/v1/webhooks/{id}/render", post(webhook_render_handler))
28102        .route("/v1/webhooks/{id}/simulate", post(webhook_simulate_handler))
28103        .route("/v1/webhooks/{id}/filters", get(webhook_filters_get_handler).put(webhook_filters_put_handler))
28104        .route("/v1/webhooks/delivery-config", get(delivery_config_handler))
28105        .route("/v1/webhooks/delivery-config", put(delivery_config_put_handler))
28106        .route("/v1/webhooks/{id}", delete(webhooks_delete_handler))
28107        .route("/v1/webhooks/{id}/toggle", post(webhooks_toggle_handler))
28108        .route("/v1/config", get(config_get_handler))
28109        .route("/v1/config", put(config_put_handler))
28110        .route("/v1/config/save", post(config_save_handler))
28111        .route("/v1/config/load", post(config_load_handler))
28112        .route("/v1/config/saved", delete(config_delete_handler))
28113        .route("/v1/config/snapshots", get(config_snapshots_list_handler))
28114        .route("/v1/config/snapshots", post(config_snapshots_save_handler))
28115        .route("/v1/config/snapshots/restore", post(config_snapshots_restore_handler))
28116        .route("/v1/audit", get(audit_handler))
28117        .route("/v1/audit/stats", get(audit_stats_handler))
28118        .route("/v1/audit/export", get(audit_export_handler))
28119        // §Fase 32.h — Replay-token retrieval. Auditors fetch the
28120        // recorded (request, response, metadata) tuple by trace_id
28121        // for regulatory replay (PCI DSS Req 10, FedRAMP AU-2,
28122        // FRE 502, 21 CFR Part 11).
28123        .route("/v1/replay/{trace_id}", get(replay_get_handler))
28124        .route("/v1/logs", get(logs_handler))
28125        .route("/v1/logs/stats", get(logs_stats_handler))
28126        .route("/v1/logs/export", get(logs_export_handler))
28127        .route("/v1/daemons", get(list_daemons_handler))
28128        .route("/v1/daemons/{name}", get(get_daemon_handler))
28129        .route("/v1/daemons/{name}", delete(delete_daemon_handler))
28130        .route("/v1/daemons/{name}/run", post(daemon_run_handler))
28131        .route("/v1/daemons/{name}/pause", post(daemon_pause_handler))
28132        .route("/v1/daemons/{name}/resume", post(daemon_resume_handler))
28133        .route("/v1/daemons/{name}/trigger", get(daemon_trigger_get_handler))
28134        .route("/v1/daemons/{name}/trigger", put(daemon_trigger_set_handler))
28135        .route("/v1/daemons/{name}/trigger", delete(daemon_trigger_clear_handler))
28136        .route("/v1/triggers", get(triggers_list_handler))
28137        .route("/v1/triggers/dispatch", post(triggers_dispatch_handler))
28138        .route("/v1/triggers/replay", post(triggers_replay_handler))
28139        .route("/v1/events/history", get(events_history_handler))
28140        .route("/v1/events/stream", get(events_stream_handler))
28141        .route("/v1/daemons/{name}/chain", get(daemon_chain_get_handler))
28142        .route("/v1/daemons/{name}/chain", put(daemon_chain_set_handler))
28143        .route("/v1/daemons/{name}/chain", delete(daemon_chain_clear_handler))
28144        .route("/v1/daemons/{name}/events", get(daemon_events_handler))
28145        .route("/v1/daemons/dependencies", get(daemons_dependencies_handler))
28146        .route("/v1/daemons/autoscale", get(daemons_autoscale_get_handler).put(daemons_autoscale_put_handler))
28147        .route("/v1/chains", get(chains_list_handler))
28148        .route("/v1/chains/graph", get(chains_graph_handler))
28149        .route("/v1/events", post(publish_event_handler))
28150        .route("/v1/events/stats", get(event_stats_handler))
28151        .route("/v1/supervisor", get(supervisor_handler))
28152        .route("/v1/supervisor/{name}/start", post(supervisor_start_handler))
28153        .route("/v1/supervisor/{name}/stop", post(supervisor_stop_handler))
28154        .route("/v1/versions", get(versions_handler))
28155        .route("/v1/versions/{name}", get(version_history_handler))
28156        .route("/v1/versions/{name}/rollback", post(rollback_handler))
28157        .route("/v1/versions/{name}/rollback/check", post(rollback_check_handler))
28158        .route("/v1/versions/{name}/diff", get(version_diff_handler))
28159        .route("/v1/session", get(session_list_handler))
28160        .route("/v1/session/remember", post(session_remember_handler))
28161        .route("/v1/session/recall/{key}", get(session_recall_handler))
28162        .route("/v1/session/persist", post(session_persist_handler))
28163        .route("/v1/session/retrieve/{key}", get(session_retrieve_handler))
28164        .route("/v1/session/query", post(session_query_handler))
28165        .route("/v1/session/mutate", post(session_mutate_handler))
28166        .route("/v1/session/purge", post(session_purge_handler))
28167        .route("/v1/session/{scope}/export", get(session_scope_export_handler))
28168        .route("/v1/axonstore", get(axonstore_list_handler).post(axonstore_create_handler))
28169        .route("/v1/axonstore/{name}", get(axonstore_get_handler).delete(axonstore_delete_handler))
28170        .route("/v1/axonstore/{name}/persist", post(axonstore_persist_handler))
28171        .route("/v1/axonstore/{name}/retrieve/{key}", get(axonstore_retrieve_handler))
28172        .route("/v1/axonstore/{name}/mutate", post(axonstore_mutate_handler))
28173        .route("/v1/axonstore/{name}/purge", post(axonstore_purge_handler))
28174        .route("/v1/axonstore/{name}/transact", post(axonstore_transact_handler))
28175        .route("/v1/dataspace", get(dataspace_list_handler).post(dataspace_create_handler))
28176        .route("/v1/dataspace/{name}", delete(dataspace_delete_handler))
28177        .route("/v1/dataspace/{name}/ingest", post(dataspace_ingest_handler))
28178        .route("/v1/dataspace/{name}/focus", post(dataspace_focus_handler))
28179        .route("/v1/dataspace/{name}/associate", post(dataspace_associate_handler))
28180        .route("/v1/dataspace/{name}/aggregate", post(dataspace_aggregate_handler))
28181        .route("/v1/dataspace/{name}/explore", get(dataspace_explore_handler))
28182        .route("/v1/shields", get(shield_list_handler).post(shield_create_handler))
28183        .route("/v1/shields/{name}", get(shield_get_handler).delete(shield_delete_handler))
28184        .route("/v1/shields/{name}/evaluate", post(shield_evaluate_handler))
28185        .route("/v1/shields/{name}/rules", post(shield_add_rule_handler))
28186        .route("/v1/corpus", get(corpus_list_handler).post(corpus_create_handler))
28187        .route("/v1/corpus/{name}", delete(corpus_delete_handler))
28188        .route("/v1/corpus/{name}/ingest", post(corpus_ingest_handler))
28189        .route("/v1/corpus/{name}/search", post(corpus_search_handler))
28190        .route("/v1/corpus/{name}/cite", post(corpus_cite_handler))
28191        .route("/v1/compute/evaluate", post(compute_evaluate_handler))
28192        .route("/v1/compute/batch", post(compute_batch_handler))
28193        .route("/v1/compute/functions", get(compute_functions_handler))
28194        .route("/v1/mandates", get(mandate_list_handler).post(mandate_create_handler))
28195        .route("/v1/mandates/{name}", get(mandate_get_handler).delete(mandate_delete_handler))
28196        .route("/v1/mandates/{name}/evaluate", post(mandate_evaluate_handler))
28197        .route("/v1/mandates/{name}/rules", post(mandate_add_rule_handler))
28198        .route("/v1/refine", get(refine_list_handler).post(refine_start_handler))
28199        .route("/v1/refine/{id}", get(refine_status_handler))
28200        .route("/v1/refine/{id}/iterate", post(refine_iterate_handler))
28201        .route("/v1/trails", get(trail_list_handler).post(trail_start_handler))
28202        .route("/v1/trails/{id}", get(trail_get_handler))
28203        .route("/v1/trails/{id}/step", post(trail_step_handler))
28204        .route("/v1/trails/{id}/complete", post(trail_complete_handler))
28205        .route("/v1/probes", get(probe_list_handler).post(probe_create_handler))
28206        .route("/v1/probes/{id}", get(probe_get_handler))
28207        .route("/v1/probes/{id}/query", post(probe_query_handler))
28208        .route("/v1/probes/{id}/complete", post(probe_complete_handler))
28209        .route("/v1/weaves", get(weave_list_handler).post(weave_create_handler))
28210        .route("/v1/weaves/{id}", get(weave_get_handler))
28211        .route("/v1/weaves/{id}/strand", post(weave_strand_handler))
28212        .route("/v1/weaves/{id}/synthesize", post(weave_synthesize_handler))
28213        .route("/v1/corroborate", get(corroborate_list_handler).post(corroborate_create_handler))
28214        .route("/v1/corroborate/{id}", get(corroborate_get_handler))
28215        .route("/v1/corroborate/{id}/evidence", post(corroborate_evidence_handler))
28216        .route("/v1/corroborate/{id}/verify", post(corroborate_verify_handler))
28217        .route("/v1/drills", get(drill_list_handler).post(drill_create_handler))
28218        .route("/v1/drills/{id}", get(drill_get_handler))
28219        .route("/v1/drills/{id}/expand", post(drill_expand_handler))
28220        .route("/v1/drills/{id}/complete", post(drill_complete_handler))
28221        .route("/v1/forges", get(forge_list_handler).post(forge_create_handler))
28222        .route("/v1/forges/{id}", get(forge_get_handler))
28223        .route("/v1/forges/{id}/template", post(forge_template_handler))
28224        .route("/v1/forges/{id}/render", post(forge_render_handler))
28225        .route("/v1/deliberate", get(deliberate_list_handler).post(deliberate_create_handler))
28226        .route("/v1/deliberate/{id}", get(deliberate_get_handler))
28227        .route("/v1/deliberate/{id}/option", post(deliberate_option_handler))
28228        .route("/v1/deliberate/{id}/evaluate", post(deliberate_evaluate_handler))
28229        .route("/v1/deliberate/{id}/eliminate", post(deliberate_eliminate_handler))
28230        .route("/v1/deliberate/{id}/decide", post(deliberate_decide_handler))
28231        .route("/v1/consensus", get(consensus_list_handler).post(consensus_create_handler))
28232        .route("/v1/consensus/{id}", get(consensus_get_handler))
28233        .route("/v1/consensus/{id}/vote", post(consensus_vote_handler))
28234        .route("/v1/consensus/{id}/resolve", post(consensus_resolve_handler))
28235        .route("/v1/hibernate", get(hibernate_list_handler).post(hibernate_create_handler))
28236        .route("/v1/hibernate/{id}", get(hibernate_get_handler))
28237        .route("/v1/hibernate/{id}/checkpoint", post(hibernate_checkpoint_handler))
28238        .route("/v1/hibernate/{id}/suspend", post(hibernate_suspend_handler))
28239        .route("/v1/hibernate/{id}/resume", post(hibernate_resume_handler))
28240        .route("/v1/ots", get(ots_list_handler).post(ots_create_handler))
28241        .route("/v1/ots/{token}", get(ots_retrieve_handler))
28242        .route("/v1/psyche", get(psyche_list_handler).post(psyche_create_handler))
28243        .route("/v1/psyche/{id}", get(psyche_get_handler))
28244        .route("/v1/psyche/{id}/insight", post(psyche_insight_handler))
28245        .route("/v1/psyche/{id}/complete", post(psyche_complete_handler))
28246        .route("/v1/endpoints", get(endpoint_list_handler).post(endpoint_create_handler))
28247        .route("/v1/endpoints/{name}", get(endpoint_get_handler).delete(endpoint_delete_handler))
28248        .route("/v1/endpoints/{name}/call", post(endpoint_call_handler))
28249        .route("/v1/pix", get(pix_list_handler).post(pix_create_handler))
28250        .route("/v1/pix/{id}", get(pix_get_handler))
28251        .route("/v1/pix/{id}/image", post(pix_image_handler))
28252        .route("/v1/pix/{id}/annotate", post(pix_annotate_handler))
28253        .route("/v1/shutdown", post(shutdown_handler))
28254        .route("/v1/server/backup", post(server_backup_handler))
28255        .route("/v1/server/restore", post(server_restore_handler))
28256        .route("/v1/server/persist", post(server_persist_handler))
28257        .route("/v1/server/recover", post(server_recover_handler))
28258        .route("/v1/server/auto-persist", get(server_auto_persist_get_handler).put(server_auto_persist_put_handler))
28259        .route("/v1/inspect", get(inspect_list_handler))
28260        .route("/v1/inspect/{name}", get(inspect_flow_handler))
28261        .route("/v1/inspect/{name}/graph", get(inspect_graph_handler))
28262        .route("/v1/inspect/{name}/dependencies", get(inspect_dependencies_handler))
28263        .route("/v1/flows/{name}/rules", get(flow_rules_get_handler).put(flow_rules_put_handler).delete(flow_rules_delete_handler))
28264        .route("/v1/flows/{name}/validate", post(flow_validate_handler))
28265        .route("/v1/flows/{name}/quota", get(flow_quota_get_handler).put(flow_quota_put_handler).delete(flow_quota_delete_handler))
28266        .route("/v1/flows/{name}/quota/check", post(flow_quota_check_handler))
28267        .route("/v1/flows/{name}/dashboard", get(flow_dashboard_handler))
28268        .route("/v1/flows/{name}/sla", get(flow_sla_get_handler).put(flow_sla_put_handler).delete(flow_sla_delete_handler))
28269        .route("/v1/flows/{name}/sla/check", get(flow_sla_check_handler))
28270        .route("/v1/flows/{name}/canary", get(flow_canary_get_handler).put(flow_canary_put_handler).delete(flow_canary_delete_handler))
28271        .route("/v1/flows/{name}/canary/route", post(flow_canary_route_handler))
28272        .route("/v1/flows/compare", post(flows_compare_handler))
28273        .route("/v1/flows/{name}/tags", get(flow_tags_get_handler).put(flow_tags_put_handler).delete(flow_tags_delete_handler))
28274        .route("/v1/flows/by-tag", get(flows_by_tag_handler))
28275        .route("/v1/flows/group/{tag}/execute", post(flows_group_execute_handler))
28276        .route("/v1/flows/group/{tag}/dashboard", get(flows_group_dashboard_handler))
28277        .route("/v1/cors", get(cors_config_handler))
28278        .route("/v1/cors", put(cors_config_put_handler))
28279        .route("/v1/schedules", get(schedules_list_handler))
28280        .route("/v1/schedules", post(schedules_create_handler))
28281        .route("/v1/schedules/tick", post(schedules_tick_handler))
28282        .route("/v1/schedules/{name}", get(schedules_get_handler))
28283        .route("/v1/schedules/{name}", delete(schedules_delete_handler))
28284        .route("/v1/schedules/{name}/toggle", post(schedules_toggle_handler))
28285        .route("/v1/schedules/{name}/history", get(schedules_history_handler))
28286        .route("/v1/traces", get(traces_list_handler))
28287        .route("/v1/traces/stats", get(traces_stats_handler))
28288        .route("/v1/traces/diff", get(traces_diff_handler))
28289        .route("/v1/traces/search", get(traces_search_handler))
28290        .route("/v1/traces/aggregate", get(traces_aggregate_handler))
28291        .route("/v1/traces/retention", get(traces_retention_get_handler).put(traces_retention_put_handler))
28292        .route("/v1/traces/evict", post(traces_evict_handler))
28293        .route("/v1/traces/bulk", delete(traces_bulk_delete_handler))
28294        .route("/v1/traces/bulk/annotate", post(traces_bulk_annotate_handler))
28295        .route("/v1/traces/compare", post(traces_compare_handler))
28296        .route("/v1/traces/timeline", post(traces_timeline_handler))
28297        .route("/v1/traces/heatmap", get(traces_heatmap_handler))
28298        .route("/v1/traces/export", get(traces_export_handler))
28299        .route("/v1/traces/export/custom", get(traces_export_custom_handler))
28300        .route("/v1/traces/{id}", get(traces_get_handler))
28301        .route("/v1/traces/{id}/annotate", post(traces_annotate_handler))
28302        .route("/v1/traces/{id}/annotations", get(traces_annotations_handler))
28303        .route("/v1/traces/{id}/replay", post(traces_replay_handler))
28304        .route("/v1/traces/{id}/flamegraph", get(traces_flamegraph_handler))
28305        .route("/v1/traces/{id}/profile", get(traces_profile_handler))
28306        .route("/v1/traces/{id}/correlate", post(traces_correlate_handler))
28307        .route("/v1/traces/{id}/annotate-from-template", post(traces_annotate_from_template_handler))
28308        .route("/v1/traces/correlated", get(traces_correlated_handler))
28309        .route("/v1/traces/annotation-templates", get(annotation_templates_list_handler).put(annotation_templates_put_handler))
28310        .route("/v1/middleware", get(middleware_config_handler))
28311        .route("/v1/middleware", put(middleware_config_put_handler))
28312        .route("/v1/backends", get(backends_list_handler))
28313        .route("/v1/backends/{name}", put(backends_put_handler).delete(backends_delete_handler))
28314        .route("/v1/backends/{name}/check", post(backends_check_handler))
28315        .route("/v1/backends/{name}/metrics", get(backends_metrics_handler))
28316        .route("/v1/backends/{name}/fallback", get(backends_fallback_get_handler).put(backends_fallback_put_handler))
28317        .route("/v1/backends/{name}/limits", get(backends_limits_get_handler).put(backends_limits_put_handler))
28318        .route("/v1/backends/ranking", get(backends_ranking_handler))
28319        .route("/v1/backends/select", post(backends_select_handler))
28320        .route("/v1/backends/dashboard", get(backends_dashboard_handler))
28321        .route("/v1/backends/health", get(backends_fleet_health_handler))
28322        .route("/v1/backends/{name}/health", get(backends_health_handler))
28323        .route("/v1/backends/{name}/probe", get(backends_probe_get_handler).put(backends_probe_put_handler))
28324        .route("/v1/mcp", post(mcp_handler))
28325        .route("/v1/mcp/tools", get(mcp_tools_list_handler))
28326        .route("/v1/mcp/stream", post(mcp_stream_handler))
28327        .layer(axum::middleware::from_fn_with_state(
28328            state.clone(),
28329            crate::request_middleware::request_middleware_fn,
28330        ))
28331        // §Fase 32.b — Fallback handler for dynamic axonendpoint
28332        // routes. Fires when no static route above matched. Looks
28333        // up `(method, path)` in `ServerState.dynamic_routes` and
28334        // dispatches through the Fase 30/31 negotiation classifier
28335        // with the flow_name from the route. On miss returns 404
28336        // with the full registered-routes table for adopter triage.
28337        //
28338        // Strictly additive per D10 — /v1/execute is matched by the
28339        // static route ABOVE this fallback, so legacy clients see
28340        // zero behavior change. Dynamic routes are coexistent.
28341        .fallback(dynamic_endpoint_handler)
28342        .layer(axum::middleware::from_fn(
28343            crate::request_tracing::request_tracing_middleware,
28344        ))
28345        .layer(axum::middleware::from_fn(
28346            crate::tenant::tenant_extractor_middleware,
28347        ))
28348        .with_state(state.clone());
28349
28350    // Apply CORS layer
28351    let cors_layer = {
28352        let s = state.lock().unwrap();
28353        crate::cors::build_cors_layer(&s.cors_config)
28354    };
28355    let router = router.layer(cors_layer);
28356
28357    (router, state)
28358}
28359
28360// ── Server launcher ───────────────────────────────────────────────────────
28361
28362/// Start the AxonServer. This blocks until the server is shut down.
28363///
28364/// Returns exit code: 0 on clean shutdown, 2 on bind error.
28365pub fn run_serve(config: ServerConfig) -> i32 {
28366    // Initialize structured logging as the very first action
28367    let _log_guard = crate::logging::init(
28368        &config.log_level,
28369        &config.log_format,
28370        config.log_file.as_deref(),
28371    );
28372
28373    // §Fase 36.g (D7) — fail fast on an unknown server default backend.
28374    // An operator who fat-fingers the provider name learns at server
28375    // startup, not at the first production request.
28376    if let Err(msg) = validate_server_default_backend(&config.default_backend) {
28377        eprintln!("axon serve: {msg}");
28378        tracing::error!("invalid_server_default_backend");
28379        return 1;
28380    }
28381
28382    let bind_addr = config.bind_addr();
28383
28384    tracing::info!(
28385        version = AXON_VERSION,
28386        bind_addr = %bind_addr,
28387        channel = %config.channel,
28388        auth = if config.auth_enabled() { "enabled" } else { "disabled" },
28389        log_level = %config.log_level,
28390        log_format = %config.log_format,
28391        "axon_server_starting"
28392    );
28393
28394    let database_url = config.database_url.clone();
28395    let (router, shared_state) = build_router_with_state(config);
28396
28397    // Set up graceful shutdown coordinator
28398    let coordinator = {
28399        let s = shared_state.lock().unwrap();
28400        Arc::new(crate::graceful_shutdown::ShutdownCoordinator::new(s.started_at))
28401    };
28402    {
28403        let mut s = shared_state.lock().unwrap();
28404        s.shutdown = Some(coordinator.clone());
28405    }
28406
28407    // Build the tokio runtime and run the server
28408    let rt = match tokio::runtime::Runtime::new() {
28409        Ok(rt) => rt,
28410        Err(e) => {
28411            tracing::error!(error = %e, "failed_to_create_tokio_runtime");
28412            return 2;
28413        }
28414    };
28415
28416    rt.block_on(async {
28417        // Initialize PostgreSQL storage if DATABASE_URL is configured
28418        if let Some(ref db_url) = database_url {
28419            match crate::db_pool::create_pool(db_url).await {
28420                Ok(pool) => {
28421                    if let Err(e) = crate::migrations::run(&pool).await {
28422                        tracing::error!(error = %e, "db_migrations_failed_falling_back_to_memory");
28423                    } else {
28424                        let storage = Arc::new(crate::storage::StorageDispatcher::postgres(pool));
28425                        let mut s = shared_state.lock().unwrap();
28426                        s.storage = storage;
28427                        tracing::info!("db_storage_initialized");
28428                    }
28429                }
28430                Err(e) => {
28431                    tracing::error!(error = %e, "db_pool_failed_falling_back_to_memory");
28432                }
28433            }
28434        }
28435
28436        // Initialize AWS Secrets Manager client for per-tenant key resolution (M3)
28437        {
28438            let ts = Arc::new(crate::tenant_secrets::TenantSecretsClient::new().await);
28439            let mut s = shared_state.lock().unwrap();
28440            s.tenant_secrets = ts;
28441        }
28442
28443        let listener = match tokio::net::TcpListener::bind(&bind_addr).await {
28444            Ok(l) => l,
28445            Err(e) => {
28446                tracing::error!(bind_addr = %bind_addr, error = %e, "failed_to_bind");
28447                return 2;
28448            }
28449        };
28450
28451        tracing::info!(bind_addr = %bind_addr, "axon_server_listening");
28452
28453        // Spawn signal listener
28454        let signal_coord = coordinator.clone();
28455        tokio::spawn(crate::graceful_shutdown::listen_signals(signal_coord));
28456
28457        // Serve with graceful shutdown
28458        let coord_for_shutdown = coordinator.clone();
28459        let serve_result = axum::serve(listener, router)
28460            .with_graceful_shutdown(async move {
28461                coord_for_shutdown.wait().await;
28462            })
28463            .await;
28464
28465        if let Err(e) = serve_result {
28466            tracing::error!(error = %e, "axon_server_error");
28467            return 1;
28468        }
28469
28470        // Determine shutdown reason
28471        let reason = if coordinator.is_triggered() {
28472            crate::graceful_shutdown::ShutdownReason::Signal
28473        } else {
28474            crate::graceful_shutdown::ShutdownReason::Signal
28475        };
28476
28477        tracing::info!(reason = reason.as_str(), "axon_server_shutting_down");
28478
28479        // Run pre-shutdown hooks
28480        {
28481            let mut s = shared_state.lock().unwrap();
28482            crate::graceful_shutdown::run_pre_shutdown_hooks(&mut s, reason, false);
28483        }
28484
28485        0
28486    })
28487}
28488
28489// ── Tests ─────────────────────────────────────────────────────────────────
28490
28491#[cfg(test)]
28492mod tests {
28493    use super::*;
28494    use axum::body::Body;
28495    use axum::http::Request;
28496    use http_body_util::BodyExt;
28497    use tower::ServiceExt;
28498
28499    fn test_config() -> ServerConfig {
28500        ServerConfig {
28501            host: "127.0.0.1".to_string(),
28502            port: 0,
28503            channel: "memory".to_string(),
28504            auth_token: String::new(),
28505            log_level: "INFO".to_string(),
28506            log_format: "json".to_string(),
28507            log_file: None,
28508            database_url: None,
28509            config_path: None,
28510            strict_type_driven_transport: false,
28511            default_backend: None,
28512            schemas_dir: None,
28513        }
28514    }
28515
28516    fn test_config_with_auth() -> ServerConfig {
28517        ServerConfig {
28518            host: "127.0.0.1".to_string(),
28519            port: 0,
28520            channel: "memory".to_string(),
28521            auth_token: "test-secret".to_string(),
28522            log_level: "INFO".to_string(),
28523            log_format: "json".to_string(),
28524            log_file: None,
28525            database_url: None,
28526            config_path: None,
28527            strict_type_driven_transport: false,
28528            default_backend: None,
28529            schemas_dir: None,
28530        }
28531    }
28532
28533    async fn body_json(body: Body) -> serde_json::Value {
28534        let bytes = body.collect().await.unwrap().to_bytes();
28535        serde_json::from_slice(&bytes).unwrap()
28536    }
28537
28538    #[tokio::test]
28539    async fn health_endpoint() {
28540        let app = build_router(test_config());
28541        let req = Request::builder()
28542            .uri("/v1/health")
28543            .body(Body::empty())
28544            .unwrap();
28545
28546        let resp = app.oneshot(req).await.unwrap();
28547        assert_eq!(resp.status(), StatusCode::OK);
28548
28549        let json = body_json(resp.into_body()).await;
28550        assert_eq!(json["status"], "healthy");
28551        assert_eq!(json["axon_version"], AXON_VERSION);
28552        assert!(json["components"].is_array());
28553        assert!(json["uptime_secs"].is_number());
28554    }
28555
28556    #[tokio::test]
28557    async fn health_live_endpoint() {
28558        let app = build_router(test_config());
28559        let req = Request::builder()
28560            .uri("/v1/health/live")
28561            .body(Body::empty())
28562            .unwrap();
28563
28564        let resp = app.oneshot(req).await.unwrap();
28565        assert_eq!(resp.status(), StatusCode::OK);
28566
28567        let json = body_json(resp.into_body()).await;
28568        assert_eq!(json["status"], "alive");
28569    }
28570
28571    #[tokio::test]
28572    async fn health_ready_endpoint() {
28573        let app = build_router(test_config());
28574        let req = Request::builder()
28575            .uri("/v1/health/ready")
28576            .body(Body::empty())
28577            .unwrap();
28578
28579        let resp = app.oneshot(req).await.unwrap();
28580        assert_eq!(resp.status(), StatusCode::OK);
28581
28582        let json = body_json(resp.into_body()).await;
28583        assert_eq!(json["ready"], true);
28584        assert_eq!(json["status"], "healthy");
28585    }
28586
28587    #[tokio::test]
28588    async fn version_endpoint() {
28589        let app = build_router(test_config());
28590        let req = Request::builder()
28591            .uri("/v1/version")
28592            .body(Body::empty())
28593            .unwrap();
28594
28595        let resp = app.oneshot(req).await.unwrap();
28596        assert_eq!(resp.status(), StatusCode::OK);
28597
28598        let json = body_json(resp.into_body()).await;
28599        assert_eq!(json["runtime"], "native");
28600        assert_eq!(json["server"], "axon-serve");
28601    }
28602
28603    #[tokio::test]
28604    async fn metrics_endpoint() {
28605        let app = build_router(test_config());
28606        let req = Request::builder()
28607            .uri("/v1/metrics")
28608            .body(Body::empty())
28609            .unwrap();
28610
28611        let resp = app.oneshot(req).await.unwrap();
28612        assert_eq!(resp.status(), StatusCode::OK);
28613
28614        let json = body_json(resp.into_body()).await;
28615        assert_eq!(json["total_requests"], 0);
28616        assert_eq!(json["total_deployments"], 0);
28617    }
28618
28619    #[tokio::test]
28620    async fn deploy_valid_source() {
28621        let app = build_router(test_config());
28622        let source = r#"persona P { tone: "analytical" }
28623flow F() { step S { ask: "do" } }
28624run F() as P"#;
28625
28626        let req = Request::builder()
28627            .method("POST")
28628            .uri("/v1/deploy")
28629            .header("content-type", "application/json")
28630            .body(Body::from(
28631                serde_json::json!({ "source": source }).to_string(),
28632            ))
28633            .unwrap();
28634
28635        let resp = app.oneshot(req).await.unwrap();
28636        assert_eq!(resp.status(), StatusCode::OK);
28637
28638        let json = body_json(resp.into_body()).await;
28639        assert_eq!(json["success"], true);
28640        assert!(json["deployed"].as_array().unwrap().len() >= 1);
28641    }
28642
28643    #[tokio::test]
28644    async fn deploy_invalid_source() {
28645        let app = build_router(test_config());
28646        let req = Request::builder()
28647            .method("POST")
28648            .uri("/v1/deploy")
28649            .header("content-type", "application/json")
28650            .body(Body::from(
28651                serde_json::json!({ "source": "invalid {{{{" }).to_string(),
28652            ))
28653            .unwrap();
28654
28655        let resp = app.oneshot(req).await.unwrap();
28656        assert_eq!(resp.status(), StatusCode::OK);
28657
28658        let json = body_json(resp.into_body()).await;
28659        assert_eq!(json["success"], false);
28660        assert!(json["error"].as_str().is_some());
28661    }
28662
28663    #[tokio::test]
28664    async fn deploy_with_unreachable_store_warns_but_succeeds() {
28665        // §Fase 37.x.g (D8) — a declared postgresql store unreachable
28666        // at deploy is a NON-fatal warning: the deploy succeeds and the
28667        // warning is surfaced. "Deploy is honest, never brittle."
28668        let app = build_router(test_config());
28669        let source = r#"axonstore tenants {
28670  backend: postgresql
28671  connection: "not a valid dsn"
28672}
28673persona P { tone: "analytical" }
28674flow F() { step S { ask: "do" } }
28675run F() as P"#;
28676        let req = Request::builder()
28677            .method("POST")
28678            .uri("/v1/deploy")
28679            .header("content-type", "application/json")
28680            .body(Body::from(
28681                serde_json::json!({ "source": source }).to_string(),
28682            ))
28683            .unwrap();
28684        let resp = app.oneshot(req).await.unwrap();
28685        assert_eq!(resp.status(), StatusCode::OK);
28686        let json = body_json(resp.into_body()).await;
28687        assert_eq!(
28688            json["success"], true,
28689            "§37.x.g — an unreachable store does not fail the deploy"
28690        );
28691        let warns = json["store_warnings"].as_array().unwrap();
28692        assert_eq!(
28693            warns.len(),
28694            1,
28695            "§37.x.g — the unreachable store surfaces one warning"
28696        );
28697        assert_eq!(warns[0]["store"], "tenants");
28698        assert_eq!(warns[0]["d_letter"], "D8");
28699    }
28700
28701    #[tokio::test]
28702    async fn daemons_empty() {
28703        let app = build_router(test_config());
28704        let req = Request::builder()
28705            .uri("/v1/daemons")
28706            .body(Body::empty())
28707            .unwrap();
28708
28709        let resp = app.oneshot(req).await.unwrap();
28710        assert_eq!(resp.status(), StatusCode::OK);
28711
28712        let json = body_json(resp.into_body()).await;
28713        assert_eq!(json["total"], 0);
28714        assert!(json["daemons"].as_array().unwrap().is_empty());
28715    }
28716
28717    #[tokio::test]
28718    async fn daemon_not_found() {
28719        let app = build_router(test_config());
28720        let req = Request::builder()
28721            .uri("/v1/daemons/nonexistent")
28722            .body(Body::empty())
28723            .unwrap();
28724
28725        let resp = app.oneshot(req).await.unwrap();
28726        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
28727    }
28728
28729    #[tokio::test]
28730    async fn auth_required_without_token() {
28731        let app = build_router(test_config_with_auth());
28732        let req = Request::builder()
28733            .uri("/v1/metrics")
28734            .body(Body::empty())
28735            .unwrap();
28736
28737        let resp = app.oneshot(req).await.unwrap();
28738        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
28739    }
28740
28741    #[tokio::test]
28742    async fn auth_valid_token() {
28743        let app = build_router(test_config_with_auth());
28744        let req = Request::builder()
28745            .uri("/v1/metrics")
28746            .header("authorization", "Bearer test-secret")
28747            .body(Body::empty())
28748            .unwrap();
28749
28750        let resp = app.oneshot(req).await.unwrap();
28751        assert_eq!(resp.status(), StatusCode::OK);
28752    }
28753
28754    #[tokio::test]
28755    async fn auth_invalid_token() {
28756        let app = build_router(test_config_with_auth());
28757        let req = Request::builder()
28758            .uri("/v1/metrics")
28759            .header("authorization", "Bearer wrong-token")
28760            .body(Body::empty())
28761            .unwrap();
28762
28763        let resp = app.oneshot(req).await.unwrap();
28764        assert_eq!(resp.status(), StatusCode::FORBIDDEN);
28765    }
28766
28767    #[tokio::test]
28768    async fn health_no_auth_required() {
28769        // Health endpoint should work even with auth enabled
28770        let app = build_router(test_config_with_auth());
28771        let req = Request::builder()
28772            .uri("/v1/health")
28773            .body(Body::empty())
28774            .unwrap();
28775
28776        let resp = app.oneshot(req).await.unwrap();
28777        assert_eq!(resp.status(), StatusCode::OK);
28778    }
28779
28780    // ── Config tests ──────────────────────────────────────────────
28781
28782    #[test]
28783    fn config_bind_addr() {
28784        let cfg = test_config();
28785        assert_eq!(cfg.bind_addr(), "127.0.0.1:0");
28786    }
28787
28788    #[test]
28789    fn config_auth_enabled() {
28790        assert!(!test_config().auth_enabled());
28791        assert!(test_config_with_auth().auth_enabled());
28792    }
28793
28794    #[tokio::test]
28795    async fn event_publish_endpoint() {
28796        let app = build_router(test_config());
28797        let req = Request::builder()
28798            .method("POST")
28799            .uri("/v1/events")
28800            .header("content-type", "application/json")
28801            .body(Body::from(
28802                serde_json::json!({
28803                    "topic": "test.ping",
28804                    "payload": { "msg": "hello" },
28805                    "source": "unit_test"
28806                }).to_string(),
28807            ))
28808            .unwrap();
28809
28810        let resp = app.oneshot(req).await.unwrap();
28811        assert_eq!(resp.status(), StatusCode::OK);
28812
28813        let json = body_json(resp.into_body()).await;
28814        assert_eq!(json["published"], true);
28815        assert_eq!(json["topic"], "test.ping");
28816        assert_eq!(json["source"], "unit_test");
28817    }
28818
28819    #[tokio::test]
28820    async fn event_stats_endpoint() {
28821        let app = build_router(test_config());
28822
28823        // Publish an event first
28824        let req = Request::builder()
28825            .method("POST")
28826            .uri("/v1/events")
28827            .header("content-type", "application/json")
28828            .body(Body::from(
28829                serde_json::json!({ "topic": "test.x", "payload": null }).to_string(),
28830            ))
28831            .unwrap();
28832        app.clone().oneshot(req).await.unwrap();
28833
28834        // Check stats
28835        let req = Request::builder()
28836            .uri("/v1/events/stats")
28837            .body(Body::empty())
28838            .unwrap();
28839        let resp = app.oneshot(req).await.unwrap();
28840        assert_eq!(resp.status(), StatusCode::OK);
28841
28842        let json = body_json(resp.into_body()).await;
28843        assert!(json["events_published"].as_u64().unwrap() >= 1);
28844        assert!(json["topics_seen"].as_array().unwrap().len() >= 1);
28845    }
28846
28847    #[tokio::test]
28848    async fn supervisor_endpoint() {
28849        let app = build_router(test_config());
28850
28851        // Deploy a flow to register with supervisor
28852        let source = r#"persona P { tone: "analytical" }
28853flow Sup1() { step S { ask: "do" } }
28854run Sup1() as P"#;
28855        let req = Request::builder()
28856            .method("POST")
28857            .uri("/v1/deploy")
28858            .header("content-type", "application/json")
28859            .body(Body::from(
28860                serde_json::json!({ "source": source }).to_string(),
28861            ))
28862            .unwrap();
28863        app.clone().oneshot(req).await.unwrap();
28864
28865        // Check supervisor
28866        let req = Request::builder()
28867            .uri("/v1/supervisor")
28868            .body(Body::empty())
28869            .unwrap();
28870        let resp = app.clone().oneshot(req).await.unwrap();
28871        assert_eq!(resp.status(), StatusCode::OK);
28872
28873        let json = body_json(resp.into_body()).await;
28874        assert!(json["summary"].as_str().unwrap().contains("daemon"));
28875        assert!(json["daemons"].as_array().unwrap().len() >= 1);
28876    }
28877
28878    #[tokio::test]
28879    async fn supervisor_start_stop() {
28880        let app = build_router(test_config());
28881
28882        // Deploy
28883        let source = r#"persona P { tone: "analytical" }
28884flow CtlFlow() { step S { ask: "do" } }
28885run CtlFlow() as P"#;
28886        let req = Request::builder()
28887            .method("POST")
28888            .uri("/v1/deploy")
28889            .header("content-type", "application/json")
28890            .body(Body::from(
28891                serde_json::json!({ "source": source }).to_string(),
28892            ))
28893            .unwrap();
28894        app.clone().oneshot(req).await.unwrap();
28895
28896        // Start
28897        let req = Request::builder()
28898            .method("POST")
28899            .uri("/v1/supervisor/CtlFlow/start")
28900            .body(Body::empty())
28901            .unwrap();
28902        let resp = app.clone().oneshot(req).await.unwrap();
28903        assert_eq!(resp.status(), StatusCode::OK);
28904        let json = body_json(resp.into_body()).await;
28905        assert_eq!(json["started"], "CtlFlow");
28906
28907        // Stop
28908        let req = Request::builder()
28909            .method("POST")
28910            .uri("/v1/supervisor/CtlFlow/stop")
28911            .body(Body::empty())
28912            .unwrap();
28913        let resp = app.clone().oneshot(req).await.unwrap();
28914        assert_eq!(resp.status(), StatusCode::OK);
28915        let json = body_json(resp.into_body()).await;
28916        assert_eq!(json["stopped"], "CtlFlow");
28917
28918        // Start nonexistent → 404
28919        let req = Request::builder()
28920            .method("POST")
28921            .uri("/v1/supervisor/NoSuchDaemon/start")
28922            .body(Body::empty())
28923            .unwrap();
28924        let resp = app.oneshot(req).await.unwrap();
28925        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
28926    }
28927
28928    #[tokio::test]
28929    async fn metrics_include_bus_stats() {
28930        let app = build_router(test_config());
28931        let req = Request::builder()
28932            .uri("/v1/metrics")
28933            .body(Body::empty())
28934            .unwrap();
28935
28936        let resp = app.oneshot(req).await.unwrap();
28937        let json = body_json(resp.into_body()).await;
28938
28939        // Bus stats should be present
28940        assert!(json.get("bus_events_published").is_some());
28941        assert!(json.get("bus_topics_seen").is_some());
28942        assert!(json.get("supervisor_summary").is_some());
28943    }
28944
28945    #[test]
28946    fn daemon_state_serializes() {
28947        let json = serde_json::to_string(&DaemonState::Running).unwrap();
28948        assert_eq!(json, "\"running\"");
28949
28950        let json = serde_json::to_string(&DaemonState::Hibernating).unwrap();
28951        assert_eq!(json, "\"hibernating\"");
28952    }
28953
28954    #[tokio::test]
28955    async fn estimate_endpoint() {
28956        let app = build_router(test_config());
28957        let body = serde_json::json!({
28958            "source": "persona A { tone: \"neutral\" }\ncontext C { depth: shallow }\nflow F() { step S { ask: \"do\" } }\nrun F() as A within C",
28959        });
28960        let req = Request::builder()
28961            .method("POST")
28962            .uri("/v1/estimate")
28963            .header("content-type", "application/json")
28964            .body(Body::from(serde_json::to_string(&body).unwrap()))
28965            .unwrap();
28966
28967        let resp = app.oneshot(req).await.unwrap();
28968        assert_eq!(resp.status(), StatusCode::OK);
28969
28970        let json = body_json(resp.into_body()).await;
28971        assert!(json["total_tokens"].as_u64().unwrap() > 0);
28972        assert!(json["estimated_cost_usd"].as_f64().unwrap() > 0.0);
28973        assert!(json["flows"].as_array().unwrap().len() == 1);
28974        assert_eq!(json["pricing"]["name"], "claude-sonnet-4");
28975    }
28976
28977    #[tokio::test]
28978    async fn estimate_endpoint_with_model() {
28979        let app = build_router(test_config());
28980        let body = serde_json::json!({
28981            "source": "persona A { tone: \"neutral\" }\ncontext C { depth: shallow }\nflow F() { step S { ask: \"do\" } }\nrun F() as A within C",
28982            "model": "opus",
28983        });
28984        let req = Request::builder()
28985            .method("POST")
28986            .uri("/v1/estimate")
28987            .header("content-type", "application/json")
28988            .body(Body::from(serde_json::to_string(&body).unwrap()))
28989            .unwrap();
28990
28991        let resp = app.oneshot(req).await.unwrap();
28992        assert_eq!(resp.status(), StatusCode::OK);
28993
28994        let json = body_json(resp.into_body()).await;
28995        assert_eq!(json["pricing"]["name"], "claude-opus-4");
28996        assert!(json["estimated_cost_usd"].as_f64().unwrap() > 0.0);
28997    }
28998
28999    #[tokio::test]
29000    async fn estimate_endpoint_invalid_source() {
29001        let app = build_router(test_config());
29002        let body = serde_json::json!({
29003            "source": "this is not valid axon {{{",
29004        });
29005        let req = Request::builder()
29006            .method("POST")
29007            .uri("/v1/estimate")
29008            .header("content-type", "application/json")
29009            .body(Body::from(serde_json::to_string(&body).unwrap()))
29010            .unwrap();
29011
29012        let resp = app.oneshot(req).await.unwrap();
29013        assert_eq!(resp.status(), StatusCode::OK);
29014
29015        let json = body_json(resp.into_body()).await;
29016        assert_eq!(json["success"], false);
29017    }
29018
29019    #[tokio::test]
29020    async fn rate_limit_status_endpoint() {
29021        let app = build_router(test_config());
29022        let req = Request::builder()
29023            .uri("/v1/rate-limit")
29024            .body(Body::empty())
29025            .unwrap();
29026
29027        let resp = app.oneshot(req).await.unwrap();
29028        assert_eq!(resp.status(), StatusCode::OK);
29029
29030        let json = body_json(resp.into_body()).await;
29031        assert_eq!(json["enabled"], true);
29032        assert!(json["remaining"].as_u64().unwrap() > 0);
29033        assert_eq!(json["limit"], 100);
29034    }
29035
29036    #[tokio::test]
29037    async fn logs_stats_endpoint() {
29038        let app = build_router(test_config());
29039        let req = Request::builder()
29040            .uri("/v1/logs/stats")
29041            .body(Body::empty())
29042            .unwrap();
29043
29044        let resp = app.oneshot(req).await.unwrap();
29045        assert_eq!(resp.status(), StatusCode::OK);
29046
29047        let json = body_json(resp.into_body()).await;
29048        assert!(json["total_requests"].is_u64());
29049        assert!(json["buffered_entries"].is_u64());
29050        assert!(json["avg_latency_us"].is_u64());
29051    }
29052
29053    #[tokio::test]
29054    async fn logs_endpoint() {
29055        let app = build_router(test_config());
29056        let req = Request::builder()
29057            .uri("/v1/logs?limit=10")
29058            .body(Body::empty())
29059            .unwrap();
29060
29061        let resp = app.oneshot(req).await.unwrap();
29062        assert_eq!(resp.status(), StatusCode::OK);
29063
29064        let json = body_json(resp.into_body()).await;
29065        assert!(json["count"].is_u64());
29066        assert!(json["entries"].is_array());
29067    }
29068
29069    #[tokio::test]
29070    async fn keys_list_endpoint() {
29071        let app = build_router(test_config());
29072        let req = Request::builder()
29073            .uri("/v1/keys")
29074            .body(Body::empty())
29075            .unwrap();
29076
29077        let resp = app.oneshot(req).await.unwrap();
29078        assert_eq!(resp.status(), StatusCode::OK);
29079
29080        let json = body_json(resp.into_body()).await;
29081        // test_config has no auth token, so api_keys is disabled
29082        assert_eq!(json["enabled"], false);
29083        assert!(json["keys"].is_array());
29084    }
29085
29086    #[tokio::test]
29087    async fn config_get_endpoint() {
29088        let app = build_router(test_config());
29089        let req = Request::builder()
29090            .uri("/v1/config")
29091            .body(Body::empty())
29092            .unwrap();
29093
29094        let resp = app.oneshot(req).await.unwrap();
29095        assert_eq!(resp.status(), StatusCode::OK);
29096
29097        let json = body_json(resp.into_body()).await;
29098        assert_eq!(json["rate_limit"]["max_requests"], 100);
29099        assert_eq!(json["rate_limit"]["window_secs"], 60);
29100        assert!(json["rate_limit"]["enabled"].as_bool().unwrap());
29101        assert_eq!(json["request_log"]["capacity"], 1000);
29102        assert!(json["request_log"]["enabled"].as_bool().unwrap());
29103        assert!(!json["auth"]["enabled"].as_bool().unwrap());
29104    }
29105
29106    #[tokio::test]
29107    async fn config_put_endpoint() {
29108        let app = build_router(test_config());
29109        let body = serde_json::json!({
29110            "rate_limit": { "max_requests": 200 },
29111            "request_log": { "capacity": 500 }
29112        });
29113        let req = Request::builder()
29114            .method("PUT")
29115            .uri("/v1/config")
29116            .header("content-type", "application/json")
29117            .body(Body::from(serde_json::to_string(&body).unwrap()))
29118            .unwrap();
29119
29120        let resp = app.oneshot(req).await.unwrap();
29121        assert_eq!(resp.status(), StatusCode::OK);
29122
29123        let json = body_json(resp.into_body()).await;
29124        assert_eq!(json["applied"], true);
29125        assert!(json["changes"].as_array().unwrap().len() >= 2);
29126        assert_eq!(json["snapshot"]["rate_limit"]["max_requests"], 200);
29127        assert_eq!(json["snapshot"]["request_log"]["capacity"], 500);
29128    }
29129}