10 — Directus/API Exposure Readiness (views → read-only API, existing route inventory)
title: 10 — Directus/API Exposure Readiness (Branch H) date: 2026-05-31 gate: P2 / macro M8 (after M2 commit). Design-only; no Directus mutation this session.
10 — Directus/API Exposure Readiness (Branch H)
Principle (Đ28): API/Directus is a shaper, not a calculator — exposes committed views/fns verbatim.
Existing registry API routes (verified in nuxt-repo/web/server/api/registry/)
health.get.ts (gap-math — RETIRE) · counts.get.ts (disagreeing total — REPLACE) · raw-counts.get.ts
(reduce — RETIRE) · matrix.get.ts · species-matrix.get.ts · species-summary.get.ts ·
composition.get.ts · unmanaged.get.ts · system-issues*.get.ts · refresh-counts.post.ts ·
pivot-query.get.ts (already pivot-based — the GOOD pattern to follow). Plus server/api/registries/system-issues/*.
Exposure map (after M2 commit) — field → PG source, zero math
| endpoint | source | shape |
|---|---|---|
GET /api/registries-pivot/lists |
v_living_lists |
rows {code,name,list_count,count_source,pivot_code,pivot_backed} |
GET /api/registries-pivot/integrity |
v_count_integrity (aggregate) |
{leaf_rows,net_gap,drift_rows,unverified,pivot_backed,pivot_missing,status} |
GET …/integrity?detail=drift |
v_count_drift |
rows {code,gap,drift_side,drift_classification} |
GET /api/registries-pivot/tree |
v_registries_pivot_tree |
rows {node_code,parent_code,is_root,has_children} |
GET /api/registries-pivot/node/:code |
fn_registries_pivot_node_substrate(:code) |
1 row substrate + pivot_count |
GET /api/registries-pivot/count/:pivot |
pivot_count(:pivot) |
{code,name,source,value} or PIVOT_MISSING |
Each handler = single SELECT * FROM <view/fn> → JSON. Model on pivot-query.get.ts, not health.get.ts. |
Permissions / read-only role
Reuse a read-only DB role (like context_pack_readonly); no write grant. Directus: read-only on the four
view-collections; GRANT EXECUTE on fn_registries_pivot_node_substrate(text) + pivot_count(text) to the read role only.
Failure states (explicit)
missing pivot → pivot_count:null, pivot_backed:false, badge:PIVOT_MISSING; unmeasured → status:"unverified";
unknown node → fn 0 rows → 404 {error:"unknown_node"}; stale → include refreshed_at, never recompute client-side.
Security
read-only role + read-only Directus perms (no write endpoints) · counts/aggregates only (no 985k row dumps) ·
:code validated against ^(CAT|PIV|MTX)- format (not a hardcoded list) · pagination on list endpoints ·
views are cheap (read meta_catalog/pivot_results, not live 985k scans).
Optional read-only rehearsal (gated P2)
After M2, a read-only role can SELECT the rolled-back-then-committed views; until M2 there is nothing to
expose. No Directus mutation performed this session.