08 — Issue / Event / Notification Hardening (Branch H) (2026-06-01)
08 — Issue / Event / Notification Hardening (Branch H)
Reviews decision-pack doc 07 (
…/07-issue-event-notification-model.md). Adversarial. Live-verified the dormant event names this session.
H0. What the pack says (summary)
Doc 07 reuses system_issues (188,250 rows; issue_type/issue_class/sub_class free-text, coalesce_key idempotency) + event_type_registry (40) + event_outbox/event_subscription (Đ45). It proposes 16 governance issue types (folding sub_classes to ≈10 event registrations), reuses orphan-half issue types (thiếu_quan_hệ/thiếu_mã_định_danh/sai_lệch_dữ_liệu), proposes activating/generalizing dormant mother.governance.* events, and applies Đ45 register-before-emit + signal-not-data + throttle-to-summary. Auto-resolve on clean scan; coalesce dedup; retire→archive.
Overall: reuse-first is correct and the Đ45 discipline (register-before-emit, signal-not-data, throttle) is the right backbone. Defects: a live naming defect, an asymmetry (events gated but issue_type free-text), and three missing anti-spam controls the mission §11 named explicitly.
H1 — Live naming defect: the dormant events are governance.*, not mother.governance.*
- Original: doc 00 §0.3 and doc 07 §7.1 cite the dormant types as
mother.governance.blocked/mother.governance.unblocked/mother.proposal.createdetc., and propose to "activate + generalize them." - Live fact (re-verified this session): the rows are
event_type ∈ {governance.blocked, governance.unblocked, proposal.created, proposal.approved, proposal.rejected}, allactive=false. Theevent_typestrings are bare (governance.blocked);motheris the domain/owner, not part of the event_type. So the pack's reuse target names are wrong by a prefix. - Consequence (not cosmetic): the proposed new types
governance.orphan_detected,governance.island_detected,governance.approval_path_gap, etc. (doc 07 §7.2) land in the samegovernance.*event_type namespace as the existinggovernance.blocked. So they don't "fork" — they extend a shared family — which is good for SSOT, but means the registration must be deliberate about that shared namespace and about whether the new coverage events belong to themotherdomain (wheregovernance.blockedlives) or a different domain (e.g. a newgovernance/integritydomain owned by GOV-SIV). - Hardened wording: correct all references to the bare names; state explicitly: "new governance-coverage events extend the existing
governance.*event_type family. Decide the domain (the dormantgovernance.*/proposal.*are domain=mother; coverage events are GOV-SIV/COUNCIL-owned, so they likely warrant domain=governanceorsystem, notmother). Register-before-emit applies; the dormantmother-domain rows are activated only if their semantics match (governance.blocked/unblocked≈ a coverage-degraded gate — confirm before reuse, don't assume)." - Acceptance test: the registration doc (P6) lists exact
event_type+domainvalues that match live naming; no reference to a non-existentmother.governance.*string. - Open question: OQ-H1 — reuse the
mother-domain dormant rows, or register a newgovernance/integritydomain owned by GOV-SIV? (The dormant ones are mother-factory lifecycle events; coverage events are integrity events — likely a new domain is cleaner, with the dormant ones left for the mother factories.)
H2 — Issue_type is free-text (no CHECK) → 16 new values will drift (the pack's own anti-pattern)
- Original: §7.1: governance gaps "get new
issue_typevalues under the existing free-text column (no schema change)." 16 types + sub_class semantics, hand-written across 7 DOTs. - Trap:
system_issues.issue_typehas no CHECK (confirmed in pack doc 00 §0.3: "free-text"). 16 hand-typed values across multiple DOTs will drift — casing, typos,owner_gapvsOWNER_GAPvsownergap. That drift is precisely aGOVERNANCE_SCHEMA_DRIFT(doc 03 §3.2) the pack warns about — the issue layer manufactures the very drift the detector hunts. - Hardened wording: introduce a governed issue_type vocabulary — a small reference table
governance_issue_type_registry(issue_type, sub_class, default_severity, owner_gov_code, event_type, active), COUNCIL/SIV-owned, that the DOTs read (constant-from-registry, no string literals). This mirrorsevent_type_registry(register-before-use). It needs no CHECK on the bigsystem_issuestable (avoids a migration on 188k rows) — the discipline is "DOTs may only write an issue_type that exists in the vocabulary registry," enforced by the DOT code + CI (P8), not by a column CHECK. Register-before-write for issues, paralleling register-before-emit for events. - Acceptance test: a DOT writing an unregistered
issue_typefails CI; all 16 governance issue_types resolve from the vocabulary registry; grep finds no issue_type string literals in DOT code. - Open question: OQ-H2 — vocabulary registry as a new table, or reuse an existing reference table? (New small table is cleanest; it is itself a governed object owned by SIV.)
H3 — Missing anti-spam controls the mission named: cooldown, escalation, manual-suppression-with-approval
- Original: the pack has coalesce (dedup),
occurrence_count/last_seen_at, auto-resolve, and throttle-to-summary. Mission §11 additionally requires cooldown, severity escalation, and manual suppression with approval. - Gaps:
- Cooldown — coalesce stops duplicate rows, but re-notification can still fire every scan for a persistent open issue. No minimum re-notify interval.
- Severity escalation — a
warningorphan open for months should escalate tohigh(the F3 deadline mechanism), but the pack has no age-based escalation. - Manual suppression — mission §11 wants "manual suppression with approval"; the pack only auto-resolves and excludes approved-exception objects — there is no governed way for a human to mute a known-but-deferred issue.
- Hardened wording:
- Cooldown: reuse Đ45 §15.4 thresholds — re-notify on a coalesced issue at most once per cooldown window (e.g. daily for
high, immediate forcritical);occurrence_countstill bumps every scan, but notifications are rate-limited. - Escalation: an open
warningpast its remediation deadline auto-escalates tohigh(this is the F3 deadline made operational); ahighopen past its SLA escalates tocritical+ president. Escalation is computed from(severity, age, deadline), never hand-set. - Manual suppression = a governed exception: muting an issue is not a free action — it is an approved exception (Branch E) with TTL + reason + replacement_plan. A silent mute is forbidden (it would hide a real gap). The suppressed issue still shows in the summary as "suppressed-by-exception," never disappears.
- Cooldown: reuse Đ45 §15.4 thresholds — re-notify on a coalesced issue at most once per cooldown window (e.g. daily for
- Acceptance test: a persistent
highissue notifies at most once/cooldown; awarningpast deadline escalates without human action; suppressing an issue requires an exception record and the issue remains visible-but-muted in the summary. - Open question: OQ-H3 — escalation SLAs per severity (council-set thresholds, governed per G4).
H4 — Register-before-emit is enforced for events but not for issues (asymmetry)
- Original: §7.3: "register-before-emit … every NEW event_type … inserted into
event_type_registry… before any emit." But issues are free-text (H2) with no such gate. - Asymmetry: the event layer has a hard registry CHECK (can't emit an unregistered type — doc 06 says "emit unregistered type → producer fail by Đ45 anti-pattern"); the issue layer has no equivalent. So the weaker, higher-volume layer (issues: 188k rows) is the less governed one — backwards.
- Hardened wording: extend register-before-emit to register-before-write for governance issue_types via the H2 vocabulary registry. Symmetry: both issues and events must be registered before use. (This closes the asymmetry without a 188k-row migration — the gate is at the DOT/CI layer.)
- Acceptance test: as H2.
- Open question: none.
H5 — Define the issue grain the mission asked for: summary / detail / recurring / expired-exception
- Original: mission §11 asks to "define: summary issue; detail issue; recurring issue; expired exception issue." The pack has the concepts implicitly (aggregate summary event, per-object issue, coalesce for recurring, EXCEPTION-REVIEW for expiry) but never names the four issue grains.
- Hardened wording — name the four issue grains:
Grain When Cardinality Routing summary issue a scope's coverage degraded; high-cardinality class one per scope/cycle owner-agency digest detail issue a single governance-grain object orphaned one per (ref, gap)coalescedowner queue recurring issue a detail issue seen again same row, occurrence_count++cooldown-limited expired-exception issue an exception past TTL/review one per exception president + COUNCIL, critical - Acceptance test: the scanner emits the right grain per situation; a mass-orphan source yields a summary issue (not 10⁶ detail issues); an expired Direct-PG exception yields exactly one expired-exception issue.
- Open question: none.
H6 — Signal-not-data is enforced by safe_payload CHECK — but the issue body has no such CHECK
- Original: §7.3: events carry
governed_object_ref/gap_type/severity/coalesce_keyonly;event_outbox.safe_payloadCHECK enforces. Issues, however, are free-text rows with no payload CHECK. - Risk: a DOT could write object bodies / secrets into a
system_issues.detailfield (no CHECK), leaking past the event discipline through the issue channel. - Hardened wording: apply the same signal-not-data discipline to governance issue rows —
detailcarries refs + gap metadata, never object bodies or secrets; enforced by the DOT contract + CI (no payload-shaped issue details). (No new CHECK on 188k rows; gate at the writer.) - Acceptance test: a DOT attempting to write a row body into an issue detail fails CI/contract.
- Open question: none.
H-summary — Branch H verdict
| ID | Severity | Type | Disposition |
|---|---|---|---|
| H1 | high | live defect | correct names to bare governance.*; decide domain; don't assume mother.governance.* |
| H2 | high | trap (drift) | governed issue_type vocabulary registry; register-before-write |
| H3 | high | gap (mission-explicit) | add cooldown + age escalation + approval-gated suppression |
| H4 | medium | asymmetry | symmetric register-before-write for issues |
| H5 | medium | gap | name the four issue grains |
| H6 | medium | gap | signal-not-data for issue bodies too |
H1 is a concrete live-verified correction; H2+H4 close the issue-layer governance hole (the higher-volume layer was the less-governed one); H3 supplies the named anti-spam controls.