Fristenrechner: trigger picker accepts all events; cross-party follow-ups must show with annotation; scenario flags must sync with conditional checkboxes #148
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
m's diagnosis (2026-05-27 10:35)
And from the 10:29 message:
Premises verified live (2026-05-27)
A.
parent_idIS the predecessor linkSample chains (
paliad.sequencing_rules.parent_id→ parent rule'sprocedural_event):Stats over
paliad.sequencing_rules(active+published, 226 rows):parent_idset — the rule sits below a predecessor in the cascade tree.parent_id = NULL— these are top-of-tree anchors OR genuine leaves that haven't been linked yet.parent_idfor 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_rolecolumn onprocedural_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:Knuth's
FristenrechnerService.LookupFollowUpsfilters follow-ups to rules whereprimary_party = $perspective OR primary_party = 'both'. m's perspective wasclaimant. 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
Gegenseitigannotation (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 offrontend/src/fristenrechner.tsx) renders scenario checkboxes for the proceeding:with_ccr)with_amendment_r30)paliad.project_event_choices+paliad.scenariosjsonb 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_expron a sequencing_rule references a scenario key (e.g.{"requires": ["with_ccr"]}). The Verfahrensablauf checkboxes write topaliad.project_event_choices(orprojects.scenariosjsonb). The result view's conditional checkboxes only live in DOM state. Three independent stores. The design needs ONE source of truth that:projects.scenariosjsonb is the natural home — mig 145).Scope
One inventor pass covering:
sr.procedural_event_id = trigger.id" which is the anchor lookup, not the follow-up resolution).primary_partydoesn't match the user'sperspective:Gegenseitig/Cross-PartybadgeLookupFollowUpsreturns all rules; UI does the grouping/annotation; perspective stays a display qualifier in the result view.projects.scenariosjsonb shape (extend mig 145 schema if needed — minimal new keys, not a re-architect)project_event_choicesrows mapped intoprojects.scenarios(or coexist)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
event_rolecolumn onprocedural_events(m: "we DON'T carry the explicit event_rule"). The architecture stays: derive trigger-ness from parent_id chains.pkg/litigationplanner.CalculateRule). Just the lookup + render layer.?legacy=1page can deprecate naturally.Acceptance for the design pass
Deliverable:
docs/design-fristenrechner-followup-rules-2026-05-NN.mdcovering:projects.scenariosjsonb), key naming convention (with_ccr,with_amendment_r30, …), schema delta if any.FristenrechnerService.LookupFollowUps: stop filtering by party server-side, return all + the rule's primary_party; UI takes responsibility for grouping/annotation.projects.scenariosfromproject_event_choicesif both surfaces should round-trip.upc.inf.cfi.def_to_ccras trigger → result view shows: own follow-ups (checked) + cross-partyRoP.029.d(Replik auf Erwiderung zur CCR) withGegenseitigbadge, unchecked, date visible.No code yet — inventor → coder gate. Park after
DESIGN READY FOR REVIEW.Cross-references
docs/design-fristenrechner-overhaul-2026-05-26.md(S2-S6 shipped 2026-05-27).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.scenariosjsonb on projects: introduced mig 145 (Litigation Planner Slice D).paliad.project_event_choicestable: legacy scenario storage, PK(project_id, scenario_name, submission_code, choice_kind).paliad.sequencing_rules.condition_expr(jsonb): per-rule scenario predicate.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_idis the predecessor link: STILL HOLDS, numbers movedparent_idparent_idNULLThe catalog shrank and the linkage improved — the editorial backlog is 42%, not 53%.
One thing worth ruling out explicitly:
sequencing_rules.trigger_event_idexists 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 topaliad.trigger_events(bigint), a different table —procedural_events.idis 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.
LookupFollowUpsnow "returns the other side's filings — and annotates each row withis_cross_party" (m/paliad#149 Phase 2 S1).upc.inf.cfi.def_to_ccrstill has its published childRoP.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 ini18n-keys.ts.But nothing consumes any of it. No file under
frontend/src/referencesis_cross_party, nothing uses the badge key, and no client code callsGET /api/tools/fristenrechner/follow-ups— the only fristenrechner call in the client isPOST /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), notprojects.scenarios. It carries named flags (with_ccr/with_amend/with_cci) and per-rule deviations keyedrule:<uuid>, and it drives bothcondition_exprevaluation inpkg/litigationplannerand the Verfahrensablauf scenario strip.PATCH /api/projects/{id}/scenario-flagshas merge semantics withnulldeleting a key. Item 3 of the scope is done.The measurement that matters, and the problem with the headline ask
procedural_events65% 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_kindcannot stand in, because 17 of 31decisionevents 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_rolecolumn — 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.
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/fristenrechnerno longer exists as a pageIt 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 — includingdeadlines.overhaul.empty, which is m's exact "Keine Folge-Fristen für dieses Ereignis hinterlegt." string. (Checked for dynamically-built keys too, sincepartyBadgeconstructs them that way; there are none.)The live picker offers far more than this issue says
FristenrechnerService.SearchEventsbacks/api/builder/search, and it selects:So it offers every event that has a rule — not only events that appear as a predecessor. Re-measured:
procedural_eventsThe 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.FollowUpCountis computed server-side (a subquery counting rules whoseparent_idpoints at the anchor rule), serialised asfollow_up_count, and typed on the client atbuilder-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
/api/tools/fristenrechner/follow-ups, andis_cross_partyhas 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 andpartyBadge, so m's "shown, not hidden" ask is partly delivered already.event_kindcannot stand in — 17 of 31decisionevents 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_countin 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.
follow_up_count render landed + the 12 unofferable events
Branch:
mai/knuth/land-darwin-s-follow-up1. darwin's render, landed
245bce6merges darwin's354d7aaonto 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:30emitsfollow_up_count,builder-search.ts:26carries it, the render reads it.Gate, all green:
scripts/ci-test-gate.sh(-race,./internal/... ./cmd/... ./pkg/... ./scripts/...)go vet(same four scopes)scripts/check-gofmt.shHarness: own instance,
TEST_DB_SUFFIX=-knuthport 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_activealone, 200 countsis_active AND lifecycle_state='published', which is whatSearchEventsfilters on. The residue is 12 either way.)*.appeal_spawnnull.<8hex>upc.inf.cfi.*B (4, by design):
upc.{inf,rev,dmgs,pi}.cfi.appeal_spawn. Excluded deliberately by thesr.is_spawn = falseclause 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 isnull.+ the first 8 hex of the rule's own UUID, and each name is a comma-joined list of several distinct filings —null.37bd034bnames three separate submissions in one event, so no single deadline can be correct for it. Mig 136 minted them from legacydeadline_rulesrows with a NULLsubmission_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 wherenullis 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 underupc.rev.cfi.{cc_inf,def_cci,reply_def_cci,rejoin_cci}. The third duplicates the liveupc.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 isarchived. The catalog advertises them as live; the picker refuses them; nothing reconciles the two and no surface reports the disagreement. Zeropaliad.deadlinesreference 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.sqlin 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, andevent_kindcannot stand in (17 of 31decisionevents 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.
LookupFollowUpsalready returnsis_cross_partyand 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
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-writtenUPDATE.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) deletedfrontend/src/client/fristenrechner-result.ts,fristenrechner-mode-a.ts,fristenrechner-wizard.tsandverfahrensablauf.ts./tools/fristenrechnerand/tools/verfahrensablaufare 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 fromsequencing_rules JOIN procedural_eventswith no must-have-children filter. The only exclusion issr.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.
LookupFollowUpsreturns every child regardless of party and annotates each withis_cross_party(internal/services/fristenrechner_followups.go:152);queryFollowUpRowsno longer filters by party at all (:365). Shipped as m/paliad#149 Phase 2 S1.The live surface goes further than the
Gegenseitigbadge this issue asked for:verfahrensablauf-core.tsbuckets every rule intoours/court/opponentcolumns (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.tsis the single source of truth (m/paliad#149 Phase 2 P0, mig 154):GET/PATCH /api/projects/{id}/scenario-flags, ascenario-flag-changedCustomEvent 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_choiceshas 0 rows and 0 of 19 projects carry a non-emptyscenario_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:334rendersfollow_up_countper 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:
decisionevents, of which 17 DO have follow-upsSo
event_kindcannot stand in for terminality. And there is no column anywhere that could carry it: neitherpaliad.procedural_events(15 columns) norpaliad.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, notprocedural_events.Proposed shape, if it is wanted:
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.parent_idbackfill 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-upsis registered (internal/handlers/handlers.go:395) and fully implemented, including theis_cross_partyannotation — and has zero frontend callers. Alongside it, 66deadlines.overhaul.*i18n keys (132 lines across DE and EN) are referenced by nothing except the generatedi18n-keys.tstype union.That is the result view deleted in
264cc39leaving 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.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.
4aa42e7is_chain_end+followups_reviewed_atonpaliad.sequencing_rulesa1279b2Branch
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:
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_rulesTerminality is a property of (event × proceeding type), not of the event — the same event can end one procedure and continue another.
sequencing_rulesis exactly that pair, and its row is the anchor the picker already resolves.is_chain_endis the assertion; onlytrueis a claim.falsemeans nobody has said anything, which is why the unasserted wording is unchanged.followups_reviewed_atis 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_ChainEndDefaultsChangeNothingasserts against the live table that no rule is marked and no rule carries a review timestamp after the migration. Not left to theDEFAULTclause: 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
undefinedas 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.shgreen —go vetclean, full suite with-race, 0 known-failing tolerated, no new failures. All six migration gates pass.check-gofmt.shexit 0. 429 frontend tests.Issue deliberately left open.