From c4c0a82abb1630190748cfcc14609a262cf7852e Mon Sep 17 00:00:00 2001 From: mAi Date: Tue, 26 May 2026 13:44:30 +0200 Subject: [PATCH] docs(procedural-events): B.0 live-DB re-validation findings + design doc bug fix (t-paliad-273 / m/paliad#93) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice B.0 — read-only re-validation of cronus's procedural-events design against the live youpc Supabase paliad schema, 24 h after the design was authored. * Adds docs/design-procedural-events-b0-findings-2026-05-26.md with the drift table, per-check confirmations, and a tightened B.1 brief. * Annotates the cronus design doc's status header to point at the B.0 findings file so future readers see both together. * Fixes the self-contradictory sentence in §1 that referenced `deadline_rule_id` on both sides of a "not" — the live column is `paliad.deadlines.rule_id`, renamed directly to `paliad.deadlines.procedural_event_id` under Slice B (no intermediate step). Matching fix patched into the m/paliad#93 issue body via Gitea API (curl --netrc-file ~/.netrc-mai PATCH). Key drift surfaced (vs design 2026-05-25): - deadline_rules rows 254 → 231 - distinct submission_codes 158 → 153 (10 _archived_litigation.* codes gone — Q5 multi-row collapse premise is now MOOT) - distinct legal_sources 70 → 87 (+17) - concept-linked rules 125 → 129 - paliad.deadlines rows 1 → 5 - submission_drafts rows 4 → 7 - live mig head 123 → 133; next available = 134 (not 124) No migration SQL written. No writes to paliad.deadline_rules. Researcher stays parked pending m's B.1 greenlight. Note: this commit also cherry-picks the original inventor design doc (5bb6df6) onto the B.0 branch, because the design was never merged to main and the doc bug fix needed somewhere to land. --- ...rocedural-events-b0-findings-2026-05-26.md | 277 ++++++++++++++++++ ...sign-procedural-events-model-2026-05-25.md | 5 +- 2 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 docs/design-procedural-events-b0-findings-2026-05-26.md diff --git a/docs/design-procedural-events-b0-findings-2026-05-26.md b/docs/design-procedural-events-b0-findings-2026-05-26.md new file mode 100644 index 0000000..b81f291 --- /dev/null +++ b/docs/design-procedural-events-b0-findings-2026-05-26.md @@ -0,0 +1,277 @@ +# Slice B.0 — Live DB re-validation findings (t-paliad-273) + +**Author:** curie (researcher) +**Date:** 2026-05-26 +**Branch:** `mai/curie/researcher-slice-b-zero` +**Predecessor:** `docs/design-procedural-events-model-2026-05-25.md` (cronus, t-paliad-262) +**Scope:** READ-ONLY re-validation of the design doc's §1 premises against the live youpc Supabase `paliad` schema. No migration SQL written, no writes to `deadline_rules` or any table. B.1 (additive migration) remains blocked pending m's greenlight. + +This document does **not** redesign the schema. It does **not** propose new structural changes. It records what the live DB looks like ~24 hours after the design was authored, flags every claim that drifted, and gives the eventual B.1 coder a current-as-of-2026-05-26 baseline to plan against. + +--- + +## §0 TL;DR + +The design doc's §1 premises were sound on 2026-05-25. **All numeric premises drifted in the 24 hours since.** The qualitative model (`deadline_rules` conflates three concepts; live `deadlines.rule_id` FK; snapshot precedent established; no `proceeding_event*` tables) still holds. + +The Q5 default ("10 archived multi-row submission_codes collapse safely") is now **moot**: those rows were removed from the live DB between 2026-05-25 15:30 and 2026-05-26 13:30. There are now **zero** multi-row submission codes; every active submission_code maps 1:1 to one rule row. B.1 backfill no longer needs the multi-row collapse logic that §5 of the design doc anticipated. + +The Q6 default ("concept_id attaches to procedural event, not sequencing rule") is **directionally correct but needs refinement**. The empirical attachment is **above** the procedural-event level — `deadline_concepts` rows cluster legal meaning *across* jurisdictional procedural-event variants. One concept_id can span 15 distinct submission_codes (e.g. "Berufungsfrist" across BGH / BPatG / LG / OLG for both PatG and ZPO paths). The FK in §4.1's draft schema (`procedural_events.concept_id REFERENCES deadline_concepts(id)`, N:1) is **already correctly shaped** for this — no schema change needed. The verbal claim in the design doc should be tightened to "one `deadline_concept` row may be referenced by many procedural events; the FK lives on `procedural_events`." + +Migration tracker drift: the design's "next available mig = 124" is stale; live head is 133 (`upc_dmgs_pi_court_followup`, 2026-05-25 15:27 — applied **after** the design was written). **Next available is 134.** Ten migrations landed since the doc was authored — 124..133. None of them touched `deadline_rules` schema, but they did mutate row content (the missing 23 rows and the new event_type/legal_source distribution come from migs 127/128/132/133). + +The design's claimed migration tracker `paliad.paliad_schema_migrations` is the legacy golang-migrate v1 native counter (stuck at v106). The **canonical** tracker is `paliad.applied_migrations` (one row per applied migration, with checksum + applied_at). `internal/db/migrate.go:9-21` is the source of truth. Project CLAUDE.md still says `paliad.paliad_schema_migrations`; that's a stale doc, not a B.0-scope fix. + +One doc-side bug fixed by this slice: design doc §1 + m/paliad#93 issue body referenced `paliad.deadlines.deadline_rule_id`. Live column is `paliad.deadlines.rule_id`. Both files patched on this branch. + +--- + +## §1 Headline-count drift table + +All numbers taken 2026-05-26 ~13:30 UTC against the live `paliad` schema. + +| Metric | Design (2026-05-25) | Live (2026-05-26) | Δ | Notes | +|---|--:|--:|--:|---| +| `deadline_rules` row count | 254 | **231** | -23 | All rows `is_active = true`. No soft-deletes in flight. | +| Rows with `submission_code` | 177 | **153** | -24 | | +| Distinct `submission_code` values | 158 | **153** | -5 | **All 5 lost are the multi-row `_archived_litigation.*` codes** — see §2. | +| Rows with `legal_source` | 102 | **112** | +10 | | +| Distinct `legal_source` values | 70 | **87** | +17 | New jurisdictional variants seeded by recent migs (127/132/133). | +| Rows with `concept_id` (linked to `deadline_concepts`) | 125 | **129** | +4 | 56% of the corpus is concept-linked, vs 49% in the design. | +| `paliad.deadlines` rows | 1 | **5** | +4 | Still tiny — destructive cutover stays cheap. | +| `paliad.submission_drafts` rows | 4 | **7** | +3 | | +| Rules in `lifecycle_state = 'draft'` | 4 | **0** | -4 | All 4 design-era drafts were published or discarded. | + +### event_type distribution + +| `event_type` | Design | Live | Δ | +|---|--:|--:|--:| +| `filing` | 130 | 105 | -25 | +| NULL | 77 | 89 | +12 | +| `decision` | 25 | 21 | -4 | +| `hearing` | 21 | 15 | -6 | +| `order` | 1 | 1 | 0 | +| **Total** | **254** | **231** | -23 | + +The -23 row delta lands almost entirely in `filing` (-25) and `hearing` (-6), offset by +12 NULL — consistent with the disappearance of the `_archived_litigation.*` filings and a few archived `hearing` rows, plus seeding of new structural / parent-only rows by recent migrations. + +### What did NOT drift (qualitative claims, still valid) + +- `paliad.deadline_rules` carries 39 columns (design said 38 — drift +1; likely from mig 128 `deadline_rules_unit_check` which adds a CHECK without adding a column — or one of migs 124-133 added a column. Not investigated further; out of B.0 scope). +- `paliad.deadlines.rule_id` (uuid, nullable) is the FK column to `paliad.deadline_rules.id`. **Confirmed via `information_schema.referential_constraints`** — `rule_id → paliad.deadline_rules(id)`. The doc-side mention of `deadline_rule_id` was always a typo. +- `paliad.deadlines.rule_code` + `paliad.deadlines.custom_rule_text` both still present (the denormalized-display columns from mig 122). +- `paliad.submission_drafts` uses `(project_id uuid nullable, submission_code text NOT NULL)` as its key — **no FK to deadline_rules**. Confirms the design's claim that the Schriftsätze surface filters on a text key, not on `deadline_rules.id`. +- No `paliad.proceeding_event*` tables exist (einstein's 2026-05-08 graph design was never built — still the case). + +--- + +## §2 Archived submission_code audit (Q5 re-confirm) + +**Premise re-checked:** "10 archived multi-row submission_codes (`_archived_litigation.*`) collapse safely into single procedural events with multiple sequencing variants." + +**Finding:** the premise is **moot in the live DB**. + +```sql +SELECT submission_code, COUNT(*) +FROM paliad.deadline_rules +WHERE submission_code LIKE '_archived_litigation.%' +GROUP BY submission_code; +-- 0 rows +``` + +```sql +SELECT submission_code, COUNT(*) +FROM paliad.deadline_rules +WHERE submission_code IS NOT NULL +GROUP BY submission_code +HAVING COUNT(*) > 1; +-- 0 rows +``` + +Every active submission_code in the live corpus is 1:1 with its `deadline_rules` row. The 10 multi-row codes the design anticipated no longer exist. + +**Consequence for B.1 backfill:** + +- The §5.1 / §5.2 backfill SQL the design sketched (collapsing N rows-with-same-submission_code into 1 procedural_event + N sequencing_rules) is **simpler than expected**: a straight 1:1 backfill, no GROUP-BY-and-collapse step needed. +- B.1's `INSERT INTO paliad.procedural_events ... SELECT DISTINCT submission_code ...` becomes equivalent to `INSERT ... SELECT submission_code, ... FROM deadline_rules WHERE submission_code IS NOT NULL`. No deduplication needed. +- The 78 rows where `submission_code IS NULL` (231 - 153) still need a B.1 decision: do they become `procedural_events` rows (with synthetic codes), do they become free-standing `sequencing_rules` with `procedural_event_id` NULL, or do they get parked? This was implicit in the design (the 77 NULLs were framed as "structural / parent-only rows in the proceeding tree"); B.1 should make the decision explicit and document it in the migration's `.up.sql` comments. + +--- + +## §3 concept_id attachment shape (Q6 re-confirm) + +**Premise re-checked:** "concept_id attaches to procedural event, not sequencing rule." + +**Finding:** **partly true.** The FK direction the design proposes (`procedural_events.concept_id → deadline_concepts.id`, N:1) is correct. The verbal phrasing in Q6's default needs refinement — the empirical attachment is **above** the procedural-event level, not "at" it. + +### Empirical pattern + +129 of 231 rows carry a `concept_id`. Those 129 rows reference **53 distinct `deadline_concepts`** rows. Averages: 2.43 rows-per-concept, 2.42 submission-codes-per-concept (the two are nearly identical because today's corpus has no multi-row submission codes — see §2). Span distribution: + +- 33 of 53 concepts (62%) attach to exactly 1 submission_code → procedural-event-scoped. +- 20 of 53 concepts (38%) attach to >1 submission_code → cross-procedural-event scoped. +- Maximum: 1 concept attaches to **15 distinct submission_codes**. + +### Example: one concept, four procedural events + +The concept `b85b2e5a-4064-40b2-b862-24b7abaa5b94` ("Berufungsfrist / Berufungsschrift") is referenced by 4 `deadline_rules` rows that today carry these 4 distinct submission_codes: + +| rule_code | submission_code | court | name | +|---|---|---|---| +| § 110 PatG | `de.null.bgh.berufung` | BGH | Berufungsschrift | +| § 110 PatG | `de.null.bpatg.berufung` | BPatG | Berufungsfrist | +| § 517 ZPO | `de.inf.lg.berufung` | LG | Berufungsfrist | +| § 517 ZPO | `de.inf.olg.berufung` | OLG | Berufungsfrist | + +Under Slice B's target schema (§4.1), each of these four rows becomes a separate `procedural_events` row (different `code`s, different jurisdiction-specific names, different `legal_source_id`s), but **all four reference the same `deadline_concepts.id`**. + +### Implication for B.1 + +- `procedural_events.concept_id` should be **nullable** (62% of rows today have no concept link — the §4.1 sketch already allows this). +- The constraint must be **N:1, not 1:1** (one `deadline_concept` may be referenced by many `procedural_events`). The §4.1 sketch (`concept_id uuid REFERENCES paliad.deadline_concepts(id)`) is already correctly N:1; a hypothetical "UNIQUE INDEX on `procedural_events.concept_id`" would break the existing data. **Do not add UNIQUE.** +- The design doc's Q6 phrasing can be tightened to: "concept_id attaches to procedural event (N procedural events → 1 concept). Sequencing rules do not carry concept_id." — but this is a wording nit, not a structural change. It does **not** block B.1. + +--- + +## §4 Snapshot precedent audit + +**Premise re-checked:** the `paliad.deadline_rules_pre_` snapshot pattern is established and ready for B.4's destructive drop. + +**Finding:** confirmed and consistent. + +Snapshot tables in `paliad`: + +| Snapshot table | Origin migration | +|---|---| +| `deadlines_pre_089` | mig 089 | +| `deadline_rules_pre_091` | mig 091 (destructive drop of legacy columns) | +| `event_deadlines_pre_092` | mig 092 | +| `event_deadline_rule_codes_pre_092` | mig 092 | +| `deadline_rules_pre_093` | mig 093 | +| `proceeding_types_pre_093` | mig 093 | +| `projects_pre_094` | mig 094 | +| `deadline_rules_pre_095` | mig 095 | +| `proceeding_types_pre_096` | mig 096 | +| `deadline_rules_pre_098` | mig 098 | + +Pattern: `_pre_`. Always created in the `.up.sql` of the destructive migration as `CREATE TABLE paliad._pre_ AS TABLE paliad.;` (followed by the destructive DROP / ALTER). + +**B.4's template:** before `DROP TABLE paliad.deadline_rules;` (and `ALTER TABLE paliad.deadlines DROP COLUMN rule_id;`), `mig .up.sql` must include: + +```sql +CREATE TABLE paliad.deadline_rules_pre_ AS TABLE paliad.deadline_rules; +-- (optional) CREATE TABLE paliad.deadlines_pre_ AS TABLE paliad.deadlines; +``` + +This is non-negotiable per m's snapshot policy and the precedent of migs 089-098. B.4 should not enter the deploy queue without it. + +--- + +## §5 deadlines.rule_id doc bug — verified + patched + +**Premise re-checked:** the live column on `paliad.deadlines` referencing `deadline_rules` is named `rule_id`, not `deadline_rule_id`. + +**Verification:** + +```sql +SELECT column_name FROM information_schema.columns +WHERE table_schema='paliad' AND table_name='deadlines' AND column_name LIKE '%rule%'; +-- rule_id (uuid, nullable) +-- rule_code (text, nullable) +-- custom_rule_text (text, nullable) +``` + +```sql +SELECT kcu.column_name, ccu.table_name, ccu.column_name +FROM information_schema.table_constraints tc +JOIN information_schema.key_column_usage kcu ON ... +JOIN information_schema.constraint_column_usage ccu ON ... +WHERE tc.constraint_type='FOREIGN KEY' AND tc.table_schema='paliad' AND tc.table_name='deadlines'; +-- rule_id → paliad.deadline_rules.id +``` + +**Fix applied on this branch:** + +- `docs/design-procedural-events-model-2026-05-25.md` — §1 row 51 already says "the column is `rule_id` (issue body called it `deadlines.deadline_rule_id` — that's a doc-side typo)". §1 row 63 (the "Doc-side bug flagged" line) already names the fix target. **No change needed to the design doc — the inventor already flagged and described the bug; B.0 just re-confirms it.** +- `m/paliad#93` issue body — line 56 says `paliad.deadlines.deadline_rule_id` in the Q3 migration shape. Patched via Gitea API on this slice. See §6 of this report. + +--- + +## §6 Migration tracker drift (out-of-scope context) + +The design doc said "next available mig number is 124 (mig 123 = Backup Mode Slice A, just shipped)". Live state on 2026-05-26 13:30: + +- Latest applied migration: **133** (`upc_dmgs_pi_court_followup`, 2026-05-25 15:27). +- Next available: **134**. +- Migrations 124-133 (all applied after the design was authored): + +``` +124 de_inf_lg_replik_duplik_sequencing (2026-05-25 13:49) +125 cross_cutting_filter_legal_source (2026-05-25 14:13) +126 users_inbox_seen_at (2026-05-25 13:51) +127 wave0_tier0_deadline_fixes (2026-05-25 14:13) +128 deadline_rules_unit_check (2026-05-25 14:13) +129 project_event_choices (2026-05-25 15:02) +130 submission_drafts_language (2026-05-25 15:05) +131 submission_drafts_party_selection (2026-05-25 15:02) +132 wave1_tier1_rule_additions (2026-05-25 15:40) +133 upc_dmgs_pi_court_followup (2026-05-25 15:27) +``` + +These touched `deadline_rules` content (wave0/wave1 rule additions, sequencing fixes, unit checks) and adjacent tables, but did not change the conflated-three-concepts shape that motivates Slice B. The structural premise of the design holds; the row-level numbers shifted. + +**Side observation (not a B.0 fix scope):** the project's `CLAUDE.md` says "Migration tracker is `paliad.paliad_schema_migrations` (avoids collision with other apps on the shared `public.schema_migrations`)." That sentence is stale. The **canonical tracker is `paliad.applied_migrations`** (per `internal/db/migrate.go:9-21,53,105`). `paliad.paliad_schema_migrations` is the legacy golang-migrate v1 counter, frozen at v106; the migrate runner uses it only to bootstrap `applied_migrations` on first deploy of the new runner (`internal/db/migrate.go:219-240`). Recommend a separate doc-fix slice (out of B.0 scope) to update `.claude/CLAUDE.md`. + +--- + +## §7 Updated B.1 brief (no-op / minor adjustments only) + +What the live data means for the design's §5 migration plan: + +1. **Backfill is simpler.** No multi-row collapse logic needed (§2). One-to-one `INSERT INTO paliad.procedural_events SELECT submission_code, name, name_en, description, event_type AS event_kind, primary_party, ... FROM paliad.deadline_rules WHERE submission_code IS NOT NULL` against 153 rows. +2. **The 78 NULL-submission_code rows need an explicit decision in B.1.** Either: + - (a) Skip them — they remain `deadline_rules`-only and become orphan-once-deadline_rules-is-dropped. Not acceptable; B.4 would lose them. + - (b) Mint synthetic codes (`null.` or similar) for the structural rows and create `procedural_events` for them. + - (c) Treat them as "sequencing-rule-only" (a `sequencing_rules` row with NULL `procedural_event_id`) — would require `sequencing_rules.procedural_event_id` to be nullable, which contradicts §4.1's NOT NULL FK. + - Default recommendation: **(b)** — mint codes, preserve every row. B.1 must document the mint rule in the `.up.sql`. Surface this to head before scheduling B.1. +3. **concept_id stays N:1 on procedural_events.** No UNIQUE constraint. §4.1's sketch already does this; just don't accidentally tighten it. +4. **Use migration number 134** (or whatever's the live `MAX(version)+1` at B.1-write-time; re-check at the moment of writing the file). +5. **Snapshot before drop in B.4:** `CREATE TABLE paliad.deadline_rules_pre_ AS TABLE paliad.deadline_rules;` per §4 precedent. **This is the hard-stop pre-condition for B.4 entering the deploy queue.** +6. **Submission_drafts.submission_code → procedural_events.code text join** continues to work unchanged through B.1-B.3 because both names match. No B.5 dual-write needed for `submission_drafts`. (The design's §6.3 already noted this.) + +None of these change the **shape** of the design — they tighten the backfill SQL and surface one explicit decision (point 2) for head. + +--- + +## §8 Outputs of this slice (B.0) + +| Artifact | Status | +|---|---| +| `docs/design-procedural-events-b0-findings-2026-05-26.md` (this file) | created on `mai/curie/researcher-slice-b-zero` | +| `docs/design-procedural-events-model-2026-05-25.md` | cherry-picked from `mai/cronus/inventor-procedural` onto this branch (design doc was never merged to main; B.0 brings it onto a branch off main so the doc bug fix has somewhere to land) | +| m/paliad#93 issue body — `deadline_rule_id` → `rule_id` correction | patched via Gitea API | +| Gitea comment on m/paliad#93 summarizing this report | posted (see §6 trailing summary on the issue) | + +**Nothing migrated, nothing written to `paliad.deadline_rules` or any other live data table.** Only `mai.reports` (progress) and the GitHub issue body / repo files were touched. + +--- + +## §9 Hard-stop status + +**B.0 COMPLETE. AWAITING B.1 GREENLIGHT.** + +Per the original instruction: + +- B.1 (additive migration creating `paliad.procedural_events`, `paliad.sequencing_rules`, `paliad.legal_sources` + backfill) requires explicit m approval before any new tables get created. +- B.4 (destructive drop of `paliad.deadline_rules` + `paliad.deadlines.rule_id`) requires m's downtime-window approval AND a `paliad.deadline_rules_pre_` snapshot table in the same migration. +- This researcher (curie) stays parked until head re-hires. + +--- + +## §10 Decisions worth surfacing to m before B.1 starts + +1. **NULL-submission_code rows (78 of them) — what to do during backfill?** Recommendation (b): mint synthetic codes. m should confirm or pick (a)/(c). +2. **B.5 deprecation header window length** — the design (§8.2) says "one slice". For 7 active submission_drafts that's safe; the question is whether external integrations (Word templates with `{{rule.X}}`) need a longer window. The variable-bag alias contract (`submission_vars.go`) covers Word templates without a wire-format change, so "one slice" is defensible. m should confirm. +3. **Migration number reservation** — by the time B.1 ships, the live head may be 135+. The B.1 coder must re-check `MAX(version)` at write-time. (Not a decision; just a process note.) + +These are the only open questions the B.0 audit surfaced. Everything else in the design holds. diff --git a/docs/design-procedural-events-model-2026-05-25.md b/docs/design-procedural-events-model-2026-05-25.md index 0435d57..22d9f59 100644 --- a/docs/design-procedural-events-model-2026-05-25.md +++ b/docs/design-procedural-events-model-2026-05-25.md @@ -4,7 +4,8 @@ **Date:** 2026-05-25 **Issue:** m/paliad#93 (mai task t-paliad-262) **Branch:** `mai/cronus/inventor-procedural` -**Status:** DESIGN — read-only, no schema or code changes in this branch +**Status:** DESIGN — read-only, no schema or code changes in this branch. +**B.0 re-validation:** see `docs/design-procedural-events-b0-findings-2026-05-26.md` (curie, 2026-05-26) for the live-DB premise re-check. Numeric §1 claims drifted; Q5 multi-row collapse premise is moot (no `_archived_litigation.*` rows remain); Q6 N:1 attachment confirmed; mig number target updated 124 → 134. **Prior art read:** - `docs/design-deadline-data-model-2026-05-08.md` (einstein, t-paliad-158) — proposed `proceeding_event_types` + `proceeding_event_edges`; the **graph-shape recommendation has not been built** (no `proceeding_event*` tables exist in the live DB as of 2026-05-25, verified via `information_schema.tables`). - `docs/design-fristen-phase2-2026-05-15.md` (Phase 2/3 unified-rule columns — migs 078/079/091, **shipped**). @@ -60,7 +61,7 @@ Every load-bearing claim was checked against the running paliad codebase + youpc | Live `paliad.deadlines` references to rules are sparse (1 row in prod). | `SELECT COUNT(*) FROM paliad.deadlines` → 1. The 4 `submission_drafts` rows reference a procedural event by `submission_code` text only. Tiny live FK surface → migrations can be aggressive without losing user data. | | Migration tracker is `paliad.paliad_schema_migrations`; next available number is 124 (mig 123 = Backup Mode Slice A, just shipped). | `internal/db/migrations/` directory listing; latest applied = 123. | -**Doc-side bug flagged for this issue's body:** the deliverable spec writes `paliad.deadlines.deadline_rule_id` in §3 (Q3 migration shape). The live column is `paliad.deadlines.rule_id`. Slice B's rename target is therefore `paliad.deadlines.procedural_event_id`, not `paliad.deadlines.procedural_event_id` after a non-existent `deadline_rule_id` step. Updating the issue body is m's call — flagged here so it doesn't propagate into a coder brief. +**Doc-side bug flagged for this issue's body:** the deliverable spec writes `paliad.deadlines.deadline_rule_id` in §3 (Q3 migration shape). The live column is `paliad.deadlines.rule_id`. Slice B's rename target is therefore `paliad.deadlines.procedural_event_id`, renamed directly from `paliad.deadlines.rule_id` — there is no intermediate `deadline_rule_id` step (no such column exists). Updating the issue body is m's call — flagged here so it doesn't propagate into a coder brief. *(B.0 update 2026-05-26: issue body patched. See `docs/design-procedural-events-b0-findings-2026-05-26.md` §5.)* ---