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.
brunel aligned the embedded snapshot calendar with paliad-side policy from t-paliad-121: UPC vacation rows stay in the data for informational annotation but no longer trigger IsNonWorkingDay → AdjustForNonWorkingDays leaves dates intact.
Changes:
- pkg/litigationplanner/embedded/upc/holidays.go: IsNonWorkingDay now returns true only on closure (not vacation). Vacations still surface via AdjustForNonWorkingDaysWithReason for labelling.
- pkg/litigationplanner/embedded/upc/holidays.json: regenerated from live paliad.holidays. Was 5 placeholders → now 55 holidays (33 vacation + 22 closures). Includes UPC Winter Vacation rows.
- pkg/litigationplanner/embedded/upc/meta.json: snapshot version bump.
- snapshot_test.go: +42 lines covering the vacation non-blocking behavior with regression cases.
Affects youpc.org/deadlines (consumes pkg/litigationplanner via Go module replace) — picks up automatically on rebuild.
youpc.org/deadlines was rolling a deadline "from 2027-01-02 (UPC Winter
Vacation)" — i.e. across the UPC judicial vacation as if it were a
public holiday. Paliad-side t-paliad-121 already decided vacations are
informational only (the Court keeps running through them, RoP / UPC AC
decision-on-judicial-vacation 2023-05-26), and `HolidayService.Is
NonWorkingDay` in `internal/services/holidays.go` is correct. The
embedded snapshot consumed by youpc.org via Go-module replace had
drifted: `pkg/litigationplanner/embedded/upc/holidays.go:74` blocked on
both `isClosure()` AND `isVacation()`.
This commit aligns the embedded calendar with the paliad-side semantics
and ships a fresh holiday set so the existing 2026/2027 fix actually
takes effect downstream.
Code changes (`holidays.go`):
- `IsNonWorkingDay`: drop the `|| h.isVacation()` branch — only weekends
and `isClosure()` rows trigger the roll. Godoc rewritten to mirror
the paliad-side rationale (Court keeps operating, RoP cites,
vacation rows kept for informational labels).
- `isClosure()`: accept both `"public_holiday"` and `"closure"`. Live
paliad DB rows use the `public_holiday` value; the placeholder
snapshot shipped with the original Slice C used `closure` as a
hand-crafted synonym. Reconciles with
`internal/services/holidays.go:132` which already does the same
union. Required to make the regenerated JSON (full of
`public_holiday`) keep blocking DE national holidays after the
regeneration in this commit.
- Type-level godoc updated: `SnapshotHolidayCalendar` now documents
vacation-is-informational, and the `AdjustForNonWorkingDaysWithReason`
precedence note explains that `vacation` kind only fires when a
vacation row overlaps a weekend or closure that's already doing the
rolling.
Data refresh (`holidays.json`):
- Regenerated from paliad prod (postgres @ 100.99.98.201:11833,
paliad schema). 55 rows for 2026 + 2027: 22 DE public_holiday +
33 UPC vacation (25 Summer Vacation Jul 27–Aug 28, 8 Winter
Vacation Dec 24/28–31 + Jan 4–6). The previous placeholder shipped
only 5 rows (3 Sommerpause + Neujahr + Tag der Arbeit, no Winter
Vacation at all) — which is why a date landing in late Dec / early
Jan landed inside an unmodeled gap on the consumer side.
- `meta.json` bumped: version → `2026-05-27-1-holidays-only`,
`holiday_count` 5 → 55, `source_db_label` flags that only
holidays.json was refreshed (see friction note below).
Regression test (`snapshot_test.go::TestSnapshotHolidayCalendar`):
- 2026-08-04 (Tue, UPC Summer Vacation) — `IsNonWorkingDay` must be
false; `AdjustForNonWorkingDays` must NOT mutate the date.
- 2027-01-02 (Sat, m's flagged scenario) — must roll forward through
Sat/Sun, then STOP on Mon 2027-01-04 (UPC Winter Vacation, no longer
blocking). Pre-fix this rolled all the way to Thu 2027-01-07.
Cross-repo: youpc.org imports `pkg/litigationplanner` via Go-module
replace; the regenerated snapshot ships on its next rebuild. No
separate youpc.org commit needed — paliad is the source of truth.
Friction note: `cmd/gen-upc-snapshot/main.go` itself is incompatible
with the current paliad schema. Migration 140 (`140_drop_deadline_rules`)
dropped `paliad.deadline_rules`, but the generator still SELECTs from
it (main.go ~L162). Running the tool against prod fails on the rules
step. I bypassed the broken path and generated `holidays.json` directly
from the DB via psql + jq (same JSON shape that `EmbeddedHoliday`
expects, nulls filtered for `omitempty`). The other snapshot files
(rules.json, proceeding_types.json, trigger_events.json, courts.json)
remain at their pre-existing placeholder state — re-flagged in
meta.json's `source_db_label`. Refitting the generator for the post-
mig-140 schema is a separate task.
go vet + go test ./... clean (256+ Go tests pass, including the new
regression cases).
Phase 2 P0 of the deadline + procedural-events revision. Establishes
paliad.projects.scenario_flags (jsonb) + paliad.scenario_flag_catalog as
the single source of truth for per-project scenario state — replacing
the three fragmented stores athena flagged (project_event_choices,
scenarios.spec, DOM-only). All three were empty per the audit so no
data migration is needed.
The jsonb map carries two key shapes:
* named flags (whitelist via scenario_flag_catalog) — today
with_ccr / with_amend / with_cci
* per-rule selection deviations of shape "rule:<uuid>" — wired up
here for validation; the consumer UI lands in P3
Endpoints:
GET /api/projects/{id}/scenario-flags
PATCH /api/projects/{id}/scenario-flags
PATCH semantics: bool = write; null = delete (priority-driven default
returns); missing key = leave alone. The service validates every key
on write (catalog lookup + UUID rule-membership + mandatory-cannot-be-
deselected) before persisting, so a single bad key fails the whole
patch.
Frontend bind: new scenario-flags.ts client module + Mode B's flag
checkboxes (ccr-flag / inf-amend-flag / rev-amend-flag / rev-cci-flag)
now hydrate from / persist to the project's scenario_flags on every
toggle. Kontextfrei (no project) is unchanged. Cross-surface coherence
via a scenario-flag-changed CustomEvent (peer surfaces — Verfahrens-
ablauf strip, Mode B result-view — will subscribe in P3).
Mig 154 is audit-defensive (set_config of paliad.audit_reason); no
audit trigger fires on paliad.projects today but a future one will
inherit the reason. Seeds the three known flags. CHECK constraints
enforce the top-level shape (jsonb_typeof = 'object') and the
catalog key pattern (lowercase, not 'rule:%' prefix).
Verified against the live DB: 18 projects default to '{}', catalog
has 3 rows, applied_migrations advanced to 154.
Design: docs/design-deadline-system-revision-2026-05-27.md §2.3, §2.4a,
§4.1, §5 (P0 row). t-paliad-331.
brunel converted hardcoded hex to existing design tokens for the .timeline-* classes (Verfahrensablauf at ?view=timeline). No new tokens introduced; 11 existing tokens reused. Layout/spacing untouched.
Mirrors the t-paliad-326 playbook applied earlier to the Fristenrechner overhaul CSS, but on the separate timeline view that wasn't in scope then.
The /tools/verfahrensablauf?view=timeline page (and the columns-view
mirror via `.fr-col-item--*` modifiers) had hardcoded light backgrounds
that survived a dark-mode flip. Root cause: four undefined custom
properties (`--color-bg-soft`, `--color-border-soft`, `--color-accent-bg`,
`--brand-lime`) consumed by the `.timeline-*` and adjacent
`.event-card-choices-*` rules, each with a hardcoded hex / RGBA fallback.
Since neither :root nor :root[data-theme='dark'] defines those tokens,
every consumer fell through to the light fallback in both themes —
leaving the conditional-row bg, the popover surface, the option-button
bg, the chip-skipped bg, and the popover block-separator border stuck on
near-white in dark mode. Earlier t-paliad-326 covered the new
Fristenrechner overhaul CSS only; the timeline view styles are a
separate block (~L3337-3810 in `frontend/src/styles/global.css`) and
were not touched.
Migrate every consumer to the existing dual-theme tokens already used
across paliad. Same approach m approved for t-paliad-326, no new tokens
introduced (all reuse):
- Card / popover surfaces (`.event-card-choices-popover`,
`.event-card-choices-option`) → `--color-surface` (white light /
card-tint dark) so the popover reads as raised above the body in
both themes.
- Subtle raised surface for conditional row, skipped chip, option
hover (`.timeline-item--conditional .timeline-content`,
`.fr-col-item--conditional`, `.event-card-choices-chip-part--skipped`,
`.event-card-choices-option:hover/:focus-visible`) →
`--color-surface-muted`. **This is the visible bug fix m flagged.**
- Lime-tint backdrops (`.timeline-context-note` bg,
`.event-card-choices-caret:hover/:focus-visible`) →
`--color-bg-lime-tint` (lime-alpha 0.10 light / 0.12 dark).
- Active-option chip bg (`.event-card-choices-chip-part`) →
`--color-accent-soft-bg`.
- Lime accent borders / fills (`.timeline-context-note` left border,
`.timeline-date--overridden`, `.frist-date-edit-input`,
`.event-card-choices-unhide-btn`, `.event-card-choices-option--active`)
→ `--color-accent` / `--color-accent-fg`. Drops the
legacy `#c6f41c` fallback that doesn't match the current brand
(`--hlc-lime: #BFF355`).
- Neutral borders (`.event-card-choices-caret`,
`.timeline-item--hidden .timeline-content`, `.fr-col-item--hidden`,
`.timeline-item--conditional .timeline-content`,
`.event-card-choices-popover`, `.event-card-choices-option`,
`.event-card-choices-block + .event-card-choices-block`) →
`--color-border` (warm cream-derived light / cream-alpha dark).
- Popover shadow `rgba(0, 0, 0, 0.12)` → `var(--shadow-md)`
(auto-deepens to `rgba(0, 0, 0, 0.45)` in dark).
- Status red (`.event-card-choices-error`) → `--status-red-fg`
(defined in both themes; old `#b00020` fallback unreachable).
Zero new tokens introduced — every replacement uses a token already
shipped in both :root and :root[data-theme='dark']. Verified by
mounting `frontend/dist/assets/global.css` against a representative
static DOM (context-note banner, every timeline-item variant —
conditional / skipped / hidden / overridden-date / mandatory —
popover with active/inactive options, unhide button, error message,
all four party-badge stances) and toggling `data-theme` between
light and dark: conditional row bg flips from grey to muted-cream-on-
midnight, popover lifts off the body via `--shadow-md`, every chip and
border stays legible in both themes.
bun build + bun test (256 pass) + go vet clean.
m's clarification at 14:40 reframed the original "rarity" framing: every
optional rule is a per-scenario selectable card; the Verfahrensablauf
gets a three-way detail-level filter (Nur Pflicht / Gewählt / Alle
Optionen). The CCR-dropdown pattern generalises to per-rule chips.
Three folded additions:
§2.4a — Selection state + detail-level filter. NO new column on
sequencing_rules (reverts the earlier is_edge_case strawman). Extends
projects.scenario_flags jsonb to carry both named flags (with_ccr etc.)
and per-rule entries (rule:<uuid>). Storage only carries deviations
from the priority default (recommended = default-selected,
optional = default-unselected). Whitelist accepts rule:<uuid> when the
UUID resolves to an active rule on the project's PT.
§3.3a — Verfahrensablauf view-mode toggle: three-way segmented control,
localStorage persistence, default "Gewählt". Mode B result view stays
single-purpose (no view-mode toggle there).
§4.2.1 — R.109 translation chain editorial worked example: R.109.1 stays
as optional anchor; R.109.4 reparents to R.109.1 with condition_expr
{flag: with_interpreter_denied} and primary_party=both (parties, not
court); R.109.5 reparents to R.109.1 with {flag: with_translation_granted}.
Introduces two new flags to scenario_flag_catalog.
§6 UI spec updated: two mocked tree states (Gewählt + Alle Optionen)
showing the dotted-border [Aufnehmen] chips, [Entfernen] on selected
optionals, greyed-with-hint on flag-gated conditionals, and the
subtree-hide-on-unselected-ancestor render logic.
§10.0a captures the additions; §10.1 notes they don't change the slice
train (P0 + P3 take the extended scope; no new mig).
Builds on athena's Phase 1 assessment (9aee9e4) + atlas's t-paliad-327
pre-ratified subset. m's Option B direction: "overall schema for all
procedural events and how they are connected" — connection graph as the
spine.
Connection schema (§1):
- Rules are nodes, parent_id is the canonical edge, spawn rules are the
cross-PT edges, condition_expr filters the visible subgraph
- ASCII trees for the 3 largest PTs (upc.inf.cfi 25, upc.rev.cfi 17,
upc.apl post-Q5-split 16); Mermaid graph for the 4 spawn cross-PT edges
- Per-PT health table covering all 23 active primaries (17 ruled + 6 empty)
m's 12 design decisions (3 batches of 4 via AskUserQuestion):
Tier 1 — model (all 4 on-recommendation):
- Q1: parent_id is canonical, deprecate trigger_event_id
- Q2: Reparent 73 legacy globals via editorial walk
- Q3: Derive trigger discoverability from data (EXISTS)
- Q4: projects.scenario_flags jsonb (confirms t-paliad-327 design)
Tier 2 — surface (1 divergent, 3 on-recommendation):
- Q5 DIVERGENT: Reverse the upc.apl unification — split back into 3 PTs
(merits/cost/order). m: "I only wanted the approach to be unified in
the 'determinator' — but they are actually different proceedings!"
Mig P1 retargets 16 rules by event_code prefix.
- Q6: Show empty PTs with "Keine Regeln gepflegt" badge
- Q7: Fold Entry A into /tools/verfahrensablauf
- Q8: Drop /event-deadlines after 73-globals reparenting
Tier 3 — editorial (all on-recommendation):
- Q9: Lock condition_expr grammar {flag} | {op:and|or, args}
- Q10: parent-NULL filter on /admin/procedural-events
- Q11: Drop trigger_events table once route is gone
- Q12: ASCII per-PT + Mermaid spawn graph
6-slice migration train (§5): P0 scenario SSoT, P1 appeal re-split, S1/S1a
from t-paliad-327, P2 empty-PT badge, P3 Entry A, P4 editorial walk, P5
trigger_event_id deprecation. P5 gated on P4.
No code yet — coder gate held per inventor SKILL.