Workstream B frontend sweep — matches mig 098 + the Go sweep. The
/admin/rules surfaces now distinguish submission_code (the rule's
filing identifier within a proceeding, e.g. upc.inf.cfi.soc) from
rule_code (the legal citation, e.g. RoP.013.1).
Admin rules list (/admin/rules):
- Column header renamed "Code" → "Submission Code / Einreichung-Kennung"
- New "Rechtsgrundlage" column shows rule_code alongside the submission
code; the old single-column fallback (rule_code || code) is gone.
- Filter-search placeholder updated to "Name, Submission Code,
Rechtsgrundlage…"
- Rule interface: code → submission_code field.
Admin rules edit (/admin/rules/{id}/edit):
- f-code → f-submission-code; input is now read-only with a
upc.inf.cfi.soc-style placeholder (consistent with the backend
RulePatch which doesn't allow editing the submission code).
- Labels reframe rule_code as "Rechtsgrundlage (Kurzform)" and
legal_source as "Rechtsgrundlage (Langform)" so the legal-citation
pair is named consistently with the list column.
- Rule interface: code → submission_code field.
i18n: new keys admin.rules.col.submission_code,
admin.rules.col.legal_citation, admin.rules.edit.field.submission_code
in both DE + EN; old admin.rules.col.code + admin.rules.edit.field.code
removed.
bun run build clean.
Workstream B Go sweep — matches mig 098. Every place the deadline-rules
service reads/writes the per-rule identifier now uses the new column
name and the new struct field. Distinct from rule_code (legal citation)
and from proceeding_types.code (the proceeding's 3-segment code).
Touch points:
- models.DeadlineRule.Code → SubmissionCode (db + json tags renamed
in lockstep — JSON contract `submission_code` is the new shape).
- deadline_rule_service: ruleColumns SELECT list updated.
- rule_editor_service: CreateRuleInput.Code → SubmissionCode (json tag
too), INSERT + CloneAsDraft SELECT updated.
- projection_service: lookupRuleByCode → lookupRuleBySubmissionCode
(SQL WHERE clause + error message); every r.Code / parent.Code /
rule.Code / first.Code / src.rule.Code read renamed.
- fristenrechner: r.Code / prev.Code / rule.Code reads renamed in
Calculate (parent-anchor + override-key + computed-by-code map) and
in CalculateRule's LocalCode emission; the proceeding-code+submission-
code resolver query uses `submission_code = $2`.
- event_trigger_service / deadline_calculator: r.Code reads renamed.
UIDeadline.Code (the calculator's wire response) is unchanged — that
field is a separate API contract pointing at the same value; renaming
it would force every frontend deadline-renderer through a contract
break that isn't part of this workstream.
Test fixtures updated to the new SubmissionCode field name; live-DB
tests updated to the post-mig-098 prefixed values (`inf.sod` →
`upc.inf.cfi.sod` etc.). New submission_codes_shape_test asserts
every active+published row matches the 4+-segment proceeding-prefixed
shape (sibling of TestProceedingCodeShape; mirrors mig 098 §6.1).
go build ./... clean. go test ./internal/... green.
m's 2026-05-18 call (workstream B): the paliad.deadline_rules.code field
is a SUBMISSION identifier (the filing/event within a proceeding), not
the legal-citation rule code (which lives in rule_code / legal_source).
Two cleanups land in this migration:
1. DATA — prefix every existing submission code with its proceeding
code so submission codes carry the full hierarchical shape:
inf.soc (on upc.inf.cfi) → upc.inf.cfi.soc
de_inf.klage (on de.inf.lg) → de.inf.lg.klage
de_inf_bgh.revision (on de.inf.bgh) → de.inf.bgh.revision
Idempotent: WHERE NOT LIKE pt.code || '.%' skips already-prefixed
rows so re-running is a no-op.
2. SCHEMA — rename paliad.deadline_rules.code → submission_code so
future devs don't conflate it with rule_code (legal citation) or
proceeding_types.code. The rename is guarded by a column-existence
check, idempotent on a second run.
Drops + recreates the deadline_search materialized view because its
SELECT bakes `dr.code AS rule_local_code` (mig 051 §4); the rebuild
sources from `dr.submission_code` and reproduces every index from mig
051 verbatim.
Backup snapshot table paliad.deadline_rules_pre_098 captures the rows
before the prefix step; serves as the audit anchor and the down's
source.
Hard assertions (§6) gate the migration on:
- every active+published row matches the 4+-segment proceeding-prefixed
shape regex
- no NULL submission_code on active+published rows
- the column was actually renamed
Researcher draft for Workstream A — per-rule proposals for rule_code +
legal_source on the 130 active+published deadline_rules with rule_code IS
NULL. Grouped by proceeding (53 PT rows) and orphan-bucket (77 rows with
proceeding_type_id IS NULL).
~75 HIGH/MED proposals, ~47 FLAG entries pending m's call (court-set
event-markers, combined-pleading rows, ambiguous orphans, RoP
sub-paragraph spot-checks). Profiles the field convention from the 83
already-populated rows. READ-ONLY phase: no DB writes, no migration yet
— mig 097 follows once m signs off.
Side-fix candidate: normalize the one outlier RoP.49.1 -> RoP.049.1 on
rev.defence as part of mig 097.
1. /deadlines list ticking the complete-checkbox now goes through
window.confirm() before firing PATCH /api/deadlines/{id}/complete.
The deadline title is interpolated into the prompt so the user sees
what they're closing. Matches the existing window.confirm() pattern
used in projects-detail / admin-team / approvals-withdraw etc. —
no custom modal layer.
2. The cascade row "ändern" button in the deadline calculator stayed
in German on the EN side. data-i18n="deadlines.row.edit" was set
correctly but applyTranslations() only runs at page init and on
lang-toggle; the cascade re-renders on every state change without
re-hydrating, so the static "ändern" fallback in the HTML stuck.
Render the label via t() directly in the template — same pattern
the rest of the cascade uses, no hydration dependency.
Both i18n keys land on both DE and EN sides (deadlines.complete.confirm
+ existing deadlines.row.edit). bun run build clean, 2414 keys.
Sweep of frontend/src/* for the proceeding-code rename landed by
mig 096. Same scope as the Go sweep — comments + literal string
codes substituted, plus the visible additions:
- fristenrechner.tsx / verfahrensablauf.tsx UPC_TYPES gain
upc.ccr.cfi as a fourth UPC option ("Widerklage auf Nichtigkeit");
it surfaces in the picker and renders the determinator routing
notice from proceeding_mapping.ResolveCounterclaimRouting.
- i18n.ts deadlines.* keys renamed to mirror the new codes exactly
(`deadlines.upc.inf.cfi`, …). DE + EN sides in sync.
- frontend/src/client/fristenrechner.ts fristenrechnerCodeToCascadeSegment
rekeyed to new codes; upc.ccr.cfi shares the upc-inf kebab segment
because the event_categories slug taxonomy is not renamed and ccr
resolves to inf-rules anyway.
- client/views/verfahrensablauf-core.ts court-picker conditions
rewritten against the new codes.
Bun build clean (i18n-keys.ts regenerated from the canonical map).
Sweeps internal/services + internal/handlers + internal/models to use
the new proceeding codes landed by mig 096. Stable Code* constants
live in internal/services/proceeding_mapping.go so a future rename
needs to touch one file.
Substantive changes:
- proceeding_mapping.go gains ResolveCounterclaimRouting() — the
cascade resolver that routes upc.ccr.cfi (illustrative peer) back
to upc.inf.cfi with with_ccr=true as default flag (design doc S1).
- deadline_search_service.go forum-bucket map updated; upc.ccr.cfi
added to upc_cfi since it is a CFI peer.
- project_service.go CreateCounterclaim default lookup parameterised
so the SQL string carries the constant, not a literal.
- proceeding_codes_shape_test.go: new file. Validates the shape
regex standalone (always runs) and walks live DB rows asserting
every active fristenrechner row matches the new shape + every
stable Code* constant resolves to exactly one active row.
Comments and test fixtures throughout the Go tree updated to the
new shape. Tests pass under `go test ./internal/... -short`.
19 active fristenrechner codes renamed from UPPER_SNAKE to the
lowercase three-position dot-separated taxonomy ratified by m on
2026-05-18 (see docs/design-proceeding-code-taxonomy-2026-05-18.md).
IDs are stable; only the `code` STRING changes.
Adds upc.ccr.cfi as an illustrative peer of upc.inf.cfi
(is_active=true, no rules — Go code routes cascade hits back to
inf.cfi with with_ccr=true).
Also updates the soft `proceeding_type_code` references on
paliad.event_category_concepts so the soft-join through
proceeding_types.code keeps resolving, refreshes the
deadline_search materialized view, and installs the
paliad_proceeding_code_shape CHECK constraint enforcing
`^[a-z]+\\.[a-z]+\\.[a-z]+$` on every active row.
Idempotent: every UPDATE is guarded on the OLD code; INSERT uses
WHERE NOT EXISTS; CHECK is dropped-then-recreated by name. Backup
snapshot lives in paliad.proceeding_types_pre_096. Dry-run on the
live youpc DB (BEGIN; … ROLLBACK) confirmed 20 active rows on the
new shape, 0 old codes left, 1 active upc.ccr.cfi.
Captures m's 2026-05-18 ratification of the new fristenrechner
proceeding-code convention `<jurisdiction>.<X>.<Y>` and the 5
sub-decisions: ccr.cfi is an illustrative peer that routes back to
inf.cfi with with_ccr; damages-appeal stays bundled into
upc.apl.merits; NZB at BGH is a flag, not a separate proceeding;
DPMA appeals stay generic with source differentiation at rule level.
This document is the source of truth for mig 096 (lands next) and the
post-mig proceeding_mapping.go.
Codifies curie's 4 new rules + 4 patches from
docs/proposals/fristen-gap-fill-2026-05-18.md § 0.3 (m's decisions).
NEW (4):
inf.prelim UPC_INF parent=inf.soc 1mo RoP.019.1 flag=with_po
rev.prelim UPC_REV parent=rev.app 1mo RoP.019.1 flag=with_po
inf.appeal_spawn UPC_INF parent=inf.decision 2mo RoP.220.1.a always-fire → UPC_APP
rev.appeal_spawn UPC_REV parent=rev.decision 2mo RoP.220.1.a always-fire → UPC_APP
PATCH (4):
de_inf.klage legal_source NULL → 'DE.ZPO.253'
de_inf.anzeige no change (already correct — explicit in audit log)
de_inf.erwidg is_court_set false → true + §276 Abs.1 S.2 description
de_inf.berufung defensive verify legal_source = 'DE.ZPO.517'
Idempotent via WHERE NOT EXISTS (no unique index on (proceeding_type_id,
code) — mig 093 left archived rows sharing codes with their published
successors, so ON CONFLICT isn't available). UPDATEs guarded by clauses
that only fire when the row still has the old value.
Backup snapshot in paliad.deadline_rules_pre_095 (CREATE TABLE IF NOT
EXISTS); down migration restores from it. Hard assertions verify all 4
new rules landed active+published, de_inf.erwidg flipped to court-set,
both spawn rules chain to a valid proceeding_type id=11.
Dry-run verified end-to-end against the live Supabase corpus inside
BEGIN/ROLLBACK; idempotency confirmed by running INSERT+UPDATE twice
in the same transaction.
m + paliadin walked the open questions; new §0.3 records the calls so
the proposal doc reflects the final shape before m ingests via
/admin/rules. Net stays at 4 new rules (2 PO + 2 always-fire merits-
appeal spawns). de_inf.erwidg flips to court-set per ZPO §276(1) S.2.
No ccr-defendant PO, no ccr.appeal duplication, no R.263 deadline.
Drafts the 4 coverage questions the mig 093 commit body left open for
legal review (t-paliad-200 closeout):
1. Preliminary Objection (RoP 19) on UPC_INF + UPC_REV — 2 new rules,
party=defendant, 1 month from SoC/SfR served, flag-gated with_po.
2. Cross-proceeding APP spawn (RoP 220.1(a)) from UPC_INF + UPC_REV
into the UPC merits-appeal proceeding — 2 spawn rules, party=both,
2 months from R.118 decision, flag-gated with_appeal. Third
Pipeline-A relic (ccr.appeal) recommended not seeded — CCR appeal
is structurally absorbed into inf.appeal_spawn because one R.118
decision = one appeal window in the unified UPC_INF (CCR-as-flag)
model.
3. ccr.amend / rev.amend — claim "safe to drop" verified for patent
amendment (fully covered by inf.app_to_amend / rev.app_to_amend
chains under with_ccr+with_amend / with_amend flags). R.263 case-
amendment is court-discretionary; recommended NOT modelled.
4. zpo.* family — klage / vertanz / berufung redundancy verified
(de_inf.klage, de_inf.anzeige, de_inf.berufung / de_inf_olg.berufung
cover them). klageerw exposes a discrepancy on de_inf.erwidg
(6-week heuristic vs ZPO §276.1 S.2 court-set 2-week floor) — flagged
as a PATCH on the existing row, not a new rule. Task brief's mention
of "Vertagungsantrag" is a misread of zpo.vertanz (= Verteidigungs-
anzeige, not Vertagungsantrag); §227 itself recommended NOT modelled.
Net: 4 new rules drafted in Track B, 3 optional PATCHes in Track A, 12
FLAGs surfaced for m's decision before /admin/rules ingest. Appeal target
referenced by ROLE (not code) pending t-paliad-204 proceeding-code
rename — m picks final spawn_proceeding_type_id at ingest.
Per-rule template matches docs/proposals/orphan-concepts-2026-05-15.md.
Read-only research; no DB writes, no migration files. The spawn_proceeding_type_id
column is unused in live data today — these spawn rules will be the
first real consumer.
m's UX bug (2026-05-17, paliad.de prod): clicking Genehmigen/Ablehnen/
Zurückziehen on a row the viewer can't act on alerted ("Eigengenehmigung
nicht zulässig.", "Sie haben nicht die erforderliche Rolle.") after the
POST round-trip. m's ask: "approval that i cannot grant should have the
'Genehmigen' button greyed out... that would be better than showing an
error when I try."
Backend (internal/services/approval_service.go):
- ApprovalRequestView gains viewer_can_approve + viewer_is_requester
booleans. Resolved server-side per caller — false on self-authored rows
(caller == requester), true when the eligibility predicate matches.
- Extract the eligibility EXISTS-block into approvalEligibilitySQL const
and reuse it in ListPendingForApprover (WHERE), PendingCountForUser
(WHERE), and the new viewer_can_approve SELECT expression. Single
source of truth for the gate, identical to canApprove.
- ListPendingForApprover, ListSubmittedByUser, and GetRequest all bind
$1 = callerID so the SELECT computes the flags inline (one query, no
N+1). GetRequest's signature grows a callerID arg; the handler passes
the authenticated user.
Frontend (frontend/src/client/views/shape-list.ts):
- ApprovalDetail picks up the two booleans (optional — falsy is safe:
it disables, never falsely enables).
- approvalActionBtn renders the button as before but flips
btn.disabled + sets a tooltip via disabledReasonFor: approve/reject
share the viewer_can_approve gate (self → self_approval tooltip;
unauthorized → not_authorized); revoke needs viewer_is_requester.
- All three buttons still render on every pending row so users see
what's possible — the disabled+tooltip combo explains what's not.
i18n + CSS:
- 3 new keys × DE/EN: approvals.disabled.{self_approval,
not_authorized,revoke_not_requester}.
- .inbox-row-action:disabled neutralises the .btn-primary/danger/
secondary variant via opacity + not-allowed + muted tokens.
Tests:
- internal/services/approval_service_test.go::TestApprovalService_ViewerFlags
is a 4-case table-driven live-DB test (skips without TEST_DATABASE_URL):
self-authored (false/true), eligible peer (true/false), non-eligible
viewer (false/false), global_admin (true/false). Also asserts the flags
on ListPendingForApprover + ListSubmittedByUser rows.
Defence-in-depth preserved: server still rejects illegal POSTs with the
same error contract, and the alert path stays in inbox.ts for the race
where state changes between render and click.
Two issues m hit and reported in one breath while adding a project:
1. **Internal error on POST /projects** (prod-only, surfaced at 10:23). Both
ProjectService.Create and CreateCounterclaim re-referenced the uuid
parameter `$1` as `$1::text` to fill the path placeholder. Postgres'
planner deduced conflicting types for `$1` (uuid in the id column,
text in the cast) and rejected the prepared statement with 42P08
"inconsistent types deduced for parameter". The path placeholder
value is irrelevant — paliad.projects_sync_path() (BEFORE INSERT
trigger from mig 018/021) always overwrites it from id and parent
path. Fix: replace `$1::text` with a literal '' in both INSERTs,
keeping the parameter list decoupled from the id column's type.
Same comment now anchors the rationale on both call sites.
2. **CM number length — 6 digits, not 7.** m's correction; mig 018's
`^[0-9]{7}$` CHECK on paliad.projects.client_number and
matter_number was wrong. Mig 094 snapshots affected rows to
paliad.projects_pre_094, NULL-s the 3 surviving 7-digit test
values (2 client_numbers, 1 matter_number), then swaps the legacy
`projekte_*_check` constraints from {7} to {6}. Frontend pattern,
maxLength, placeholder, labels, and i18n hint flipped from 7 → 6
on both DE and EN sides; format hint reads CCCCCC.MMMMMM now.
Dry-run against live DB (BEGIN..ROLLBACK):
- Fixed Create SQL: trigger populates path = id::text (36 chars). ✓
- Mig 094: 2 rows snapshotted, 0 clients/matters remain after clear,
0 rows violate the new 6-digit CHECK. ✓
go build, go test ./internal/..., bun run build all clean.
Lorenz's Slice 9 (t-paliad-195) deferred mig 093 because 40 active
paliad.deadline_rules still pointed at the 7 litigation-category
proceeding_types (INF, REV, CCR, APM, APP, AMD, ZPO_CIVIL). Phase 3
Slice 5 (mig 087/088) already retired the category from project-binding;
this migration retires it from the rule corpus.
PLAN CHOICE (audit-gated, paliadin-approved): archive-all-40 rather than
the original re-parent plan. The audit found that 23 of 40 Pipeline-A
rules share their `code` with an existing fristenrechner rule on the
proposed re-parent target (e.g. inf.oral exists on both INF and
UPC_INF). Re-parenting would leave two rules with identical
(proceeding_type_id, code), breaking the implicit per-proceeding
rule_code identity contract keyed off by projection / search /
rule_editor. The fristenrechner rules are clearly the production
version (proper German names, legal_source pinned to UPC.RoP citations,
full bilateral chains, intra-proceeding counterclaim handling); the
Pipeline-A rules are stubs (English-only, mostly NULL legal_source,
duration_value=0 for 28 of 40, no spawn_proceeding_type_id wiring).
Migration 093 sequence (atomic):
1. Snapshot proceeding_types_pre_093 + deadline_rules_pre_093 as
permanent audit anchors.
2. INSERT _archived_litigation pt (category='archived',
is_active=false, jurisdiction='UPC') to home the rules.
3. UPDATE all 40 rules → archive pt + lifecycle_state='archived' +
is_active=false. Captured in paliad.deadline_rule_audit via the
mig 079 trigger.
4. DELETE the 7 litigation rows from paliad.proceeding_types (now
safe — nothing references them).
5. Hard assertions: 0 litigation rows survive, exactly 40 rules on
the archive pt, every snapshot row matches a surviving rule by id.
Critical FK note: deadline_rules.proceeding_type_id is ON DELETE CASCADE
→ proceeding_types(id). A naive DELETE of the 7 litigation rows would
cascade-delete all 40 rules and break the FK from the 1 live deadline
("Lecker Frist", completed) that still references inf.rejoin/INF.
Re-homing the rules before deleting the pt rows is mandatory.
Verified via BEGIN..ROLLBACK against live DB: assertions pass, all 30
intra-litigation parent_id chains preserved, the live deadline FK
stays valid.
Test impact:
internal/services/project_service_test.go:72 used to look up
category='litigation' AND code='INF' to exercise the Slice 5 negative
case. Post-mig-093 that lookup returns NULL. Rewritten to fetch any
category <> 'fristenrechner' row (the _archived_litigation pt is the
canonical post-093 row); defence-in-depth coverage of both the Go
service guard and the mig 088 SQL trigger is preserved.
SURFACED FOR LEGAL REVIEW (4 coverage questions the audit found, to be
triaged as follow-up tasks):
1. inf.prelim (Preliminary Objection, RoP 19, 1 month) — not present
on UPC_INF. Possible coverage gap; legal review to decide whether
to add it to the fristenrechner ruleset.
2. inf.appeal / rev.appeal / ccr.appeal as cross-proceeding spawns
into UPC_APP (2 months, UPC.RoP.220.1) — fristenrechner UPC_APP
currently starts standalone with no spawn from UPC_INF/UPC_REV.
Possible UX gap; Pipeline-A versions had
spawn_proceeding_type_id=NULL so they weren't functional spawns
either.
3. ccr.amend / rev.amend (spawn rules) — superseded by
inf.app_to_amend / rev.app_to_amend on UPC_INF / UPC_REV. Safe to
drop; no action needed.
4. zpo.klage / zpo.vertanz / zpo.klageerw / zpo.berufung — no UPC
analogue; redundant with the DE_INF / DE_INF_OLG / DE_INF_BGH and
DE_NULL / DE_NULL_BGH chains. Safe to drop; no action needed.
Files:
internal/db/migrations/093_retire_litigation_category.up.sql (new)
internal/db/migrations/093_retire_litigation_category.down.sql (new)
internal/services/project_service_test.go (test rewrite)
EventDeadlineService.Calculate now reads source rows from
paliad.deadline_rules directly (WHERE trigger_event_id IS NOT NULL),
joining via UUID instead of title_de string. The legacy SELECTs against
paliad.event_deadlines + paliad.event_deadline_rule_codes are gone.
Migration 092:
- Snapshots both legacy tables into _pre_092 audit anchors.
- Adds paliad.deadline_rules.rule_codes text[] and backfills the 72
multi-code citations from event_deadline_rule_codes via the
sequence_order = 1000 + ed.id convention from mig 085 (70 of 77
Pipeline-C deadlines carry codes; 7 are codeless).
- Hard assertion ties source-junction-row count to backfilled
text[]-element count — any sequence_order mismatch aborts the drop.
- Drops the mig 086 read-only trigger (orphan once event_deadlines
goes away).
- Drops paliad.event_deadlines + paliad.event_deadline_rule_codes.
- Final assertion: >=77 active deadline_rules with trigger_event_id
NOT NULL — Slice 3 corpus must not have collapsed.
- audit_reason wrapper at top so the deadline_rules UPDATE row-trigger
records the reason in deadline_rule_audit.
Verified via BEGIN..ROLLBACK against the live paliad DB: 72 codes
backfilled into 70 rule_codes arrays, multi-code rules (RoP.029.a +
RoP.030 for ed_id=6) preserve their ordering, composite rules
(combine_op=max) remain intact, both tables drop cleanly, all
assertions pass.
Parity test rebound to deadline_rules — independent computation still
re-runs applyDuration against raw column values for date/composite
parity. EventDeadlineResult.ID stays int64 via the sequence_order -
1000 convention so the public /api/tools/event-deadlines wire shape
is unchanged.
Closes the Determinator cascade redesign. Three intertwined pieces:
1. The mode row is gone — the `🔍 Direkt suchen` icon at the top of the
row stack now toggles an inline search overlay over the cascade
instead of routing to the legacy B2 surface. Results render into the
same `#fristen-b1-results` container the cascade uses, so users see
one consistent concept-card layout regardless of whether they
reached the rule via cascade narrowing or free-text search. ESC
inside the input clears it on the first press and collapses on the
second; "← Zurück zum Entscheidungsbaum" restores cascade + state.
Deep-link `?mode=filter` still routes to the legacy B2 panel for
backwards-compatible shared URLs but is no longer exposed in the
cascade UI.
2. Mobile responsive per design §7. Three breakpoints layer onto the
`.fristen-row` primitive: <640px (phone — chips full-width single
column, ändern permanently visible, answer wraps to its own line),
<768px (tablet — head wraps so ändern moves down, chips
single-column), <1024px (small desktop / large tablet — chips drop
to 2-column auto-fill). Active row autoscrolls into view on every
render with 60px headroom; the helper is a no-op when the row is
already visible so desktop doesn't jitter.
3. Auto-walk tooltip polish: 200ms fade-in + slide-down via an
is-entering transition state; mobile (<640px) flips the insertion
point so the tip lands below the prefilled row rather than above;
any chip pick or ändern click counts as user-engagement and
dismisses the tip (in addition to the explicit × button).
Refs: docs/design-determinator-row-cascade-2026-05-13.md §6 + §7 + §10 Slice 3.
Wires the project context into the Determinator row stack so a UPC INF
matter doesn't need to be hand-walked through five obvious cascade picks.
Auto-walk descends single-option chains as `is-prefilled` rows, the inbox
row vanishes for UPC matters (CMS implied), and the first prefilled row
carries the project reference inline ("aus Akte: HL-2024-001").
Backend: `internal/services/proceeding_mapping.go` adds
MapLitigationToFristenrechner — single source of truth for bridging the
litigation conceptual codes (INF / REV / APP / CCR / AMD / APM / OPP) onto
fristenrechner codes (UPC_INF / DE_INF / EPA_OPP / …). Ambiguous combos
(APP+DE, ZPO_CIVIL, AMD+DE) return ok=false; callers degrade to "no
narrowing" instead of guessing. Table-driven test covers every documented
mapping plus the ambiguous-degrade cases.
Frontend: `buildRowStack` filters cascade children by project context
along the proceeding axis (kebab segment lookup against the project's
fristenrechner code); auto-walks while filtered scope narrows to one;
caps depth via `cascadeAutoWalkStopAfter` after an "ändern" on a prefilled
row so the user lands at an active chip set without the auto-walk
re-engaging. Result panel narrows on the post-auto-walk effective slug,
not the URL slug. A one-time inline tooltip ("Diese Schritte ergeben sich
aus Ihrer Akte") surfaces when ≥2 rows render prefilled — dismissal flag
persists in localStorage.
Narrowing is purely additive: an Akte without a fristenrechner code
(11/11 live projects pre-Slice-5 were NULL) degrades to today's
forum-only behaviour. Slice 3 (mobile polish + search relocation) follows.
Refs: docs/design-determinator-row-cascade-2026-05-13.md §10 Slice 2 + §4 + §5.
Replace the four-layer Pathway B mess (mode radio + perspective chip strip
+ inbox chip strip + breadcrumb cascade) with a single `.fristen-row`
primitive rendered in a top-down stack. Every decision — mode, perspective,
inbox, cascade depth N — now uses the same shape (label · picked answer ·
inline "ändern") and three states (is-active / is-answered / is-prefilled).
The user finally sees their full decision path at a glance instead of
chasing breadcrumb crumbs after each drill. Click on any answered row (or
its ändern affordance) re-actives it; ändern on a cascade depth drops the
descendants (same drop-descendants semantic as today's breadcrumb-click).
Reset link and `🔍 Direkt suchen` escape-hatch live at the top of the stack
per design §6 Option B; the mode-toggle radio is gone, routing to
?mode=filter now flows through the mode row.
Visual-only refactor — narrowing engine (inboxFilterAllowsForums +
perspectiveAllowsParty) is unchanged. Slice 2 will add project-driven
prefills + auto-walk; Slice 3 covers mobile polish and search relocation.
Refs: docs/design-determinator-row-cascade-2026-05-13.md §10 Slice 1.
Phase 3 Slice 9 frontend cleanup. The backend's UIDeadline wire
shape stopped emitting (isMandatory, isOptional) in this slice;
the matching legacy-fallback branch in priorityRendering is now
dead code. Drops:
- CalculatedDeadline TS interface: isMandatory + isOptional
fields removed. `priority` is required (not optional) since
every backend response now populates it.
- priorityRendering(): collapsed to a clean switch on `priority`.
Unknown priority falls back to "render as mandatory" (safe
default; never silently drop a rule) — the legacy
(isMandatory, isOptional) inference is gone.
- Save-modal optional-badge rendering in fristenrechner.ts now
reads `dl.priority === "optional"` directly (was previously
`dl.priority === "optional" || dl.isOptional`).
- Timeline row's optional-badge rendering in
verfahrensablauf-core.ts switched from `!dl.isMandatory` to
`dl.priority === "optional"`. Slightly different semantic —
pre-Slice-9 the badge fired on every non-mandatory row
(recommended + optional + informational); post-Slice-9 only
on opt-in rules (RoP.151 pattern). Recommended + informational
are surfaced via their own rendering tier (notice card for
informational) so the badge change tightens the meaning.
Frontend build clean; no i18n keys removed (the priority labels
shipped in Slice 8 stay live).
Phase 3 Slice 9 test cleanup. Seeds + assertions no longer touch
the legacy columns (mig 091 dropped them).
- projection_service_test.go (Slice 7 fixtures): INSERT seeds
drop the is_mandatory / is_optional columns from the
paliad.deadline_rules column list. Defaults are fine; the
spawn-graph test doesn't read those.
- rule_editor_service_test.go (Slice 11a fixtures): same drop
on the SLICE11A_PREVIEW seed.
- fristenrechner_test.go (Slice 8 wire-shape assertion): drops
the wireFlagsFromPriority round-trip check (the bool pair is
no longer on the wire). The enum-membership invariant
survives. evalConditionExpr table-driven test rewritten —
legacy condition_flag fallback cases removed (the fallback
is gone in Slice 9), pure-jsonb cases retained.
- deadline_rule_service_test.go (Slice 2 backfill integrity):
legacy-pair bucket assertion dropped; the priority-non-NULL
invariant still holds via the CHECK constraint. The
condition_flag cross-check now joins the pre-mig-091 snapshot
when present (a future cleanup slice drops the snapshot
along with this code path).
Build + tests green.
Phase 3 Slice 9 Go cleanup. With mig 091's column drops live, the
service layer stops reading + emitting the legacy shape:
- models.DeadlineRule: drop IsMandatory, IsOptional, ConditionFlag,
ConditionRuleID fields. Comment block flags Slice 9 as the
closeout slice.
- DeadlineRuleService.ruleColumns: SELECT no longer enumerates the
dropped columns. The post-Slice-9 schema is the live shape.
- FristenrechnerService.UIDeadline: drops IsMandatory + IsOptional
fields. Frontend reads `priority` directly post-Slice-8; the
legacy emit was kept "for one release" and that release is now.
- evalConditionExpr signature: drops the conditionFlag fallback
param. NULL / "null" expressions return true (unconditional);
the legacy text[] fallback was the only reason for the second
param. New helpers hasConditionExpr + extractFlagsFromExpr fill
the gaps (alt-swap guard + RuleCalculation.FlagsRequired list).
- FristenrechnerService.Calculate + calculateByTriggerEvent +
EventTriggerService.Trigger: switched to the new (single-arg)
evalConditionExpr; alt-swap guard now uses
hasConditionExpr(r.ConditionExpr) instead of the dropped
len(r.ConditionFlag) > 0 check.
- FristenrechnerService.CalculateRule: RuleCalculationRule.IsMandatory
derived from priority via wireFlagsFromPriority (kept for the
result-card panel TS contract). FlagsRequired walks the jsonb
gate tree to enumerate {"flag":"X"} leaves (replaces the
dropped condition_flag enumeration).
- RuleEditorService.Create + CloneAsDraft INSERT statements:
dropped is_mandatory / is_optional / condition_flag from the
column lists. Live shape only.
Test fixtures (projection_service_test.go, rule_editor_service_test.go,
fristenrechner_test.go) all updated to write the live shape on
seed; the evalConditionExpr table-driven test dropped its legacy
fallback cases (the fallback no longer exists) and now exercises
20 pure-jsonb scenarios across AND/OR/NOT compositions.
The deadline_rule_service_test backfill assertion lost its
(is_mandatory, is_optional) bucket cross-check (those columns are
gone); the priority-non-NULL invariant still holds via the CHECK
constraint. condition_flag cross-check now joins the pre-mig-091
snapshot table (when present) instead of the live row.
Phase 3 Slice 9 Step E (design §3.E, §9.1). m approved the
downtime window 2026-05-15 ("paliad ist nicht in use heute,
downtime ist egal") so the destructive drops can land.
Drops four superseded columns on paliad.deadline_rules:
is_mandatory → priority='mandatory' | other (Slice 2 mig 083)
is_optional → priority='optional' (Slice 2 mig 083)
condition_flag → condition_expr (Slice 2 mig 084)
condition_rule_id → DEAD (no live rows, Q13 m's approved drop)
Pre-drop snapshot: paliad.deadline_rules_pre_091 (id +
the four columns + snapshotted_at). Lets the down-migration
restore values to existing rows; a follow-up cleanup slice drops
the snapshot table once the rule editor's migration-export flow
has been used to roll any post-drop edits back into version
control.
Hard assertions at end:
- count(priority IS NULL) == 0 (Slice 2 mig 083 must have run).
- count(rule with pre-drop condition_flag but no condition_expr)
== 0 (Slice 2 mig 084 must have populated every row).
Both raise EXCEPTION on violation — fails the migration loudly
before legacy code paths get pulled out from under the unified
calculator.
Audit-reason wrapper set; ALTER TABLE DROP COLUMN doesn't fire
the mig 079 row-level trigger, but the wrapper is the standard
Phase 3 pattern.
Sibling drops deferred — see live-data audit in head ping:
- mig 092 (event_deadlines + trigger_events tables): SKIPPED.
trigger_events has 33 event_types FKs + 77 deadline_rules
FKs; event_deadlines + event_deadline_rule_codes still
consumed by EventDeadlineService.Calculate for the frontend's
"Was kommt nach…" tab (/api/tools/event-deadlines is still
in use post-Slice-3 delegate).
- mig 093 (retire litigation category): SKIPPED. 40 active
deadline_rules still reference litigation-category
proceeding_types (the Pipeline-A INF/REV/CCR/APM/APP/AMD/
ZPO_CIVIL rules; Slice 5 retired them from project-binding,
not from the rule corpus).
Both deferrals are tracked in the head ping; the litigation drop
can land after a focused slice that splits the Pipeline-A rules
off the litigation category onto a fristenrechner-side parent.
The event_deadlines drop needs EventDeadlineService.Calculate
to stop reading the source rows first.
The Dokploy compose .env file got the new vars during the operational
flip but the docker-compose.yml environment block didn't list them, so
docker-compose silently dropped them during container start.
Adds PALIADIN_BACKEND / AICHAT_URL / AICHAT_TOKEN / AICHAT_PERSONA to
the environment block with safe defaults (PALIADIN_BACKEND=legacy,
AICHAT_PERSONA=paliadin). Existing deployments without aichat envs set
keep the legacy path; flipping PALIADIN_BACKEND=aichat in Dokploy now
takes effect on next deploy.
Discovered while doing the aichat Phase B activation flip.
Per m's 2026-05-13 decision (m/mAi#207 §13 Q4): the paliadin SKILL.md
and references/sql-recipes.md are now owned by aichat. The aichat repo
already has the equivalents committed at skills/aichat/paliadin/ on
mai/darwin/issue-207-aichat (verified before this commit). Aichat's
own deploy doc handles installation on mRiver.
Deleted:
scripts/skills/paliadin/SKILL.md
scripts/skills/paliadin/references/sql-recipes.md
scripts/install-paliadin-skill
Legacy LocalPaliadinService / RemotePaliadinService still depend on
~/.claude/skills/paliadin/ being present on whichever host they run
against. Until those paths retire (Phase C / Q15), operators install
the skill manually from m/mAi/skills/aichat/paliadin/.
CLAUDE.md updated:
- PALIADIN_SESSION_PREFIX row points readers at m/mAi for the skill
SoT and notes the legacy paths still expect a manual install.
- New env-var rows for PALIADIN_BACKEND / AICHAT_URL / AICHAT_TOKEN /
AICHAT_PERSONA so the operator runbook for the Phase B flip is
self-contained.
Adds the Phase B paliad-side migration: a thin HTTP client of the
centralized aichat backend shipped in m/mAi#207 Phase A (darwin's
mai/darwin/issue-207-aichat branch). Implements the same services.Paliadin
interface as LocalPaliadinService / RemotePaliadinService — handler
plumbing is unchanged, the cutover is a single env-var flip.
internal/services/aichat_paliadin.go (~530 LoC):
- POST /chat/turn + POST /chat/reset + GET /chat/health via the aichat
JSON envelope (mirrors m/mAi internal/aichat/api/types.go verbatim;
no module import to keep paliad self-contained).
- Per-turn HS256 JWT mint (uses paliadin_jwt.go from the prior commit)
when SUPABASE_JWT_SECRET is configured. Aichat owns file write +
cleanup; we just sign and ship.
- Service-wide health-gate cache (10 s success window, no failure
cache — failures re-probe so recovery surfaces immediately).
- Per-user-window primer cache. Pulls up to MaxPrimerTurns prior
exchanges from paliad.paliadin_turns and ships them in TurnRequest.
Primer so a pane respawn (pane_spawned=true in response) doesn't
strand the user with a cold claude. Cleared on ResetSession +
pane_spawned response.
- Username from email_localpart per m's §13 Q2 pick (sanitized inside
aichat). Nil-DB fallback: "user-<uuid8>".
- Maps aichat's typed wire errors (auth_failed, persona_unknown,
mriver_unreachable, bootstrap_failed, timeout, shim_error) onto
paliad's existing audit-row codes — preserves the German i18n table
in paliadin.ts unchanged (no new strings needed per design §11).
cmd/server/main.go:
- PALIADIN_BACKEND env: "aichat" → AichatPaliadinService, anything
else → existing remote/local/disabled tree. Default = legacy, so
every existing deploy is byte-identical until flipped.
- buildAichatPaliadinConfig validates AICHAT_URL + AICHAT_TOKEN at
boot; AICHAT_PERSONA defaults to "paliadin". JWT secret threaded
in so per-user RLS is on by default.
Tests cover constructor defaults, health-gate caching + retry +
expiry, ResetSession wiring, error-envelope decoding + classifier,
HTTP-layer auth/JSON wiring via a roundTripper, JWT mint integration,
TurnContext → meta packing, and the env-gate helper. go test ./...
green. NOT self-merged — head owns the merge per task instructions.
Restored from mai/planck/paliadin-per-user-rls (parked, see m/paliad#12
cancel note). The aichat Phase B path (next commit) consumes mintTurnJWT
to sign a short-lived HS256 token per turn, scoped to the calling user
(sub=userID, role=authenticated, aud=authenticated, iss=paliad/paliadin).
Aichat passes the raw token through to the claude pane on mRiver via a
per-turn file (managed by aichat's runner, not paliad's transport). The
SKILL.md reads it and `SET LOCAL request.jwt.claims = …` before every
paliad.* query, which makes RLS evaluate as the user instead of as
service role.
TTL: 2 min default — covers aichat's 120 s persona timeout + HTTP slack,
short enough that a leaked JWT is uninteresting. Each turn mints fresh;
no caching.
No call sites yet — paliadin_remote.go / paliadin.go are unchanged on
this commit. The plumbing arrives with AichatPaliadinService.
Surfaces the Slice 11a admin API at /admin/rules so editors can drive
the rule lifecycle without curling. Three new pages, each gated by
adminGate on the route + sidebar reveal via /api/me:
/admin/rules — list page with filters (proceeding,
trigger event, lifecycle chips, fuzzy
search) and a second "Orphans" tab that
loads paliad.deadline_rule_backfill_orphans
via the new GET /admin/api/orphans
endpoint. Pick-chip on each candidate
fires the reason modal → POST resolve.
"+ Neue Regel" opens the same reason modal
with minimal required fields (name DE/EN
+ duration) and routes to the edit page
on success.
/admin/rules/{id}/edit — full form (37 columns grouped: identity /
proceeding / timing / party / display /
lifecycle / condition). Side panel hosts
the preview widget (trigger date + flags
→ GET .../preview, drafts only) and the
audit-log timeline (paginated, 20 per
page). Bottom action bar adapts to
lifecycle_state — save-draft + publish on
drafts, clone on published/archived,
archive on draft/published, restore on
archived. Every action opens the reason
modal with ≥10-char client-side guard per
Slice 11a edge case #4.
/admin/rules/export — minimal SQL preview + "Download as file"
/ "Copy to clipboard". Optional `since`
audit-id scopes the export window.
condition_expr ships with a raw JSON textarea + inline parse
validation; the tree-builder is out of scope for Slice 11b (raw JSON
is sufficient given the existing 172-row corpus and validates the
same grammar live). The dependency on document.querySelectorAll for
form binding follows the admin-event-types / admin-audit-log
playbook — no new component substrate needed.
Wiring:
- frontend/build.ts: 3 new entrypoints + 3 new HTML writes.
- frontend/src/admin.tsx: new "Regeln verwalten" card with ICON_TABLE.
- frontend/src/components/Sidebar.tsx: two new admin nav entries
(Regeln + Regel-Migrations).
- frontend/src/client/i18n.ts: 162 new keys (DE+EN), under
admin.rules.* and admin.rules.edit.* and admin.rules.export.*.
- frontend/src/styles/global.css: new admin-rules-* CSS block
appended (chips, pills, audit timeline, edit-grid, preview list,
orphan cards, export pre). Uses paliad's existing CSS tokens so
light/dark/auto themes inherit automatically.
Route registration:
- GET /admin/rules — list page shell
- GET /admin/rules/{id}/edit — edit page shell
- GET /admin/rules/export — export page shell
All routes adminGate + gateOnboarded, so non-admin users 404 before
the shell even loads. Backend audit and lifecycle invariants from
Slice 11a stay authoritative; the frontend never bypasses them.
Slice 11b backend addition for the orphan-resolution flow in the
/admin/rules UI. The Slice 10 fuzzy-match backfill (mig 089) staged
legacy paliad.deadlines rows the matcher could not bind to a unique
deadline_rule into paliad.deadline_rule_backfill_orphans. This adds
the two endpoints the editor needs to surface and resolve them:
GET /admin/api/orphans — unresolved staging rows,
hydrated with the candidate
rule rows in one round-trip.
POST /admin/api/orphans/{id}/resolve — picks a rule_id from the
candidate set, writes it onto
the deadline, and flips
resolved_at + resolved_rule_id
on the staging row in a single
tx.
The methods live on RuleEditorService because they share the same admin
surface and audit semantics; resolved_rule_id + resolved_at on the
staging row is the audit trail (mig 089 COMMENT). reason is captured
into paliad.audit_reason in the same tx so any future audit trigger on
paliad.deadlines picks it up automatically.
Typed errors:
ErrOrphanAlreadyResolved → 409 in handler
ErrOrphanCandidateMismatch → 400 in handler
Route ordering matches Slice 11a's pattern: the static path is
registered alongside the existing /admin/api/rules family inside the
adminGate block in handlers.go.
Live-DB tests (TEST_DATABASE_URL-gated) for Phase 3 Slice 11a:
TestRuleEditorService_Lifecycle — full create→update→publish→archive
→restore round-trip on synthetic fixtures (SLICE11A_TEST_PT
proceeding + rules). Asserts:
1. Create returns lifecycle_state='draft' with published_at=NULL.
2. UpdateDraft on a draft succeeds and lands the patch.
3. CloneAsDraft from a published row creates a new draft with
draft_of pointing at the source.
4. Publish flips draft → published, sets published_at, AND archives
the cloned-from peer (verified by re-reading the peer's
lifecycle_state post-publish).
5. Archive flips published → archived.
6. Restore flips archived → published.
7. ListAudit returns ≥ 3 rows newest-first with non-empty reason
strings (the mig 079 trigger captured them).
8. Empty audit_reason on UpdateDraft → ErrAuditReasonRequired.
9. UpdateDraft on a published row → ErrInvalidLifecycleState.
10. Restore on a non-archived row → ErrInvalidLifecycleState.
TestRuleEditorService_Preview — calculator override hook coverage
(SLICE11A_PREVIEW_PT proceeding + a published rule). Clone the
root rule, patch DurationValue 30 → 60 on the draft, call Preview
at trigger_date=2026-01-15. Asserts:
- Baseline Calculate (no overrides) returns the published rule's
dueDate (~30 days after trigger).
- Preview returns a DIFFERENT dueDate (substitutes the draft's
60-day duration via RuleOverrides) — sanity check that the
override pipeline reached the calculator and shifted the date.
- Both responses are non-empty (the rule is reachable).
Cleanup: WHERE name LIKE 'SLICE11A_TEST_%' / 'SLICE11A_PREVIEW_%'
AND code = 'SLICE11A_TEST_PT' / 'SLICE11A_PREVIEW_PT' so production
rules are untouched. audit_reason set on every seed / cleanup write
so the mig 079 trigger doesn't reject the seed transactions.
Build clean, full local test suite green; this test skips when
TEST_DATABASE_URL is unset.