Two SELECTs still referenced paliad.deadlines.rule_id after mig 140
(Slice B.4) dropped that column in favour of sequencing_rule_id:
- internal/services/deadline_service.go:268 — DeadlineService.
ListVisibleForUser. Powers /api/events?type=deadline (dashboard
deadline rail, /deadlines page, every status bucket). Threw
`pq: column f.rule_id does not exist` on every request → 500
for any authenticated user hitting the dashboard.
- internal/services/projection_service.go:1250 — collectActualsForOverrides.
Same column on `paliad.deadlines d`. Logged once per projection
pass (`ERROR service: projection: deadlines: ...`); aliased the
rename to `rule_id` so the receiving struct tag still scans.
Live container logs confirmed the failure mode — a 60-row burst of
`pq: column f.rule_id does not exist at position 3:36 (42703)` starting
the minute the post-B0 container came up (mig 140 had applied to the
DB but the SELECT still used the dropped name). EXPLAIN against the
live schema after the edit plans cleanly; the LEFT JOIN to
paliad.deadline_rules_unified on sequencing_rule_id was already correct
(only the SELECT projection was stale).
Root cause: mig 140 commit (1129bab) renamed the JOIN to
`f.sequencing_rule_id` but left the SELECT clause on the older name.
The model tag is already `db:"sequencing_rule_id" json:"rule_id"`, so
the wire shape is unchanged — only the column reference flips.
bun build clean, go vet ./... clean, go test ./... green.
Builds on B1 (commit 6c1d8cc). After this slice a user can compose a
multi-proceeding scenario kontextfrei: stack proceedings, flip
perspective per-triplet, toggle scenario flags, auto-spawn child
proceedings on flag transitions, and mark individual event cards as
planned / filed / skipped — all auto-saved to paliad.scenario_*.
PRD §7.1 B2 acceptance shipped:
- Multi-triplet stack: top-level proceedings sorted by ordinal,
child proceedings nested inline with a left lime border.
- Per-triplet controls bar: perspective radio (none / claimant /
defendant), Detailgrad pill (selected / all options), Entfernen
action. Each control PATCHes the proceeding row and re-renders the
affected triplet.
- Per-triplet flag strip: every paliad.scenario_flag_catalog row
rendered as a checkbox, bound to scenario_proceedings.scenario_flags.
Active flags also surface as chips in the triplet header for quick
legibility.
- Spawn nesting: when `with_ccr` flips ON on upc.inf.cfi the builder
auto-POSTs an upc.ccr.cfi child proceeding linked via
parent_scenario_proceeding_id; flip OFF deletes the child (events
cascade via the schema). The SPAWN_MAP table is data-driven so
future spawn flags slot in.
- 3-state event cards (planned / filed / skipped):
overlayEventStates walks the rendered .fr-col-item nodes (the
data-rule-id hook added to verfahrensablauf-core in this slice)
and stamps each card with data-builder-state + per-state action
buttons (File / Skip / Reset to planned). Filed cards prompt for
a date; skipped cards prompt for an optional reason. POSTs or
PATCHes paliad.scenario_events keyed by sequencing_rule_id.
- Per-card optional horizon chip: stores horizon_optional on the
scenario_event row, increment / decrement chip on every card.
The full surface awaits a calc-engine "optionals available"
counter (PRD §3.4 follow-up); the persistence layer + UX hook are
in place so the wiring lands without another schema touch.
- Page-header Stichtag drives default dates for every triplet (the
triplet's per-stichtag override path is wired but the per-triplet
Stichtag input is a B3+ affordance).
verfahrensablauf-core.renderColumnsBody now stamps data-rule-id (and
data-submission-code as a future hook) on every .fr-col-item root —
non-breaking enhancement; the legacy /tools/* pages don't read either
attribute. Verified by re-running the existing 57-test suite.
Backend: one new read-only endpoint
GET /api/builder/scenario-flag-catalog passes through
ScenarioFlagsService.ListCatalog so the builder doesn't need a
per-project round-trip to render flag toggles.
bun run build clean (3050 i18n keys), go vet ./... clean, go test ./...
clean, frontend bun test (verfahrensablauf-core suite) 57 / 57 pass.
Replaces cronus's U0-U4 catalog at /tools/procedures with a
persistence-backed builder shell on top of B0's API surface
(/api/builder/scenarios/*, t-paliad-340).
PRD §7.1 B1 acceptance shipped:
- Page header: scenario picker, name action, Akte picker stub,
Stichtag input, search input, save status indicator.
- Entry-mode radio (cold-open active; event-triggered + akte
rendered disabled for B3/B4 layout stability).
- Empty canvas with "Neues Szenario starten" CTA and a 5-most-recent
list rendered when the user has saved scenarios.
- Side panel "Meine Szenarien" with the Aktiv bucket; clicking an
item loads the scenario into the canvas.
- Add-proceeding inline picker (Forum chip row → Verfahren chip row
→ Hinzufügen). UPC v1; other forums chipped but disabled.
- First proceeding triplet renders end-to-end via
verfahrensablauf-core.calculateDeadlines + renderColumnsBody (the
existing 3-column proaktiv|court|reaktiv body, read-only in B1).
- Auto-save with 500ms debounce on name + stichtag patches; save
status flips idle → saving → saved/error in the page header.
New client modules under frontend/src/client/:
- builder.ts — orchestrator (URL state, fetch, auto-save loop,
canvas render, scenario-list re-paint).
- builder-picker.ts — inline Forum/Verfahren popover for the
add-proceeding affordance.
- builder-triplet.ts — single-triplet header + body wrapper.
procedures.tsx rewritten as the shell scaffolding (sidebar, page
header, mode radio, two-column body); procedures.ts now boots the
builder instead of toggling the 4-tab catalog.
Legacy U0-U4 modules (verfahrensablauf.ts, verfahrensablauf-state.ts,
VerfahrensablaufBody.tsx, procedures' tab toggle in client/procedures.ts,
fristenrechner-* mounts) are no longer reachable from /tools/procedures
but kept in the tree for the B6 cleanup sweep per PRD §7.4.
i18n.ts grew 60 keys × 2 langs under builder.*. global.css grew a
self-contained .builder-* block at the file tail.
bun run build, go vet ./..., and go test ./... all green.
Two paired engine semantics fixes:
1. trigger_event_id is now the authoritative semantic anchor. When a
rule carries trigger_event_id, the engine no longer falls back to
the proceeding's trigger date — it resolves the anchor via
CalcOptions.TriggerEventAnchors keyed by paliad.trigger_events.code.
Missing anchor renders the rule as IsConditional (empty date) and
propagates via courtSet so descendants also surface as conditional.
Fixes the RoP.109.5 bug where the engine fabricated a date 2 weeks
before the user's SoC instead of waiting for the oral_hearing date.
2. priority='optional' rules are suppressed from the default
Calculate output. Callers (paliad /tools/procedures,
youpc.org/deadlines) opt in via CalcOptions.IncludeOptional=true to
restore the legacy "show optional applications" behaviour. The
suppression cascades through skippedIDs so child rules drop too.
Wire shape additions:
- CalcOptions.IncludeOptional bool
- CalcOptions.TriggerEventAnchors map[string]string
- Timeline.RulesAwaitingAnchor int (count of suppressed-by-missing-
anchor rules, for caller telemetry / "N rules need an anchor" UX)
Existing before-court-set-anchor tests opt in to IncludeOptional=true
to preserve their non-optional-related test intent.
Refs: youpcorg/head delegations #2568 + #2570, m/paliad#153 (Litigation
Builder PRD path).
The Markdown inline scanner (parseInlineSpans) treats _ and * as
italic delimiters. A placeholder like {{project.case_number}} fed
through the scanner had its underscores consumed as italic markers,
leaving {{project.casenumber}} in the composed OOXML. The v1
placeholder pass then looked up the wrong key, surfacing
[KEIN WERT: project.casenumber] in the preview. The form ↔ preview
highlighting also stopped working because data-var attributes
mismatched between the input (snake_case) and the rendered span
(stripped).
parseInlineSpans now detects {{ at the cursor and skips ahead to
the matching }}, copying the entire placeholder verbatim into the
current text run. Unmatched {{ falls through to the existing
character handling so legal prose with stray braces still renders.
Tests: regression test for underscored keys (single + multiple +
mixed-with-italics), direct guard on parseInlineSpans, and an
italic-around-placeholder structural test.
t-paliad-340 — B0 of edison's 7-slice train (PRD §7.1). DB-only: schema
+ RLS land, dev-only test route exercises the surface, no user-facing
change. B1 wires the actual builder UI on top.
Migration 157 (additive on the legacy mig-145 scenarios table — 0 rows
in prod, safe to relax):
- paliad.scenarios gets owner_id / status / origin_project_id /
promoted_project_id / stichtag / notes. spec drops NOT NULL and the
scenarios_unique_per_scope constraint drops (the builder allows
multiple scratch + Unbenanntes Szenario rows per user).
- New tables: scenario_proceedings, scenario_events, scenario_shares.
- paliad.projects.origin_scenario_id for the promote-to-project audit
trail (the FK lands now; the wizard ships in B5).
- paliad.can_see_scenario(uuid) STABLE SECURITY DEFINER helper covering
owner / share / global_admin / two legacy paths.
- Replacement RLS on scenarios + RLS on the three new tables; legacy
service + handlers stay live and unchanged.
PRD §5.1 deviations called out in the migration header:
- proceeding_type_id is integer (live schema), not uuid (PRD draft).
- FK target is paliad.users, matching the rest of paliad's schema.
Go surface:
- ScenarioBuilderService — list/create/get-deep/patch scenarios,
add/patch/delete proceedings, add/patch/delete events,
add/delete shares. Writes wrap in transactions with set_config(
paliad.audit_reason, ..., true) per event_choice_service.go pattern.
- /api/builder/scenarios/* — handlers register under a builder/
prefix so the legacy /api/scenarios surface still works.
- /dev/scenario-builder — single-page HTML form gated to
PaliadinOwnerEmail, exercises the B0 surface without Postman.
- Live-DB integration test (TEST_DATABASE_URL gated) covers
create + list + deep-get + share + visibility negatives + patch.
Audit-first: every DDL block ran clean via BEGIN/ROLLBACK against
the live DB before commit; end-to-end sanity (insert chain + CHECK
constraints + CASCADE-on-delete) verified via the Supabase MCP.
bun build clean. go vet + go test -short ./... green.
PRD for the columnar litigation planner replacing today's 4-tab catalog
at /tools/procedures with a Litigation Builder backed by a new Scenario
DB. Captures 20 chip-picker decisions (5 batches via AskUserQuestion)
covering: unified-builder shape with 3 entry modes (cold-open /
event-triggered / Akte), separate paliad.scenarios table with
multi-proceeding constellations, auto-save + named-list, per-proceeding
flags + perspective + Detailgrad, 3-state event cards
(planned/filed/skipped), per-event-card optional horizon, vertical
stacked column-triplets with inline spawn children, universal search
(events + scenarios + Akten), 3-step promote-to-project wizard,
read-only team sharing, desktop v1 + mobile basic-read.
Includes data model deltas (4 new tables + 1 column on
paliad.projects), 6-slice migration plan from the current live U0-U4
catalog, and coder hand-off notes. Cross-proceeding peer triggers and
DE/EPA/DPMA full expansion deferred to v1.1.
Selected .tracker-pill.is-active used --color-accent-fg, which in dark
mode resolves to lime → lime text on lime background, unreadable.
Switch to --color-accent-dark (midnight in both modes) so the selected
pill has midnight text on lime in both light + dark. Same pattern as
the older .filter-pill.active rule.
The workflow tracker (T1-T4) replaces every consumer of the entry-mode
modules. Verified via grep that no non-deleted file imports the
following before removal:
Deleted (10 files):
- client/fristenrechner-mode-a.ts (Mode A search panel)
- client/fristenrechner-wizard.ts (Mode B guided wizard)
- client/fristenrechner-wizard.test.ts
- client/fristenrechner-result.ts (post-commit result-view)
- client/fristenrechner-result.test.ts
- client/verfahrensablauf.ts (Verfahrensablauf panel client)
- client/views/event-card-choices.ts (per-card choice popover —
only verfahrensablauf.ts consumed it)
- client/views/verfahrensablauf-state.ts (URL + storage helpers —
only verfahrensablauf.ts consumed it)
- client/views/verfahrensablauf-state.test.ts
- components/VerfahrensablaufBody.tsx (the 4-tab proceeding picker
body — no consumer after T1)
Kept (still load-bearing):
- client/views/verfahrensablauf-core.ts — procedures-tracker uses
calculateDeadlines + CalculatedDeadline + escHtml + formatDate.
- client/views/verfahrensablauf-core.test.ts
- client/verfahrensablauf-detail-mode.ts — procedures-tracker uses
filterByDetailMode under the per-proceeding "Alle Optionen"
toggle (T4).
- client/verfahrensablauf-detail-mode.test.ts
The .css classes (.fristen-wizard-*, .verfahrensablauf-*) still live
in global.css; they're cheap orphans (no selector match in the new
DOM) and a CSS housekeeping pass is outside this train's scope. The
i18n keys (deadlines.flag.*, deadlines.detail.*, deadlines.view.*,
deadlines.side.*) likewise stay — some are used dynamically via tDyn
on the tracker, others remain candidates for a future i18n sweep.
Frontend tests: 217 pass (264 → 217, the deltas are the 3 deleted
test files: fristenrechner-result, fristenrechner-wizard,
verfahrensablauf-state). Build + go vet clean.
t-paliad-338
The final tracker layer per design §3.4 / §3.6 / §11 polish list:
- Per-proceeding "· Gewählt · / Alle Optionen" toggle (§3.4) lives in
the card header next to the show/hide button. State persists in
localStorage per proceeding code, so a page with multiple cards
can keep one expanded without affecting siblings. Toggle drives the
detail mode for filterByDetailMode + sets includeHidden=true on the
calc, so previously-skipped conditional rules re-surface muted.
- Appeal-target chip group (§3.2 #3) renders below the header on
proceedings with applies_to_target rules — today only upc.apl.unified.
Endentscheidung / Kostenentscheidung / Anordnung / Schadensbemessung
/ Bucheinsicht. Picking a target re-fetches the calc with the
appealTarget param so the timeline narrows to the matching subset.
- Cross-party muted treatment (§3.6) — when the find-header Partei
pill is set, rows whose primary_party is the opposite side render
with a "Gegen." badge and a muted style. Court / both / informational
rows are never cross-party.
- "Unselected" + "hidden" styling — under "Alle Optionen" the rules
that filterByDetailMode stamps __detailUnselected on render dotted
italic, and previously-skipped (isHidden) rules render at reduced
opacity. Honest preview of what the user is NOT considering.
- Cross-surface scenario-flag-changed listener — the tracker now reseeds
its flags state when Mode B / Verfahrensablauf / Verlauf patches the
same project's flags, so toggling there flows through here without a
refresh.
Out of T4 (court-set choices_offered chip groups and the court-set date
override from appointments) — those need a follow-up backend pass to
surface the choicesOffered payload on TimelineEntry through the calc
response in a usable shape. The data field exists on CalculatedDeadline
but isn't yet wired to a paint route on the tracker.
t-paliad-338
Wires the workflow tracker to projects via ?project=<uuid>, per design
§6.4 + §11.Q5:
- loadAkte fetches /api/projects/{id}, /api/projects/{id}/timeline
and /api/projects/{id}/scenario-flags in parallel:
1. Project title + proceeding_type — pre-seeds the Verfahren pill.
2. Timeline events → ActualsMap keyed by deadline_rule_id with
status (done / overdue / open / court_set), due / completed
date, and deadline / appointment ids.
3. scenario_flags → seeds state.flags so the gating-flag checkboxes
render in the persisted state. Per-rule rule:<uuid> flags stay
out of the calc payload (they drive priority deviations via
isRuleSelected, handled by the existing detail-mode filter).
- Auto-pin: the first render with no explicit ?event= pins the most
recent status='done' deadline. URL pin (shared link) is preserved.
- Per-node overlay: each node carries the actuals badge — ✓ (done +
strike-through), ⚠ (overdue + red wash), 📅 (open ≠ projected), ◇
(open ≡ projected). Date column shows the actual date.
- Fork write-back: PATCH /api/projects/{id}/scenario-flags fires on
every flag toggle so Mode B / Verlauf / dashboard re-render with the
same scenario on next visit. Fire-and-forget; UI doesn't wait.
- Find-header summary chips: "Akte: <title>" alongside "Anker: <name>"
+ "{n} Verfahren".
Out of T3 (deferred):
- ?project= picker UI (today's user navigates here from /projects/{id}
via deep-link).
- Per-rule rule:<uuid> flag write-back (priority deviations) — the
detail-mode filter doesn't take an interactive toggle yet.
- Cross-surface scenario-flag-changed CustomEvent listener — patching
fires the event, the tracker just doesn't yet re-render on incoming
ones (T4 polish).
t-paliad-338
Layers the anchor / focus interactivity on top of T1's shell per
design §6.1–§6.5:
- Click-to-pin (📌) on every node with a real rule_id sets the anchor.
Clicking the already-anchored pin un-pins. URL state ?event=<id>.
- Anchored node renders with a "── DU BIST HIER ──" divider beneath
its meta line + the lime left-band styling. The find-header summary
surfaces "Anker: <name>" so the user can confirm where they are.
- Fokus chip (🔍) on the anchored node toggles zoom (?zoom=1). Zoom
renders the anchor's parent chain as a breadcrumb at the top of the
proceeding card and renders only the anchored subtree below. A
"{n} weitere Schritte verborgen" footer reports what zoom hid.
- Multi-proceeding scope (§6.5): when an anchor is pinned and >1
proceeding is visible, non-anchored proceedings auto-collapse to a
one-line header card with a [zeigen] / [ausblenden] toggle. The
user's explicit expansions persist for the current anchor; pinning
a different node clears them.
- Auto-pinning from the search input (T1's single-hit behaviour) now
routes through onAnchorChanged so the multi-proc scope kicks in
consistently.
Anchor + zoom state writes through history.replaceState — sharable URL.
Un-pinning clears zoom and restores the full multi-proceeding view
automatically (lastAnchor tracking).
t-paliad-338
Direct-replace per m's Q7 divergent pick in atlas's design
(docs/design-procedures-workflow-tracker-2026-05-27.md §9): /tools/procedures
drops the 4-tab catalog (U0-U4 shipped this morning) for the single
canonical workflow-tracker shape.
T1 ships:
- Sticky find header — search input, forum / Verfahren / Partei pill
rows, global Stichtag, live result summary.
- Per-proceeding timeline cards — one card per matched proceeding,
rendered as a chained tree by parent_id with priority-styled bullets
(mandatory solid, recommended muted, optional dotted, informational
faded, court-set blue). Party badge per node.
- Cold-open default: the 6 curated proceedings from design §8 / §11.Q4
(upc.inf.cfi, upc.rev.cfi, upc.apl.unified, de.inf.lg, epa.opp.opd,
dpma.opp.dpma) render stacked with a hint above.
- Scenario-flag forks — per-proceeding "Optionen" strip on each card's
header surfaces the applicable flags (with_ccr, with_amend, with_cci)
derived from condition_expr or a fallback map. Tick re-runs the calc.
- URL state: ?q, ?forum, ?procs, ?party, ?trigger_date, ?event, ?flags.
?event= scroll-highlights the matching node (no zoom yet — T2 layers).
- Legacy ?mode= dropped silently on first state write so bookmarks
self-clean. /tools/fristenrechner + /tools/verfahrensablauf 301s
still resolve here.
Floor T1 honours: every catalog workflow it replaces — pick proceeding
(forum + Verfahren pills), search event (search input → auto-narrow +
?event= anchor), wizard narrowing (pills compose), Akte entry
(?project= read-only for T1; full overlay in T3).
Per-node fork placement (the design's stated final shape — checkbox on
the gating node itself, not a card-level strip) is a T2 refinement;
T1 keeps forks scoped per proceeding so they're not the global-page
strip m's bug #5 flagged.
Aux-proceedings inline-expandable (design §5) and the appeal-target
chip group are scoped to T4; the calculator currently doesn't surface
isSpawn / spawnProceedingCode through TimelineEntry to support them.
t-paliad-338
atlas shipped the workflow-tracker design after m's 21:01 grilling-round reframe (single timeline-with-forks, find=search+pills+result-timelines, aux inline, zoom from within full tree). 510-line doc, 2 rewrite iterations.
7 Qs answered in 2 batches (4+3). 5 on-recommendation, 2 divergent:
- Q3 (divergent): multi-proceeding anchor scope — auto-collapse other proceedings to header-only (new §6.5)
- Q7 (divergent): migration strategy — direct replace at T1, no feature flag (§9)
4-slice + cleanup train. T1 ships minimum-viable tracker visibly at /tools/procedures, replacing the catalog UI knuth shipped today.
Inventor parks. Head dispatches Sonnet coder (NOT atlas per project memory directive).
5 picks on-recommendation, 2 diverged:
Q3 (multi-proceeding anchor scope): m picked 'other timelines auto-collapse to header-only' over the recommended 'stay expanded'. Added §6.5 with the header-card render rule.
Q7 (migration cadence): m picked 'direct replace at T1, no flag' over the recommended flag-gated dev. §9 rewritten end-to-end: T1 ships the minimum-viable tracker visibly to users, replacing the catalog UI in the same PR. T2-T4 layer zoom + Akte + polish. T5 is cleanup-only.
The 5 on-rec picks: inline checkbox forks (Q1), sibling-collapse zoom (Q2), 6 curated defaults on cold open (Q4), latest-done-deadline Akte anchor (Q5), global Stichtag (Q6) — all locked as drafted in §1-§8.
Ready for review. Coder gate held; head decides T1 hire.
m's reframe (2026-05-27 20:43): /tools/procedures should be a workflow
tracker, not a catalog browser. Pick any procedural event, see backward
(predecessors) + self (where I am) + forward (successors), with
scenario_flags as togglable predicates and alternative constellations
explorable.
This shift-1 doc covers:
- 4-tab UX redo (single-pane radio-revealed entry form to fix the
pre-form-leak bug)
- Anchor visualisation (vertical waterfall with anchor at centre line)
- Three views — Anchor / Verfahren / Konstellationen — toggle preserves
anchor + scenario state
- Forward walk (current constellation only by default, conditional
reveal toggle, view-mode toggle reused from atlas P3)
- Backward walk (3 hops default, Akte mode overlays paliad.deadlines
actuals onto template chain)
- Compound rules drawer (per-anchor Querverweise affordance — column
shape owned by curie editorial workstream)
- Constellation viewer (inline per-flag preview drawer + full
Constellation view for browse)
- Akte entry (anchor derives from latest completed deadline)
- Migration: T1-T5 flag-gated dev under ?tracker=1, then hard-cut
Coder gate held. 11 open questions for m staged for AskUserQuestion in
4+4+3 batches. Decisions append as §13 before the
TRACKER DESIGN READY FOR REVIEW signal.
Per m's Q11 divergence in the design (no 2-week dual-ship), this slice
flips /tools/fristenrechner and /tools/verfahrensablauf to permanent 301
redirects to /tools/procedures and deletes the legacy frontend pages.
Bookmarks resolve via Location preservation of query params; no
?legacy=1 escape, no in-product affordance pointed back at the retired
URLs after the merge.
Server:
- handleFristenrechnerPage + handleVerfahrensablaufPage now 301 to
/tools/procedures, carrying any query string through unchanged.
- pillDrillURL in deadline_search_service.go retargets to
/tools/procedures so freshly indexed search pills land on the new
page directly (cached snapshots still work via the 301).
Frontend:
- Deleted src/fristenrechner.tsx, src/verfahrensablauf.tsx,
src/client/fristenrechner.ts.
- src/client/verfahrensablauf.ts loses its DOMContentLoaded auto-boot
and the now-unused initI18n / initSidebar imports; procedures.ts is
the sole caller of initVerfahrensablauf().
- frontend/build.ts drops the legacy entrypoints and renderXxx HTML
outputs.
- Sidebar.tsx, Header.tsx, index.tsx, paliadin-context.ts repointed
to /tools/procedures.
- Unused nav.fristenrechner / nav.verfahrensablauf /
tools.verfahrensablauf.* i18n keys removed.
Tests:
- verfahrensablauf_test.go rewritten to assert both legacy URLs return
301 with the correct Location (query string preserved).
Mounts the full Verfahrensablauf wizard — proceeding picker, perspective
chooser, date inputs, scenario flag rows, detail-mode toggle, view
toggle, timeline-container — under the /tools/procedures "Verfahren
wählen" tab. Per-rule scenario_flags chips (P0 SSoT) and the
Aufnehmen/Entfernen affordances reach the unified page unchanged since
they're delegated handlers on the timeline-container.
Refactor steps:
- Extracted the wizard body markup into a shared TSX component
(components/VerfahrensablaufBody) used by both verfahrensablauf.tsx
(legacy) and procedures.tsx (unified). U4 will retire the legacy
page; the shared component lets U3 ship without code duplication.
- Lifted the verfahrensablauf.ts DOMContentLoaded body into
initVerfahrensablauf() and re-exported it. The legacy auto-boot
stays in place but skips itself when #procedures-panel-proceeding
is present, so the unified page imports the module without
double-init. procedures.ts calls initVerfahrensablauf() the first
time the proceeding tab activates, gated by a one-shot flag to
preserve module-local selectedType / lastResponse across tab
toggles.
m flagged 2026-05-27 20:26: archived rules (e.g. the 5 mig 152 Mängelbeseitigung clones) clutter the /admin/procedural-events default view. They were correctly archived by mig 152 but visually noisy alongside active rules.
Fix: default activeLifecycle = 'published'. The 'Alle' chip still exists for when the user wants to see drafts + archived; 'Archived' chip surfaces them on demand. Initial view shows only the active corpus.
Mounts mountWizard() into #procedures-panel-wizard when the Geführt tab
activates. Same 5-row wizard, same backend (event search + follow-ups
probe) as the legacy /tools/fristenrechner. On R4 launchResult, the
wizard hands off to mountResultView which renders into the same
overhaul-root inside the panel.
The wizard renders into #fristen-overhaul-mode-host while Mode A and
the result view write into #fristen-overhaul-root. To keep those IDs
unique in the DOM — both modes look up via document.getElementById —
the host scaffold is no longer static on the search panel. The new
installOverhaulHost() helper tears down any existing host and installs
a fresh one inside the active tab's panel before each mount, so two
parallel hosts can't cross-wire when the user toggles between the
Direkt-suchen and Geführt tabs.
The U1/U2 placeholders are dropped from the panel markup since the
panels are populated dynamically now.
Mounts mountModeA() into #procedures-panel-search when the Direkt-suchen
tab activates. The legacy fristenrechner-mode-a code runs unchanged
inside a wrapper that reseeds the #fristen-overhaul-root /
#fristen-overhaul-mode-host scaffold on every tab activation, so
re-clicking the tab always restores a fresh Mode A surface even if the
previous interaction committed an event into the result view.
`?event=<code>` deep links still resolve: boot detects the param,
activates the search tab, and hands directly to mountResultView() —
the result lands inside the same root, the user sees the picked
event's follow-up rules with the Direkt-suchen tab as the visible
context.
Search-box-in-filter-strip composition with chip filters (m's Q3
divergence) lands later, after Mode B + Verfahrensablauf are folded —
the unified state machine pulls all three behind one search input.
First slice of the unified procedural-events tool train. Ships only the
page chrome — route, sidebar/header, filter strip with search box, four
entry-mode tabs (Verfahren wählen / Direkt suchen / Geführt / Aus Akte),
and the host containers later slices mount their UI into. No data wiring.
Per m's decisions (design §11.5): URL is English (/tools/procedures, not
/tools/verfahren); all four tabs visible from boot (not a single-default
landing); search box lives in the top filter strip and will compose with
chip filters once U1+ wire them.
U1 fills #procedures-panel-search (Mode A), U2 fills -wizard (Mode B),
U3 fills -proceeding + #procedures-output-tree (Verfahrensablauf), U4
hard-cuts /tools/fristenrechner and /tools/verfahrensablauf to 301
redirects and drops the legacy pages.
- audit of 6 surfaces with question→dimension matrix
- proposal: fold Fristenrechner + Verfahrensablauf into /tools/verfahren
- 4 entry paths converge on tree + linear output shapes
- mobile narrow-viewport rules + 3 worked personas
- 5-slice migration train (U0-U4), no DB migration
- 12 open questions in 3 batches for AskUserQuestion
Slice B.6 / S6 renamed the canonical edit URL from /admin/rules/{id}/edit
to /admin/procedural-events/{id}/edit. The backend handler + 301 redirect
landed, but the client-side regex in admin-rules-edit.ts:110 was missed —
it still only matches the legacy /admin/rules/.../edit shape. Result:
visiting the canonical URL from the list page shows 'Ungültige
Verfahrensschritt-ID in der URL.' even though the rule exists.
Fix: regex accepts both '/admin/procedural-events/{id}/edit' (canonical)
and '/admin/rules/{id}/edit' (legacy, kept for stale tabs / bookmarks
during the deprecation window).
m flagged 2026-05-27 17:57 on rule cc439590 (RoP.262.2, upc.inf.cfi).
ritchie shipped the final two slices of the Phase 2 train.
P2 — condition_expr write-validator:
- New internal/services/condition_expr_validator.go (136 LoC) — locks the grammar to {flag:<str>} OR {op:'and'|'or', args:[<leaf>|<composite>]} per design §4.1
- RuleEditorService.Create + Update reject non-conforming expressions
- 166-LoC test coverage; all 18 existing condition_expr rows validate
P4 (partial) — trigger_events deprecation (mig 156):
- NULLs out the 2 hybrid rules' trigger_event_id (parent_id is the canonical edge per §2.1)
- Adds 'Deprecated: see m/paliad#149' header on the legacy /api/tools/event-deadlines route
- Does NOT drop paliad.trigger_events nor the 5 read sites — those are gated on the editorial reparenting of the 73 orphan globals (NULL proceeding_type_id, served only via trigger_event_id). Editorial work is m's, not coder scope.
Comment on m/paliad#149 (issuecomment-10436) enumerates the exact next steps for the eventual follow-up coder once editorial reparenting completes.
Phase 2 train: P0 + S1+S1a + P1 + P3 + P2 + P4 partial — ALL shipped. Final P4 step waits on editorial.
Phase 2 P4 partial-scope (head approved 2026-05-27 15:24). The full
drop of paliad.trigger_events + the legacy route + 5 read sites is
gated on an editorial backfill that's not in coder scope — 73 active
sequencing_rules carry proceeding_type_id IS NULL and are addressed
ONLY via trigger_event_id today. Dropping anything would break those
73 orphans.
What this lands:
1. Mig 156 — NULL out trigger_event_id on the 2 hybrid rules that
carry BOTH parent_id AND trigger_event_id. Per design §2.1 /
m's Q1, parent_id is the canonical predecessor link; the
hybrid trigger_event_id was redundant. The 2 rules' parent_id
chains keep the live edge. Live-DB verified post-apply: 0
active hybrid rules remain.
2. Deprecation + Link headers on POST /api/tools/event-deadlines
per RFC 8594 / RFC 9745. The route stays functional so the 73
orphans keep working until reparenting lands.
What this does NOT land (gated on editorial):
- DROP TABLE paliad.trigger_events
- DROP COLUMN paliad.sequencing_rules.trigger_event_id
- Remove the legacy /api/tools/event-deadlines handler
- Remove EventDeadlineService + ExportService::1680 sheet
- Remove deadline_rule_service.go:226 label-fallback path
- Remove event_type_service.go:40+414 reads (33 event_types still
reference trigger_event_id)
- Update cmd/gen-upc-snapshot/main.go:185-202 to skip trigger_events
- Drop the sequencing_rules_trigger_event_id_fkey FK
All of the above lands in a follow-up mig once the orphan count
hits zero. Comment to follow on m/paliad#149 with the editorial-
backlog list.
Verified: live-DB pre/post hybrid count (0 active hybrids remain);
mig idempotent; go vet clean.
Design: docs/design-deadline-system-revision-2026-05-27.md §2.1
(parent_id canonical), §3.4 (legacy route fate), §4.3 (table fate),
§5 (slice train P5 row). t-paliad-331.
Phase 2 P2 (design §4.1). Locks the condition_expr grammar to:
CondExpr := { "flag": "<known_flag>" }
| { "op": "and"|"or", "args": [<CondExpr>, ...] }
Where <known_flag> must exist in paliad.scenario_flag_catalog (today:
with_ccr / with_amend / with_cci; editorial adds via the catalog
table as needed).
Wire-time validation in RuleEditorService.Create and UpdateDraft —
the rule editor surfaces a 400 with a friendly message before the row
hits the DB. Empty / JSON null inputs pass through (the "no gate"
shape; stored as NULL column).
The validator:
* walks the JSON tree once, collecting every leaf flag name
* rejects mutually-exclusive shapes (leaf + composite in one node)
* rejects empty args, bad op values, empty flag strings
* does ONE batch lookup of the collected leaf names against the
catalog (regardless of expression depth)
Tests:
* 9 shape-only unit tests covering every reject path (no DB needed)
* TestValidateConditionExpr_LiveCatalog covers 6 good shapes + 2
unknown-flag cases against the live catalog
* TestConditionExpr_AllLiveRowsValidate runs the validator over
every active+published condition_expr in paliad.sequencing_rules
to enforce the §4.1 invariant on every deploy (today's 18 rows
all conform — verified via Supabase MCP pre-flight)
Live-DB tests skip cleanly when TEST_DATABASE_URL is unset (same
posture as sibling live tests in this package).
Design: docs/design-deadline-system-revision-2026-05-27.md §4.1
(grammar formalisation). t-paliad-331.
ritchie shipped m's headline UX (paliadin priority signal 14:58):
'The new timeline filters for optional / mandatory / show only selected
is what I am most waiting for. I want this to be consolidated for all
our deadlines so we can simulate all proceedings.'
Three-way detail-level filter above the Verfahrensablauf result panel:
- Nur Pflicht — only priority='mandatory' rules
- Gewählt (default) — mandatory + recommended + every explicit per-rule override in projects.scenario_flags
- Alle Optionen — every rule, unselected ones rendered dotted-border + muted
State persists per-user via localStorage['verfahrensablauf:view_mode']. Per-rule Aufnehmen/Entfernen chips wire to projects.scenario_flags via the P0 SSoT (rule:<uuid> entries).
New files: verfahrensablauf-detail-mode.ts (125), verfahrensablauf-detail-mode.test.ts (96), filter wiring in verfahrensablauf.ts (+204) and views/verfahrensablauf-core.ts (+37). 63 LoC CSS (dotted-border treatment).
bun build clean, 264 frontend tests pass (8 new), go vet clean.
Ritchie continuing with P2 (condition_expr write-validator) then P4 (legacy deprecation).
m's headline UX ask (2026-05-27 14:58, paliadin priority signal):
"The new timeline filters for optional / mandatory / show only
selected is what I am most waiting for. I want this to be
consolidated for all our deadlines so we can simulate all
proceedings."
Phase 2 P3. Adds a three-way detail-level filter above the result
panel on /tools/verfahrensablauf:
( ) Nur Pflicht — only priority='mandatory' rules
(•) Gewählt — mandatory + recommended (default) + every
explicit per-rule override the user has set
in projects.scenario_flags
( ) Alle Optionen — every rule, with unselected ones rendered
dotted-border + muted so the user sees what
they're NOT considering
State persists per-user via localStorage["verfahrensablauf:view_mode"].
The filter is pure client-side narrowing on the calc payload — flipping
the toggle re-renders instantly without a fresh backend call.
Per-rule selection (design §2.4a): every optional / recommended card
now carries an [Aufnehmen] / [Entfernen] chip. Clicking writes a
"rule:<uuid>" entry into the project's scenario_flags via the P0 SSoT
PATCH endpoint, recording only deviations from the priority default:
recommended + entfernen → rule:<uuid> = false (explicit deselect)
optional + aufnehmen → rule:<uuid> = true (explicit select)
flipping back to the default deletes the entry
Mandatory rules never expose the chip — they cannot be deselected.
Wire-shape change: CalculatedDeadline gains `ruleId` (the backend already
emits it as `ruleId` in TimelineEntry; only the frontend interface needed
to surface it).
Conditional handling: a conditional rule whose predicate doesn't fire
is treated as unselected in "Gewählt" mode (even when priority=
mandatory) — mandatory means "must be filed IF the predicate fires",
not "always render". The "Alle Optionen" view re-surfaces it so the
lawyer can see what scenario would unlock it.
Cross-surface coherence: hydrating ?project=<id> reads scenario_flags
from the SSoT and pre-fills the existing flag checkboxes (with_ccr /
with_amend / with_cci) so the page reflects the project's persisted
state on first paint. Every flag toggle + every per-rule chip click
PATCHes back. The page also listens for the scenario-flag-changed
CustomEvent fired by peer surfaces (Mode B Fristenrechner result-view)
and re-renders without a fresh fetch.
i18n: 5 new keys (deadlines.detail.{label,mandatory_only,selected,
all_options,optional_unselected_hint,aufnehmen,entfernen}) DE + EN.
CSS: dotted-border + muted treatment on .timeline-item-header--
unselected; .timeline-selection-chip with --add (lime accent) and
--remove (discreet muted) variants.
Tests: 8 new unit tests covering isRuleSelected (4 priority × 2 flag
state matrix) and filterByDetailMode (3 modes × default/override cases).
Verified: bun build clean, bun test 264/264 (8 new), go vet clean.
Design: docs/design-deadline-system-revision-2026-05-27.md §2.4a
(selection state model), §3.3a (view-mode toggle), §6 (Entry A spec).
t-paliad-331. Re-prioritised by m via paliadin 14:58.
Phase 2 P1 / m's Q5 divergence (2026-05-27, verbatim):
"Reverse the unification as suggested in 3. They are different
proceedings, I only wanted the approach to be unified in the
'determinator' — but they are actually different proceedings!"
Mig 155 reverts the mig-096 unification:
Before: id=160 upc.apl.unified active (16 rules), id=11/19/20 inactive
After: id=11 upc.apl.merits (7 rules), id=19 upc.apl.cost (2 rules),
id=20 upc.apl.order (7 rules) all active; id=160 inactive
The 16 rules under id=160 split cleanly by event_code prefix; all 10
parent_id edges among them are bucket-local (pre-flight audit), so
the tree shape survives the rebind unchanged.
Spawn FK retarget: pi.cfi.appeal_spawn flips from 11 (merits) → 20
(orders track) per design §3.1 — PI appeals land on orders, not
merits. The inf/rev/dmgs spawns keep target=11 (merits), now active.
Determinator routing layer (proceeding_mapping.go) keeps its single
"Berufung" front door per m's intent — only the data shape changes.
Pre-flight verified: 0 projects bound to id=160, 0 scenarios reference
upc.apl. Zero data migration on the project side.
Tests: lookup_events_test.go assertions on the three appeal_target
buckets updated to the new codes (endentscheidung → upc.apl.merits,
schadensbemessung → upc.apl.merits, bucheinsicht → upc.apl.order).
Same rule set, post-split coordinates.
Snapshot regen (pkg/litigationplanner/embedded/upc/) deferred: the
current snapshot only contains inf+rev so the apl re-split doesn't
shift its contents; regenerating would surface unrelated active PTs
and pollute this slice. Tracked as a follow-up.
Verified: go vet clean, go test ./internal/services/... -run
LookupEvents|proceeding_codes clean.
Design: docs/design-deadline-system-revision-2026-05-27.md §3.1
(re-split mig), §1.3 (spawn graph post-Q5). t-paliad-331.
Phase 2 S1 + S1a (pre-ratified from t-paliad-327, folded into the
Phase 2 train).
S1 — Cross-party display:
- FristenrechnerService.LookupFollowUps stops filtering by party
server-side; queryFollowUpRows drops the perspective WHERE clause
and returns every published+active child.
- Server now computes is_cross_party per row (true only when
perspective ∈ {claimant,defendant} AND primary_party is the
opposite side; NULL/both/court is never cross-party).
- FollowUpRule wire shape gains the boolean.
- Frontend renderRule adds a "Gegenseitig" badge + is-cross-party
row class (muted styling, disabled checkbox affordance).
- defaultChecked returns false for cross-party rows.
- countSelected + submitWriteBack skip cross-party rows
unconditionally — even if a user manually checks the box, they
describe opposing-side filings and don't belong in our Akte set
(design §2.4 write-back exclusion).
- i18n: deadlines.overhaul.crossparty.badge / .tooltip (DE+EN).
- CSS: .fristen-overhaul-rule-crossparty + .is-cross-party row
modifier.
S1a — Spawn-only picker filter:
- SearchEvents WHERE now adds `sr.is_spawn = false` so spawn rules
(e.g. appeal_spawn, the inf.cfi → upc.apl.merits hop) no longer
surface as picker hits. Spawn rules are consequences, not
triggers — a lawyer searching "Berufung" wants the appeal-tree
root, not the inf.cfi spawn link.
- Terminal leaves (Duplik etc.) stay pickable per design §2.2's
carve-out: their own anchor is non-spawn, so they surface and
render an honest empty follow-up list.
Honest UX: hiding cross-party follow-ups lied about what the
workflow does next (cf. RoP.029.d falling off when perspective=
claimant on def_to_ccr — the workflow continues, just on the
defendant's docket). The fix makes the data legible without
contaminating the write-back path.
Verified: go vet clean, bun build clean, bun test 256/256,
go test ./internal/services/... -run LookupFollowUps... clean.
Design: docs/design-deadline-system-revision-2026-05-27.md §2.4
(cross-party) + §2.2 (spawn-only picker). t-paliad-331.