| name | add-route |
| description | Add (or rename, or change the shape of) an HTTP route in the nowledge axum service following the project's documented multi-file workflow. Use when the user says "add a route", "add an endpoint", "expose a new API", "wire up POST/GET/PUT/PATCH/DELETE /v1/...", or when changing an existing route's path/method/schema. |
add-route
A new route in nowledge touches five files in a fixed order. Follow this checklist exactly — missing a step is the most common drift between the running service and the API docs.
Inputs to gather first
Before writing anything:
- HTTP method (
GET, POST, PUT, PATCH, DELETE).
- Path (e.g.
/v1/state/profile/facts/{fact_key}).
- Rust handler name (snake_case, matches
src/routes.rs::build_router registration).
- Group label for the manifest — pick from the existing
group values in doc/api_manifest.json (e.g. Health, Harness, Eval, Company Docs); introduce a new group only for a genuinely new API surface.
- Authentication mode:
None | UserGuard | UserGuard; owner default may apply | AdminGuard.
- Request and response schema names from
src/models.rs (or new types you'll add there).
Step 1 — code
Edit in this order:
src/models.rs — add or update the request/response structs and any DTOs. Derive Debug, Clone, Serialize, Deserialize and skip empty Option<...> with #[serde(skip_serializing_if = "Option::is_none")]. If the route is owner-scoped, the request should carry an Option<String> owner_user_id.
src/store.rs — add the domain logic. Keep mutations behind StoreData and the RwLock. Use util::new_id("prefix") for new IDs and EventIndexResolver::idempotency_hash for any idempotency key.
src/repository.rs — only if the route needs to persist through the Meili backend. Add the trait method on KnowledgeRepository, implement on both MeiliKnowledgeRepository and MemoryKnowledgeRepository. Memory impl is usually a no-op.
src/routes.rs — register the handler in build_router and write the handler function. Use State<AppState>, the appropriate guard, and return Result<Json<Response>, ApiError>. For owner-scoped routes, call guard.apply_owner_default(&mut req.owner_user_id)? early.
Step 2 — tests
Add a regression test in tests/api_spec.rs (or a new dedicated test file if the surface is large). Use the existing helpers:
app() for unauthenticated runs.
authed_app() for owner-isolation tests (users u1, u2, admin).
call(app, method, uri, body) and call_with_token(app, method, uri, body, Some(token)) for dispatch.
At minimum cover the happy path. For owner-scoped routes, always add an owner-mismatch test (request from u1 for u2's data → 403). The owner-isolation invariant is load-bearing.
Step 3 — docs (do not skip)
Three coordinated updates:
-
Create doc/api/{method_lowercase}_{path_with_underscores}.md using the template from doc/api/AGENTS.md. Path braces are dropped; segments joined with _. Examples:
GET /v1/fs/read → get_v1_fs_read.md
POST /v1/admin/history/user-event-indexes:reconcile → post_v1_admin_history_user_event_indexes_reconcile.md
Section order: Title (# METHOD /path), ## Summary, ## Handler, ## Path Parameters, ## Query Parameters, ## JSON Body Parameters (with Schema: line referencing the type in src/models.rs), ## Response, ## Errors and Access Rules, ## Internal Logic Call Graph (a Mermaid flowchart TD).
-
Add a row to the index table in doc/README.md, alphabetized within its method group:
| `METHOD` | `/v1/path` | `handler_name` | [api/<file>.md](api/<file>.md) |
-
Append an entry to doc/api_manifest.json:
{
"method": "METHOD",
"path": "/v1/path",
"handler": "handler_name",
"group": "GroupLabel",
"file": "api/<file>.md"
}
Step 4 — verify
Run the /verify gauntlet. If you bumped a public response shape, also rerun the relevant integration block by hand to confirm the docs match runtime behavior.
Cross-check
After everything is in place:
grep -nE '\.route\("' src/routes.rs | wc -l
ls doc/api/ | wc -l
jq 'length' doc/api_manifest.json
The doc count and manifest count must match the number of registered routes. When the count changes, update the "Total documented APIs" line in doc/README.md to match.