KB-5E95

RS4A-09 — Trigger / Gate Side-Effect Closure — 2026-06-21

8 min read Revision 1
rs4atrigger-closurecontext-packactivationclosed-at-registrationP4-closeddesign-only2026-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_tiers AND NEW.status='active'.
  • Live config: dot_config.context_pack_dot_register_watch_tiers = ["A","B","C"]; dot_tools.tier CHECK allows only A/B (live constraint) — so any A/B insert set to active is in-watch.
  • The trigger is SECURITY DEFINER and fires AFTER INSERT OR UPDATE OF status, tier.
  • The side effect is a pg_notify signal 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-consumer stays 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:

  1. Never set status='active' for a watch-tier (A/B/C) row ⇒ the §1 notify condition is not met ⇒ no context_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.
  2. Do not touch dot_config gates. process_dot_runtime.real_run_enabled=false and iu_core.operator_runtime_enabled=false (live) must remain unchanged. Violation ⇒ WOULD_OPEN_GATE.
  3. Do not wire dot_agent_api_contract. Registration creates no agent-API binding.
  4. No context-pack notification at registration — guaranteed structurally by #1 (inert status).
  5. Account for birth side-effects. The duplicate birth_trigger_dot_tools / trg_birth_dot_tools pair 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.
  6. 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 status is 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, currently context_pack_mode='warn' but consumer body unread → treat as activation risk, fail-closed).
  • PO-3 gate-unchanged proof — verifier confirms both dot_config runtime gates remain false.
  • PO-4 no-agent-wire proof — verifier confirms dot_agent_api_contract not 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.