Recovery during the prod outage uncovered a second mig 098 bug: §6.2
assertion '0 NULL submission_code on active+published rows' counted
the 77 orphan rules (proceeding_type_id IS NULL, cross-cutting
Wiedereinsetzung / Schriftsatznachreichung pattern) and rejected the
migration. Patch: gate the NULL count on `proceeding_type_id IS NOT
NULL` so orphans pass through. Migration already applied to prod via
manual recovery with the same patched assertion; this commit aligns
the in-repo file with the deployed state.
Mig 098 (t-paliad-209, ohm) crash-looped paliad.de prod for ~2h: §6.1
assertion regex `^[a-z_]+\.[a-z_]+\.[a-z_]+\.[a-z_]+(\..*)?$` rejects
EPA rule codes that carry the statutory rule number in the suffix —
e.g. `epa.opp.boa.r106`, `epa.grant.exa.r71_3`, `epa.opp.opd.r116`,
`epa.opp.opd.r79_further`, `epa.opp.boa.entsch2`, `epa.opp.boa.r116`.
Migration's UPDATE step succeeds against these rows; the transactional
assertion blows them up; rollback leaves the migration tracker dirty
at version 98 and the container refuses to start.
Patch: allow `[a-z_0-9]` per segment instead of `[a-z_]` in both the
SQL assertion (mig 098 §6.1) and the matching Go shape regex
(submission_codes_shape_test.go). Same change in both spots so the
runtime sanity test stays aligned with the SQL invariant.
Manual recovery already applied: forced
`paliad.paliad_schema_migrations.version` back to 97 with `dirty=false`
so the next deploy retries mig 098 from scratch against the patched
file. No data state changed (mig 098 ran inside a transaction and
fully rolled back — snapshot table, prefix UPDATE, and column rename
all reverted).
go build ./... clean. TestProceedingCodeShapeRegexStandalone green.
Five intertwined fixes m surfaced in the interactive session:
1. **Jurisdiction prefix on the picked proceeding** — the collapsed
summary chip and the result header now read "UPC Verletzungsverfahren"
/ "DE Verletzungsklage (LG)" instead of the bare proceeding name.
Disambiguates the 4 redundancies in the corpus once the picker
collapses. Driven by .proceeding-group[data-forum] which is already
on every group.
2. **Trigger Event label = root rule** — step 2's "Auslösendes Ereignis"
line now shows the first event in the proceeding (e.g. Klageerhebung,
Nichtigkeitsklage) instead of the proceeding name. Populated from
the calc response (isRootEvent=true) on every render; em-dash
placeholder while step 3 hasn't rendered yet. lang-change keeps it
coherent.
3. **Flag rows on /tools/verfahrensablauf** — Slice 1 of t-paliad-179
stripped the with_ccr / with_amend / with_cci toggles when it lifted
the shared renderer; they never came back. Lifted the 4 existing
rows from fristenrechner.tsx plus 2 new with_po rows (RoP 19.1
preliminary objection, mig 095) — same wiring + show/hide rules on
both surfaces. with_amend stays nested under with_ccr on upc.inf.cfi
(R.30 only with a CCR).
4. **Rule references → youpc.org/laws links** — new
BuildLegalSourceURL(src) maps the structured legal_source code to
the youpc permalink for the UPC corpus (UPCRoP / UPCA / UPCS today;
39 of 91 active rules carry UPC.RoP.* and now link). DE/EPA/EU
bodies have no youpc home yet and render as plain display text —
filed as m/paliad#39. Wired through UIDeadline.LegalSourceDisplay +
LegalSourceURL so deadlineCardHtml can render <a target="_blank"
rel="noopener"> when the URL is set.
5. **R.19 label: "Vorab-Einrede" → "Einspruch"** — m's correction. DE
only (EN canonical UPC RoP term stays "Preliminary objection").
Client-side change only — i18n + JSX fallbacks. The matching DB
rename on the two rule-name rows folds into joule's broader mig 097
(legal-citation backfill, t-paliad-208 follow-up). The live UPDATE
applied during the session is captured under that audit reason; the
no-op when joule's mig re-applies is harmless.
Build hygiene:
- go build ./... + go vet ./... clean
- new test TestBuildLegalSourceURL covers UPC corpus + DE/EPA/EU
fall-through + edge cases (empty input, malformed source)
- bun run build clean (2417 i18n keys total)
Rebased on origin/main @ d126913 (ohm's submission_code rename
workstream B) — no conflicts in this commit's surface area.
Branch: mai/fermi/interactive-session. NOT self-merged.
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.