KB-2968 rev 7
23-P3D2 — Notification Triggers + Board — Execution Prompt (rev7)
36 min read Revision 7
23-P3D2 — Notification Triggers + Board — Execution Prompt (rev7)
Date: 2026-05-08 Status: PROMPT rev7 — SELF-CONTAINED. Chờ final review. CHƯA dispatch. Report: knowledge/dev/laws/dieu44-trien-khai/reports/23-p3d2-notification-triggers-report.md Rev6→Rev7: Fix test order (draft→comment→apply), active object verify, latest_readers evidence. §1 Preflight + §2 Variant A/B: unchanged from rev6, inline below.
Hard Boundaries
- ❌ No P3D1 table/function changes. No Pack 23 function changes.
- ❌ No gateway/birth trigger / vector / LISTEN/NOTIFY / retention / external queue / global read flag / Hermes.
Setup
#!/usr/bin/env bash
CONTAINER="${PG_CONTAINER:-postgres}"
DB="${PG_DB:-directus}"
DBUSER="${PG_USER:-directus}"
PSQL=(docker exec -i "$CONTAINER" psql -U "$DBUSER" -d "$DB" -v ON_ERROR_STOP=1)
PSQL_NOSTOP=(docker exec -i "$CONTAINER" psql -U "$DBUSER" -d "$DB")
TS=$(date +%Y%m%d-%H%M%S)
LOG="/tmp/23-p3d2.${TS}.log"
exec > >(tee -a "$LOG") 2>&1
PREFLIGHT_STATUS=""; FN_STATUS="NOT_RUN"; TEST_FAIL=0; PHASE_STATUS=""
P23_HASHES_BEFORE=""; P23_HASHES_AFTER=""
P3D1_HASHES_BEFORE=""; P3D1_HASHES_AFTER=""
FN_OWNER=""; VERSION_HAS_ACTOR=""
TEST_ADDR="test/p3d2/pilot-${TS}"; TEST_UNIT_ID=""; TEST_DRAFT_ID=""
EVT_EVIDENCE=""; READ_EVIDENCE=""
TEST_EVENT_CLEANUP="NOT_RUN"; FAIL_CLEANUP="NOT_RUN"
ACTIVE_OBJ_CHECK="NOT_RUN"
US=$'\x1f'
echo "=== P3D2 START $TS ==="
§1. Preflight (identical to rev6)
echo "=== PREFLIGHT ==="
for TBL in iu_notification_event iu_notification_read; do
E=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_tables WHERE schemaname='public' AND tablename='$TBL';" | tr -d ' ')
[ "$E" = "1" ] || { PREFLIGHT_STATUS="FAIL"; echo "FAIL: $TBL"; }
done
if [ -z "$PREFLIGHT_STATUS" ]; then
UQ1=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_index i JOIN pg_class c ON c.oid=i.indexrelid WHERE c.relname='uq_notif_event_type_ref' AND i.indisunique=true AND i.indpred IS NOT NULL;" | tr -d ' ')
UQ2=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_index i JOIN pg_class c ON c.oid=i.indexrelid WHERE c.relname='uq_notif_read_event_actor' AND i.indisunique=true;" | tr -d ' ')
for CN in chk_notif_event_type_stream fk_notif_read_event; do
E=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_constraint WHERE conname='$CN';" | tr -d ' ')
[ "$E" = "1" ] || { PREFLIGHT_STATUS="FAIL"; echo "FAIL: $CN"; }
done
CASCADE=$("${PSQL[@]}" -t -A -c "SELECT confdeltype FROM pg_constraint WHERE conname='fk_notif_read_event';")
[ "$UQ1" = "1" ] && [ "$UQ2" = "1" ] && [ "$CASCADE" = "c" ] || { PREFLIGHT_STATUS="FAIL"; echo "FAIL: idx/fk"; }
fi
if [ -z "$PREFLIGHT_STATUS" ]; then
P3D1_HASHES_BEFORE=$("${PSQL[@]}" -t -A -c "SELECT string_agg(p.proname||'='||md5(p.prosrc),'|' ORDER BY p.proname) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public' AND p.proname IN ('fn_iu_unread','fn_iu_mark_read');")
P23_HASHES_BEFORE=$("${PSQL[@]}" -t -A -c "SELECT string_agg(p.proname||'='||md5(p.prosrc),'|' ORDER BY p.proname) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public' AND p.proname IN ('fn_iu_edit_plan','fn_iu_create_edit_draft','fn_iu_comment_edit_draft','fn_iu_comment','fn_iu_apply_edit_draft','fn_iu_edit','fn_iu_create','fn_content_hash','fn_iu_verify_invariants','fn_iu_save');")
P23_COUNT=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public' AND p.proname IN ('fn_iu_edit_plan','fn_iu_create_edit_draft','fn_iu_comment_edit_draft','fn_iu_comment','fn_iu_apply_edit_draft','fn_iu_edit','fn_iu_create','fn_content_hash','fn_iu_verify_invariants','fn_iu_save');" | tr -d ' ')
[ "$P23_COUNT" = "10" ] || { PREFLIGHT_STATUS="FAIL"; echo "FAIL: P23=$P23_COUNT"; }
fi
if [ -z "$PREFLIGHT_STATUS" ]; then
for FN in fn_iu_notif_comment fn_iu_notif_draft fn_iu_notif_version fn_iu_notification_board; do
E=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public' AND p.proname='$FN';" | tr -d ' ')
[ "$E" = "0" ] || { PREFLIGHT_STATUS="FAIL"; echo "FAIL: $FN exists"; }
done
for TRG in trg_aa_iu_notif_comment trg_aa_iu_notif_draft trg_aa_iu_notif_version; do
E=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_trigger WHERE tgname='$TRG' AND NOT tgisinternal;" | tr -d ' ')
[ "$E" = "0" ] || { PREFLIGHT_STATUS="FAIL"; echo "FAIL: $TRG exists"; }
done
fi
if [ -z "$PREFLIGHT_STATUS" ]; then
for CHECK in "unit_edit_comment:id" "unit_edit_comment:unit_id" "unit_edit_comment:author_ref" "unit_edit_comment:comment_kind" "unit_edit_comment:author_type" \
"unit_edit_draft:id" "unit_edit_draft:unit_id" "unit_edit_draft:created_by" "unit_edit_draft:canonical_address" "unit_edit_draft:draft_status" \
"unit_version:id" "unit_version:unit_id" "unit_version:version_seq"; do
TBL=${CHECK%%:*}; COL=${CHECK##*:}
E=$("${PSQL[@]}" -t -c "SELECT count(*) FROM information_schema.columns WHERE table_schema='public' AND table_name='$TBL' AND column_name='$COL';" | tr -d ' ')
[ "$E" = "1" ] || { PREFLIGHT_STATUS="FAIL"; echo "FAIL: $TBL.$COL"; }
done
VERSION_HAS_ACTOR=$("${PSQL[@]}" -t -c "SELECT count(*) FROM information_schema.columns WHERE table_schema='public' AND table_name='unit_version' AND column_name='created_by';" | tr -d ' ')
if [ "$VERSION_HAS_ACTOR" = "0" ]; then
FB=$("${PSQL[@]}" -t -c "SELECT count(*) FROM information_schema.columns WHERE table_schema='public' AND table_name='unit_edit_draft' AND column_name='applied_by';" | tr -d ' ')
[ "$FB" = "1" ] || { PREFLIGHT_STATUS="FAIL"; echo "FAIL: no fallback"; }
fi
fi
if [ -z "$PREFLIGHT_STATUS" ]; then
POL=$("${PSQL[@]}" -t -A -c "SELECT value FROM dot_config WHERE key='iu_edit.policy.default_mode';")
[ "$POL" = "require_review" ] || { PREFLIGHT_STATUS="FAIL"; echo "FAIL: policy=$POL"; }
FN_OWNER=$("${PSQL[@]}" -t -A -c "SELECT proowner::regrole FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public' AND p.proname='fn_iu_save';")
[ -n "$FN_OWNER" ] || { PREFLIGHT_STATUS="FAIL"; echo "FAIL: owner"; }
fi
if [ -z "$PREFLIGHT_STATUS" ]; then PREFLIGHT_STATUS="PASS"; fi
echo "PREFLIGHT=$PREFLIGHT_STATUS"
§2A. VARIANT A (unit_version.created_by EXISTS) — identical to rev6
if [ "$PREFLIGHT_STATUS" = "PASS" ] && [ "$VERSION_HAS_ACTOR" = "1" ]; then
echo "=== CREATE VARIANT A ==="
FN_EXIT=0
"${PSQL[@]}" -v fn_owner="$FN_OWNER" <<'FNSQL' || FN_EXIT=$?
BEGIN;
SELECT set_config('app.p3d2_owner', :'fn_owner', true);
CREATE FUNCTION public.fn_iu_notif_comment() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,public AS $$
BEGIN
IF NEW.comment_kind='system' OR NEW.author_type='system' THEN RETURN NEW; END IF;
INSERT INTO public.iu_notification_event(event_type,event_stream,unit_id,canonical_address,ref_id,actor_ref,source)
VALUES('comment_added','comment',NEW.unit_id,(SELECT canonical_address FROM public.information_unit WHERE id=NEW.unit_id),NEW.id,NEW.author_ref,'trg_aa_iu_notif_comment') ON CONFLICT DO NOTHING;
RETURN NEW;
END;$$;
CREATE FUNCTION public.fn_iu_notif_draft() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,public AS $$
BEGIN
IF NEW.draft_status != 'open' THEN RETURN NEW; END IF;
INSERT INTO public.iu_notification_event(event_type,event_stream,unit_id,canonical_address,ref_id,actor_ref,source)
VALUES('draft_created','review',NEW.unit_id,NEW.canonical_address,NEW.id,NEW.created_by,'trg_aa_iu_notif_draft') ON CONFLICT DO NOTHING;
RETURN NEW;
END;$$;
CREATE FUNCTION public.fn_iu_notif_version() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,public AS $$
BEGIN
IF NEW.version_seq <= 1 THEN RETURN NEW; END IF;
INSERT INTO public.iu_notification_event(event_type,event_stream,unit_id,canonical_address,ref_id,actor_ref,source)
VALUES('version_applied','update',NEW.unit_id,(SELECT canonical_address FROM public.information_unit WHERE id=NEW.unit_id),NEW.id,NEW.created_by,'trg_aa_iu_notif_version') ON CONFLICT DO NOTHING;
RETURN NEW;
END;$$;
CREATE FUNCTION public.fn_iu_notification_board(p_actor text DEFAULT NULL,p_stream text DEFAULT NULL,p_limit integer DEFAULT 50) RETURNS SETOF jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path=pg_catalog,public AS $fn$
DECLARE v_lim int;
BEGIN
IF p_stream IS NOT NULL AND p_stream NOT IN ('comment','review','update') THEN RETURN; END IF;
v_lim := GREATEST(1,LEAST(COALESCE(p_limit,50),500));
RETURN QUERY SELECT jsonb_build_object('event_id',e.id,'event_type',e.event_type,'stream',e.event_stream,'unit_id',e.unit_id,'address',e.canonical_address,'ref_id',e.ref_id,'actor',e.actor_ref,'created_at',e.created_at,
'is_read_for_actor',CASE WHEN p_actor IS NULL THEN NULL WHEN e.actor_ref=p_actor THEN true ELSE EXISTS(SELECT 1 FROM public.iu_notification_read r WHERE r.event_id=e.id AND r.actor_ref=p_actor) END,
'unread_for_actor',CASE WHEN p_actor IS NULL THEN NULL WHEN e.actor_ref=p_actor THEN false ELSE NOT EXISTS(SELECT 1 FROM public.iu_notification_read r WHERE r.event_id=e.id AND r.actor_ref=p_actor) END,
'read_status_source',CASE WHEN p_actor IS NULL THEN 'not_applicable' WHEN e.actor_ref=p_actor THEN 'implicit_self' WHEN EXISTS(SELECT 1 FROM public.iu_notification_read r WHERE r.event_id=e.id AND r.actor_ref=p_actor) THEN 'explicit_read' ELSE 'unread' END,
'read_at_for_actor',(SELECT r.read_at FROM public.iu_notification_read r WHERE r.event_id=e.id AND r.actor_ref=p_actor LIMIT 1),
'latest_readers',COALESCE((SELECT jsonb_agg(jsonb_build_object('actor',r2.actor_ref,'read_at',r2.read_at) ORDER BY r2.read_at DESC) FROM (SELECT actor_ref,read_at FROM public.iu_notification_read WHERE event_id=e.id ORDER BY read_at DESC LIMIT 5) r2),'[]'::jsonb),
'next_action',CASE e.event_type WHEN 'draft_created' THEN 'fn_iu_apply_edit_draft' WHEN 'comment_added' THEN 'inspect_comment' WHEN 'version_applied' THEN 'inspect_version' END,
'guidance',CASE e.event_type WHEN 'draft_created' THEN 'Draft awaiting review.' WHEN 'comment_added' THEN 'New comment.' WHEN 'version_applied' THEN 'Content updated.' END
) FROM public.iu_notification_event e LEFT JOIN public.unit_edit_draft d ON e.event_type='draft_created' AND d.id=e.ref_id
WHERE (p_stream IS NULL OR e.event_stream=p_stream) AND (e.event_type!='draft_created' OR d.draft_status='open')
ORDER BY e.created_at DESC LIMIT v_lim;
END;$fn$;
CREATE TRIGGER trg_aa_iu_notif_comment AFTER INSERT ON public.unit_edit_comment FOR EACH ROW EXECUTE FUNCTION public.fn_iu_notif_comment();
CREATE TRIGGER trg_aa_iu_notif_draft AFTER INSERT ON public.unit_edit_draft FOR EACH ROW EXECUTE FUNCTION public.fn_iu_notif_draft();
CREATE TRIGGER trg_aa_iu_notif_version AFTER INSERT ON public.unit_version FOR EACH ROW EXECUTE FUNCTION public.fn_iu_notif_version();
REVOKE ALL ON FUNCTION public.fn_iu_notif_comment() FROM PUBLIC;
REVOKE ALL ON FUNCTION public.fn_iu_notif_draft() FROM PUBLIC;
REVOKE ALL ON FUNCTION public.fn_iu_notif_version() FROM PUBLIC;
REVOKE ALL ON FUNCTION public.fn_iu_notification_board(text,text,integer) FROM PUBLIC;
DO $$ DECLARE v_owner text := current_setting('app.p3d2_owner');
BEGIN EXECUTE format('ALTER FUNCTION public.fn_iu_notif_comment() OWNER TO %I',v_owner);
EXECUTE format('ALTER FUNCTION public.fn_iu_notif_draft() OWNER TO %I',v_owner);
EXECUTE format('ALTER FUNCTION public.fn_iu_notif_version() OWNER TO %I',v_owner);
EXECUTE format('ALTER FUNCTION public.fn_iu_notification_board(text,text,integer) OWNER TO %I',v_owner);
END$$;
COMMIT;
FNSQL
echo "FN_EXIT=$FN_EXIT"; [ "$FN_EXIT" = "0" ] && FN_STATUS="OK" || { FN_STATUS="FAIL"; PHASE_STATUS="FAIL"; }
fi
§2B. VARIANT B (unit_version.created_by ABSENT) — identical to rev6
if [ "$PREFLIGHT_STATUS" = "PASS" ] && [ "$VERSION_HAS_ACTOR" = "0" ]; then
echo "=== CREATE VARIANT B ==="
FN_EXIT=0
"${PSQL[@]}" -v fn_owner="$FN_OWNER" <<'FNSQL' || FN_EXIT=$?
BEGIN;
SELECT set_config('app.p3d2_owner', :'fn_owner', true);
CREATE FUNCTION public.fn_iu_notif_comment() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,public AS $$
BEGIN
IF NEW.comment_kind='system' OR NEW.author_type='system' THEN RETURN NEW; END IF;
INSERT INTO public.iu_notification_event(event_type,event_stream,unit_id,canonical_address,ref_id,actor_ref,source)
VALUES('comment_added','comment',NEW.unit_id,(SELECT canonical_address FROM public.information_unit WHERE id=NEW.unit_id),NEW.id,NEW.author_ref,'trg_aa_iu_notif_comment') ON CONFLICT DO NOTHING;
RETURN NEW;
END;$$;
CREATE FUNCTION public.fn_iu_notif_draft() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,public AS $$
BEGIN
IF NEW.draft_status != 'open' THEN RETURN NEW; END IF;
INSERT INTO public.iu_notification_event(event_type,event_stream,unit_id,canonical_address,ref_id,actor_ref,source)
VALUES('draft_created','review',NEW.unit_id,NEW.canonical_address,NEW.id,NEW.created_by,'trg_aa_iu_notif_draft') ON CONFLICT DO NOTHING;
RETURN NEW;
END;$$;
CREATE FUNCTION public.fn_iu_notif_version() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,public AS $$
BEGIN
IF NEW.version_seq <= 1 THEN RETURN NEW; END IF;
INSERT INTO public.iu_notification_event(event_type,event_stream,unit_id,canonical_address,ref_id,actor_ref,source)
VALUES('version_applied','update',NEW.unit_id,(SELECT canonical_address FROM public.information_unit WHERE id=NEW.unit_id),NEW.id,
COALESCE((SELECT d.applied_by FROM public.unit_edit_draft d WHERE d.applied_version_ref=NEW.id LIMIT 1),'system:apply'),
'trg_aa_iu_notif_version') ON CONFLICT DO NOTHING;
RETURN NEW;
END;$$;
CREATE FUNCTION public.fn_iu_notification_board(p_actor text DEFAULT NULL,p_stream text DEFAULT NULL,p_limit integer DEFAULT 50) RETURNS SETOF jsonb LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path=pg_catalog,public AS $fn$
DECLARE v_lim int;
BEGIN
IF p_stream IS NOT NULL AND p_stream NOT IN ('comment','review','update') THEN RETURN; END IF;
v_lim := GREATEST(1,LEAST(COALESCE(p_limit,50),500));
RETURN QUERY SELECT jsonb_build_object('event_id',e.id,'event_type',e.event_type,'stream',e.event_stream,'unit_id',e.unit_id,'address',e.canonical_address,'ref_id',e.ref_id,'actor',e.actor_ref,'created_at',e.created_at,
'is_read_for_actor',CASE WHEN p_actor IS NULL THEN NULL WHEN e.actor_ref=p_actor THEN true ELSE EXISTS(SELECT 1 FROM public.iu_notification_read r WHERE r.event_id=e.id AND r.actor_ref=p_actor) END,
'unread_for_actor',CASE WHEN p_actor IS NULL THEN NULL WHEN e.actor_ref=p_actor THEN false ELSE NOT EXISTS(SELECT 1 FROM public.iu_notification_read r WHERE r.event_id=e.id AND r.actor_ref=p_actor) END,
'read_status_source',CASE WHEN p_actor IS NULL THEN 'not_applicable' WHEN e.actor_ref=p_actor THEN 'implicit_self' WHEN EXISTS(SELECT 1 FROM public.iu_notification_read r WHERE r.event_id=e.id AND r.actor_ref=p_actor) THEN 'explicit_read' ELSE 'unread' END,
'read_at_for_actor',(SELECT r.read_at FROM public.iu_notification_read r WHERE r.event_id=e.id AND r.actor_ref=p_actor LIMIT 1),
'latest_readers',COALESCE((SELECT jsonb_agg(jsonb_build_object('actor',r2.actor_ref,'read_at',r2.read_at) ORDER BY r2.read_at DESC) FROM (SELECT actor_ref,read_at FROM public.iu_notification_read WHERE event_id=e.id ORDER BY read_at DESC LIMIT 5) r2),'[]'::jsonb),
'next_action',CASE e.event_type WHEN 'draft_created' THEN 'fn_iu_apply_edit_draft' WHEN 'comment_added' THEN 'inspect_comment' WHEN 'version_applied' THEN 'inspect_version' END,
'guidance',CASE e.event_type WHEN 'draft_created' THEN 'Draft awaiting review.' WHEN 'comment_added' THEN 'New comment.' WHEN 'version_applied' THEN 'Content updated.' END
) FROM public.iu_notification_event e LEFT JOIN public.unit_edit_draft d ON e.event_type='draft_created' AND d.id=e.ref_id
WHERE (p_stream IS NULL OR e.event_stream=p_stream) AND (e.event_type!='draft_created' OR d.draft_status='open')
ORDER BY e.created_at DESC LIMIT v_lim;
END;$fn$;
CREATE TRIGGER trg_aa_iu_notif_comment AFTER INSERT ON public.unit_edit_comment FOR EACH ROW EXECUTE FUNCTION public.fn_iu_notif_comment();
CREATE TRIGGER trg_aa_iu_notif_draft AFTER INSERT ON public.unit_edit_draft FOR EACH ROW EXECUTE FUNCTION public.fn_iu_notif_draft();
CREATE TRIGGER trg_aa_iu_notif_version AFTER INSERT ON public.unit_version FOR EACH ROW EXECUTE FUNCTION public.fn_iu_notif_version();
REVOKE ALL ON FUNCTION public.fn_iu_notif_comment() FROM PUBLIC;
REVOKE ALL ON FUNCTION public.fn_iu_notif_draft() FROM PUBLIC;
REVOKE ALL ON FUNCTION public.fn_iu_notif_version() FROM PUBLIC;
REVOKE ALL ON FUNCTION public.fn_iu_notification_board(text,text,integer) FROM PUBLIC;
DO $$ DECLARE v_owner text := current_setting('app.p3d2_owner');
BEGIN EXECUTE format('ALTER FUNCTION public.fn_iu_notif_comment() OWNER TO %I',v_owner);
EXECUTE format('ALTER FUNCTION public.fn_iu_notif_draft() OWNER TO %I',v_owner);
EXECUTE format('ALTER FUNCTION public.fn_iu_notif_version() OWNER TO %I',v_owner);
EXECUTE format('ALTER FUNCTION public.fn_iu_notification_board(text,text,integer) OWNER TO %I',v_owner);
END$$;
COMMIT;
FNSQL
echo "FN_EXIT=$FN_EXIT"; [ "$FN_EXIT" = "0" ] && FN_STATUS="OK" || { FN_STATUS="FAIL"; PHASE_STATUS="FAIL"; }
fi
§3. Pilot + Tests (rev7: draft → comment → apply order)
if [ "$FN_STATUS" = "OK" ]; then
echo "=== PILOT ==="
# Birth (T9)
BIRTH_S=$("${PSQL[@]}" -t -A -c "SELECT (public.fn_iu_save('$TEST_ADDR','P3D2 pilot $TS','agent:p3d2'))->>'status';")
TEST_UNIT_ID=$("${PSQL[@]}" -t -A -c "SELECT id FROM information_unit WHERE canonical_address='$TEST_ADDR';")
BIRTH_VER=$("${PSQL[@]}" -t -c "SELECT count(*) FROM iu_notification_event WHERE event_type='version_applied' AND unit_id='$TEST_UNIT_ID';" | tr -d ' ')
# rev7-F1: Create draft FIRST (before comment test)
TEST_DRAFT_ID=$("${PSQL[@]}" -t -A -c "SELECT (public.fn_iu_save('$TEST_ADDR','P3D2 draft $TS','agent:opus',NULL,NULL,'draft'))->>'draft_id';")
echo "PILOT: uid=$TEST_UNIT_ID draft=$TEST_DRAFT_ID"
echo "=== TESTS ==="
# T1-T4: Structure + hashes (same as rev6)
T1="PASS"
for SIG in "public.fn_iu_notif_comment()" "public.fn_iu_notif_draft()" "public.fn_iu_notif_version()" "public.fn_iu_notification_board(text,text,integer)"; do
R=$("${PSQL[@]}" -t -A -c "SELECT to_regprocedure('$SIG');"); [ -n "$R" ] && [ "$R" != "-" ] || { T1="FAIL_$SIG"; TEST_FAIL=$((TEST_FAIL+1)); }
done; echo "T1=$T1"
T2="PASS"
for PAIR in "trg_aa_iu_notif_comment:unit_edit_comment" "trg_aa_iu_notif_draft:unit_edit_draft" "trg_aa_iu_notif_version:unit_version"; do
TRG=${PAIR%%:*}; TBL=${PAIR##*:}
E=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_trigger t JOIN pg_class c ON t.tgrelid=c.oid WHERE t.tgname='$TRG' AND c.relname='$TBL' AND NOT t.tgisinternal;" | tr -d ' ')
[ "$E" = "1" ] || { T2="FAIL_$TRG"; TEST_FAIL=$((TEST_FAIL+1)); }
done; echo "T2=$T2"
P23_HASHES_AFTER=$("${PSQL[@]}" -t -A -c "SELECT string_agg(p.proname||'='||md5(p.prosrc),'|' ORDER BY p.proname) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public' AND p.proname IN ('fn_iu_edit_plan','fn_iu_create_edit_draft','fn_iu_comment_edit_draft','fn_iu_comment','fn_iu_apply_edit_draft','fn_iu_edit','fn_iu_create','fn_content_hash','fn_iu_verify_invariants','fn_iu_save');")
[ "$P23_HASHES_AFTER" = "$P23_HASHES_BEFORE" ] && echo "T3=PASS" || { echo "T3=FAIL"; TEST_FAIL=$((TEST_FAIL+1)); }
P3D1_HASHES_AFTER=$("${PSQL[@]}" -t -A -c "SELECT string_agg(p.proname||'='||md5(p.prosrc),'|' ORDER BY p.proname) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public' AND p.proname IN ('fn_iu_unread','fn_iu_mark_read');")
[ "$P3D1_HASHES_AFTER" = "$P3D1_HASHES_BEFORE" ] && echo "T4=PASS" || { echo "T4=FAIL"; TEST_FAIL=$((TEST_FAIL+1)); }
# T7: Draft event (already created above — verify event exists)
DE_COUNT=$("${PSQL[@]}" -t -c "SELECT count(*) FROM iu_notification_event WHERE event_type='draft_created' AND ref_id='$TEST_DRAFT_ID';" | tr -d ' ')
[ "$DE_COUNT" = "1" ] && [ -n "$TEST_DRAFT_ID" ] && echo "T7=PASS" || { echo "T7=FAIL de=$DE_COUNT did=$TEST_DRAFT_ID"; TEST_FAIL=$((TEST_FAIL+1)); }
# T5: Comment event — rev7-F1: on existing open draft with explicit context
CE_PRE=$("${PSQL[@]}" -t -c "SELECT count(*) FROM iu_notification_event WHERE event_type='comment_added' AND unit_id='$TEST_UNIT_ID';" | tr -d ' ')
T5_RAW=$("${PSQL[@]}" -t -A -c "SELECT (public.fn_iu_comment('$TEST_ADDR','agent:opus','P3D2 comment','general','agent',jsonb_build_object('draft_id','$TEST_DRAFT_ID')))->>'status';")
CE_POST=$("${PSQL[@]}" -t -c "SELECT count(*) FROM iu_notification_event WHERE event_type='comment_added' AND unit_id='$TEST_UNIT_ID';" | tr -d ' ')
echo "T5: status=$T5_RAW ce=$CE_PRE→$CE_POST"
if [ "$T5_RAW" = "comment_added" ] || [ "$CE_POST" = "$((CE_PRE+1))" ]; then
T5_REF=$("${PSQL[@]}" -t -A -c "SELECT ref_id FROM iu_notification_event WHERE event_type='comment_added' AND unit_id='$TEST_UNIT_ID' ORDER BY created_at DESC LIMIT 1;")
if [ -n "$T5_REF" ]; then
T5_EX=$("${PSQL[@]}" -t -c "SELECT count(*) FROM unit_edit_comment WHERE id='$T5_REF';" | tr -d ' ')
[ "$T5_EX" = "1" ] && echo "T5=PASS" || { echo "T5=FAIL ref_not_found"; TEST_FAIL=$((TEST_FAIL+1)); }
else echo "T5=FAIL ref_empty"; TEST_FAIL=$((TEST_FAIL+1)); fi
else echo "T5=FAIL status=$T5_RAW"; TEST_FAIL=$((TEST_FAIL+1)); fi
# T8+T6: Apply + system comment suppression
CE_PRE_AP=$("${PSQL[@]}" -t -c "SELECT count(*) FROM iu_notification_event WHERE event_type='comment_added' AND unit_id='$TEST_UNIT_ID';" | tr -d ' ')
VE_PRE=$("${PSQL[@]}" -t -c "SELECT count(*) FROM iu_notification_event WHERE event_type='version_applied' AND unit_id='$TEST_UNIT_ID';" | tr -d ' ')
"${PSQL[@]}" -c "SELECT public.fn_iu_apply_edit_draft('$TEST_DRAFT_ID','reviewer:gpt','P3D2 approve');" >/dev/null
CE_POST_AP=$("${PSQL[@]}" -t -c "SELECT count(*) FROM iu_notification_event WHERE event_type='comment_added' AND unit_id='$TEST_UNIT_ID';" | tr -d ' ')
VE_POST=$("${PSQL[@]}" -t -c "SELECT count(*) FROM iu_notification_event WHERE event_type='version_applied' AND unit_id='$TEST_UNIT_ID';" | tr -d ' ')
[ "$VE_POST" = "$((VE_PRE+1))" ] && echo "T8=PASS" || { echo "T8=FAIL"; TEST_FAIL=$((TEST_FAIL+1)); }
[ "$CE_POST_AP" = "$CE_PRE_AP" ] && echo "T6=PASS" || { echo "T6=FAIL"; TEST_FAIL=$((TEST_FAIL+1)); }
# T9: Birth
[ "$BIRTH_VER" = "0" ] && echo "T9=PASS" || { echo "T9=FAIL=$BIRTH_VER"; TEST_FAIL=$((TEST_FAIL+1)); }
# T10: Actionable
T10=$("${PSQL[@]}" -t -A -c "SELECT count(*) FROM fn_iu_unread('gpt','review') AS u(j) WHERE j->>'ref_id'='$TEST_DRAFT_ID';")
[ "$T10" = "0" ] && echo "T10=PASS" || { echo "T10=FAIL"; TEST_FAIL=$((TEST_FAIL+1)); }
# T11: Per-actor read isolation
T11_GPT=$("${PSQL[@]}" -t -A -c "SELECT count(*) FROM fn_iu_unread('gpt') AS u(j) WHERE j->>'address'='$TEST_ADDR';")
if [ "$T11_GPT" -ge "1" ]; then
"${PSQL[@]}" -c "WITH u AS (SELECT j FROM fn_iu_unread('gpt') AS f(j) WHERE j->>'address'='$TEST_ADDR'),ids AS (SELECT array_agg((j->>'event_id')::uuid) AS a FROM u) SELECT fn_iu_mark_read((SELECT a FROM ids),'gpt');" >/dev/null 2>&1
T11_GPT_A=$("${PSQL[@]}" -t -A -c "SELECT count(*) FROM fn_iu_unread('gpt') AS u(j) WHERE j->>'address'='$TEST_ADDR';")
T11_CDX=$("${PSQL[@]}" -t -A -c "SELECT count(*) FROM fn_iu_unread('agent:codex') AS u(j) WHERE j->>'address'='$TEST_ADDR';")
[ "$T11_GPT_A" = "0" ] && [ "$T11_CDX" -ge "1" ] && echo "T11=PASS" || { echo "T11=FAIL"; TEST_FAIL=$((TEST_FAIL+1)); }
else echo "T11=FAIL gpt=0"; TEST_FAIL=$((TEST_FAIL+1)); fi
# T12: Self-exclusion via board
T12_SRC=$("${PSQL[@]}" -t -A -c "SELECT j->>'read_status_source' FROM fn_iu_notification_board('agent:opus') AS u(j) WHERE j->>'address'='$TEST_ADDR' AND j->>'actor'='agent:opus' LIMIT 1;")
T12_UNR=$("${PSQL[@]}" -t -A -c "SELECT j->>'unread_for_actor' FROM fn_iu_notification_board('agent:opus') AS u(j) WHERE j->>'address'='$TEST_ADDR' AND j->>'actor'='agent:opus' LIMIT 1;")
[ "$T12_SRC" = "implicit_self" ] && [ "$T12_UNR" = "false" ] && echo "T12=PASS" || { echo "T12=FAIL src=$T12_SRC"; TEST_FAIL=$((TEST_FAIL+1)); }
# T13: Idempotency
LAST_REF=$("${PSQL[@]}" -t -A -c "SELECT ref_id FROM iu_notification_event WHERE event_type='comment_added' AND unit_id='$TEST_UNIT_ID' ORDER BY created_at DESC LIMIT 1;")
if [ -n "$LAST_REF" ]; then
T13_PRE=$("${PSQL[@]}" -t -c "SELECT count(*) FROM iu_notification_event WHERE event_type='comment_added' AND ref_id='$LAST_REF';" | tr -d ' ')
"${PSQL_NOSTOP[@]}" -c "INSERT INTO iu_notification_event(event_type,event_stream,unit_id,canonical_address,ref_id,actor_ref,source) VALUES('comment_added','comment','$TEST_UNIT_ID','$TEST_ADDR','$LAST_REF','test','test') ON CONFLICT DO NOTHING;" 2>/dev/null
T13_POST=$("${PSQL[@]}" -t -c "SELECT count(*) FROM iu_notification_event WHERE event_type='comment_added' AND ref_id='$LAST_REF';" | tr -d ' ')
[ "$T13_POST" = "$T13_PRE" ] && [ "$T13_PRE" = "1" ] && echo "T13=PASS" || { echo "T13=FAIL"; TEST_FAIL=$((TEST_FAIL+1)); }
else echo "T13=FAIL ref_empty"; TEST_FAIL=$((TEST_FAIL+1)); fi
# T14: Source safety + board read-only
T14="PASS"
for FN in fn_iu_notif_comment fn_iu_notif_draft fn_iu_notif_version; do
PROBE=$("${PSQL[@]}" -t -A -F "$US" <<EOSQL
SELECT prosrc ~* 'insert[[:space:]]+into[[:space:]]+(public\.)?iu_notification_event',prosrc ~* 'insert[[:space:]]+into[[:space:]]+(public\.)?(information_unit|unit_version|unit_edit_comment|unit_edit_draft|iu_notification_read)',prosrc ~* 'update[[:space:]]+(public\.)?(information_unit|unit_version)',prosrc ~* 'delete[[:space:]]+from',prosrc ~* 'canonical_writer'
FROM pg_proc WHERE proname='$FN' AND pronamespace='public'::regnamespace;
EOSQL
)
P1=$(echo "$PROBE"|cut -d"$US" -f1);P2=$(echo "$PROBE"|cut -d"$US" -f2);P3=$(echo "$PROBE"|cut -d"$US" -f3);P4=$(echo "$PROBE"|cut -d"$US" -f4);P5=$(echo "$PROBE"|cut -d"$US" -f5)
[ "$P1" = "t" ] && [ "$P2" = "f" ] && [ "$P3" = "f" ] && [ "$P4" = "f" ] && [ "$P5" = "f" ] || { T14="FAIL_$FN"; TEST_FAIL=$((TEST_FAIL+1)); }
done
BOARD_PROBE=$("${PSQL[@]}" -t -A -F "$US" <<'EOSQL'
SELECT prosrc ~* 'insert[[:space:]]+into',prosrc ~* 'update[[:space:]]',prosrc ~* 'delete[[:space:]]+from',prosrc ~* 'canonical_writer'
FROM pg_proc WHERE proname='fn_iu_notification_board' AND pronamespace='public'::regnamespace;
EOSQL
)
B1=$(echo "$BOARD_PROBE"|cut -d"$US" -f1);B2=$(echo "$BOARD_PROBE"|cut -d"$US" -f2);B3=$(echo "$BOARD_PROBE"|cut -d"$US" -f3);B4=$(echo "$BOARD_PROBE"|cut -d"$US" -f4)
[ "$B1" = "f" ] && [ "$B2" = "f" ] && [ "$B3" = "f" ] && [ "$B4" = "f" ] || { T14="FAIL_board"; TEST_FAIL=$((TEST_FAIL+1)); }
echo "T14=$T14"
# T15: Security
T15="PASS"; T15_D=""
for SIG in "fn_iu_notif_comment()" "fn_iu_notif_draft()" "fn_iu_notif_version()" "fn_iu_notification_board(text,text,integer)"; do
META=$("${PSQL[@]}" -t -A -F "$US" -c "SELECT prosecdef,proconfig,proowner::regrole FROM pg_proc WHERE oid=to_regprocedure('public.$SIG');")
SD=$(echo "$META"|cut -d"$US" -f1);PC=$(echo "$META"|cut -d"$US" -f2);OWN=$(echo "$META"|cut -d"$US" -f3)
[ "$SD" = "t" ] || T15_D="${T15_D}SD_$SIG "; echo "$PC"|grep -q "pg_catalog" || T15_D="${T15_D}SP_$SIG "; [ "$OWN" = "$FN_OWNER" ] || T15_D="${T15_D}OWN_$SIG "
done
PUB=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_proc p LEFT JOIN LATERAL aclexplode(p.proacl) x ON true WHERE p.proname IN('fn_iu_notif_comment','fn_iu_notif_draft','fn_iu_notif_version','fn_iu_notification_board') AND p.pronamespace='public'::regnamespace AND x.grantee=0 AND x.privilege_type='EXECUTE';" | tr -d ' ')
[ "$PUB" != "0" ] && T15_D="${T15_D}PUB "
[ -n "$T15_D" ] && { T15="FAIL"; TEST_FAIL=$((TEST_FAIL+1)); }
echo "T15=$T15 $T15_D"
# T16: Board read_status_source + latest_readers (rev7-F6)
T16_GPT=$("${PSQL[@]}" -t -A -c "SELECT j->>'read_status_source' FROM fn_iu_notification_board('gpt') AS u(j) WHERE j->>'address'='$TEST_ADDR' AND j->>'read_status_source'='explicit_read' LIMIT 1;")
T16_READERS=$("${PSQL[@]}" -t -A -c "SELECT j->>'latest_readers' FROM fn_iu_notification_board('gpt') AS u(j) WHERE j->>'address'='$TEST_ADDR' AND j->>'read_status_source'='explicit_read' LIMIT 1;")
T16_CDX=$("${PSQL[@]}" -t -A -c "SELECT j->>'read_status_source' FROM fn_iu_notification_board('agent:codex') AS u(j) WHERE j->>'address'='$TEST_ADDR' AND (j->>'unread_for_actor')::boolean=true LIMIT 1;")
T16_HAS_GPT="false"; echo "$T16_READERS"|grep -q '"gpt"' && T16_HAS_GPT="true"
[ "$T16_GPT" = "explicit_read" ] && [ "$T16_HAS_GPT" = "true" ] && [ "$T16_CDX" = "unread" ] && echo "T16=PASS" || { echo "T16=FAIL gpt=$T16_GPT has=$T16_HAS_GPT cdx=$T16_CDX"; TEST_FAIL=$((TEST_FAIL+1)); }
echo "TEST_FAIL=$TEST_FAIL"
fi
§4. Cleanup + Final
# Evidence before cleanup
if [ "$FN_STATUS" = "OK" ] && [ -z "$PHASE_STATUS" ]; then
EVT_EVIDENCE=$("${PSQL[@]}" -t -A -c "SELECT jsonb_build_object('total',(SELECT count(*) FROM iu_notification_event WHERE canonical_address='$TEST_ADDR'),'comment',(SELECT count(*) FROM iu_notification_event WHERE event_type='comment_added' AND canonical_address='$TEST_ADDR'),'draft',(SELECT count(*) FROM iu_notification_event WHERE event_type='draft_created' AND canonical_address='$TEST_ADDR'),'version',(SELECT count(*) FROM iu_notification_event WHERE event_type='version_applied' AND canonical_address='$TEST_ADDR'));")
READ_EVIDENCE=$("${PSQL[@]}" -t -A -c "SELECT jsonb_build_object('total_read',(SELECT count(*) FROM iu_notification_read WHERE event_id IN (SELECT id FROM iu_notification_event WHERE canonical_address='$TEST_ADDR')),'gpt_read',(SELECT count(*) FROM iu_notification_read r JOIN iu_notification_event e ON e.id=r.event_id WHERE e.canonical_address='$TEST_ADDR' AND r.actor_ref='gpt'));")
echo "EVT=$EVT_EVIDENCE READ=$READ_EVIDENCE"
fi
# Cleanup on PASS
if [ "$TEST_FAIL" = "0" ] && [ "$FN_STATUS" = "OK" ] && [ -z "$PHASE_STATUS" ]; then
echo "=== CLEANUP ==="
TEST_EVT_IDS=$("${PSQL[@]}" -t -A -c "SELECT COALESCE(array_agg(id),'{}'::uuid[]) FROM iu_notification_event WHERE canonical_address='$TEST_ADDR';")
"${PSQL[@]}" -c "DELETE FROM iu_notification_read WHERE event_id=ANY('$TEST_EVT_IDS'::uuid[]);"
"${PSQL[@]}" -c "DELETE FROM iu_notification_event WHERE canonical_address='$TEST_ADDR';"
REM_E=$("${PSQL[@]}" -t -c "SELECT count(*) FROM iu_notification_event WHERE canonical_address='$TEST_ADDR';" | tr -d ' ')
REM_R=$("${PSQL[@]}" -t -c "SELECT count(*) FROM iu_notification_read WHERE event_id=ANY('$TEST_EVT_IDS'::uuid[]);" | tr -d ' ')
[ "$REM_E" = "0" ] && [ "$REM_R" = "0" ] && TEST_EVENT_CLEANUP="PASS" || { TEST_EVENT_CLEANUP="FAIL"; PHASE_STATUS="FAIL"; }
echo "TEST_EVENT_CLEANUP=$TEST_EVENT_CLEANUP"
fi
# Fail cleanup
if { [ "$TEST_FAIL" != "0" ] || [ "$PHASE_STATUS" = "FAIL" ] || [ "$FN_STATUS" = "FAIL" ]; } && [ "$PREFLIGHT_STATUS" = "PASS" ]; then
echo "=== FAIL CLEANUP ==="
for TRG in trg_aa_iu_notif_comment trg_aa_iu_notif_draft trg_aa_iu_notif_version; do
TBL=$("${PSQL_NOSTOP[@]}" -t -A -c "SELECT c.relname FROM pg_trigger t JOIN pg_class c ON t.tgrelid=c.oid WHERE t.tgname='$TRG' AND NOT t.tgisinternal;" 2>/dev/null)
[ -n "$TBL" ] && "${PSQL_NOSTOP[@]}" -c "DROP TRIGGER IF EXISTS $TRG ON public.$TBL;" 2>&1
done
for FN in fn_iu_notif_comment fn_iu_notif_draft fn_iu_notif_version; do
"${PSQL_NOSTOP[@]}" -c "DROP FUNCTION IF EXISTS public.$FN();" 2>&1
done
"${PSQL_NOSTOP[@]}" -c "DROP FUNCTION IF EXISTS public.fn_iu_notification_board(text,text,integer);" 2>&1
REM_FN=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public' AND p.proname IN('fn_iu_notif_comment','fn_iu_notif_draft','fn_iu_notif_version','fn_iu_notification_board');" | tr -d ' ')
REM_TRG=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_trigger WHERE tgname IN('trg_aa_iu_notif_comment','trg_aa_iu_notif_draft','trg_aa_iu_notif_version') AND NOT tgisinternal;" | tr -d ' ')
[ "$REM_FN" = "0" ] && [ "$REM_TRG" = "0" ] && FAIL_CLEANUP="CLEAN" || { FAIL_CLEANUP="CRITICAL"; PHASE_STATUS="CRITICAL"; }
echo "FAIL_CLEANUP=$FAIL_CLEANUP"
fi
# rev7-F4: Active object verification before final PASS
ACTIVE_OBJ_COUNT=0
if [ "$FN_STATUS" = "OK" ] && [ "$TEST_FAIL" = "0" ] && [ -z "$PHASE_STATUS" ]; then
ACTIVE_FN=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname='public' AND p.proname IN('fn_iu_notif_comment','fn_iu_notif_draft','fn_iu_notif_version','fn_iu_notification_board');" | tr -d ' ')
ACTIVE_TRG=$("${PSQL[@]}" -t -c "SELECT count(*) FROM pg_trigger WHERE tgname IN('trg_aa_iu_notif_comment','trg_aa_iu_notif_draft','trg_aa_iu_notif_version') AND NOT tgisinternal;" | tr -d ' ')
ACTIVE_OBJ_COUNT=$((ACTIVE_FN+ACTIVE_TRG))
[ "$ACTIVE_OBJ_COUNT" = "7" ] && ACTIVE_OBJ_CHECK="PASS" || { ACTIVE_OBJ_CHECK="FAIL"; PHASE_STATUS="CRITICAL"; }
echo "ACTIVE_OBJ: fn=$ACTIVE_FN trg=$ACTIVE_TRG total=$ACTIVE_OBJ_COUNT check=$ACTIVE_OBJ_CHECK"
fi
echo "=== FINAL ==="
if [ "$PHASE_STATUS" = "CRITICAL" ]; then true
elif [ -z "$PHASE_STATUS" ] && [ "$TEST_FAIL" = "0" ] && [ "$TEST_EVENT_CLEANUP" = "PASS" ] && [ "$ACTIVE_OBJ_CHECK" = "PASS" ]; then PHASE_STATUS="PASS"
elif [ -z "$PHASE_STATUS" ]; then PHASE_STATUS="FAIL"; fi
NOTIF_RUNTIME="INACTIVE"; [ "$PHASE_STATUS" = "PASS" ] && NOTIF_RUNTIME="ACTIVE"
echo "phase_status=$PHASE_STATUS"
echo "fn=$FN_STATUS test_fail=$TEST_FAIL"
echo "test_event_cleanup=$TEST_EVENT_CLEANUP"
echo "fail_cleanup=$FAIL_CLEANUP"
echo "active_object_count=$ACTIVE_OBJ_COUNT"
echo "active_object_check=$ACTIVE_OBJ_CHECK"
echo "p23_unchanged=$([ "$P23_HASHES_AFTER" = "$P23_HASHES_BEFORE" ] && echo true || echo false)"
echo "p3d1_unchanged=$([ "$P3D1_HASHES_AFTER" = "$P3D1_HASHES_BEFORE" ] && echo true || echo false)"
echo "version_actor_variant=$([ "$VERSION_HAS_ACTOR" = "1" ] && echo A_created_by || echo B_fallback)"
echo "comment_suppression=comment_kind_and_author_type"
echo "self_read_policy=implicit_self_read"
echo "board_scope=actionable_current_events"
echo "notification_runtime=$NOTIF_RUNTIME"
echo "notification_board=$([ "$PHASE_STATUS" = "PASS" ] && echo ACTIVE || echo INACTIVE)"
echo "per_actor_read_state=$([ "$PHASE_STATUS" = "PASS" ] && echo ACTIVE || echo INACTIVE)"
echo "hermes_readiness=BLOCKED_PENDING_REVIEW"
echo "evt_evidence=$EVT_EVIDENCE"
echo "read_evidence=$READ_EVIDENCE"
echo "latest_readers_gpt_verified=$(echo "$T16_READERS"|grep -q '"gpt"' && echo true || echo false)"
echo "test_draft_id=$TEST_DRAFT_ID"
echo "comment_context=draft_id"
echo "comment_route=fn_iu_comment_with_explicit_context"
echo "cleanup_scope=canonical_address_only"
echo "pilot_rows_retained=true"
echo "next_required_pack=P3D3_CONTEXT_UPDATE_FOR_NOTIFICATION_COMMANDS"
echo "p3d3_context_must_document=fn_iu_unread,fn_iu_mark_read,fn_iu_notification_board,implicit_self_read,actionable_board_scope"
echo "LOG=$LOG"
echo "=== NOTIFICATION SYSTEM ==="
echo "fn_iu_unread(actor) -- hộp thư chưa đọc"
echo "fn_iu_mark_read(event_ids, actor) -- đánh dấu đã xem"
echo "fn_iu_notification_board(actor) -- bảng tổng hợp (implicit_self/explicit_read/unread)"
echo "=== AGENT: UPLOAD REPORT ==="
echo "Path: knowledge/dev/laws/dieu44-trien-khai/reports/23-p3d2-notification-triggers-report.md"
echo "Upload even on FAIL/CRITICAL."
P3D2 rev7 | SELF-CONTAINED | draft→comment→apply order | active obj verify | 16 tests | CHƯA dispatch | Chờ GPT/User review