23-P3D4C0X — Universal PG-to-Outside Signal Architecture (Design Note rev1)
23-P3D4C0X — Universal PG-to-Outside Signal Architecture (Design Note rev1)
Date: 2026-05-08 Status: DESIGN rev1 — read-only architecture review. NO implementation. NO mutation. Prompt: knowledge/dev/laws/dieu44-trien-khai/prompts/23-p3d4c0x-universal-event-outbox-notification-architecture-review-prompt.md (rev2) Report: knowledge/dev/laws/dieu44-trien-khai/reports/23-p3d4c0x-universal-event-outbox-notification-architecture-review-report.md Reviewer: Claude Opus 4.7 (1M context) Scope: KB design note + report only. Không PG/Directus/Nuxt mutation. P3D4C1 vẫn pause.
§0. Tóm tắt điều hành
Thiết kế cơ chế Universal Event/Outbox + Notification/Signal Pipeline trên PG để mọi domain (IU, Birth Registry, Governance, TAC, KG, System/Đ43, DOT, Health) có thể phát tín hiệu ra ngoài (Directus → Nuxt, Agent SQL, future webhook/email) qua một hạ tầng chung 5 lớp, tận dụng tối đa PG-native (trigger, function, view, pg_cron, LISTEN/NOTIFY).
IU notification runtime hiện hành (P3D1+P3D2 ACTIVE) — iu_notification_event, iu_notification_read, fn_iu_unread, fn_iu_mark_read, fn_iu_notification_board, 3 triggers — không phá vỡ. Khuyến nghị: UNIVERSAL_WITH_IU_COMPAT (chi tiết §O).
Boundary cứng: Universal event KHÔNG phải general activity log. Chỉ ghi governance-significant facts (entity birth/lifecycle/approval/alert/DOT result) — không ghi mỗi row change, không telemetry/metrics, không debug log.
§A. Domain × Event Type × Actor × Frequency Matrix (Step 0)
Dữ liệu trích từ 13 luật + Pack 23/P3D + Đ43 context-pack runtime. Frequency là order-of-magnitude ước tính dùng cho sizing, không SLA.
| # | Domain | Event Type | Event Stream | Subject Table | Actor (producer) | Recipient (default) | Phase 1? | Frequency |
|---|---|---|---|---|---|---|---|---|
| 1 | iu | comment_added | comment | unit_edit_comment | author_ref | reviewer + creator | ✅ ACTIVE | ~10s/day |
| 2 | iu | draft_created | review | unit_edit_draft | created_by | reviewer pool | ✅ ACTIVE | ~10s/day |
| 3 | iu | version_applied | update | unit_version | created_by | watchers + creator | ✅ ACTIVE | ~10s/day |
| 4 | iu | new_piece_created | update | unit_version (seq=1) | created_by | watchers (debounced) | ⏳ P3D4C1 paused | bursty (import) |
| 5 | iu | document_imported | update | unit_version (rollup) | worker | watchers | ⏳ P3D4C1 paused | per import job |
| 6 | birth_registry | entity_born | birth | (any registered table) | created_by | governance owner | Phase 2 | irregular |
| 7 | birth_registry | conformance_warning | alert | birth_registry rows | gate trigger | owner + reviewer | Phase 2 | irregular |
| 8 | governance | checkpoint_due | task | governance_checkpoint | gov system | agency owner | Phase 2 | scheduled |
| 9 | governance | approval_requested | review | approval_request | requester | approver pool | Phase 2 | irregular |
| 10 | governance | approval_decided | update | approval_decision | approver | requester + watchers | Phase 2 | irregular |
| 11 | tac | text_unit_birth | birth | normative_registry | created_by | gov owner | Phase 2 | low (laws) |
| 12 | tac | review_pending | review | text_review_request | requester | reviewer | Phase 2 | irregular |
| 13 | tac | indexing_complete | update | tac_index_log | indexer | owner | Phase 3 | per ingest |
| 14 | kg | provenance_change | update | kg_edge | edge writer | owner | Phase 3 | bursty |
| 15 | kg | constraint_violation | alert | kg_constraint_log | constraint engine | owner | Phase 3 | irregular |
| 16 | system | context_pack_built | update | context_pack_request | dot-context-pack-build | sysop | Phase 2 (Đ43 native) | every 3h cron |
| 17 | system | context_pack_failed | alert | context_pack_request | builder | sysop | Phase 2 | rare |
| 18 | system | red_zone_violation | alert | system_issues | health check | sysop + agency | Phase 2 | irregular |
| 19 | dot | dot_executed | update | dot_execution_log | dot | sysop | Phase 2 | per DOT run |
| 20 | dot | dot_failed | alert | dot_execution_log | dot | sysop + agency | Phase 2 | rare |
| 21 | dot | dot_apr_required | review | admin_fallback_log | dot-apr-execute | agency reviewer | Phase 2 | rare |
| 22 | health | health_check_alert | alert | system_health_checks | health executor | sysop + owner | Phase 2 | per finding |
| 23 | health | issue_opened | alert | system_issues (Đ22) | health/triage | owner agency | Phase 2 | irregular |
| 24 | health | issue_resolved | update | system_issues | resolver | reporters + watchers | Phase 2 | irregular |
Stream taxonomy chốt: comment | review | update | birth | task | alert | health (7 streams). Đủ phủ các domain trên. Mỗi event_type map cứng vào 1 stream (CHECK constraint cặp).
Total Phase 1 (already ACTIVE): 3 event types (rows 1-3) — IU only. Total Phase 2 (proposed expansion): +18 types across 6 domains. Total Phase 3 (future): +3 types (KG provenance, TAC indexing).
§B. 5-Layer Pipeline Design
┌─────────────────────────────────────────────────────────────────┐
│ Layer 1 — FACT CAPTURE (hot path, O(1)) │
│ AFTER INSERT trigger → INSERT INTO {domain}_event_pending │
│ KHÔNG: COUNT/JOIN/aggregation. CHỈ: 1 INSERT + 1 PK lookup. │
└──────────────────────────────┬──────────────────────────────────┘
│ pending row + stable_key
▼
┌─────────────────────────────────────────────────────────────────┐
│ Layer 2 — PROCESSING (worker, deferred) │
│ pg_cron worker tick (90-120s) │
│ - pg_try_advisory_lock(hashtext('worker_name')) │
│ - SELECT … FOR UPDATE SKIP LOCKED (debounce window passed) │
│ - Group by stable_key (source_doc/import_batch/run_id/…) │
│ - Emit durable events │
│ - exception-safe: log error, release lock │
└──────────────────────────────┬──────────────────────────────────┘
│ durable event row
▼
┌─────────────────────────────────────────────────────────────────┐
│ Layer 3 — DURABLE EVENT + READ STATE │
│ event_outbox (universal) │
│ event_read (per-actor read state) │
│ Implicit self-read | Explicit read | Unread │
│ ON DELETE CASCADE (read → event) │
└──────────────────────────────┬──────────────────────────────────┘
│ event + read state
▼
┌─────────────────────────────────────────────────────────────────┐
│ Layer 4 — ROUTING / RECIPIENT │
│ event_subscription (config: who subscribes to what) │
│ v_event_recipient (view: domain × event_type → actor list) │
│ event_delivery (per-recipient state — Phase 2) │
└──────────────────────────────┬──────────────────────────────────┘
│ recipient list
▼
┌─────────────────────────────────────────────────────────────────┐
│ Layer 5 — EXPOSURE / DELIVERY │
│ • PG view → Directus collection (auto-discover) │
│ • SQL function fn_*_unread, fn_*_board (Agent inbox) │
│ • Nuxt → reads Directus only (Assembly First, no PG direct) │
│ • LISTEN/NOTIFY (Phase 2 evaluate — real-time Agent push) │
│ • Webhook/email/chat adapters (Phase 3+ — design seam only) │
└─────────────────────────────────────────────────────────────────┘
Computation placement (cấm trộn lớp):
| Computation | Layer |
|---|---|
| Trigger check WHEN | L1 |
| Single column lookup (canonical_address) | L1 |
| COUNT/aggregate, JOIN multi-table | L2 |
| Debounce window | L2 |
| Group by stable_key | L2 |
Idempotency ON CONFLICT DO NOTHING |
L1 (event already exists) hoặc L3 (durable) |
| Read state insert | L3 (qua fn_event_mark_read) |
| Recipient resolution | L4 (view) |
| Filter actionable (draft.status='open') | L4 (LEFT JOIN trong fn_unread) |
| UI assembly | L5 (Directus/Nuxt) |
§C. Event Envelope Contract
Đã chuẩn hoá từ rev2 prompt + IU schema P3D1. Một event = một row trong event_outbox.
-- Universal event envelope (Phase 2 target schema)
CREATE TABLE event_outbox (
-- Identity
event_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
-- Domain & taxonomy
event_domain text NOT NULL, -- 'iu','birth_registry','governance','tac','kg','system','dot','health'
event_type text NOT NULL, -- e.g. 'draft_created','dot_failed','health_check_alert'
event_stream text NOT NULL, -- 'comment','review','update','birth','task','alert','health'
event_severity text, -- 'info','warning','critical' — optional, alert/health only
-- Subject (entity that the event is about)
event_subject_type text NOT NULL, -- entity species ('information_unit','dot_execution_log',...)
event_subject_ref uuid, -- entity FK (if uuid PK)
event_subject_table text NOT NULL, -- source PG table (for FK-less polymorphic events)
canonical_address text NOT NULL CHECK (btrim(canonical_address) != ''),
-- Actor (who caused / who fired)
actor_ref text NOT NULL CHECK (btrim(actor_ref) != ''),
source_system text NOT NULL, -- 'trigger','worker','function','dot','health_executor'
source_function text, -- pg_proc name producing this event
-- Correlation (link related events)
correlation_id text, -- e.g. import_batch_ref, dot_run_id, build_id, session_id
causation_id uuid, -- event_id that caused this event
-- Provenance (where the underlying fact came from)
source_document_ref text,
import_batch_ref text,
-- Payload — METADATA ONLY
payload_classification text NOT NULL DEFAULT 'safe_metadata'
CHECK (payload_classification IN ('safe_metadata','restricted')),
safe_payload jsonb NOT NULL DEFAULT '{}'::jsonb,
-- ALLOWED: counts, status, severity, lifecycle keys, refs, addresses
-- FORBIDDEN: body content, vector, secret, business text, personal data
-- Timing
occurred_at timestamptz NOT NULL DEFAULT now(), -- when fact happened
created_at timestamptz NOT NULL DEFAULT now(), -- when event row inserted
processed_at timestamptz, -- (Phase 2 — for delivery sweeper)
-- Constraints
CHECK (event_domain IN ('iu','birth_registry','governance','tac','kg','system','dot','health')),
CHECK (event_stream IN ('comment','review','update','birth','task','alert','health')),
CHECK (event_severity IS NULL OR event_severity IN ('info','warning','critical')),
-- per-domain (event_type, stream) coupling enforced by domain config (§F)
CHECK (
safe_payload IS NOT NULL
AND NOT (safe_payload ? 'body')
AND NOT (safe_payload ? 'content')
AND NOT (safe_payload ? 'vector')
AND NOT (safe_payload ? 'secret')
AND NOT (safe_payload ? 'token')
)
);
-- Idempotency (universal): a given subject + event_type fires at most once.
CREATE UNIQUE INDEX uq_event_outbox_subject_type
ON event_outbox (event_domain, event_type, event_subject_table, event_subject_ref)
WHERE event_subject_ref IS NOT NULL;
-- Hot indexes
CREATE INDEX idx_event_outbox_domain_stream_created
ON event_outbox (event_domain, event_stream, created_at DESC);
CREATE INDEX idx_event_outbox_correlation
ON event_outbox (correlation_id) WHERE correlation_id IS NOT NULL;
CREATE INDEX idx_event_outbox_severity_created
ON event_outbox (event_severity, created_at DESC) WHERE event_severity IN ('warning','critical');
Cấm trong payload: body, raw content, vector, secret, token, password, personal data. CHECK constraint nâng (whitelist key only). Compliance với hard boundary "no body/raw payload/vector/secret exposure".
Trace fields lý do (rev2 contribution):
correlation_id— gom event của cùng job/import/build/run. Worker rollup dùng key này.causation_id— chuỗi nhân quả (event A gây event B). Dùng cho audit trail (ví dụ: dot_apr_required → dot_executed → dot_failed).
§D. Recipient / Subscriber / Routing Model
Nguyên lý: Actor (producer) ≠ Recipient (consumer). Tách bạch để hỗ trợ broadcast, role routing, mute, escalation mà không phá hot path.
D.1 Recipient typology
| Recipient kind | Format actor_ref |
Ví dụ |
|---|---|---|
| Human user | user:{login} |
user:huyen |
| Named AI agent | {slug} hoặc agent:{slug} |
gpt, opus, agent:codex |
| Role-bound AI | {role}:{slug} |
reviewer:gpt |
| Governance agency | agency:{code} |
agency:apr |
| Role/group | role:{name} |
role:reviewer, role:sysop |
| System service | svc:{name} |
svc:hermes (future) |
D.2 Subscription table (config-driven, Phase 2)
CREATE TABLE event_subscription (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
recipient_ref text NOT NULL, -- actor_ref hoặc role:NAME
event_domain text, -- NULL = any domain
event_type text, -- NULL = any type in domain
event_stream text, -- NULL = any stream
scope_subject_table text, -- NULL = any subject; else restrict
scope_filter jsonb DEFAULT '{}'::jsonb, -- predicate (e.g. {"agency":"x"})
mute boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now(),
CHECK (btrim(recipient_ref) != '')
);
CREATE INDEX idx_event_sub_recipient ON event_subscription(recipient_ref);
CREATE INDEX idx_event_sub_domain_type ON event_subscription(event_domain, event_type);
Resolution rule (PG view):
CREATE VIEW v_event_recipient AS
SELECT
e.event_id,
s.recipient_ref,
s.mute
FROM event_outbox e
JOIN event_subscription s
ON (s.event_domain IS NULL OR s.event_domain = e.event_domain)
AND (s.event_type IS NULL OR s.event_type = e.event_type)
AND (s.event_stream IS NULL OR s.event_stream = e.event_stream)
AND (s.scope_subject_table IS NULL OR s.scope_subject_table = e.event_subject_table)
WHERE NOT s.mute;
D.3 Phase 1 default — broadcast minus self
Nếu event_subscription chưa có row khớp → fall back broadcast (mọi known actor trừ creator). Đây là behavior hiện tại của P3D1+P3D2; giữ nguyên để không phá runtime.
D.4 Implicit self-read (universal rule)
Quy tắc đồng bộ với P3D2: actor tạo event mặc nhiên đã đọc (read_status_source='implicit_self'). Không tạo row trong event_read; fn_unread filter qua WHERE event.actor_ref != p_actor OR p_include_self.
Ưu điểm: zero-write trên hot path, idempotent, không phụ thuộc subscription resolution.
D.5 Escalation seam (Phase 3+)
Design seam (không implement): nếu event severity='critical' và không reader nào ack trong N phút → emit follow-up event *_unacked với recipient mở rộng (agency, sysop). Cron job + view, không cần infra mới. Tham chiếu §F về grouping.
D.6 Câu trả lời 4 câu hỏi rev2-Q2
| Câu hỏi | Phương án |
|---|---|
| Per-domain subscription? Per-event-type? Per-entity? | Cả ba, nhờ NULL-wildcard trên 4 cột (domain/type/stream/subject_table) + scope_filter jsonb cho per-entity predicate. |
| Creator implicit self-read: universal rule? | Có — quy tắc cứng tại fn_unread, không insert row read. |
| Agency/role routing: PG function or config table? | Config table (event_subscription) + view resolution. Function chỉ dùng cho mark_read và unread query. Lý do: dễ thêm/bớt agency mà không phải redeploy function. |
| Escalation target: design seam? | Yes (D.5). Cron + view, không hot path. |
§E. Notification Lifecycle States (Phase 1 vs Later)
| State | Mô tả | Phase 1 | Implementation |
|---|---|---|---|
| pending | Trong staging table, chưa rollup | ✅ (P3D4C1 paused) | event_pending.processed_at IS NULL |
| emitted | Worker đã tạo durable event | ✅ ACTIVE | row tồn tại trong event_outbox |
| read | Recipient explicit mark | ✅ ACTIVE | event_read.event_id exists for actor |
| implicit_self | Creator auto-read | ✅ ACTIVE | computed (no row) |
| unread | Chưa đọc, không phải creator | ✅ ACTIVE | computed (no row) |
| suppressed | Mute/filter qua subscription | Phase 2 | event_subscription.mute=true hoặc no match |
| acknowledged | Recipient confirm action taken | Phase 2 | new col event_read.ack_at; chỉ cho stream='task','alert' |
| resolved | Subject entity resolved (issue closed, draft applied) | Phase 1 (filter only) | fn_unread LEFT JOIN subject; ẩn nếu lifecycle ≠ open. Không insert row read. |
| failed/retry | Worker error | Phase 1 (P3D4C1 paused) | event_pending.processed_at IS NULL + event_worker_log.error_text |
| dead_letter | Permanent fail | Phase 2 | new col event_pending.error_count >= N (threshold), separate triage view |
Phase 1 = đã ACTIVE hoặc thuộc P3D4C1 paused. Mọi thứ khác là design seam, không implement.
§F. Generalized Grouping / Debounce
Nguyên lý: Grouping key là stable identifier of the originating job, không phải timing. Timing chỉ là debounce window cho phép rollup chạy trễ.
F.1 Grouping key catalog
| Grouping context | Stable key column(s) | Ví dụ |
|---|---|---|
| IU import từ document | source_document_ref |
"law/dieu44/v3.3" |
| IU import bulk | import_batch_ref |
"import-2026-05-08-001" |
| DOT execution | correlation_id = dot_run_id |
"dot-context-pack-build:20260508-015552-abcd" |
| Đ43 context-pack build | correlation_id = build_id |
same as Đ43 §6 |
| Workflow checkpoint batch | correlation_id = workflow_run_id |
"wf-{uuid}" |
| Birth registry batch | correlation_id = birth_batch_id |
"birth-{date}" |
| Health check sweep | correlation_id = sweep_id |
"hc-sweep-{ts}" |
| Agency checkpoint | correlation_id = checkpoint_id |
"ckpt-{id}" |
Implementation: worker dùng COALESCE(source_document_ref, import_batch_ref, correlation_id) AS stable_key rồi GROUP BY stable_key HAVING count(*) >= threshold → emit rollup event; còn lại → emit piece-level. (Khớp P3D4C1 prompt rev3 §4 đã thiết kế cho IU; tổng quát hoá sang mọi domain chỉ là thêm 1 column correlation_id vào staging.)
F.2 Debounce universal vs per-domain
Khuyến nghị: universal debounce default 90s (clamped 60-300s qua dot_config), per-domain override qua event_domain_config (Phase 2 — chưa cần Phase 1).
| Domain | Debounce mặc định | Lý do |
|---|---|---|
| iu | 90s | bursty import, user-acceptable latency |
| birth_registry | 90s | giống iu |
| dot | 30s (override) | cron run quan trọng, response sớm |
| health | 30s (override) | alert nhanh |
| system (Đ43) | 0s (immediate) | build lifecycle |
| tac, kg, governance | 90s | normal |
F.3 Universal vs per-domain worker
Khuyến nghị Phase 1: 1 worker function fn_event_worker_tick() đọc tất cả event_pending, group theo (event_domain, stable_key). Ưu: 1 cron, 1 advisory lock, 1 log table. Nhược: nếu một domain lỗi, lock có thể block domain khác trong cùng tick (nhưng exception-safe đã giải quyết — release lock trên error).
Cân nhắc Phase 2: nếu volume một domain >> domain khác → tách worker (one cron per domain). Quyết định data-driven, không premature.
§G. PG Signal Channels Map
| Channel | Cơ chế PG | Phase 1 | Use case |
|---|---|---|---|
| Directus collection (read projection) | PG view → Directus auto-discover | ✅ (P3D3 design ready) | Human board, AI sees same SoT |
| Agent SQL inbox | fn_event_unread(), fn_event_mark_read(), fn_event_board() |
✅ ACTIVE (P3D2) | Direct PG access cho AI |
| Nuxt display | Read từ Directus REST API | ✅ (Assembly First) | Human UI |
| DOT/ops health view | PG view (e.g. v_event_recent_alerts) |
✅ Phase 1 | sysop monitoring |
| LISTEN/NOTIFY | pg_notify('event_universal', json) từ trigger sau insert vào outbox |
Evaluate Phase 2 | Real-time Agent push (nếu agent connect persistent) |
| Webhook adapter | Cron sweeper đọc event_delivery chưa gửi → curl |
Phase 3+ design seam | Slack/email/external |
| Email/chat | Same as webhook | Phase 3+ design seam | — |
Ranh giới: Phase 1 và 2 không cần external scheduler. Chỉ pg_cron + Directus + Nuxt + Agent SQL. Webhook/email là adapter (PG-driven cron, không phải external service mới).
LISTEN/NOTIFY trade-off: mạnh khi agent persistent connection nhưng agent hiện tại chạy on-demand (Codex, Opus session) — polling qua fn_unread đã đủ. Defer Phase 2.
§H. Universal vs Per-Domain Tables — Strategy
| Câu hỏi | Khuyến nghị Phase 1 | Khuyến nghị Phase 2 | Justification |
|---|---|---|---|
| 1 universal pending vs N per-domain | N per-domain (giữ iu_notification_pending); Phase 2 bổ sung pending nếu domain mới cần debounce |
N per-domain | Pending là staging, không phải SoT lâu dài. Per-domain index/CHECK linh hoạt hơn. Volume mỗi pending nhỏ. |
| 1 universal event vs N per-domain | Universal event_outbox cho domain Phase 2+; IU giữ iu_notification_event qua compat view |
Universal hoá hoàn toàn (PHASED_CONVERGENCE) | Đa-domain query (board cross-domain) phải JOIN N table nếu per-domain — không hiệu quả; CHECK/index unified dễ duy trì. |
| 1 universal read state vs N | Universal event_read (event_id FK polymorphic không cần — dùng outbox.id) |
Universal | Read state nhỏ, query per-actor luôn đi qua event_id; one table = one index per actor. |
| 1 worker vs N | 1 worker với group by domain | 1 worker until volume forces split | Operational simplicity. |
Performance note: universal event_outbox với index (event_domain, event_stream, created_at DESC) xử lý tốt 1M+ rows. Partition theo created_at (monthly) là Phase 4 nếu volume vượt threshold.
Permission isolation: dùng RLS hoặc view per-domain để cấp quyền (ví dụ Directus role X chỉ thấy domain Y). Không cần tách table.
§I. IU Migration / Compatibility Recommendation
I.1 Hiện trạng (P3D1+P3D2 ACTIVE, deployed)
iu_notification_event(10 cols, 7 CHECK constraints, 6 indexes, FK→information_unit)iu_notification_read(4 cols, unique on (event_id, actor_ref), CASCADE)- 6 functions:
fn_iu_unread,fn_iu_mark_read,fn_iu_notification_board,fn_iu_notif_comment,fn_iu_notif_draft,fn_iu_notif_version - 3 triggers:
trg_aa_iu_notif_comment/draft/version - Pilot rows retained (P3D2 test fixture)
- Hash boundary: Pack 23 functions unchanged; P3D1+P3D2 hashes locked
I.2 4 options đánh giá
| Option | Approach | Risk | Effort | Hot-path impact | Compat | Verdict |
|---|---|---|---|---|---|---|
| A — UNIVERSAL_FIRST | Rename iu_notification_event → event_outbox, migrate data, drop iu_* fn, rewrite to fn_event_* |
High — agent context-pack nói fn_iu_unread, fn_iu_mark_read, fn_iu_notification_board là front door. Đổi tên sẽ phá Đ43 context (which is currently advertising these names) và mọi agent đang dùng. Directus collection re-register. |
High | None (tables eventually rename) | ❌ Breaking | REJECT |
| B — Parallel + migrate | Create event_outbox, dual-write (trigger fires both iu + outbox), gradually migrate readers, then drop iu |
Medium — dual-write doubles trigger work; data consistency window | Medium-High | 2× INSERT per fact | ⚠️ Temporary | Acceptable but wasteful |
| C — UNIVERSAL_WITH_IU_COMPAT ⭐ | Build event_outbox for new domains. Keep iu_notification_event as-is. Provide compatibility one of: (i) IU stays separate (zero risk) or (ii) iu_notification_event becomes a VIEW over event_outbox WHERE event_domain='iu' after one-time data copy and fn_iu_* redirected. |
Low (ii) — Medium (i) | Low-Medium | None | ✅ fn_iu_* API invariant |
RECOMMENDED |
| D — IU_SPECIFIC_JUSTIFIED | Keep IU notification standalone forever; new domains build separate notif tables | Low | Low | None | N/A | Diverges → tech debt |
I.3 Khuyến nghị: Option C — UNIVERSAL_WITH_IU_COMPAT (variant C-i first, C-ii later)
C-i (Phase 2 entry):
event_outboxis created alongsideiu_notification_event. New domains (Đ43 system, DOT, Health, Birth, TAC, KG, Governance) write only toevent_outbox.- IU stays on
iu_notification_event— zero migration risk to active runtime. - Cross-domain board query: a UNION view
v_event_unifiedexposesiu_notification_eventcolumns mapped toevent_outboxshape:CREATE VIEW v_event_unified AS SELECT 'iu' AS event_domain, event_type, event_stream, 'information_unit' AS event_subject_type, unit_id AS event_subject_ref, 'information_unit' AS event_subject_table, canonical_address, ref_id, actor_ref, source AS source_system, NULL::text AS correlation_id, NULL::uuid AS causation_id, NULL::text AS source_document_ref, NULL::text AS import_batch_ref, 'safe_metadata' AS payload_classification, payload AS safe_payload, created_at AS occurred_at, created_at, NULL::timestamptz AS processed_at, id AS event_id FROM iu_notification_event UNION ALL SELECT event_domain, event_type, event_stream, event_subject_type, event_subject_ref, event_subject_table, canonical_address, NULL::uuid AS ref_id, actor_ref, source_system, correlation_id, causation_id, source_document_ref, import_batch_ref, payload_classification, safe_payload, occurred_at, created_at, processed_at, event_id FROM event_outbox; - Hot-path impact: 0.
fn_iu_unread,fn_iu_mark_read,fn_iu_notification_boardkhông đổi.
C-ii (Phase 3, only after stability proven):
- One-time copy
iu_notification_event→event_outboxwithevent_domain='iu'. - Drop physical
iu_notification_event; create viewiu_notification_event AS SELECT … FROM event_outbox WHERE event_domain='iu'. iu_notification_read→event_readsimilarly.fn_iu_*rewritten to queryevent_outboxfiltered by domain. API surface (function names + return shape) unchanged.- Risk mitigated: Đ43 context-pack chỉ đang quảng cáo function names, không advertise table names → fn_iu_* signature giữ nguyên thì context-pack không cần re-build.
Hard constraint cho cả C-i và C-ii: KHÔNG phá fn_iu_unread, fn_iu_mark_read, fn_iu_notification_board — đây là front-door API của Đ44 IU runtime đã advertised. Bất kỳ thay đổi nào phải ở implementation, không signature.
§J. Đ43 Deep Assessment
Câu hỏi sub-1 đến sub-5 từ rev2 prompt §3 Q8.
J.1 Sub-1: Context-pack build/verify/health là universal event?
Có, nhưng map theo lifecycle:
context_pack_build_started(event_domain='system', stream='update') — optional Phase 2context_pack_built(PASS) — Phase 2 (đã liệt kê row 16 trong matrix)context_pack_failed— Phase 2 alert (row 17)context_pack_promoted(staging→live swap) — Phase 2
correlation_id = build_id → group multiple sub-events của cùng build.
J.2 Sub-2: dot_config dùng chung hay giữ DOT-only?
Khuyến nghị: dùng chung (universal namespace).
dot_config hiện đã chứa keys notification.debounce_seconds, notification.batch_piece_threshold (P3D4C1 prompt §5 đã thiết kế). Mở rộng:
event.debounce_seconds.default = 90event.debounce_seconds.{domain}= overrideevent.worker_lock_name = 'event_worker'
dot_config namespace bằng prefix (event.*, notification.*, dot.*, health.*) — không tách table.
J.3 Sub-3: Tránh duplicate worker/log/health với Đ43
Nhận diện overlap:
- Đ43 đã có
context_pack_requesttable + workerdot-context-pack-build.sh+context_pack_verify+ advisory lock. - Universal event hệ thống có
event_pending+fn_event_worker_tick+ advisory lock.
Khuyến nghị:
- Không duplicate. Đ43 worker (filesystem-side) gửi event vào
event_outbox(event_domain='system') sau khi PASS/FAIL — coi như event producer. - Universal
fn_event_worker_tickkhông tự rebuild context-pack — chỉ rollup events từ Đ43. - Đ43
system_health_checks,system_issuestiếp tục là SoT cho health (Đ22 lifecycle) — universal event chỉ phát thông báo về thay đổi state (issue_opened/resolved → row 23-24).
J.4 Sub-4: DOT execution pair (DOT + verify) biểu diễn trong universal schema?
Pair model:
dot_executed(event_type, stream='update') vớipayload.{exit_code, duration_ms}dot_verified(event_type, stream='update') vớicausation_id = dot_executed.event_iddot_failed(event_type, stream='alert', severity='warning') nếu exit_code != 0correlation_id = dot_run_id→ group cả pair.
Cú pháp correlation_id + causation_id đủ để truy ngược pair mà không cần extra table.
J.5 Sub-5: Universal event feed Đ43 context-pack?
Có, qua red_zones + new section recent_alerts:
- Đ43 §6 đã có 8 sections cố định (
project_map,laws_index,dot_registry,entities_overview,db_map,red_zones,architecture_mmd,project_map_json). - Đề xuất không thêm section mới (giữ stable schema). Thay vào đó: universal event feed
red_zones.md.tmplquapg_querysource — section render top N eventsseverity in ('warning','critical')trong 24h gần nhất. Đ43 đã hỗ trợ source='pg_query' (template).
→ Universal event hệ thống không phá Đ43 contract; bổ sung vào red_zones là additive.
§K. Law Ownership Proposal
| Component | Candidate owner | Justification |
|---|---|---|
| Event schema contract (envelope, CHECK, payload classification) | HP (NT13 PG-first) + Đ44 (event là một loại "object" trong universe) | Schema chung cho mọi domain → thuộc HP nguyên tắc. Nội dung specific (IU events) thuộc Đ44 §X mới. |
| Notification routing (subscription, recipient) | Đ37 (Governance Org) vì agency/role là khái niệm Đ37; HOẶC luật mới Đ45 "Universal Event/Signal" | Routing dựa trên agency/role là Đ37; ban hành tách law mới sạch hơn. |
| Directus exposure (collection mapping) | Đ35 (DOT Governance) + Đ43 (System Context) | Directus là exposure infrastructure, được Đ35/Đ43 quản. |
| Display rule (UI semantics) | Đ28 (Display) | Existing law. |
| Worker / pg_cron / advisory lock | HP NT13 + Đ33 (PG/schema) | PG-native infra. |
| Inclusion criteria (governance-significant) | HP nguyên tắc mới hoặc Đ44 § | Critical boundary; cần luật. |
Khuyến nghị: KHÔNG ban hành luật mới ngay. P3D4C0X ghi note "candidate Đ45 Universal Event/Signal Law" rồi quyết sau Phase 2 PoC. Phase 1+2 dùng Đ44 extension (vì IU notification đã thuộc Đ44) cộng với cross-references vào Đ37/Đ35/Đ43.
§L. Directus / Nuxt Exposure Strategy
Đồng bộ với P3D3 Directus exposure design.
| Element | Phase 1 | Phase 2 |
|---|---|---|
| PG view per domain | v_iu_notification_board (existing) |
v_event_universal_board, v_event_alerts_recent, v_dot_runs_recent, v_health_open_issues |
| Directus collection | per-view (auto-discover) | same |
| Per-actor filter | Custom endpoint passing actor_ref (P3D3 §4 recommendation) |
same — universal endpoint /event/board?actor=... |
| Permission per role | Directus role gates | same + RLS optional |
| Nuxt | Reuse existing Directus → assembly screens. Không code mới. | TreeView 3-cột (domain × stream × event) optional; dựa Assembly First |
Generic board + domain filter: Khuyến nghị một board v_event_universal_board(actor) + UI filter bar (domain in ('iu','dot',…), severity in ('warning','critical'), stream). Thay vì N board cứng. Reduce UI surface.
Assembly First compliance: không tạo Nuxt page mới; mọi exposure qua Directus collection được render bởi existing Nuxt assembly machinery (Đ7 Assembly First).
§M. Inclusion Criteria — "Governance-Significant" Definition
Bộ tiêu chí cứng để quyết một fact có được phép vào universal event hay không. Không thoả ≥ 1 → không phát event.
M.1 Inclusion (positive) — phải thoả ≥ 1
- ✅ Lifecycle change của entity được Đ0-G/Đ44 governance. Ví dụ: tạo, draft, approve, version_apply, archive, retire.
- ✅ Approval/review request mà human/AI cần action. Ví dụ: draft_created, approval_requested, dot_apr_required.
- ✅ Health/integrity alert có severity warning/critical. Ví dụ: health_check_alert, red_zone_violation, kg_constraint_violation.
- ✅ DOT execution result (Đ35 NT12 paired DOT). Ví dụ: dot_executed, dot_failed.
- ✅ System milestone (Đ43 lifecycle). Ví dụ: context_pack_built, context_pack_failed.
- ✅ Agency/governance task assigned. Ví dụ: checkpoint_due, approval_decided.
- ✅ Ingest milestone affecting governed entities. Ví dụ: document_imported, indexing_complete.
M.2 Exclusion (negative) — không bao giờ là event
- ❌ Mỗi UPDATE row một column.
- ❌ Telemetry/metrics (CPU, latency, throughput).
- ❌ Audit trail bình thường (đã có audit log riêng nếu cần — tách hệ thống).
- ❌ Debug/trace logs.
- ❌ Read access (no "read" event).
- ❌ Internal worker tick result (worker log riêng đã có).
- ❌ Any change without governance owner / lifecycle.
M.3 Decision rubric (cứng)
Producer hỏi 3 câu:
1. Entity có lifecycle ≥ 2 state, governance owner, identity ổn định? (Đ44 OQC tiêu chí Object)
2. Fact này representing transition of state hoặc requesting human/AI action?
3. Severity ≥ 'info' và actionable hoặc visible cho recipient?
Nếu 3 câu = YES → event qualified.
Nếu thiếu 1 → KHÔNG, dùng audit log/metrics riêng.
M.4 Enforcement seam
CHECK constraint trên event_outbox.event_domain + event_type — chỉ allow combos đã đăng ký vào event_type_registry (Phase 2 config table). Producer tries unknown combo → INSERT fail → trigger drops event silently (ON CONFLICT DO NOTHING semantics nếu dùng), hoặc raise notice (sysop catches).
§N. PG-First Exploitation Checklist
| Layer | PG Feature | Phase 1 evidence |
|---|---|---|
| Fact capture | AFTER INSERT trigger, WHEN predicate |
✅ P3D2 trg_aa_iu_notif_* |
| Pending staging | Table + partial index WHERE processed_at IS NULL |
⏳ P3D4C1 paused |
| Worker | pg_cron, pg_try_advisory_lock, plpgsql function with EXCEPTION block |
⏳ P3D4C1 paused (cron rev3) |
| Durable event | Table + CHECK + partial UNIQUE index for idempotency | ✅ uq_notif_event_type_ref |
| Read state | Table + UNIQUE constraint (event_id, actor_ref) |
✅ uq_notif_read_event_actor |
| Routing | View JOIN subscription | (Phase 2) — not implemented |
| Exposure | View → Directus auto-discover | ✅ design ready (P3D3) |
| Signal | LISTEN/NOTIFY |
(Phase 2 evaluate) |
| Config | dot_config namespace |
partial (notif keys) |
| Lifecycle filter | LEFT JOIN unit_edit_draft in fn_unread |
✅ actionable filter |
| Provenance | columns correlation_id, causation_id, source_*_ref |
(Phase 2) — partial in iu schema |
No external scheduler/tool/service required for Phase 1 + Phase 2.
§O. Recommendation — Pick 1
| Option | Mô tả | Verdict |
|---|---|---|
| UNIVERSAL_FIRST | Build universal trước, migrate IU ngay. | ❌ Risk cao, phá Đ44 front-door API |
| UNIVERSAL_WITH_IU_COMPAT | Build universal cho domain mới; IU compat (variant C-i) → eventual convergence (variant C-ii). | ⭐ CHỌN |
| IU_SPECIFIC_JUSTIFIED | Giữ IU riêng forever. | ❌ Tạo tech debt + duplicate infra |
| PHASED_CONVERGENCE | IU dần converge — tương đương C-ii nhưng trải dài | Acceptable nhưng C bao gồm rồi |
Final recommendation: UNIVERSAL_WITH_IU_COMPAT (tức Option C, C-i Phase 2 → C-ii Phase 3).
Lý do chọn
- Zero hot-path impact trên runtime IU đã ACTIVE.
- Zero breaking change với
fn_iu_*API mà Đ43 context-pack đang advertise. - Cross-domain board khả thi qua
v_event_unifiedview — không cần wait migration. - Migration path C-ii rõ ràng cho Phase 3 sau khi Phase 2 chứng minh stable.
- Operational cost thấp — 1 worker, 1 cron, 1 lock cho mọi domain mới. Reuse pg_cron + advisory_lock pattern đã chạy.
- No external dependency — pg_cron + plpgsql + view + Directus là đủ.
Next pack proposed (chỉ proposal, không dispatch)
P3D4C0Y — Phase 2 PoC scope plan:
- §1 IU compat plan (C-i variant): tạo
event_outbox,event_read,event_subscriptionschemas; dual-write trigger experimental cho 1 domain mới (gợi ý:system_issuestừ Đ22) — chỉ DDL design, chưa apply. - §2 Migration windows + rollback contract.
- §3 Test suite: parity với P3D1+P3D2; cross-domain board test; subscription routing test.
P3D4C0Y phải lại qua GPT/User review trước khi dispatch. P3D4C1 (IU staging+worker) vẫn pause và được đánh giá lại sau P3D4C0Y vì có thể bị thay thế hoặc generalized.
§P. Verification Block
phase_status=PASS
laws_surveyed=13/13
domain_event_matrix=PASS
five_layer_design=PASS
event_envelope_proposed=PASS
recipient_model_proposed=PASS
lifecycle_states_evaluated=PASS
lifecycle_phase1_defined=PASS
grouping_generalized=PASS
signal_channels_mapped=PASS
table_strategy_proposed=PASS
iu_migration_strategy=C
dieu43_deep_assessed=PASS
law_ownership_proposed=PASS
directus_strategy=PASS
inclusion_criteria_defined=PASS
pg_first_checklist=PASS
not_activity_log_boundary=PASS
recommendation=UNIVERSAL_WITH_IU_COMPAT
no_pg_mutation=true
no_directus_mutation=true
no_nuxt_code=true
no_hermes=true
no_codex_dispatch=true
no_external_scheduler=true
no_p3d4c1_resume=true
no_new_runtime_tables=true
no_iu_notification_runtime_change=true
no_body_payload_exposure=true
next_required_pack=P3D4C0Y_UNIVERSAL_PHASE2_POC_SCOPE_PLAN_REVIEW
P3D4C0X rev1 design note | Universal PG-to-outside signal architecture | 5 layers + envelope + recipient + lifecycle + grouping + signal + tables + IU compat + Đ43 + ownership + exposure + inclusion + PG-first | Recommendation: UNIVERSAL_WITH_IU_COMPAT | KB-only, no mutation