let d = import "adr-defaults.ncl" in d.make_adr { id = "adr-007", title = "API Surface Discoverability via #[onto_api] Proc-Macro", status = 'Accepted, date = "2026-03-23", context = "ontoref-daemon exposes ~28 HTTP routes across api.rs, sync.rs, and other handler modules. Before this decision, the authoritative route list existed only in the axum Router definition — undiscoverable without reading source. MCP agents, CLI users, and the web UI had no machine-readable way to enumerate routes, their auth requirements, parameter shapes, or actor restrictions. OpenAPI was considered but rejected as a runtime dependency that would require schema maintenance separate from the handler code. The `#[onto_api]` proc-macro in `ontoref-derive` addresses this by making the handler annotation the single source of truth: the macro emits `inventory::submit!(ApiRouteEntry{...})` at link time, and `api_catalog::catalog()` collects them via `inventory::collect!`. No runtime registry, no startup allocation, no separate schema file.", decision = "Every HTTP handler in ontoref-daemon must carry `#[onto_api(method, path, description, auth, actors, params, tags)]`. The proc-macro (in `crates/ontoref-derive`) emits `inventory::submit!(ApiRouteEntry{...})` at link time. `GET /api/catalog` calls `api_catalog::catalog()` — a pure function over `inventory::iter::()` — and returns the annotated surface as JSON. The web UI at `/ui/{slug}/api` renders it with client-side filtering. `describe api [--actor] [--tag] [--auth] [--fmt]` queries this endpoint from the CLI. The MCP tool `ontoref_api_catalog` calls `catalog()` directly without HTTP. This surfaces the complete API to three actors (browser, CLI, MCP agent) from one annotation site per handler.", rationale = [ { claim = "Compile-time registration eliminates drift", detail = "inventory uses linker sections (.init_array on ELF, __mod_init_func on Mach-O) to collect ApiRouteEntry items at link time. A handler that exists in the binary but lacks #[onto_api] is detectable — cargo test or a Grep constraint catches the gap. A handler that has #[onto_api] but is removed will automatically disappear from catalog(). The annotation and the implementation are co-located and co-deleted.", }, { claim = "Zero runtime overhead and zero startup allocation", detail = "inventory::iter::() walks a linked-list built by the linker — no HashMap, no Arc, no lazy_static. catalog() is a pure function that sorts and returns &'static references. This satisfies the ontoref axiom 'Protocol, Not Runtime': the catalog is available without daemon state, without DB, without cache warmup.", }, { claim = "Three-surface consistency without duplication", detail = "Browser (api_catalog.html), CLI (describe api), and MCP (ontoref_api_catalog) all read the same inventory. A manual registry or OpenAPI spec would require three update sites per route change. With #[onto_api], changing a route's auth requirement is a one-line annotation edit that propagates to all surfaces on next build.", }, ], consequences = { positive = [ "API surface is always current: catalog() reflects exactly the handlers compiled into the binary", "Agents (MCP) can call ontoref_api_catalog on cold start to understand the full HTTP surface without prior knowledge", "describe api --actor agent filters to actor-appropriate routes; agents can self-serve their available endpoints", "New handlers without #[onto_api] are caught by the Grep constraint before merge", "inventory (MIT, 0.3.x) has no transitive deps — passes deny.toml audit", ], negative = [ "#[onto_api] parameters are stringly-typed — a misspelled auth value is not caught at compile time (only at review/Grep)", "inventory linker trick is platform-specific: supported on Linux (ELF), macOS (Mach-O), Windows (PE) but not on targets that lack .init_array equivalent", "Proc-macro adds a new crate (ontoref-derive) to the workspace; ontoref-ontology users who only need zero-dep struct loading do not need it", ], }, alternatives_considered = [ { option = "OpenAPI / utoipa with generated JSON schema", why_rejected = "Requires maintaining a separate schema artifact (openapi.json) and a runtime schema struct tree. The schema can drift from actual handler signatures. utoipa adds ~15 transitive deps including serde_yaml. Violates 'Protocol, Not Runtime' — the schema becomes a runtime artifact rather than a compile-time invariant.", }, { option = "Manual route registry (Vec in main.rs)", why_rejected = "A manually maintained Vec has guaranteed drift: handlers are added, routes change, and the Vec is updated inconsistently. Proven failure mode in the previous session where insert_mcp_ctx listed 15 tools while the router had 27.", }, { option = "Runtime reflection via axum Router introspection", why_rejected = "axum does not expose a stable introspection API for registered routes. Workarounds (tower_http trace layer capture, method_router hacks) are brittle across axum versions and cannot surface handler metadata (auth, actors, params).", }, ], constraints = [ { id = "onto-api-on-all-handlers", claim = "Every public HTTP handler in ontoref-daemon must carry #[onto_api(...)]", scope = "ontoref-daemon (crates/ontoref-daemon/src/api.rs, crates/ontoref-daemon/src/sync.rs)", severity = 'Hard, check = 'Grep { pattern = "#\\[onto_api", paths = ["crates/ontoref-daemon/src/api.rs", "crates/ontoref-daemon/src/sync.rs"], must_be_empty = false, }, rationale = "catalog() is only as complete as the set of annotated handlers. Unannotated handlers are invisible to agents, CLI, and the web UI — equivalent to undocumented and unauditable routes.", }, { id = "inventory-feature-gate", claim = "inventory must remain a workspace dependency gated behind the 'catalog' feature of ontoref-derive; ontoref-ontology must not depend on inventory", scope = "ontoref-ontology (Cargo.toml), ontoref-derive (Cargo.toml)", severity = 'Hard, check = 'Grep { pattern = "inventory", paths = ["crates/ontoref-ontology/Cargo.toml"], must_be_empty = true, }, rationale = "ontoref-ontology is the zero-dep adoption surface (ADR-001). Adding inventory — even as an optional dep — violates that contract and makes protocol adoption heavier for downstream crates that only need typed NCL loading.", }, ], related_adrs = ["adr-001"], ontology_check = { decision_string = "Use #[onto_api] proc-macro + inventory linker registration as the single source of truth for the HTTP API surface; surface via GET /api/catalog, describe api CLI subcommand, and ontoref_api_catalog MCP tool", invariants_at_risk = ["protocol-not-runtime"], verdict = 'Safe, }, }