23-P3D4 — Directus Notification Exposure — Design Review Note (rev1)
23-P3D4 — Directus Notification Exposure — Design Review Note (rev1)
Date: 2026-05-08 Scope: Design review + read-only inventory. NO implementation. Source prompt:
knowledge/dev/laws/dieu44-trien-khai/prompts/23-p3d4-directus-exposure-design-review-prompt.md(rev4) Reviewer: Claude Opus 4.7 (1M)
0. Mandatory Law Pre-Read
| # | File | Status |
|---|---|---|
| 1 | knowledge/dev/laws/constitution.md (HP v4.6.3) |
READ |
| 2 | knowledge/dev/laws/law-07-assembly-first.md (Đ7) |
READ |
| 3 | knowledge/dev/ssot/data-connection-law.md |
READ |
| 4 | Đ28 (Luật Kỹ thuật Hiển thị v2.0) | REFERENCED via constitution table; must be consulted before any human-facing display work |
| 5 | knowledge/dev/laws/dieu44-trien-khai/design/23-p3d-ui-boundary-directus-nuxt-assembly-note.md |
READ |
Plus P3D context files (Step 1): 23-p3d3-user-notification-board-directus-exposure-design.md, iu-agent-front-door-context.md, 23-p3d2-notification-triggers-report.md, 23-p3d3-notification-context-directus-exposure-report.md — all READ.
0B. Law Jurisdiction Map
| Tầng | Thẩm quyền luật | Ví dụ vi phạm | P3D4 stance |
|---|---|---|---|
| PG runtime (notification) | PG-first (Đ7), Assembly First, IU laws, Đ33 v2.1 | Code Nuxt để bù PG | PG đã ACTIVE (P3D2); P3D4 không mutate PG |
| Directus exposure | Directus/DOT (Đ35 v5.2), data-connection-law, Đ36 (Collection Protocol), Đ32 (Approval) | Click UI sửa config thay vì DOT | P3D4 review only; UI view-only |
| Nuxt display | Đ28 (Luật Hiển thị v2.0), TreeView/Vue Flow read-only, data-connection-law Cổng | Thêm business logic vào Nuxt | P3D4 không commit Nuxt code; existing assembly chỉ |
| Human interaction | UI/display + data-connection (Cổng → Não → Kho) | User sửa content qua Directus UI | Read-only metadata board only |
| Agent/Codex dispatch | User-approval, agent-operation rules | Agent tự dispatch Codex | Không dispatch trong P3D4 |
| Hermes automation | Review riêng — hermes_readiness=BLOCKED_PENDING_REVIEW (P3D2) |
Trộn Hermes vào exposure design | Không trộn |
Hard rule (Constitution + Đ7 §0-H + UI Boundary): "No layer may solve another layer's problem by bypassing that layer's law/tooling." — P3D4 stays inside design + inventory.
A. Inventory Results
A1. Directus inventory — inventory_directus_access=FULL_READ_ONLY
Method: Directus MCP read-only API (directus_health, directus_list_collections, directus_get_schema, directus_get_items). No new token created. No UI mutation. URL: https://directus.incomexsaigoncorp.vn.
Findings:
| Object | Result |
|---|---|
| Health | ok |
| Collections discovered | 210 user / 237 total |
iu_notification_event |
PRESENT — listed in directus_list_collections (whitelisted, no description) |
iu_notification_read |
PRESENT — listed in directus_list_collections (whitelisted, no description) |
iu_notification_event schema fetch |
HTTP 403 FORBIDDEN — token role lacks read permission |
iu_notification_event items fetch |
HTTP 403 FORBIDDEN — same |
| Existing flows/endpoints for IU notification | None observed (collection list does not surface a notification-specific flow; no custom endpoints attested in P3D2/P3D3 reports) |
| PG view auto-discovery | N/A — no notification PG view exists yet (see A2) |
Interpretation: Directus has auto-registered both iu_* tables (consistent with PG schema sync), but no role permission grant has been configured for them. This is the correct posture pre-DOT package: tables visible to admin only, not exposed.
A2. PG inventory — inventory_pg_access=FULL_READ_ONLY
Method: SSH alias contabo → docker exec postgres psql -U directus -d directus, read-only \d, \df, pg_proc, pg_trigger, information_schema.views. No DDL/DML executed. No secret created.
Findings (verbatim PG state on 2026-05-08):
Tables:
public.iu_notification_event(ownerdirectus) — columns:id uuid PK,event_type text NN,event_stream text NN,unit_id uuid NN,canonical_address text NN,ref_id uuid,actor_ref text NN,source text NN,payload jsonb NN default '{}',created_at timestamptz NN default now().- Indexes:
iu_notification_event_pkey,idx_notif_event_created,idx_notif_event_stream_created,idx_notif_event_unit_created,uq_notif_event_type_ref(UNIQUE partial WHERE ref_id IS NOT NULL). - Check constraints:
chk_notif_event_actor_ref_nonempty,chk_notif_event_canonical_address_nonempty,chk_notif_event_source_nonempty,chk_notif_event_stream(comment|review|update),chk_notif_event_type(comment_added|draft_created|version_applied),chk_notif_event_type_stream(compound). - FK:
fk_notif_event_unit (unit_id) → information_unit(id).
- Indexes:
public.iu_notification_read(ownerdirectus) — columns:id uuid PK,event_id uuid NN,actor_ref text NN,read_at timestamptz NN default now().- Indexes:
iu_notification_read_pkey,idx_notif_read_actor_event,uq_notif_read_event_actorUNIQUE (event_id, actor_ref). - FK:
fk_notif_read_event (event_id) → iu_notification_event(id) ON DELETE CASCADE.
- Indexes:
Functions (all SECURITY DEFINER = t):
fn_iu_notif_comment()→triggerfn_iu_notif_draft()→triggerfn_iu_notif_version()→triggerfn_iu_unread(p_actor text, p_stream text, p_include_self boolean, p_limit integer)→SETOF jsonbfn_iu_mark_read(p_event_ids uuid[], p_actor text)→jsonbfn_iu_notification_board(p_actor text DEFAULT NULL, p_stream text DEFAULT NULL, p_limit integer DEFAULT 50)→SETOF jsonb
Triggers (all AFTER INSERT FOR EACH ROW, tgtype=5):
trg_aa_iu_notif_commentonunit_edit_commenttrg_aa_iu_notif_draftonunit_edit_drafttrg_aa_iu_notif_versiononunit_version
Views: SELECT … FROM information_schema.views WHERE table_name ILIKE '%notif%' → 0 rows. No PG view exists yet for notification board overview.
A3. Inventory assumptions documented
- Directus token in MCP belongs to a non-admin role (item access denied). Schema/permission view of these collections at admin level was NOT performed in this session — admin-level audit deferred to package P3D4B unless surfaced earlier.
- Hermes production daemon NOT inspected (out of scope; remains
BLOCKED_PENDING_REVIEW). - No auth/identity mapping table (Directus user →
actor_ref) was searched; the conventionuser:huyenis documented in the P3D3 design and front-door context only.
B. 8 Design Questions Answered
Q1. Directus native capability — what can be exposed without custom code?
Both iu_notification_event and iu_notification_read are already auto-registered as Directus collections (confirmed in A1). With a DOT-applied permissions grant, Directus can natively expose them as read-only REST collections — no custom code. Filtering by actor_ref/event_stream/created_at is supported by Directus query language. Limitation: Directus collection access cannot invoke PG functions (fn_iu_unread, fn_iu_notification_board) because they take parameters; collection-level filters operate over base tables only.
Q2. PG primitive most appropriate.
- Phase 1 board overview (no per-actor): a read-only PG VIEW (
v_iu_notification_boardor similar, NOT YET CREATED) joiningiu_notification_event⟕iu_notification_readto surface latest_readers + per-event aggregate. Directus auto-discovers views as collections; adding a permissions-grant via DOT is sufficient. - Per-actor unread/board: requires PG function invocation with parameters → either (a) Directus custom endpoint/flow that calls
fn_iu_*(DOT-managed), or (b) keep agent path (direct PG SQL) and provide humans only the overview view. (a) needs DOT support. - Materialized view: not justified now (write volume is small; freshness matters).
- Custom Directus endpoint: only if assembly fails — see Q3.
Q3. fn_iu_notification_board via Directus without custom code?
Not directly: Directus collections cannot call parameterised SETOF jsonb functions natively. Two assembly paths remain:
- Directus Flow (operation:
execPostgreSQL/SQL orwebhookto a small endpoint) — but Directus 11.5.1 lacks isolated-vm (per memory note), so script ops are constrained. - DOT-managed custom endpoint extension registered via Đ35 v5.2 (DOT Governance) — DOT-supported only.
Alternative (recommended for human Phase 1): expose the view version (no per-actor parameter) to humans; keep
fn_iu_*reserved for AI/Agent direct PG path (already working — front-door context).
Q4. Phase-1 human monitoring — is a PG read-only view sufficient? Yes. Required surface:
- All events (id, type, stream, unit_id, canonical_address, actor_ref, created_at, payload meta keys);
- Latest readers (top 5 actors by read_at desc) — joinable from
iu_notification_read; - Read-state overview (count of distinct readers per event);
- Originating actor (
actor_refof event). This fits a single view definition. Per-actor unread/inbox is deferred to a controlled second package (matches P3D2 boundary "no global read flag", and P3D3 staged plan).
Q5. Human actor identity.
Convention from front-door context (verbatim): gpt, opus, agent:codex, reviewer:gpt, user:huyen. For Directus exposure, the mapping Directus user → actor_ref must be defined (e.g. via a Directus user-extra field or a deterministic rule). This mapping is NOT in scope for P3D4 — flagged as a P3D4B prerequisite. Until then, the read-only overview is actor-agnostic (no per-user filter), which is safe.
Q6. Human mark-read — defer Phase 1?
Defer. Per P3D2 (no global read flag, per-actor only) and front-door context (Per-actor, implicit_self, explicit_read). Mark-read is a write action; allowing humans to write through Directus collides with the "user no content edit" boundary unless executed via a controlled, reviewed Directus custom endpoint that calls fn_iu_mark_read. That endpoint is a separate reviewed package (not P3D4).
Q7. Security / permissions.
- Read-only at all surfaces.
- Metadata-only default: surface event metadata + read-state aggregates; do not join
information_unit.body/unit_version.body. - User MUST NOT be able to create/update/delete IU rows via Directus UI for these collections — permissions grant must be
readonly oniu_notification_event,iu_notification_read, and the futurev_iu_notification_board. - PG function ownership already correct:
SECURITY DEFINER, ownerdirectus, no PUBLIC EXECUTE (P3D2 evidence). - No new Directus token; reuse existing approved access.
Q8. Nuxt boundary. No new Nuxt code. Notification surface, when displayed, must reuse existing Nuxt assembled screens reading from Directus REST/SDK. TreeView / Vue Flow read-only mode (data-connection-law) applies if a graph view is desired. Đ28 (Luật Hiển thị v2.0) governs any human-facing render and must be checked before display work commences. Đ33 §13 exception is not sought here.
C. Recommendation
Option C — Staged read-only board first via reviewed DOT package; mark-read later as a separate controlled action.
Rationale:
- Aligns with P3D2 boundary (Phase-1 no global read flag, no human write) and P3D3 staged plan.
- Limits initial blast radius to a single PG VIEW + a single Directus read permissions-grant (both DOT-managed).
- Defers the harder problem (Directus user → actor_ref mapping, parameterised function invocation, write action
fn_iu_mark_read) to a second review with a clear gate. - Preserves AI/Agent path unchanged (direct PG
fn_iu_*).
(Options A and B are subsumed: A = the read-only piece of C; B = the mark-read piece, deferred.)
D. Body Content Exposure Decision
body_content_exposure=NOT_INCLUDED
metadata_only_exposure_default=PASS
Rationale: iu_notification_event already references the originating IU/draft/version/comment by unit_id + ref_id + canonical_address (text reference) without embedding body. The recommended view + collection exposure surfaces only event metadata and per-actor read-state aggregates. Any join to information_unit / unit_version body content requires a separate review (OUT_OF_SCOPE_REQUIRES_SEPARATE_REVIEW if requested later).
E. DOT Package Outline for Next Pack (P3D4B)
High-level; NO implementation here.
- PG view (read-only):
- Candidate name:
v_iu_notification_board. - Definition (sketch):
iu_notification_event e LEFT JOIN LATERAL (SELECT array_agg(actor_ref ORDER BY read_at DESC) FILTER (...) AS latest_readers, count(*) AS read_count FROM iu_notification_read r WHERE r.event_id = e.id LIMIT 5) ... - Owner
directus,GRANT SELECT TO directusonly; no PUBLIC. - Indexed via underlying tables.
- Candidate name:
- Directus exposure plan:
- Apply DOT to grant a NEW read-only role (e.g.
notif_board_reader)readpermission onv_iu_notification_board(and minimal projection ofiu_notification_event/iu_notification_readif needed). - Do NOT grant
create,update,deleteon any of these collections to any non-admin role. - No custom endpoint, no flow, no extension in P3D4B.
- Apply DOT to grant a NEW read-only role (e.g.
- Role/permission intent:
- Human read-only: limited columns; no body fields.
- AI/Agent: unchanged (PG SQL).
- Read-only vs mark-read boundary:
- P3D4B: read-only board view exposure only.
- Mark-read (write action via
fn_iu_mark_read) and per-actor parameterised endpoints: separate package with its own review (Directus user→actor_ref mapping is a prerequisite).
- Rollback / review:
- Rollback = REVOKE + DROP VIEW (DOT-driven), no external dependency.
- Quiet-period after grant (24–48h) before stacking the mark-read package.
- Đ28 consult required if any new Nuxt rendering is proposed.
- Goal: every step DOT-driven, no ad-hoc UI clicking, no Nuxt code, no PG function exposed by parameter in P3D4B.
F. Constitution / Law Compliance Checklist
PG source of truth: PASS
Directus DOT-only: PASS
Directus UI view-only: PASS
User no content editing in Directus: PASS
Nuxt no-code/no-business-logic: PASS
Existing TreeView assembly path respected: PASS
No direct PG from Nuxt: PASS
No Codex dispatch: PASS
G. Law Jurisdiction Compliance Checklist
PG work stays in PG domain: PASS
Directus exposure stays in Directus/DOT domain: PASS
Nuxt display stays in display-only domain: PASS
User interaction does not bypass Directus: PASS
Directus does not become content-edit workflow: PASS
Nuxt does not become workflow/business logic: PASS
Hermes/agents not mixed into exposure design: PASS
No duplicate implementation across PG/Directus/Nuxt: PASS
P3D4 design review note rev1 | Inventory-driven | No implementation | Recommendation: C (staged) | Next: P3D4B_DIRECTUS_DOT_EXPOSURE_PACKAGE_REVIEW