Fristenrechner: trigger picker accepts all events; cross-party follow-ups must show with annotation; scenario flags must sync with conditional checkboxes #148

Open
opened 2026-05-27 08:37:37 +00:00 by mAi · 5 comments
Collaborator

m's diagnosis (2026-05-27 10:35)

We dont carry the explicit event_rule, we derive the trigger from whether an event is mentioned as a previous event in a sequence. […] Either way, the system needs to be that we can select from ALL POSSIBLE EVENTS and then see the followups, optional and mandatory.

And from the 10:29 message:

'Mit Widerklage auf Nichtigkeit' / 'Mit Antrag auf Patentänderung (R.30)' — These options in the Verfahrensablauf should be in sync with the optional checkboxes in the submission cards. So if I decide to include the Counterclaim for Revocation, it should be there.

Premises verified live (2026-05-27)

Sample chains (paliad.sequencing_rules.parent_id → parent rule's procedural_event):

child rule parent rule predecessor event
Berufungsbegründung (§ 111 PatG) § 110 PatG Berufungsfrist
Anzeige der Verteidigungsbereitschaft (§ 276 ZPO) (anchor) Klageerhebung
Beschwerdebegründung (Art. 108 EPÜ) (anchor) Entscheidung
Berufungserwiderung (RoP.235.1) RoP.224.2.a Berufungsbegründung
Replik (RoP.029.b) RoP.023 Klageerwiderung
Duplik (RoP.029.c) RoP.029.b Replik
Erwiderung auf CCR (RoP.029.a) RoP.023 Klageerwiderung
Replik auf Erwiderung zur CCR (RoP.029.d) RoP.029.a Erwiderung auf CCR

Stats over paliad.sequencing_rules (active+published, 226 rows):

  • 107 (47%) have parent_id set — the rule sits below a predecessor in the cascade tree.
  • 119 (53%) have parent_id = NULL — these are top-of-tree anchors OR genuine leaves that haven't been linked yet.
  • 67 unique rules act as parent_id for at least one child — i.e. 67 events are real triggers in today's data.

m's architectural intent: derive trigger-ness from the data (a rule whose parent_id points at me means I'm a trigger). No need for an explicit event_role column on procedural_events.

B. The bug m saw (def_to_ccr → "Keine Folge-Fristen") is a perspective-filter overreach, not missing data

The event upc.inf.cfi.def_to_ccr (Erwiderung auf Nichtigkeitswiderklage, claimant-filed) DOES have a published child rule:

child  : RoP.029.d → upc.inf.cfi.reply_def_ccr (Replik auf Erwiderung zur CCR)
parent : RoP.029.a → upc.inf.cfi.def_to_ccr
party  : defendant

Knuth's FristenrechnerService.LookupFollowUps filters follow-ups to rules where primary_party = $perspective OR primary_party = 'both'. m's perspective was claimant. The child rule (defendant) got dropped, surfacing "Keine Folge-Fristen" — even though the workflow continues with the other side's next move.

The fix is semantic: a cross-party follow-up isn't "not a follow-up" — it's the other side's next move. The result view should render it with a Gegenseitig annotation (and keep it unchecked by default for write-back, since the user doesn't file deadlines for the opposing side). Hiding them breaks the user's mental model of the cascade.

C. Verfahrensablauf scenario flags ↔ result-view CONDITIONAL group are independent today

The legacy Procedure-mode page (/tools/fristenrechner?legacy=1, untouched upper half of frontend/src/fristenrechner.tsx) renders scenario checkboxes for the proceeding:

  • Mit Widerklage auf Nichtigkeit (with_ccr)
  • Mit Antrag auf Patentänderung (with_amendment_r30)
  • (others per paliad.project_event_choices + paliad.scenarios jsonb mig 145)

The new result view (S2-S6) renders a CONDITIONAL group of follow-ups (rules with condition_expr IS NOT NULL) with their own checkboxes — independent from the Verfahrensablauf scenario flags. m's intent: these should share state.

Today's missing wiring: condition_expr on a sequencing_rule references a scenario key (e.g. {"requires": ["with_ccr"]}). The Verfahrensablauf checkboxes write to paliad.project_event_choices (or projects.scenarios jsonb). The result view's conditional checkboxes only live in DOM state. Three independent stores. The design needs ONE source of truth that:

  • Persists scenario flags per project (projects.scenarios jsonb is the natural home — mig 145).
  • Drives Verfahrensablauf checkbox state on render.
  • Drives result-view conditional-rule visibility + checked-state on render.
  • Round-trips: ticking CCR in either surface persists, and the other surface picks it up on next render / live via a tiny event bus.

Scope

One inventor pass covering:

  1. Confirm + document the parent_id semantic. Update the design-fristenrechner-overhaul-2026-05-26.md §4.2 to make explicit: "follow-ups = sequencing_rules WHERE parent_id = anchor_rule.id" (already what knuth coded, but un-named in the original design — the doc said "anchored on sr.procedural_event_id = trigger.id" which is the anchor lookup, not the follow-up resolution).
  2. Cross-party follow-ups display contract. Spec out the result-view rendering when a follow-up's primary_party doesn't match the user's perspective:
    • Show the row
    • Add a Gegenseitig / Cross-Party badge
    • Default unchecked
    • Greyed-out visual treatment but the date is still computed + visible
    • Backend change: LookupFollowUps returns all rules; UI does the grouping/annotation; perspective stays a display qualifier in the result view.
  3. Scenario state binding. Design the single source of truth + the binding to both surfaces:
    • projects.scenarios jsonb shape (extend mig 145 schema if needed — minimal new keys, not a re-architect)
    • Verfahrensablauf checkbox read/write contract
    • Result-view conditional checkbox read/write contract
    • Tiny event bus (DOM CustomEvent or a single GET endpoint refreshed on focus — pick the lightest path)
    • Migration: today's project_event_choices rows mapped into projects.scenarios (or coexist)
  4. Editorial backlog flagged. 53% of rules have parent_id = NULL. Many of those should chain back to an anchor. NOT in scope for this design — flagged as a follow-up editorial task. The new picker UX should still cleanly handle leaves ("this event has no recorded follow-ups" is honest).

Out of scope

  • Adding an explicit event_role column on procedural_events (m: "we DON'T carry the explicit event_rule"). The architecture stays: derive trigger-ness from parent_id chains.
  • Editorial backfill of the 119 parent_id-null rules. Separate ticket once the picker/result-view UX is right.
  • Changing the calculator (pkg/litigationplanner.CalculateRule). Just the lookup + render layer.
  • Replacing the legacy Procedure-mode page. The Verfahrensablauf wiring lives in the new dual-mode UI; the legacy ?legacy=1 page can deprecate naturally.

Acceptance for the design pass

Deliverable: docs/design-fristenrechner-followup-rules-2026-05-NN.md covering:

  • Documented parent_id semantic (sequencing_rules.parent_id = predecessor rule in the cascade — child fires after parent's deadline).
  • Cross-party follow-up display spec (when to show, how to badge, default-checked rules per perspective).
  • Scenario state shape (in projects.scenarios jsonb), key naming convention (with_ccr, with_amendment_r30, …), schema delta if any.
  • Verfahrensablauf ↔ result-view binding (event bus or polling, write/read flow).
  • Backend changes to FristenrechnerService.LookupFollowUps: stop filtering by party server-side, return all + the rule's primary_party; UI takes responsibility for grouping/annotation.
  • Migration plan: backfill projects.scenarios from project_event_choices if both surfaces should round-trip.
  • Worked example: claimant user picks upc.inf.cfi.def_to_ccr as trigger → result view shows: own follow-ups (checked) + cross-party RoP.029.d (Replik auf Erwiderung zur CCR) with Gegenseitig badge, unchecked, date visible.
  • Worked example: user opens Verfahrensablauf → ticks 'Mit CCR' → switches to result-view of any UPC INF event → the conditional CCR-rule appears pre-checked.

No code yet — inventor → coder gate. Park after DESIGN READY FOR REVIEW.

Cross-references

  • Original Fristenrechner overhaul design: docs/design-fristenrechner-overhaul-2026-05-26.md (S2-S6 shipped 2026-05-27).
  • Proceeding_types taxonomy design: docs/design-proceeding-types-taxonomy-2026-05-26.md (mig 153 shipped 2026-05-27) — parallel surgical taxonomy fix, gives mental model for derive-don't-add design.
  • paliad.scenarios jsonb on projects: introduced mig 145 (Litigation Planner Slice D).
  • paliad.project_event_choices table: legacy scenario storage, PK (project_id, scenario_name, submission_code, choice_kind).
  • paliad.sequencing_rules.condition_expr (jsonb): per-rule scenario predicate.
## m's diagnosis (2026-05-27 10:35) > We dont carry the explicit event_rule, we derive the trigger from whether an event is mentioned as a previous event in a sequence. […] Either way, the system needs to be that we can select from ALL POSSIBLE EVENTS and then see the followups, optional and mandatory. And from the 10:29 message: > 'Mit Widerklage auf Nichtigkeit' / 'Mit Antrag auf Patentänderung (R.30)' — These options in the Verfahrensablauf should be in sync with the optional checkboxes in the submission cards. So if I decide to include the Counterclaim for Revocation, it should be there. ## Premises verified live (2026-05-27) ### A. `parent_id` IS the predecessor link Sample chains (`paliad.sequencing_rules.parent_id` → parent rule's `procedural_event`): | child rule | parent rule | predecessor event | |---|---|---| | Berufungsbegründung (§ 111 PatG) | § 110 PatG | Berufungsfrist | | Anzeige der Verteidigungsbereitschaft (§ 276 ZPO) | (anchor) | Klageerhebung | | Beschwerdebegründung (Art. 108 EPÜ) | (anchor) | Entscheidung | | Berufungserwiderung (RoP.235.1) | RoP.224.2.a | Berufungsbegründung | | Replik (RoP.029.b) | RoP.023 | Klageerwiderung | | Duplik (RoP.029.c) | RoP.029.b | Replik | | **Erwiderung auf CCR** (RoP.029.a) | RoP.023 | **Klageerwiderung** | | Replik auf Erwiderung zur CCR (RoP.029.d) | RoP.029.a | **Erwiderung auf CCR** | Stats over `paliad.sequencing_rules` (active+published, 226 rows): - 107 (47%) have `parent_id` set — the rule sits below a predecessor in the cascade tree. - 119 (53%) have `parent_id = NULL` — these are top-of-tree anchors OR genuine leaves that haven't been linked yet. - 67 unique rules act as `parent_id` for at least one child — i.e. **67 events are real triggers in today's data**. m's architectural intent: derive trigger-ness from the data (a rule whose parent_id points at me means I'm a trigger). No need for an explicit `event_role` column on `procedural_events`. ### B. The bug m saw (def_to_ccr → "Keine Folge-Fristen") is a perspective-filter overreach, not missing data The event `upc.inf.cfi.def_to_ccr` (Erwiderung auf Nichtigkeitswiderklage, claimant-filed) DOES have a published child rule: ``` child : RoP.029.d → upc.inf.cfi.reply_def_ccr (Replik auf Erwiderung zur CCR) parent : RoP.029.a → upc.inf.cfi.def_to_ccr party : defendant ``` Knuth's `FristenrechnerService.LookupFollowUps` filters follow-ups to rules where `primary_party = $perspective OR primary_party = 'both'`. m's perspective was `claimant`. The child rule (`defendant`) got dropped, surfacing "Keine Folge-Fristen" — even though the workflow continues with the other side's next move. The fix is semantic: a cross-party follow-up isn't "not a follow-up" — it's *the other side's next move*. The result view should render it with a `Gegenseitig` annotation (and keep it unchecked by default for write-back, since the user doesn't file deadlines for the opposing side). Hiding them breaks the user's mental model of the cascade. ### C. Verfahrensablauf scenario flags ↔ result-view CONDITIONAL group are independent today The legacy Procedure-mode page (`/tools/fristenrechner?legacy=1`, untouched upper half of `frontend/src/fristenrechner.tsx`) renders scenario checkboxes for the proceeding: - Mit Widerklage auf Nichtigkeit (`with_ccr`) - Mit Antrag auf Patentänderung (`with_amendment_r30`) - (others per `paliad.project_event_choices` + `paliad.scenarios` jsonb mig 145) The new result view (S2-S6) renders a **CONDITIONAL** group of follow-ups (rules with `condition_expr IS NOT NULL`) with their own checkboxes — independent from the Verfahrensablauf scenario flags. m's intent: these should share state. Today's missing wiring: `condition_expr` on a sequencing_rule references a scenario key (e.g. `{"requires": ["with_ccr"]}`). The Verfahrensablauf checkboxes write to `paliad.project_event_choices` (or `projects.scenarios` jsonb). The result view's conditional checkboxes only live in DOM state. Three independent stores. The design needs ONE source of truth that: - Persists scenario flags per project (`projects.scenarios` jsonb is the natural home — mig 145). - Drives Verfahrensablauf checkbox state on render. - Drives result-view conditional-rule visibility + checked-state on render. - Round-trips: ticking CCR in either surface persists, and the other surface picks it up on next render / live via a tiny event bus. ## Scope One inventor pass covering: 1. **Confirm + document the parent_id semantic.** Update the design-fristenrechner-overhaul-2026-05-26.md §4.2 to make explicit: "follow-ups = sequencing_rules WHERE parent_id = anchor_rule.id" (already what knuth coded, but un-named in the original design — the doc said "anchored on `sr.procedural_event_id = trigger.id`" which is the anchor lookup, not the follow-up resolution). 2. **Cross-party follow-ups display contract.** Spec out the result-view rendering when a follow-up's `primary_party` doesn't match the user's `perspective`: - Show the row - Add a `Gegenseitig` / `Cross-Party` badge - Default unchecked - Greyed-out visual treatment but the date is still computed + visible - Backend change: `LookupFollowUps` returns all rules; UI does the grouping/annotation; perspective stays a *display* qualifier in the result view. 3. **Scenario state binding.** Design the single source of truth + the binding to both surfaces: - `projects.scenarios` jsonb shape (extend mig 145 schema if needed — minimal new keys, not a re-architect) - Verfahrensablauf checkbox read/write contract - Result-view conditional checkbox read/write contract - Tiny event bus (DOM CustomEvent or a single GET endpoint refreshed on focus — pick the lightest path) - Migration: today's `project_event_choices` rows mapped into `projects.scenarios` (or coexist) 4. **Editorial backlog flagged.** 53% of rules have `parent_id = NULL`. Many of those should chain back to an anchor. NOT in scope for this design — flagged as a follow-up editorial task. The new picker UX should still cleanly handle leaves ("this event has no recorded follow-ups" is honest). ### Out of scope - Adding an explicit `event_role` column on `procedural_events` (m: "we DON'T carry the explicit event_rule"). The architecture stays: derive trigger-ness from parent_id chains. - Editorial backfill of the 119 parent_id-null rules. Separate ticket once the picker/result-view UX is right. - Changing the calculator (`pkg/litigationplanner.CalculateRule`). Just the lookup + render layer. - Replacing the legacy Procedure-mode page. The Verfahrensablauf wiring lives in the new dual-mode UI; the legacy `?legacy=1` page can deprecate naturally. ## Acceptance for the design pass Deliverable: `docs/design-fristenrechner-followup-rules-2026-05-NN.md` covering: - Documented parent_id semantic (sequencing_rules.parent_id = predecessor rule in the cascade — child fires after parent's deadline). - Cross-party follow-up display spec (when to show, how to badge, default-checked rules per perspective). - Scenario state shape (in `projects.scenarios` jsonb), key naming convention (`with_ccr`, `with_amendment_r30`, …), schema delta if any. - Verfahrensablauf ↔ result-view binding (event bus or polling, write/read flow). - Backend changes to `FristenrechnerService.LookupFollowUps`: stop filtering by party server-side, return all + the rule's primary_party; UI takes responsibility for grouping/annotation. - Migration plan: backfill `projects.scenarios` from `project_event_choices` if both surfaces should round-trip. - Worked example: claimant user picks `upc.inf.cfi.def_to_ccr` as trigger → result view shows: own follow-ups (checked) + cross-party `RoP.029.d` (Replik auf Erwiderung zur CCR) with `Gegenseitig` badge, unchecked, date visible. - Worked example: user opens Verfahrensablauf → ticks 'Mit CCR' → switches to result-view of any UPC INF event → the conditional CCR-rule appears pre-checked. No code yet — inventor → coder gate. Park after `DESIGN READY FOR REVIEW`. ## Cross-references - Original Fristenrechner overhaul design: `docs/design-fristenrechner-overhaul-2026-05-26.md` (S2-S6 shipped 2026-05-27). - Proceeding_types taxonomy design: `docs/design-proceeding-types-taxonomy-2026-05-26.md` (mig 153 shipped 2026-05-27) — parallel surgical taxonomy fix, gives mental model for derive-don't-add design. - `paliad.scenarios` jsonb on projects: introduced mig 145 (Litigation Planner Slice D). - `paliad.project_event_choices` table: legacy scenario storage, PK `(project_id, scenario_name, submission_code, choice_kind)`. - `paliad.sequencing_rules.condition_expr` (jsonb): per-rule scenario predicate.
mAi self-assigned this 2026-05-27 08:37:37 +00:00
Author
Collaborator

Re-measured against today's data. Two of the three premises have moved, and the headline ask cannot be built from the catalog as it stands.

What I ran — read-only queries against prod, plus the fristenrechner service and client code. Not re-deriving May's premises; re-checking them, because migs 151/155/156 moved this catalog.


May 2026 now
published + active rules 226 192
with parent_id 107 (47%) 112 (58%)
parent_id NULL 119 (53%) 80 (42%)
distinct parents (= triggers) 67 72

The catalog shrank and the linkage improved — the editorial backlog is 42%, not 53%.

One thing worth ruling out explicitly: sequencing_rules.trigger_event_id exists and is set on 39 rules, which looks like the "explicit event_rule" the issue says we do not carry. It is not. It FKs to paliad.trigger_events (bigint), a different table — procedural_events.id is a uuid. It is not a second route into the event catalog and does not affect this issue.

Premise B — cross-party follow-ups: FIXED ON THE SERVER, NOT VISIBLE TO A USER

The perspective-filter overreach is gone. LookupFollowUps now "returns the other side's filings — and annotates each row with is_cross_party" (m/paliad#149 Phase 2 S1). upc.inf.cfi.def_to_ccr still has its published child RoP.029.dupc.inf.cfi.reply_def_ccr, primary_party = defendant. The badge exists in both languages — deadlines.overhaul.crossparty.badge → "Gegenseitig" / "Other side" — and is registered in i18n-keys.ts.

But nothing consumes any of it. No file under frontend/src/ references is_cross_party, nothing uses the badge key, and no client code calls GET /api/tools/fristenrechner/follow-ups — the only fristenrechner call in the client is POST /api/tools/fristenrechner (verfahrensablauf-core.ts:1168). Server built, endpoint live, UI never wired. So the annotation you asked for is still not on screen.

Premise C — three independent stores: RESOLVED, differently than proposed

The single source of truth landed as paliad.projects.scenario_flags (jsonb), not projects.scenarios. It carries named flags (with_ccr / with_amend / with_cci) and per-rule deviations keyed rule:<uuid>, and it drives both condition_expr evaluation in pkg/litigationplanner and the Verfahrensablauf scenario strip. PATCH /api/projects/{id}/scenario-flags has merge semantics with null deleting a key. Item 3 of the scope is done.


The measurement that matters, and the problem with the headline ask

count
active procedural_events 204
…with at least one rule 192
selectable as a trigger today 72
invisible as a trigger 132

65% of the active catalog cannot be picked, and the picker neither refuses nor explains — it is simply absent.

The deliverable as framed — "an empty result must say WHICH of the two it is" — cannot be built from this data. Nothing records terminality. There is no is_terminal; event_kind cannot stand in, because 17 of 31 decision events do have follow-ups. So "this event cannot be a trigger" and "nobody has written its follow-ups yet" are indistinguishable in the catalog, not just in the UI. Any code claiming to tell them apart today would be guessing, which is the defect this issue is about, one layer down.


Proposed split — three tasks, not one

1. Offer every active event, with an honest empty state. Buildable now. The picker lists all 204; a selected event with no recorded follow-ups says exactly that — "no follow-ups are recorded for this event in this proceeding type" — and does not claim which of the two cases it is, because the data cannot support the claim. Honest and incomplete beats confident and wrong.

2. Wire the cross-party annotation to the UI. Server, endpoint and both translations are done; the client never calls it. Small, and it is the half of your ask that is closest to shipping.

3. Record terminality — editorial, and needs your call. Only this makes the empty state definitive. It means a per-event judgement across the 132, by someone who knows the procedure, plus somewhere to put it. Note the issue's out-of-scope list rules out an event_role column — a terminality flag is a different thing, but it is close enough that it should be your decision rather than assumed.

Doing 1 and 2 without 3 leaves the empty state truthful but vague. Doing 3 without 1 fixes nothing a user can see. I would take them in that order.


Not closing — that is yours. No code changed yet; this is the measurement pass.

## Re-measured against today's data. Two of the three premises have moved, and the headline ask cannot be built from the catalog as it stands. **What I ran** — read-only queries against prod, plus the fristenrechner service and client code. Not re-deriving May's premises; re-checking them, because migs 151/155/156 moved this catalog. --- ### Premise A — `parent_id` is the predecessor link: **STILL HOLDS**, numbers moved | | May 2026 | now | |---|---|---| | published + active rules | 226 | **192** | | with `parent_id` | 107 (47%) | **112 (58%)** | | `parent_id` NULL | 119 (53%) | **80 (42%)** | | distinct parents (= triggers) | 67 | **72** | The catalog shrank and the linkage improved — the editorial backlog is 42%, not 53%. **One thing worth ruling out explicitly:** `sequencing_rules.trigger_event_id` exists and is set on 39 rules, which looks like the "explicit event_rule" the issue says we do not carry. It is not. It FKs to `paliad.trigger_events` (bigint), a different table — `procedural_events.id` is a uuid. It is not a second route into the event catalog and does not affect this issue. ### Premise B — cross-party follow-ups: **FIXED ON THE SERVER, NOT VISIBLE TO A USER** The perspective-filter overreach is gone. `LookupFollowUps` now *"returns the other side's filings — and annotates each row with `is_cross_party`"* (m/paliad#149 Phase 2 S1). `upc.inf.cfi.def_to_ccr` still has its published child `RoP.029.d` → `upc.inf.cfi.reply_def_ccr`, `primary_party = defendant`. The badge exists in both languages — `deadlines.overhaul.crossparty.badge` → "Gegenseitig" / "Other side" — and is registered in `i18n-keys.ts`. **But nothing consumes any of it.** No file under `frontend/src/` references `is_cross_party`, nothing uses the badge key, and **no client code calls `GET /api/tools/fristenrechner/follow-ups`** — the only fristenrechner call in the client is `POST /api/tools/fristenrechner` (`verfahrensablauf-core.ts:1168`). Server built, endpoint live, UI never wired. So the annotation you asked for is still not on screen. ### Premise C — three independent stores: **RESOLVED**, differently than proposed The single source of truth landed as **`paliad.projects.scenario_flags`** (jsonb), not `projects.scenarios`. It carries named flags (`with_ccr` / `with_amend` / `with_cci`) *and* per-rule deviations keyed `rule:<uuid>`, and it drives both `condition_expr` evaluation in `pkg/litigationplanner` and the Verfahrensablauf scenario strip. `PATCH /api/projects/{id}/scenario-flags` has merge semantics with `null` deleting a key. Item 3 of the scope is done. --- ## The measurement that matters, and the problem with the headline ask | | count | |---|---| | active `procedural_events` | **204** | | …with at least one rule | 192 | | …**selectable as a trigger today** | **72** | | …**invisible as a trigger** | **132** | 65% of the active catalog cannot be picked, and the picker neither refuses nor explains — it is simply absent. **The deliverable as framed — "an empty result must say WHICH of the two it is" — cannot be built from this data.** Nothing records terminality. There is no `is_terminal`; `event_kind` cannot stand in, because **17 of 31 `decision` events do have follow-ups**. So "this event cannot be a trigger" and "nobody has written its follow-ups yet" are indistinguishable *in the catalog*, not just in the UI. Any code claiming to tell them apart today would be guessing, which is the defect this issue is about, one layer down. --- ## Proposed split — three tasks, not one **1. Offer every active event, with an honest empty state.** Buildable now. The picker lists all 204; a selected event with no recorded follow-ups says exactly that — *"no follow-ups are recorded for this event in this proceeding type"* — and does **not** claim which of the two cases it is, because the data cannot support the claim. Honest and incomplete beats confident and wrong. **2. Wire the cross-party annotation to the UI.** Server, endpoint and both translations are done; the client never calls it. Small, and it is the half of your ask that is closest to shipping. **3. Record terminality — editorial, and needs your call.** Only this makes the empty state definitive. It means a per-event judgement across the 132, by someone who knows the procedure, plus somewhere to put it. Note the issue's out-of-scope list rules out an `event_role` column — a terminality flag is a different thing, but it is close enough that it should be your decision rather than assumed. Doing 1 and 2 without 3 leaves the empty state truthful but vague. Doing 3 without 1 fixes nothing a user can see. I would take them in that order. --- *Not closing — that is yours. No code changed yet; this is the measurement pass.*
Author
Collaborator

Correction to my own comment above: I was reading a surface that is no longer live

Before anyone builds against the split I proposed — the picker this issue describes has been replaced, and the core complaint is substantially already fixed. Correcting it here rather than leaving a wrong plan on the issue.

/tools/fristenrechner no longer exists as a page

It 301-redirects to /tools/procedures, which is the Litigation Builder (m/paliad#153 B1). The "overhaul" result-view design that this issue and my comment both reference was superseded by #151 and then #153.

Measured: all 66 deadlines.overhaul.* i18n keys are defined and none is referenced by any rendering code — including deadlines.overhaul.empty, which is m's exact "Keine Folge-Fristen für dieses Ereignis hinterlegt." string. (Checked for dynamically-built keys too, since partyBadge constructs them that way; there are none.)

The live picker offers far more than this issue says

FristenrechnerService.SearchEvents backs /api/builder/search, and it selects:

FROM paliad.sequencing_rules sr
JOIN paliad.procedural_events pe ON pe.id = sr.procedural_event_id

So it offers every event that has a rule — not only events that appear as a predecessor. Re-measured:

count
active procedural_events 204
offered by the live picker 192
active events with no rule at all 12
offered events with zero follow-ups 120
events that have follow-ups 72

The headline from my earlier comment — "72 selectable, 132 invisible" — describes the old picker. It is the right number for the behaviour this issue was filed against and the wrong number for today. The real residue of "offer all events" is 12, not 132.

The field the honest empty state needs is already on the wire

EventSearchHit.FollowUpCount is computed server-side (a subquery counting rules whose parent_id points at the anchor rule), serialised as follow_up_count, and typed on the client at builder-search.ts:26 — and rendered nowhere.

So "tell the user whether this event leads anywhere" needs no schema, no new endpoint and no new query. It needs the field displayed.

What this does to the split I proposed

  • Slice 2 (wire the cross-party annotation) has no host. The builder's event-triggered mode is B3 and only partly present; nothing calls /api/tools/fristenrechner/follow-ups, and is_cross_party has no renderer. My "server and translations are done, just wire it" was wrong — there is no event-mode result list to wire into. Separately, the Verfahrensablauf canvas already shows the opponent's moves via party columns and partyBadge, so m's "shown, not hidden" ask is partly delivered already.
  • Slice 1 shrinks a long way. Most of "offer all events" happened as a side effect of #151/#153.
  • Slice 3 (record terminality) is unchanged and still m's. Nothing records whether an event is terminal; event_kind cannot stand in — 17 of 31 decision events do have follow-ups. So an empty result still cannot honestly say which of the two cases it is.

Revised smallest useful step

Render follow_up_count in the builder's event-search results. One already-computed field, and it serves m's "select from ALL POSSIBLE EVENTS and then see the followups" directly — by telling the user before they pick whether an event leads anywhere. Then the 12 rule-less events; then cross-party once B3 has a result list to host it.


No code changed. Not closing — that is yours.

## Correction to my own comment above: I was reading a surface that is no longer live Before anyone builds against the split I proposed — **the picker this issue describes has been replaced, and the core complaint is substantially already fixed.** Correcting it here rather than leaving a wrong plan on the issue. ### `/tools/fristenrechner` no longer exists as a page It 301-redirects to `/tools/procedures`, which is the **Litigation Builder** (m/paliad#153 B1). The "overhaul" result-view design that this issue and my comment both reference was superseded by #151 and then #153. Measured: all **66 `deadlines.overhaul.*` i18n keys are defined and none is referenced by any rendering code** — including `deadlines.overhaul.empty`, which is m's exact *"Keine Folge-Fristen für dieses Ereignis hinterlegt."* string. (Checked for dynamically-built keys too, since `partyBadge` constructs them that way; there are none.) ### The live picker offers far more than this issue says `FristenrechnerService.SearchEvents` backs `/api/builder/search`, and it selects: ```sql FROM paliad.sequencing_rules sr JOIN paliad.procedural_events pe ON pe.id = sr.procedural_event_id ``` So it offers every event that **has a rule** — not only events that appear as a predecessor. Re-measured: | | count | |---|---| | active `procedural_events` | 204 | | **offered by the live picker** | **192** | | active events with no rule at all | **12** | | offered events with **zero** follow-ups | 120 | | events that have follow-ups | 72 | The headline from my earlier comment — *"72 selectable, 132 invisible"* — describes the **old** picker. It is the right number for the behaviour this issue was filed against and the wrong number for today. The real residue of "offer all events" is **12**, not 132. ### The field the honest empty state needs is already on the wire `EventSearchHit.FollowUpCount` is computed server-side (a subquery counting rules whose `parent_id` points at the anchor rule), serialised as `follow_up_count`, and **typed on the client** at `builder-search.ts:26` — and rendered nowhere. So "tell the user whether this event leads anywhere" needs no schema, no new endpoint and no new query. It needs the field displayed. ### What this does to the split I proposed - **Slice 2 (wire the cross-party annotation) has no host.** The builder's event-triggered mode is B3 and only partly present; nothing calls `/api/tools/fristenrechner/follow-ups`, and `is_cross_party` has no renderer. My "server and translations are done, just wire it" was wrong — there is no event-mode result list to wire into. Separately, the Verfahrensablauf canvas **already shows the opponent's moves** via party columns and `partyBadge`, so m's "shown, not hidden" ask is partly delivered already. - **Slice 1 shrinks a long way.** Most of "offer all events" happened as a side effect of #151/#153. - **Slice 3 (record terminality) is unchanged and still m's.** Nothing records whether an event is terminal; `event_kind` cannot stand in — 17 of 31 `decision` events do have follow-ups. So an empty result still cannot honestly say *which* of the two cases it is. ### Revised smallest useful step Render `follow_up_count` in the builder's event-search results. One already-computed field, and it serves m's *"select from ALL POSSIBLE EVENTS and then see the followups"* directly — by telling the user **before** they pick whether an event leads anywhere. Then the 12 rule-less events; then cross-party once B3 has a result list to host it. --- *No code changed. Not closing — that is yours.*
Author
Collaborator

follow_up_count render landed + the 12 unofferable events

Branch: mai/knuth/land-darwin-s-follow-up

1. darwin's render, landed

245bce6 merges darwin's 354d7aa onto today's main. Clean merge, no conflicts.

The event-search row now shows the follow-up count before the user commits to an event. The zero state says "Keine Folgefristen hinterlegt" / "No follow-ups recorded" — a statement about the catalog, never about the procedure. A test pins that wording against eight forbidden words (endet, ends, final, terminal, …) so a later "improvement" into something decisive fails and points at why it cannot be.

Re-verified after the merge, since #139 Slice B touched the same editor family: the payload chain is intact end to end — fristenrechner_search_events.go:30 emits follow_up_count, builder-search.ts:26 carries it, the render reads it.

Gate, all green:

check result
scripts/ci-test-gate.sh (-race, ./internal/... ./cmd/... ./pkg/... ./scripts/...) pass — 0 known-failing tolerated, no new failures
go vet (same four scopes) clean
scripts/check-gofmt.sh clean
frontend suite 411 pass, 0 fail

Harness: own instance, TEST_DB_SUFFIX=-knuth port 15488, applied_migrations HEAD=206.

2. The 12 unofferable events — three findings, not one

Full write-up: docs/findings-fristenrechner-unofferable-events-2026-07-29.md (c911a35). Measured on prod, read-only — no catalog row was changed.

The premise needs one correction. The residue is not "12 events with no rule at all". Zero active+published events have no rule; all 12 have exactly one. (204 counts is_active alone, 200 counts is_active AND lifecycle_state='published', which is what SearchEvents filters on. The residue is 12 either way.)

group n answer
B — *.appeal_spawn 4 By design. No fix.
A1 — null.<8hex> 5 Miscatalogued
A2 — upc.inf.cfi.* 3 Editorial backlog (superseded)

B (4, by design): upc.{inf,rev,dmgs,pi}.cfi.appeal_spawn. Excluded deliberately by the sr.is_spawn = false clause per #149 Phase 2 §2.2 — a spawn rule opens the appeal tree, it is not a trigger a user picks. Mig 155 records your Q5 decision on exactly these four rows. Verified this costs the user nothing: every appeal tree is reachable through its own offerable root (upc.apl.merits.notice, upc.apl.order.with_leave, …). These should stay unofferable.

A1 (5, miscatalogued): null.02ae9c1f, null.37bd034b, null.7b548c48, null.cec1a865, null.ec2a1274. Two signatures make these import artifacts rather than legal concepts: the code is null. + the first 8 hex of the rule's own UUID, and each name is a comma-joined list of several distinct filings — null.37bd034b names three separate submissions in one event, so no single deadline can be correct for it. Mig 136 minted them from legacy deadline_rules rows with a NULL submission_code. Mig 151 already retired 35 of the 40-row family; it deduped only name-groups with more than one member, and these five carry unique comma-joined names, so they fell out of scope by construction, not by judgement.

(de.null.* is a different, healthy family — 17 active events where null is a middle path segment. Do not sweep on %null%.)

A2 (3, superseded): upc.inf.cfi.def_to_infr_cc, upc.inf.cfi.reply_def_infr_cc, upc.inf.cfi.rejoin_to_reply. The first two are legally misplaced, which is why their rules were retired — a counterclaim for infringement does not arise inside an infringement action; there the defendant's counterclaim is for revocation. The live rows already exist and are offerable under upc.rev.cfi.{cc_inf,def_cci,reply_def_cci,rejoin_cci}. The third duplicates the live upc.inf.cfi.rejoin. The retirement was right.

The one defect common to all 8 of group A: the rule was archived and the event was not. Each still carries is_active=true, lifecycle_state='published' while its only rule is archived. The catalog advertises them as live; the picker refuses them; nothing reconciles the two and no surface reports the disagreement. Zero paliad.deadlines reference any of them.

Worth recording: that retirement has no migration behind it. The eight rules are in no sequencing_rules_pre_* snapshot and no .up.sql in the tree updates them — migs 151, 152 and 155 are the only migrations that deactivate rules and none of their scopes covers these eight. An admin-UI or hand edit retired them with no record of who or why.

Not decided here, deliberately

Terminality is yours. Nothing records whether an event ends a chain — no is_terminal, and event_kind cannot stand in (17 of 31 decision events do have follow-ups). That is exactly why the new zero state stops at "none recorded".

Cross-party annotation still waits for #153 B3 to have an event-mode result list to host it. LookupFollowUps already returns is_cross_party and both translations exist, but no client calls the endpoint — wiring a renderer to a surface that does not exist would add a fifth built-never-wired rather than fixing the fourth.

Suggested next steps, for you to rule on

  1. Archive the 8 group-A event rows to match the rules retired in 2026-05 — is_active=false, lifecycle_state='archived', no deletion, snapshot first, per the mig 151/152 precedent. Live filing content, so: a migration, not a hand-written UPDATE.
  2. Only after that, add the invariant as a guard: an active+published event must have at least one active+published rule. Note the wording — published, not offerable — or the four legitimate spawn events turn it red. Today it would fail on the eight, so it cannot go in first.
## follow_up_count render landed + the 12 unofferable events **Branch:** `mai/knuth/land-darwin-s-follow-up` ### 1. darwin's render, landed [`245bce6`](https://mgit.msbls.de/m/paliad/commit/245bce6) merges darwin's [`354d7aa`](https://mgit.msbls.de/m/paliad/commit/354d7aa) onto today's main. Clean merge, no conflicts. The event-search row now shows the follow-up count before the user commits to an event. The zero state says *"Keine Folgefristen hinterlegt" / "No follow-ups recorded"* — a statement about the catalog, never about the procedure. A test pins that wording against eight forbidden words (`endet`, `ends`, `final`, `terminal`, …) so a later "improvement" into something decisive fails and points at why it cannot be. Re-verified after the merge, since #139 Slice B touched the same editor family: the payload chain is intact end to end — `fristenrechner_search_events.go:30` emits `follow_up_count`, `builder-search.ts:26` carries it, the render reads it. **Gate, all green:** | check | result | |---|---| | `scripts/ci-test-gate.sh` (`-race`, `./internal/... ./cmd/... ./pkg/... ./scripts/...`) | pass — 0 known-failing tolerated, no new failures | | `go vet` (same four scopes) | clean | | `scripts/check-gofmt.sh` | clean | | frontend suite | **411 pass, 0 fail** | Harness: own instance, `TEST_DB_SUFFIX=-knuth` port 15488, applied_migrations HEAD=206. ### 2. The 12 unofferable events — three findings, not one Full write-up: [`docs/findings-fristenrechner-unofferable-events-2026-07-29.md`](https://mgit.msbls.de/m/paliad/src/branch/mai/knuth/land-darwin-s-follow-up/docs/findings-fristenrechner-unofferable-events-2026-07-29.md) ([`c911a35`](https://mgit.msbls.de/m/paliad/commit/c911a35)). Measured on prod, read-only — no catalog row was changed. **The premise needs one correction.** The residue is not "12 events with no rule at all". **Zero** active+published events have no rule; all 12 have exactly one. (204 counts `is_active` alone, 200 counts `is_active AND lifecycle_state='published'`, which is what `SearchEvents` filters on. The residue is 12 either way.) | group | n | answer | |---|---|---| | B — `*.appeal_spawn` | **4** | **By design. No fix.** | | A1 — `null.<8hex>` | **5** | **Miscatalogued** | | A2 — `upc.inf.cfi.*` | **3** | **Editorial backlog** (superseded) | **B (4, by design):** `upc.{inf,rev,dmgs,pi}.cfi.appeal_spawn`. Excluded deliberately by the `sr.is_spawn = false` clause per #149 Phase 2 §2.2 — a spawn rule *opens* the appeal tree, it is not a trigger a user picks. Mig 155 records your Q5 decision on exactly these four rows. Verified this costs the user nothing: every appeal tree is reachable through its own offerable root (`upc.apl.merits.notice`, `upc.apl.order.with_leave`, …). **These should stay unofferable.** **A1 (5, miscatalogued):** `null.02ae9c1f`, `null.37bd034b`, `null.7b548c48`, `null.cec1a865`, `null.ec2a1274`. Two signatures make these import artifacts rather than legal concepts: the code is `null.` + the first 8 hex of *the rule's own UUID*, and each name is a comma-joined list of several distinct filings — `null.37bd034b` names three separate submissions in one event, so no single deadline can be correct for it. Mig 136 minted them from legacy `deadline_rules` rows with a NULL `submission_code`. Mig 151 already retired 35 of the 40-row family; it deduped only name-groups with more than one member, and these five carry unique comma-joined names, so they fell out of scope by construction, not by judgement. (`de.null.*` is a different, healthy family — 17 active events where `null` is a middle path segment. Do not sweep on `%null%`.) **A2 (3, superseded):** `upc.inf.cfi.def_to_infr_cc`, `upc.inf.cfi.reply_def_infr_cc`, `upc.inf.cfi.rejoin_to_reply`. The first two are legally misplaced, which is why their rules were retired — a counterclaim *for infringement* does not arise inside an infringement action; there the defendant's counterclaim is for revocation. The live rows already exist and are offerable under `upc.rev.cfi.{cc_inf,def_cci,reply_def_cci,rejoin_cci}`. The third duplicates the live `upc.inf.cfi.rejoin`. The retirement was right. **The one defect common to all 8 of group A:** the rule was archived and the event was not. Each still carries `is_active=true, lifecycle_state='published'` while its only rule is `archived`. The catalog advertises them as live; the picker refuses them; nothing reconciles the two and no surface reports the disagreement. Zero `paliad.deadlines` reference any of them. Worth recording: that retirement has **no migration behind it**. The eight rules are in no `sequencing_rules_pre_*` snapshot and no `.up.sql` in the tree updates them — migs 151, 152 and 155 are the only migrations that deactivate rules and none of their scopes covers these eight. An admin-UI or hand edit retired them with no record of who or why. ### Not decided here, deliberately **Terminality is yours.** Nothing records whether an event ends a chain — no `is_terminal`, and `event_kind` cannot stand in (17 of 31 `decision` events do have follow-ups). That is exactly why the new zero state stops at "none recorded". **Cross-party annotation** still waits for #153 B3 to have an event-mode result list to host it. `LookupFollowUps` already returns `is_cross_party` and both translations exist, but no client calls the endpoint — wiring a renderer to a surface that does not exist would add a fifth built-never-wired rather than fixing the fourth. ### Suggested next steps, for you to rule on 1. Archive the 8 group-A **event** rows to match the rules retired in 2026-05 — `is_active=false`, `lifecycle_state='archived'`, no deletion, snapshot first, per the mig 151/152 precedent. Live filing content, so: a migration, not a hand-written `UPDATE`. 2. **Only after that**, add the invariant as a guard: an active+published event must have at least one active+published rule. Note the wording — *published*, not *offerable* — or the four legitimate spawn events turn it red. Today it would fail on the eight, so it cannot go in first.
Author
Collaborator

Re-measured 2026-07-30 — most of this issue has been overtaken, one piece genuinely remains

No code written. This comment is the measurement.

The structural fact: the surface this issue describes was deleted

Commit 264cc39 (2026-05-29, t-paliad-350, m/paliad#153 PRD §7.4) deleted frontend/src/client/fristenrechner-result.ts, fristenrechner-mode-a.ts, fristenrechner-wizard.ts and verfahrensablauf.ts. /tools/fristenrechner and /tools/verfahrensablauf are now 301 redirects to /tools/procedures — the Litigation Builder.

This issue was filed 2026-05-27. The surface it targets was removed two days later. That is the most likely reason it sat two months with zero comments.

The four asks, re-checked against the live tree and prod

1. "Select from ALL POSSIBLE EVENTS" — already true.

SearchEvents (internal/services/fristenrechner_search_events.go:84) selects from sequencing_rules JOIN procedural_events with no must-have-children filter. The only exclusion is sr.is_spawn = false (:115) — deliberate and documented, from m/paliad#149 Phase 2 §2.2: a spawn row is a consequence, not a trigger, and terminal leaves stay pickable.

The May premise — "we derive the trigger from whether an event is mentioned as a previous event in a sequence"no longer holds.

2. Cross-party follow-ups shown, not hidden — answered, and better than this issue specified.

LookupFollowUps returns every child regardless of party and annotates each with is_cross_party (internal/services/fristenrechner_followups.go:152); queryFollowUpRows no longer filters by party at all (:365). Shipped as m/paliad#149 Phase 2 S1.

The live surface goes further than the Gegenseitig badge this issue asked for: verfahrensablauf-core.ts buckets every rule into ours / court / opponent columns (ColumnsRow). The opposing side's next move is visible by construction rather than shown-with-a-caveat.

3. Verfahrensablauf toggles ↔ submission-card checkboxes must agree — shipped.

frontend/src/client/scenario-flags.ts is the single source of truth (m/paliad#149 Phase 2 P0, mig 154): GET/PATCH /api/projects/{id}/scenario-flags, a scenario-flag-changed CustomEvent bus for cross-surface coherence, and dual-write from the builder (builder.ts:1066).

The migration/backfill this issue's scope §3 anticipated is not needed: measured on prod today, paliad.project_event_choices has 0 rows and 0 of 19 projects carry a non-empty scenario_flags. Both stores it wanted reconciled are empty.

4. Show the follow-up count before committing to an event — shipped 2026-07-29.

frontend/src/client/builder-search.ts:334 renders follow_up_count per picker row, with a test (builder-search-followups.test.ts) that cites this issue.

What genuinely remains

The empty state still means two things. Measured on prod 2026-07-30:

  • 188 events offered by the picker
  • 116 of them have zero follow-ups
  • 31 decision events, of which 17 DO have follow-ups

So event_kind cannot stand in for terminality. And there is no column anywhere that could carry it: neither paliad.procedural_events (15 columns) nor paliad.sequencing_rules (34 columns) has anything recording "this event ends the chain" or "the follow-ups have been authored".

The 2026-07-29 work already reached this conclusion and wrote it into the code rather than guessing — the picker's zero state says "Keine Folgefristen hinterlegt" (a statement about the catalog) and deliberately does not say "ends here" (a statement about the procedure).

This cannot be closed by UI work. It needs somewhere to record terminality, and terminality is per (event × proceeding_type) — the same event can end one procedure and continue another — so it belongs on sequencing_rules, not procedural_events.

Proposed shape, if it is wanted:

  • A — mechanism. One migration on paliad.sequencing_rules: followups_reviewed_at timestamptz + is_chain_end boolean NOT NULL DEFAULT false. Three honest states instead of today's two — count > 0 shows the count; count = 0 AND reviewed says the event ends the chain; count = 0 AND not reviewed keeps today's wording. Defaults make all 116 read exactly as they do now, so the migration cannot make the UI claim anything new.
  • B — copy in the picker row and the result surface. Small.
  • C — the editorial call: which of the 116 are genuinely terminal. That is legal judgement over 116 events, not engineering. This issue already put the comparable parent_id backfill out of scope for the same reason.

A and B are cheap. C is what decides whether A and B are worth building, because until C happens the user-visible change is nil.

Separate finding: dead surface behind a live route

GET /api/tools/fristenrechner/follow-ups is registered (internal/handlers/handlers.go:395) and fully implemented, including the is_cross_party annotation — and has zero frontend callers. Alongside it, 66 deadlines.overhaul.* i18n keys (132 lines across DE and EN) are referenced by nothing except the generated i18n-keys.ts type union.

That is the result view deleted in 264cc39 leaving its server half and its copy behind. Not touched in this pass — recorded so the decision to clean it up is deliberate rather than accidental.


Measured by cronus on mai/cronus/issue-148-trigger-picker (no commits — the deliverable was the measurement). Issue deliberately left open: the remaining piece needs m's call on part C.

## Re-measured 2026-07-30 — most of this issue has been overtaken, one piece genuinely remains No code written. This comment is the measurement. ### The structural fact: the surface this issue describes was deleted Commit [`264cc39`](https://mgit.msbls.de/m/paliad/commit/264cc39) (2026-05-29, t-paliad-350, m/paliad#153 PRD §7.4) deleted `frontend/src/client/fristenrechner-result.ts`, `fristenrechner-mode-a.ts`, `fristenrechner-wizard.ts` and `verfahrensablauf.ts`. `/tools/fristenrechner` and `/tools/verfahrensablauf` are now **301 redirects to `/tools/procedures`** — the Litigation Builder. This issue was filed **2026-05-27**. The surface it targets was removed **two days later**. That is the most likely reason it sat two months with zero comments. ### The four asks, re-checked against the live tree and prod **1. "Select from ALL POSSIBLE EVENTS" — already true.** `SearchEvents` (`internal/services/fristenrechner_search_events.go:84`) selects from `sequencing_rules JOIN procedural_events` with **no must-have-children filter**. The only exclusion is `sr.is_spawn = false` (`:115`) — deliberate and documented, from m/paliad#149 Phase 2 §2.2: a spawn row is a consequence, not a trigger, and terminal leaves stay pickable. The May premise — *"we derive the trigger from whether an event is mentioned as a previous event in a sequence"* — **no longer holds**. **2. Cross-party follow-ups shown, not hidden — answered, and better than this issue specified.** `LookupFollowUps` returns every child regardless of party and annotates each with `is_cross_party` (`internal/services/fristenrechner_followups.go:152`); `queryFollowUpRows` no longer filters by party at all (`:365`). Shipped as m/paliad#149 Phase 2 S1. The live surface goes further than the `Gegenseitig` badge this issue asked for: `verfahrensablauf-core.ts` buckets every rule into **`ours` / `court` / `opponent`** columns (`ColumnsRow`). The opposing side's next move is visible *by construction* rather than shown-with-a-caveat. **3. Verfahrensablauf toggles ↔ submission-card checkboxes must agree — shipped.** `frontend/src/client/scenario-flags.ts` is the single source of truth (m/paliad#149 Phase 2 P0, mig 154): `GET`/`PATCH /api/projects/{id}/scenario-flags`, a `scenario-flag-changed` CustomEvent bus for cross-surface coherence, and dual-write from the builder (`builder.ts:1066`). The migration/backfill this issue's scope §3 anticipated is **not needed**: measured on prod today, `paliad.project_event_choices` has **0 rows** and **0** of 19 projects carry a non-empty `scenario_flags`. Both stores it wanted reconciled are empty. **4. Show the follow-up count before committing to an event — shipped 2026-07-29.** `frontend/src/client/builder-search.ts:334` renders `follow_up_count` per picker row, with a test (`builder-search-followups.test.ts`) that cites this issue. ### What genuinely remains **The empty state still means two things.** Measured on prod 2026-07-30: - **188** events offered by the picker - **116** of them have zero follow-ups - **31** `decision` events, of which **17 DO have follow-ups** So `event_kind` cannot stand in for terminality. And there is **no column anywhere** that could carry it: neither `paliad.procedural_events` (15 columns) nor `paliad.sequencing_rules` (34 columns) has anything recording "this event ends the chain" or "the follow-ups have been authored". The 2026-07-29 work already reached this conclusion and wrote it into the code rather than guessing — the picker's zero state says "Keine Folgefristen hinterlegt" (a statement about the catalog) and deliberately does not say "ends here" (a statement about the procedure). **This cannot be closed by UI work.** It needs somewhere to record terminality, and terminality is per (event × proceeding_type) — the same event can end one procedure and continue another — so it belongs on `sequencing_rules`, not `procedural_events`. Proposed shape, if it is wanted: - **A — mechanism.** One migration on `paliad.sequencing_rules`: `followups_reviewed_at timestamptz` + `is_chain_end boolean NOT NULL DEFAULT false`. Three honest states instead of today's two — count > 0 shows the count; count = 0 AND reviewed says the event ends the chain; count = 0 AND not reviewed keeps today's wording. Defaults make all 116 read exactly as they do now, so the migration cannot make the UI claim anything new. - **B — copy** in the picker row and the result surface. Small. - **C — the editorial call**: which of the 116 are *genuinely* terminal. That is legal judgement over 116 events, not engineering. This issue already put the comparable `parent_id` backfill out of scope for the same reason. A and B are cheap. **C is what decides whether A and B are worth building**, because until C happens the user-visible change is nil. ### Separate finding: dead surface behind a live route `GET /api/tools/fristenrechner/follow-ups` is registered (`internal/handlers/handlers.go:395`) and fully implemented, including the `is_cross_party` annotation — and has **zero frontend callers**. Alongside it, **66** `deadlines.overhaul.*` i18n keys (132 lines across DE and EN) are referenced by **nothing** except the generated `i18n-keys.ts` type union. That is the result view deleted in `264cc39` leaving its server half and its copy behind. Not touched in this pass — recorded so the decision to clean it up is deliberate rather than accidental. --- Measured by cronus on `mai/cronus/issue-148-trigger-picker` (no commits — the deliverable was the measurement). Issue deliberately left open: the remaining piece needs m's call on part C.
Author
Collaborator

The remaining piece is built — mechanism only, nothing on screen changes yet

Follows the re-measurement above. m's call on 2026-07-30: build the mechanism, author later.

4aa42e7 mig 215 — is_chain_end + followups_reviewed_at on paliad.sequencing_rules
a1279b2 the three states in the picker

Branch mai/cronus/issue-148-trigger-picker. Not merged; not applied to prod — the deploy applies migrations at boot.

What it does

The zero-follow-up state used to mean two things. It now renders three:

follow-ups > 0 the count — unchanged
zero, asserted „Dieses Ereignis beendet die Kette“ / „This event ends the chain“
zero, unasserted „Keine Folgefristen hinterlegt“ — unchanged

The third is what every one of the 242 rules shows today, because nothing is asserted yet. So this ships with no visible change, which is what you chose: the column exists, marking happens incrementally whenever you or a lawyer gets to it.

Why the columns sit on sequencing_rules

Terminality is a property of (event × proceeding type), not of the event — the same event can end one procedure and continue another. sequencing_rules is exactly that pair, and its row is the anchor the picker already resolves.

is_chain_end is the assertion; only true is a claim. false means nobody has said anything, which is why the unasserted wording is unchanged. followups_reviewed_at is provenance — it separates „false because nobody looked“ from „false because someone looked and it is not terminal“, which is what lets a later pass find the unreviewed remainder. No surface reads it.

A CHECK ties them together: a rule cannot be marked terminal without a review timestamp. An assertion with nobody behind it would print „ends the chain“ with no author.

The property that was tested rather than assumed

TestSequencingRules_ChainEndDefaultsChangeNothing asserts against the live table that no rule is marked and no rule carries a review timestamp after the migration. Not left to the DEFAULT clause: a DEFAULT is a statement about future INSERTs and says nothing about the 242 rows that already existed when it ran. It carries a negative control, because „zero marked rows“ would read the same if the columns were missing or the table empty.

The wire field is optional, so a client running against a pre-215 server degrades to the unasserted wording instead of reading undefined as terminal.

Still open, and not mine to do

Which of the 116 zero-follow-up events are genuinely terminal. That is legal judgement over 116 events. The mechanism is ready for it; nothing marks itself.

Untouched

GET /api/tools/fristenrechner/follow-ups — the dead endpoint from the separate finding above. Adding a field to surface with zero callers is not a fix, so it was left exactly as it is.

Gate

scripts/ci-test-gate.sh green — go vet clean, full suite with -race, 0 known-failing tolerated, no new failures. All six migration gates pass. check-gofmt.sh exit 0. 429 frontend tests.

Issue deliberately left open.

## The remaining piece is built — mechanism only, nothing on screen changes yet Follows the [re-measurement above](https://mgit.msbls.de/m/paliad/issues/148#issuecomment-17641). m's call on 2026-07-30: **build the mechanism, author later.** | | | |---|---| | [`4aa42e7`](https://mgit.msbls.de/m/paliad/commit/4aa42e7) | mig 215 — `is_chain_end` + `followups_reviewed_at` on `paliad.sequencing_rules` | | [`a1279b2`](https://mgit.msbls.de/m/paliad/commit/a1279b2) | the three states in the picker | Branch `mai/cronus/issue-148-trigger-picker`. Not merged; not applied to prod — the deploy applies migrations at boot. ### What it does The zero-follow-up state used to mean two things. It now renders three: | | | |---|---| | follow-ups > 0 | the count — unchanged | | zero, **asserted** | „Dieses Ereignis beendet die Kette“ / „This event ends the chain“ | | zero, **unasserted** | „Keine Folgefristen hinterlegt“ — unchanged | The third is what every one of the 242 rules shows today, because nothing is asserted yet. **So this ships with no visible change**, which is what you chose: the column exists, marking happens incrementally whenever you or a lawyer gets to it. ### Why the columns sit on `sequencing_rules` Terminality is a property of (event × proceeding type), not of the event — the same event can end one procedure and continue another. `sequencing_rules` is exactly that pair, and its row is the anchor the picker already resolves. `is_chain_end` is the assertion; **only `true` is a claim**. `false` means nobody has said anything, which is why the unasserted wording is unchanged. `followups_reviewed_at` is provenance — it separates „false because nobody looked“ from „false because someone looked and it is not terminal“, which is what lets a later pass find the unreviewed remainder. No surface reads it. A CHECK ties them together: a rule cannot be marked terminal without a review timestamp. An assertion with nobody behind it would print „ends the chain“ with no author. ### The property that was tested rather than assumed `TestSequencingRules_ChainEndDefaultsChangeNothing` asserts against the live table that **no** rule is marked and **no** rule carries a review timestamp after the migration. Not left to the `DEFAULT` clause: a DEFAULT is a statement about future INSERTs and says nothing about the 242 rows that already existed when it ran. It carries a negative control, because „zero marked rows“ would read the same if the columns were missing or the table empty. The wire field is optional, so a client running against a pre-215 server degrades to the unasserted wording instead of reading `undefined` as terminal. ### Still open, and not mine to do **Which of the 116 zero-follow-up events are genuinely terminal.** That is legal judgement over 116 events. The mechanism is ready for it; nothing marks itself. ### Untouched `GET /api/tools/fristenrechner/follow-ups` — the dead endpoint from the separate finding above. Adding a field to surface with zero callers is not a fix, so it was left exactly as it is. ### Gate `scripts/ci-test-gate.sh` green — `go vet` clean, full suite with `-race`, 0 known-failing tolerated, no new failures. All six migration gates pass. `check-gofmt.sh` exit 0. 429 frontend tests. Issue deliberately left open.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/paliad#148
No description provided.