RS4A-09 — Trigger / Gate Side-Effect Closure — 2026-06-21
RS4A-09 — Trigger / Gate Side-Effect Closure — 2026-06-21
Macro: RS4A · Mục tiêu I
Deliverable: 09 of 14 · design-only
Builds on: RS3B-08 + RS3C-09 (13 user triggers + 4 internal FK reconciled) and a live function-body read (this macro) that closes caveat P4 for the activation trigger.
Gate: REGISTRATION_HOLD · REGISTRATION_CAN_PROCEED = NO
Status: TRIGGER_CLOSURE_DEFINED_P4_CONDITION_READ — the activation notify condition is now read from the function body and is conditional; the closed-at-registration invariant is specified with proof obligations. One residual caveat remains: the context_pack_event consumer body was not traced this cycle (kept open as G7-consumer).
1. P4 closed: live fn_context_pack_on_dot_register body
I read the function definition live (pg_get_functiondef, db directus, 2026-06-21). Verbatim logic:
CREATE OR REPLACE FUNCTION public.fn_context_pack_on_dot_register()
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public','pg_catalog'
AS $function$
DECLARE v_tiers JSONB;
BEGIN
SELECT value::jsonb INTO v_tiers FROM dot_config WHERE key='context_pack_dot_register_watch_tiers';
IF v_tiers IS NULL OR jsonb_array_length(v_tiers)=0 THEN RETURN NEW; END IF;
IF EXISTS (SELECT 1 FROM jsonb_array_elements_text(v_tiers) t WHERE t=COALESCE(NEW.tier,''))
AND NEW.status='active' THEN
PERFORM pg_notify('context_pack_event',
json_build_object('kind','dot_register','code',NEW.code,'tier',NEW.tier,'op',TG_OP)::text);
END IF;
RETURN NEW;
END; $function$
Closed facts (P4):
- The notify is conditional, NOT unconditional: it fires only if
NEW.tier ∈ watch_tiersANDNEW.status='active'. - Live config:
dot_config.context_pack_dot_register_watch_tiers = ["A","B","C"];dot_tools.tierCHECK allows onlyA/B(live constraint) — so any A/B insert set toactiveis in-watch. - The trigger is
SECURITY DEFINERand firesAFTER INSERT OR UPDATE OF status, tier. - The side effect is a
pg_notifysignal only (no row write), but it is an automation activation surface. - Consumer mode:
dot_config.context_pack_mode = 'warn'(live) — currently non-activating, but the consumer body was not traced ⇒G7-consumerstays open; do not assert the consumer is inert without reading it.
Therefore (precision per P4): "every active insert certainly emits a notification" is refined to "an insert/update that sets a watch-tier (A/B/C) row to status='active' emits a context_pack_event notification." A non-active (inert) insert does not satisfy the condition and emits nothing. This is exactly the lever the closed-at-registration invariant uses.
2. dot_tools trigger inventory (RS3C-09, reconciled) and registration-relevant side effects
13 user triggers (12 enabled + 1 disabled trg_count_dot_tools) + 4 internal FK constraint triggers (tgisinternal=true). Registration-relevant:
| Trigger | Timing/event | Function | Effect at registration |
|---|---|---|---|
trg_before_birth_gate_dot_tools |
BEFORE INSERT | fn_birth_gate |
may block insert (gate) |
trg_auto_code_dot_tools |
BEFORE INSERT | gen_code_dot_tools |
auto-generates code (overrides registrar's heuristic code, D18) |
trg_normalize_dot_filepath |
BEFORE INSERT/UPDATE OF file_path | fn_normalize_dot_filepath |
normalizes file_path (root cause of D11 dedup mismatch) |
trg_validate_dot_origin_dot_tools |
BEFORE INSERT/UPDATE OF _dot_origin | fn_validate_dot_origin |
origin gate |
birth_trigger_dot_tools + trg_birth_dot_tools |
AFTER INSERT | fn_birth_registry_auto('code') |
duplicate pair — both create a birth-registry row (cross-table, not a 2nd dot_tools row) |
trg_context_pack_dot_register |
AFTER INSERT OR UPDATE OF status,tier | fn_context_pack_on_dot_register |
the activation notify (§1) |
trg_count_dot_tools |
AFTER (statement) | refresh_registry_count |
DISABLED |
Caveat (carried): the BEFORE-INSERT function bodies (
fn_birth_gate,gen_code_dot_tools,fn_normalize_dot_filepath,fn_validate_dot_origin) were not read this cycle; their effects are inferred from name/timing. They are noted as design inputs, not proven internals.
3. Closed-at-Registration invariant (the contract)
A governed registration write MUST satisfy ALL:
- Never set
status='active'for a watch-tier (A/B/C) row ⇒ the §1 notify condition is not met ⇒ nocontext_pack_event. Register an inert/non-active status; activation is a separate Owner-gated UPDATE (RS4A-04 Phase 6) with its own authority nonce. Violation ⇒ACTIVATION_AT_REGISTRATION. - Do not touch
dot_configgates.process_dot_runtime.real_run_enabled=falseandiu_core.operator_runtime_enabled=false(live) must remain unchanged. Violation ⇒WOULD_OPEN_GATE. - Do not wire
dot_agent_api_contract. Registration creates no agent-API binding. - No context-pack notification at registration — guaranteed structurally by #1 (inert status).
- Account for birth side-effects. The duplicate
birth_trigger_dot_tools/trg_birth_dot_toolspair auto-creates a birth-registry row; the contract treats this as expected, idempotent, non-activating; the duplicate must not double-effect in a way that registers a second identity. - Activation is separate — a distinct phase, distinct authority, distinct nonce (RS4A-04 Phase 6; RS4A-07 separate consume).
4. Proof obligations
- PO-1 inert-status proof — post-commit verifier confirms
statusis a non-activating value (RS4A-04 Phase 4). - PO-2 notify-not-emitted proof — by construction (inert status ⇒ §1 condition false); if any active update is unavoidable, must additionally prove the consumer is inert (
G7-consumer, currentlycontext_pack_mode='warn'but consumer body unread → treat as activation risk, fail-closed). - PO-3 gate-unchanged proof — verifier confirms both
dot_configruntime gates remainfalse. - PO-4 no-agent-wire proof — verifier confirms
dot_agent_api_contractnot written. - PO-5 birth-idempotency proof — exactly one birth-registry effect per registration despite the duplicate trigger pair.
5. Adversarial cases (registration activation surface)
| ID | Scenario | Expected | Fail-open signal |
|---|---|---|---|
| TG-01 | registrar inserts status='active', tier B |
REJECT ACTIVATION_AT_REGISTRATION (matches the source defect D05/D19) |
notify emitted at registration |
| TG-02 | registration sets watch-tier active via UPDATE | REJECT — notify fires on UPDATE too (AFTER … UPDATE OF status,tier) |
treating UPDATE as exempt |
| TG-03 | registration flips process_dot_runtime.real_run_enabled |
REJECT WOULD_OPEN_GATE |
gate opened in registration txn |
| TG-04 | registration wires dot_agent_api_contract |
REJECT | agent API wired at registration |
| TG-05 | duplicate birth trigger creates two birth rows | HOLD — prove idempotent single birth effect | double birth effect accepted |
| TG-06 | inert insert, but consumer auto-activates on notify | FAIL_CLOSED until consumer body proven inert (G7-consumer) |
assuming consumer inert without proof |
6. Status
- Trigger closure:
TRIGGER_CLOSURE_DEFINED; P4 closed at the producer (notify condition read live =tier∈watch ∧ status='active'). G7(activation side-effect) fenced by the inert-status invariant;G7-consumer(context_pack consumer body) remains OPEN as a residual activation risk (do not overclaim notification/consumer inertness).- No mutation, no gate flip, no trigger patch. Gate
REGISTRATION_HOLD·CAN_PROCEED = NO.