557f9a4cce742f5fe167c1ebc46e6bca59145453
451 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 3af71e772b |
fix(paliadin): fall back to one-shot when aichat persona lacks streaming
Symptom: paliadin chat returns "Verbindung verloren" because aichat's
paliadin persona is not configured with streaming support — every
RunTurnStream() call gets back HTTP 400 unsupported_streaming and the
SSE stream closes empty.
Fix: when RunTurnStream() detects "unsupported_streaming" in the
upstream error, transparently retry against /chat/turn (non-streaming)
with the same body. The full response gets emitted as a single
StreamChunk + StreamMeta so the SSE relay sees identical event
ordering. Persistence (completeTurn + markPrimed) mirrors the one-shot
RunTurn() path.
No real-time chunking until the persona is reconfigured upstream, but
the chat works end-to-end. Once the paliadin persona supports streaming
on aichat, this code path goes dormant — the unsupported_streaming
branch is only entered when the upstream actually returns that error.
Diagnostic logs from commit
|
|||
| 235e68496b | Merge: t-paliad-311 — backup exporter drift-resistant + 4 broken ORDER BY cols fixed (m/paliad#140) | |||
| 8125caf49a |
test(backup): add TEST_DATABASE_URL-gated live smokes for org export
Two complementary live tests (both skipped without TEST_DATABASE_URL): - TestResolveOrgSheets_LiveSchemaSnapshot — runs the schema probe + SQL composer the way the backup runner does at the start of every run, then executes each resolved SELECT against the live DB (wrapped in LIMIT 1 to keep table reads cheap). A future column rename in a table our spec still names triggers this test and surfaces in CI before /admin/backups breaks. - TestWriteOrg_LiveSmoke — end-to-end pipeline against a real DB: schema probe, REPEATABLE READ tx, every sheet query, xlsx + JSON + per-sheet CSV assembly, outer zip framing. Spot-checks meta.RowCounts and the zip magic bytes; doesn't materialise the full bundle to disk. Both tests exercise the exact failure mode m/paliad#140 reproduced (hardcoded ORDER BY against a renamed column) so CI catches regressions once TEST_DATABASE_URL is wired. m/paliad#140 |
|||
| b97f170c1d |
chore: footer "by" + paliadin diagnostic logs
- Footer: "© 2026 Paliad — ein Werkzeug von / a tool by" → "© 2026 Paliad — by" (both DE + EN). - Paliadin streaming handler now log.Printf on every error path (StreamError, silence_timeout, backend nil/err) so the next "Verbindung verloren" failure produces a server-side trace. Previous behaviour: silent SSE close + empty paliad logs, impossible to diagnose. |
|||
| 935ea23038 |
refactor(backup): make orgSheetQueries drift-resistant
Refactor orgSheetQueries() into orgSheetSpecs() returning declarative
(SheetName, Table, OrderBy []string) triples instead of free-form SQL,
with composeOrgSheetSQL() as a pure builder and resolveOrgSheets() as
the DB-touching orchestrator.
At backup time the resolver:
1. probes information_schema.columns once for every spec table,
2. composes SELECT * FROM <table> ORDER BY <columns-that-exist>,
3. logs WARN per ORDER BY column dropped because it's gone.
A future column rename or removal can no longer break /admin/backups:
the worst case is one sheet temporarily losing sort stability, and the
WARN log surfaces which spec needs updating.
Sheets needing custom projections (documents drops ai_extracted) keep
the SQL override path. All other org-scope sheets — entity + ref__ —
declare their ORDER BY as a column list.
Tests:
- 6 composeOrgSheetSQL unit tests cover the drift behaviour with no
DB needed (missing column, all-missing, override bypass, declared
order preserved, unknown table)
- Existing registry-shape tests (no duplicates, no paliadin leakage,
ref__ prefix, ORDER BY-for-determinism) updated to the spec API
- Full internal/services suite green
m/paliad#140
|
|||
| ee0a9ea6cb |
fix(submissions): order catalog by sequence_order, not alphabetic submission_code
The Schriftsätze list rendered procedurally meaningless: Berufungsbegründung ahead of Klageerhebung etc. because the ORDER BY was alphabetic by submission_code within each proceeding. Add dr.sequence_order ASC as the primary intra-proceeding sort; submission_code stays as the deterministic tiebreaker for rules sharing a sequence_order. deadline_rules.sequence_order is already populated for every published filing rule (verified via paliad.deadline_rules_unified). Pure read-side fix; no schema or data change. |
|||
| da464813b7 |
fix(backup): repair 4 broken ORDER BY columns in orgSheetQueries
Backup export was 100% broken because four sheets referenced columns that no longer exist (or never did) in their target tables: - email_templates: ORDER BY id → key, lang (composite PK) - policy_audit_log: ORDER BY changed_at → created_at - ref__deadline_event_types: ORDER BY rule_id → deadline_id (post-rename) - ref__event_category_concepts: ORDER BY category_id → event_category_id Audited every entry in orgSheetQueries() against information_schema.columns; these were the only mismatches. Patch unblocks /admin/backups → Generate. Drift-resistant refactor (m/paliad#140 Part B) follows in a separate commit. m/paliad#140 |
|||
| d1aa0f72c0 | Merge: t-paliad-305 — Slice B.3: read cutover via paliad.deadline_rules_unified view (mig 139); legacy writes retire in B.4 (m/paliad#93) | |||
| 83be122b19 |
fix(backup): export ORDER BY uses binding_id, not calendar_binding_id
paliad.appointment_caldav_targets's join column is named binding_id (mig 101). The backup sheet exporter referenced calendar_binding_id which doesn't exist, so /admin/backups generate failed with 42703. Single-char fix. Also flags follow-up: hardcoded ORDER BY columns on every sheet in orgSheetQueries() are fragile under schema renames — a separate slice (m/paliad#140) tracks making the exporter flexible to drift (e.g. probe information_schema or use NULLS LAST id-only). |
|||
| df592f9fc4 |
feat(db,services): Slice B.3 read cutover — flip reads to paliad.deadline_rules_unified view backed by sr+pe+ls (t-paliad-305 / m/paliad#93)
The new tables (mig 136) and the dual-write that keeps them in sync (B.2) have been steady-state in prod since mig 136 deployed at 13:24 UTC today. Drift verified clean before this commit: deadline_rules=231, sequencing_rules=231, procedural_events=231 (153 codes + 78 synthetic), legal_sources=87, zero mismatches across counts, FK integrity, lifecycle, is_active. This commit flips READ paths to source data from the new tables via a backwards-compatible view, leaving the dual-write WRITE paths untouched for B.4 to retire alongside the destructive drop. * internal/db/migrations/139_deadline_rules_unified_view.up.sql (new) — CREATE VIEW paliad.deadline_rules_unified projecting sr+pe+ls back into the legacy paliad.deadline_rules column shape. Same column names + types so the Go-side change is a 1-token substitution per query with no struct or scanner edits. Post-apply DO block asserts view row count = sequencing_rules row count (FK NOT NULL on procedural_event_id guarantees they match). * 10 service / handler files — every SELECT FROM paliad.deadline_rules (or JOIN paliad.deadline_rules) flipped to use the view: - internal/handlers/submissions.go (Schriftsätze list) - internal/services/deadline_rule_service.go (8 read sites) - internal/services/rule_editor_service.go (3 read sites — ListRules, getByID, validateSpawnNoCycle) - internal/services/rule_editor_orphans.go (candidate-rule lookup) - internal/services/submission_vars.go (loadPublishedRule) - internal/services/deadline_service.go (deadlines list join) - internal/services/fristenrechner.go (calculator reads) - internal/services/projection_service.go (projection reads) - internal/services/event_deadline_service.go (event→rule join) - internal/services/export_service.go (3 export sites — ref__deadline_rules) Verified semantically safe on live (read-only smoke): - 231 rows in view match 231 in legacy. - name + event_type pair: 231/231 match. - legal_source: 231/231 match (NULL on both sides treated as match). - submission_code: 153 non-NULL codes match exactly; the 78 synthetic 'null.<8hex>' codes diverge from legacy NULL but no reader filters on NULL submission_code (verified handlers/submissions.go: synthetic-code rules all have NULL event_type so the WHERE event_type = 'filing' filter excludes them; the Schriftsätze surface returns the same 105 rows). Scope decisions documented (deviation from design §5.3): - B.3 ships the READ flip only. WRITE paths (RuleEditorService Create / UpdateDraft / CloneAsDraft / Publish / flipLifecycle) retain the dual-write from B.2 — they write to both legacy and new tables. B.4 (destructive drop) will retire the legacy writes in the same slice that drops the table, avoiding a transient state where the legacy writes have no purpose. - The B.2 drift-check ticker (StartDualWriteDriftCheckLoop) stays active for the same reason: dual-write continues, so the invariants the loop checks remain meaningful. This shape is paliadin-approvable on a "good solution > strict phase boundary" reading of m's greenlight. If paliadin pushes back and wants the legacy writes removed in B.3, the refactor is ~300 LOC across the 5 RuleEditorService write methods + buildPatchSets split into PE/SR sets — schedulable as B.3.5 before B.4. Build + vet clean. TestMigrations_NoDuplicateSlot passes. |
|||
| 8f1a287549 | Merge: t-paliad-305 — Slice B.2: dual-write to deadline_rules + procedural_events/sequencing_rules/legal_sources (m/paliad#93) | |||
| 38ebccc907 |
feat(services): Slice B.2 dual-write — RuleEditorService writes deadline_rules AND procedural_events / sequencing_rules / legal_sources (t-paliad-305 / m/paliad#93)
Keeps the parallel new tables (mig 136, Slice B.1) in lock-step with
the legacy paliad.deadline_rules table through every write path on
RuleEditorService. Read paths stay on deadline_rules in B.2 — B.3
flips them and stops legacy writes.
* internal/services/dual_write.go (new) —
- syncDualWriteFromDeadlineRule(ctx, tx, id): idempotent UPSERT of
legal_sources + procedural_events + sequencing_rules from the
just-written deadline_rules row. Pure SQL projection, no Go-side
struct mapping. Synthetic-code mint expression is byte-identical
to mig 136 ('null.' || first 8 hex of stripped uuid).
- syncDeadlineDualLinks(ctx, tx, deadlineID): mirrors a deadline's
legacy rule_id back-link onto deadlines.procedural_event_id +
sequencing_rule_id. Handles NULL rule_id naturally (collapses both
new columns to NULL).
- CheckDualWriteDrift(ctx, conn): nine read-only count queries +
integrity joins. Returns DualWriteDriftReport. HasDrift() bool for
log routing.
- StartDualWriteDriftCheckLoop(ctx, conn, interval): goroutine ticker
that runs CheckDualWriteDrift every `interval` (default 6h) for
the lifetime of ctx. Clean run logs at INFO; drift at WARN with
full report.
* internal/services/rule_editor_service.go —
- Create / UpdateDraft / CloneAsDraft / Publish / flipLifecycle
each call syncDualWriteFromDeadlineRule(ctx, tx, id) after the
deadline_rules mutation, before tx.Commit. Publish syncs BOTH the
published draft AND the cloned-from peer it just archived as a
cascade. The audit_reason already set via setAuditReasonTx
propagates to the new-table writes (same TX, same session).
* internal/services/rule_editor_orphans.go —
- ResolveOrphan calls syncDeadlineDualLinks after UPDATE
paliad.deadlines SET rule_id = $1, so the parallel new columns
follow the legacy back-link.
* internal/services/deadline_service.go —
- DeadlineService.Update calls syncDeadlineDualLinks when
input.RuleSet is true (auto/custom rule swap from t-paliad-258).
* cmd/server/main.go —
- Spawns StartDualWriteDriftCheckLoop alongside CalDAV sync and
reminder scanner. Inherits bgCtx so the goroutine stops on
SIGTERM. Interval 6h.
* internal/services/dual_write_test.go (new) —
- TestDualWrite_RuleEditorLifecycle: Create → UpdateDraft → Publish
→ Archive, asserts the new tables mirror at each step. Final
CheckDualWriteDrift returns zero drift.
- TestDualWrite_SyntheticCodeForNullSubmission: rule created with
submission_code=NULL gets a 'null.<8hex>' procedural_events row
matching mig 136's mint expression byte-for-byte.
Scope decisions documented in the commit:
- B.2 keeps read paths on deadline_rules. paliadin's "Read paths fall
back to legacy" reads as "reads stay on legacy as the safety net
while drift-check validates the new tables". B.3 swaps reads to
new tables only AND stops writing to deadline_rules — that's a
separate slice per the design's §5.2/§5.3 split.
- B.2 does NOT modify submission_drafts, projection_service, the
Fristenrechner calculator, the SubmissionVarsService, the
Schriftsätze list query, or any other reader. They keep reading
deadline_rules unchanged. The new tables are populated in parallel
for B.3's cutover.
- Audit triggers on deadline_rules continue to fire as before. The
new tables have no audit triggers yet (a later slice can add
parallel audit rows once the new tables are authoritative).
- Drift-check uses default 6h interval — short enough that a broken
dual-write surfaces within the same business day, long enough that
the count-COUNTs don't churn the pool. Override via the caller in
cmd/server.
Hard rules followed:
- audit_reason set on every TX before any deadline_rules mutation
(existing pattern; new-table writes share the same reason).
- No destructive op (B.2 is strictly additive in behaviour).
- New helpers idempotent (UPSERT ON CONFLICT DO UPDATE) — safe to
call twice, safe to re-run after a partial failure.
Build + vet clean. TestMigrations_NoDuplicateSlot passes.
|
|||
| cd5f752a0e |
feat(litigationplanner): scenarios — paliad.scenarios jsonb table + Catalog API + engine adapter (Slice D, t-paliad-306, m/paliad#124 §5)
A scenario is a named composition of existing proceedings + flags +
per-card choices + anchor dates. Users compose, they don't author —
spec references existing rules by submission_code; never creates new
rules. Per m's 2026-05-26 AskUserQuestion picks (doc commit
|
|||
| 5cff38ff3c |
feat(deadlines): mig 138 backfill applies_to_target — Schadensbemessung (merits) + Bucheinsicht (order)
After Slice B1's Berufung unification (mig 134), the picker exposed five appeal targets but only three carried rules. Schadensbemessung and Bucheinsicht returned empty timelines. m's 2026-05-26 decision (#134): R.224 is uniform across substantive R.118 decisions, and R.220.2 / R.224.2.b / R.235.2 / R.237 / R.238.2 are uniform across the orders they appeal — so the existing merits-track and order-track rules can carry the missing targets via a non-destructive applies_to_target extension. Audit of live `paliad.deadline_rules` for upc.apl.unified (proceeding_type_id=160): - 7 endentscheidung rules → extend with 'schadensbemessung' - 7 anordnung rules → extend with 'bucheinsicht' - 2 kostenentscheidung rules — untouched (distinct leave-to-appeal track) Migration: - set_config('paliad.audit_reason', …) at top of UP and DOWN — required by the mig 079 deadline_rule_audit_trigger on every UPDATE. - Audit-first DO block lists every row to be touched (pre/post state) and RAISE EXCEPTIONs on pre-condition drift (missing proceeding_type, wrong rule counts, partial-run carry-over of the new targets). - Two narrow UPDATEs keyed off upc.apl.unified + existing target + absence of new target. - Post-sanity asserts schad=7, buch=7, end=7, anord=7, cost=2 — hard RAISE EXCEPTION on any drift. - DOWN strips both new targets via array_remove with the same WHERE. - No deadline_rules.updated_at writes; column exists but the migration is single-purpose and leaves it as-is. Dry-run via Supabase MCP confirmed: - UP yields {schad:7, buch:7, end:7, anord:7, cost:2} on prod. - DOWN restores {schad:0, buch:0, end:7, anord:7, cost:2}. - DB returned to pre-state; the real golang-migrate boot path will apply 138 cleanly at next deploy. Version bump 137→138: cronus's mig 137 (proceeding_role_labels, #132) merged to main while this branch was in flight. Rebased onto current main, renamed files, rewrote all "mig 137" references inside the SQL + test code. Test: - lookup_events_test.go: the schadensbemessung empty-result assertion becomes the inverse (rules expected). Adds a parallel bucheinsicht assertion. Same anchor-row shape check as the existing endentscheidung case (DepthFromAnchor=1, target ∈ AppliesToTarget, proceeding_type = upc.apl.unified). - `go test ./...` green post-rebase, including pkg/litigationplanner/ appeal_target_label_test.go added by cronus's mig 137. Refs: m/paliad#134, t-paliad-303. Lessons applied from mig 134 hotfixes: audit_reason set_config, no updated_at writes, audit live DB before drafting, RAISE EXCEPTION on integrity violations. |
|||
| 9da4715137 |
feat(litigationplanner): Berufung tile UX — collapse side selectors + appeal-target trigger label (t-paliad-301, m/paliad#132)
Two bugs from the Slice B1 Berufung rollout, one fix surface:
Bug A — duplicate side selectors collapse into ONE proactive-side
picker with per-proceeding role labels. The Verfahrensablauf used to
show both ?side= (Klägerseite/Beklagtenseite) AND ?appellant= (same
labels in case-form) on the Berufung tile. Now: one side picker, with
labels that swap to Berufungskläger/Berufungsbeklagter on the unified
upc.apl.unified tile (and Antragsteller/Antragsgegner Nichtigkeit on
upc.rev.cfi, Einsprechende(r)/Patentinhaber(in) on epa.opp.*).
Bug B — 'Auslösendes Ereignis' label derives from appeal_target on
the unified Berufung tile (5 target-specific strings) instead of the
proceeding's own trigger_event_label. Endentscheidung (R.118) /
Kostenentscheidung / Anordnung / Entscheidung im
Schadensbemessungsverfahren / Anordnung der Bucheinsicht.
Migration 137 (additive, no triggers on proceeding_types — verified
via mcp__supabase__execute_sql before drafting; no updated_at on the
table — lesson from mig 134 HOTFIX 3; no audit_reason setup needed):
- ADD COLUMN role_proactive_label_de (text NULL)
- ADD COLUMN role_proactive_label_en (text NULL)
- ADD COLUMN role_reactive_label_de (text NULL)
- ADD COLUMN role_reactive_label_en (text NULL)
- Audit-first DO block lists the rows the UPDATE will touch.
- Backfill 4 proceedings (upc.apl.unified + upc.rev.cfi +
epa.opp.opd + epa.opp.boa); every other proceeding stays NULL
and the renderer falls back to default labels.
- Down drops the 4 columns.
Package additions (pkg/litigationplanner):
- ProceedingType gains 4 *string fields (RoleProactive/Reactive
LabelDE/EN) — db tags match the new columns; existing scans pick
them up via the proceedingTypeColumns extension.
- TriggerEventLabelForAppealTarget(target, lang) — Go-side map of
the 5 appeal-target slugs to their DE/EN trigger-event labels.
Empty result on unknown target signals "fall back to proceeding's
own trigger_event_label".
- Engine override: when CalcOptions.AppealTarget is set, the
resulting Timeline.TriggerEventLabel/EN are replaced from the
per-target map.
Frontend:
- Removed #appellant-row div (was a separate 3-radio selector
duplicating side).
- Dropped ?appellant= URL state + the change handler + the init
readback. The engine still consumes "appellant" — sourced from
currentSide for role-swap proceedings; null otherwise.
- applyRoleLabels(proceedingType) swaps the side-row radio labels
from a hardcoded ROLE_LABELS map mirroring mig 137's backfill.
Falls back to deadlines.side.claimant/defendant i18n keys for
proceedings without overrides.
- syncTriggerEventLabel reads data.triggerEventLabel from the calc
response — which the engine override now sets per appeal_target,
so no client-side mapping needed.
- i18n cleanup: removed orphan deadlines.appellant.* keys (label /
claimant / defendant / none) in both DE + EN.
Tests:
- pkg/litigationplanner/appeal_target_label_test.go pins the 5×2
label matrix + a coverage test that fails if a new entry in
AppealTargets is added without populating the label switch.
Acceptance:
- go build + go test all green (incl. new lp test).
- bun run build clean (i18n codegen drops 4 keys, regenerates).
- Live-DB audit before drafting confirmed: 4 target columns don't
exist on proceeding_types, zero triggers on the table, exact
column inventory matches the design.
|
|||
| 16ec8c490a | Merge: t-paliad-273 — Slice B.1: additive procedural_events / sequencing_rules / legal_sources (mig 136) (m/paliad#93) | |||
| 5901d40b79 |
fix(mig 134): remove non-existent updated_at column reference (HOTFIX 3)
paliad.proceeding_types has no updated_at column. Removing the UPDATE ... SET ..., updated_at = now() clause from both up and down migrations. Third bug in cronus's Slice B1 mig 134 — production still down. Verified columns on paliad.proceeding_types via prod-snapshot.sql: id, code, name, description, jurisdiction, category, default_color, sort_order, is_active, name_en, display_order, trigger_event_label_de, trigger_event_label_en, appeal_target (added by this mig). Refs t-paliad-292, m/paliad#124. No new issue filed — single-line emergency fix during head's incident response. |
|||
| 4f94697377 |
fix(litigationplanner): mig 134 set_config('paliad.audit_reason') (HOTFIX 2, t-paliad-300, m/paliad#131)
Mig 134's step 4 UPDATEs paliad.deadline_rules to reassign 16 rules
to the unified upc.apl.unified proceeding_type. The mig-079 audit
trigger requires set_config('paliad.audit_reason', …, true) before
any mutation — mig 134 missed it, causing the migration runner to
abort with P0001 "audit reason required for UPDATE" on every boot
after #130 landed.
Adds the canonical set_config call at the top of both up + down,
matching the pattern from mig 082, 099, 100, 103, 106, 110, 127, 129.
|
|||
| 75833082fc |
feat(db): mig 136 — additive procedural_events / sequencing_rules / legal_sources tables (Slice B.1, t-paliad-273 / m/paliad#93)
Creates the three new tables that split today's paliad.deadline_rules
into its three latent concepts, plus two nullable link columns on
paliad.deadlines for B.2 dual-write.
ADDITIVE ONLY. paliad.deadline_rules is untouched. deadlines.rule_id
stays in place — it remains the authoritative deadline → rule link
until B.3 cutover flips reads and B.4 drops the legacy table.
* paliad.legal_sources — distinct citations (87 rows backfilled).
pretty_de/pretty_en deferred (Go
legalSourcePretty still computes them
on read; future slice backfills).
* paliad.procedural_events — 153 rows from distinct submission_codes
+ 78 synthetic-code rows for the
NULL-submission_code branch (m's pick
via paliadin 2026-05-26: mint
'null.<8hex>' codes so every rule row
has a procedural event, preserving the
NOT NULL FK on sequencing_rules).
* paliad.sequencing_rules — 1:1 with deadline_rules (231 rows). id
inherited from deadline_rules.id so any
existing deadlines.rule_id FK resolves
transitively to the new sequencing_rule
during the dual-write window.
* paliad.deadlines.procedural_event_id, sequencing_rule_id (nullable,
backfilled by JOIN on the inherited id).
Audit-first pattern (mirrors mig 135): PRE pass counts what we're about
to backfill + refuses to run if multi-row submission_codes have crept
back in (B.0 found zero; the assertion guards against a future
re-archival or rule-editor bug). POST pass asserts the four
invariants — procedural_events count, sequencing_rules 1:1,
legal_sources distinct-citation match, FK integrity — and RAISE
EXCEPTIONs on any mismatch so the transaction rolls back cleanly.
Design deviations from §4.1 (documented in the migration header):
- procedural_events.event_kind is NULLABLE. 89 live rules have NULL
event_type today (structural / parent-only rows in the proceeding
tree). Tightening to NOT NULL with 'other' fallback would lose
semantics; a later slice can do it after reclassification.
- legal_sources.pretty_de / pretty_en are NULLABLE. Materialising them
requires the Go-side legalSourcePretty(); deferred to a Go-driven
slice. Read path keeps computing them from the citation in the
meantime.
- submission_drafts is NOT modified (instruction scope is explicit:
tables + deadlines columns only).
Down migration: drops the two deadlines columns first, then
sequencing_rules → procedural_events → legal_sources in FK-safe
order. No data loss possible (deadline_rules is the source of truth
through B.3).
Test: internal/db/migration_136_test.go restates the four
invariants in Go so they survive PL/pgSQL refactors. Skipped without
TEST_DATABASE_URL.
Verified on live (read-only): 153 distinct codes + 78 distinct
synthetic-code candidates = 231 = deadline_rules row count. 87
distinct legal_sources. Zero 8-hex synthetic-code collisions in the
live UUIDs.
Hard-stop: B.2 dual-write requires explicit m greenlight before
RuleEditorService starts writing to the new tables. B.4 destructive
drop additionally requires m's downtime window + a
paliad.deadline_rules_pre_<N> snapshot in the same migration.
|
|||
| e2d75c391d |
fix(litigationplanner): rename upc.apl → upc.apl.unified (HOTFIX, t-paliad-299, m/paliad#130)
mig 134 was inserting code='upc.apl' (2 segments) into paliad.proceeding_types, which carries paliad_proceeding_code_shape CHECK requiring 3 dot-segments OR '^_archived_'. Every container restart hit the constraint, rolled the migration TXN back, and crash-looped paliad.de. Rename the unified Berufung code to 'upc.apl.unified' (3 segments, satisfies the constraint, preserves design intent). The pre-existing constraint is a useful jurisdiction.category.specific invariant — keep it, fix the new row. Touched only string literals: - mig 134 up.sql + down.sql (insert, lookups, post-checks) - frontend/src/verfahrensablauf.tsx (UPC_TYPES code + i18nKey) - frontend/src/client/verfahrensablauf.ts (APPELLANT_AXIS + APPEAL_TARGET sets) - frontend/src/client/i18n.ts (DE + EN translation rows) - frontend/src/i18n-keys.ts (auto-regen via bun build) - internal/services/lookup_events_test.go (anchor-row assertion) Verified: `grep -rn "'upc\.apl'\|\"upc\.apl\""` returns zero hits. go build, bun run build, go test ./... all green. |
|||
| 989941c648 |
feat(litigationplanner): primary_party CHECK constraint + IsValidPrimaryParty helper (Slice B3, m/paliad#124 §18.3)
Tightens paliad.deadline_rules.primary_party from free-text to a CHECK
constraint over the canonical four-value vocab (claimant / defendant /
court / both). NULL stays valid for the 78 cross-cutting orphan
concept seeds (Wiedereinsetzung, Versäumnisurteil-Einspruch,
Schriftsatznachreichung, Weiterbehandlung) — they have no
proceeding_type_id binding so they're outside the calculator's path;
loosening the CHECK to "IS NULL OR IN (…)" keeps them valid without
backfill gymnastics.
Migration 135 (audit-first):
- DO block RAISEs NOTICE for every non-conforming row + RAISEs
EXCEPTION if any dirty rows exist (manual cleanup required).
Live audit (Supabase, 2026-05-26 §18.0) confirmed zero dirty rows
on the current corpus; the audit pass stays in the migration as
safety against future drift.
- ALTER TABLE … ADD CONSTRAINT deadline_rules_primary_party_chk
CHECK (primary_party IS NULL OR primary_party IN
('claimant', 'defendant', 'court', 'both'))
- Post-migration distribution NOTICE so the operator sees the
final per-value count.
- Down = DROP CONSTRAINT. No data revert needed.
Package additions (pkg/litigationplanner):
- PrimaryParty* constants (PrimaryPartyClaimant / Defendant / Court
/ Both) + PrimaryParties[] ordered list + IsValidPrimaryParty(s)
predicate. Empty string is "no value supplied" = valid (NULL maps
to empty on the wire); non-empty must match one of the four
canonical values.
- Sibling unit tests (primary_party_test.go) pin the four-value
vocab + the chip order + IsValidAppealTarget's matching shape.
Rule-editor validation hook (rule_editor_service.go):
- Create() validates input.PrimaryParty before INSERT.
- UpdateDraft() validates patch.PrimaryParty before UPDATE.
- Both surface a user-friendly 400 with the canonical vocab listed
instead of leaking the raw PG CHECK constraint-violation message.
- Uses errors.Is(err, ErrInvalidInput) so handler 400 routing
continues to work.
services/fristenrechner.go cleanup:
- The B2-inlined isValidPartyForLookup helper is replaced with the
canonical lp.IsValidPrimaryParty. No behaviour change.
No frontend changes — the rule-editor's primary_party UI already
constrains to the four values via a select; the validation hook is
defense-in-depth.
Audit:
- go build + go test (incl. new lp unit tests) all green
- Pre-migration audit confirmed: 26 claimant + 26 defendant + 38
court + 63 both + 78 NULL = 231 total, all in canonical vocab
- event_categories.party (text[] array, narrower semantic) is
NOT touched in this migration per the design doc's
"out of scope, separate follow-up" decision
|
|||
| d5bf82314a |
feat(litigationplanner): multi-axis catalog query API (Slice B2, m/paliad#124 §18.2)
New Catalog.LookupEvents(ctx, axes, depth) method exposes a unified
graph query over paliad.deadline_rules + paliad.proceeding_types + the
deadline_concept_event_types junction. Used by the Determinator
cascade, the scenarios surface (Slice D), and any future "show me
events matching X" query — centralises a fan-out that today is
duplicated across multiple client-side paths.
Package additions (pkg/litigationplanner):
- EventLookupAxes: optional Jurisdiction / *ProceedingTypeID / Party
/ *EventCategoryID / AppealTarget. All fields optional; the empty
value (or nil pointer) is "no filter on this axis". Multiple
non-zero axes apply as AND.
- EventLookupDepth: "next" (1 hop downstream) or "all-following"
(full chain).
- EventMatch: Rule + ProceedingType + Priority + DepthFromAnchor +
*ParentRuleID (populated only when the parent itself is in the
returned set, so the frontend can render a tree).
- Catalog interface gains LookupEvents.
paliad-side implementation (internal/services/fristenrechner.go):
- SQL pass with progressively-built WHERE clauses (one $N
placeholder per non-zero axis). EventCategoryID uses an EXISTS
subquery against paliad.event_category_concepts joined via
concept_id.
- Post-fetch parent_id graph walk in Go for depth control. Loads
the per-proceeding rule corpus via DeadlineRuleService.List so
children whose parent_id is in the anchor set can be added even
when those children don't match the axes themselves. AllFollowing
iterates to fixpoint; Next stops after one pass.
- DepthFromAnchor computed by walking each result row up the
parent_id chain until it hits an anchor (iteration-bounded to
prevent infinite loops on hypothetical cycles).
- Unknown axis values (jurisdiction="XX", party="foo",
appealTarget="invalid") silently fall through as "no filter on
this axis" — a stale frontend chip should not drop the entire
result set.
- "published + active" gate (lifecycle_state='published' AND
is_active=true) matches LoadProceeding's WHERE clause.
- Results ordered by (proceeding_type_id, sequence_order) so the
frontend can render without re-sorting.
Tests (internal/services/lookup_events_test.go):
- Live-DB driven (skipped without TEST_DATABASE_URL, matches the
existing TestCalculateRule pattern).
- Cases: UPC-jurisdiction returns the UPC corpus only;
party=defendant scopes anchor matches to defendant rules;
unknown jurisdiction falls through; appeal_target=endentscheidung
returns the merits rules from B1 mig 134;
appeal_target=schadensbemessung returns empty (no rules seeded).
No schema delta. No frontend wiring (the new HTTP endpoint at
GET /api/tools/lookup-events can land in a follow-up slice — the
package + paliad-side impl are the deliverable here).
|
|||
| 07acf7b4a2 |
feat(litigationplanner): Berufung unification — one upc.apl + 5 appeal_target chips (Slice B1, m/paliad#124 §18.1)
Collapses the 3 UPC appeal proceeding_types (upc.apl.merits 7 rules,
upc.apl.cost 2, upc.apl.order 7 = 16 total across 3 codes) into ONE
unified upc.apl proceeding type + a per-rule applies_to_target[]
discriminator. The verfahrensablauf picker now shows one "Berufung"
tile; after picking it, the user selects which decision the appeal is
directed AT via a 5-chip group (Endentscheidung / Kostenentscheidung /
Anordnung / Schadensbemessung / Bucheinsicht) and the engine filters
rules whose applies_to_target contains the picked slug.
m's 2026-05-26 decision: Schadensbemessung-as-appeal is a NEW first-
class target with its OWN rule set (no shared inheritance from
merits). The 5 enum values are all defined + addressable; for now
schadensbemessung and bucheinsicht return empty timelines until rules
are seeded in a follow-up slice (likely via /admin/rules or pairing
with t-paliad-193 orphan-concept-seed).
Migration 134 (additive only):
- ADD proceeding_types.appeal_target text (CHECK on 5 slugs OR NULL)
- ADD deadline_rules.applies_to_target text[] (CHECK each element
in the 5 slugs)
- INSERT the unified upc.apl row (inherits sort/color from
upc.apl.merits)
- Audit-first RAISE NOTICE pass listing every row about to be
touched + a post-migration sanity check
- Reassign rule rows: merits → applies_to_target={endentscheidung},
cost → {kostenentscheidung}, order → {anordnung}
- Archive (is_active=false, NOT DELETE) the 3 old proceeding_types
so historical FKs stay intact
- Down migration restores is_active=true on the 3 old types, points
rules back by their applies_to_target stamp, drops the unified
row, drops both columns. Safe.
Package additions (pkg/litigationplanner):
- AppealTarget* constants + AppealTargets[] ordered list +
IsValidAppealTarget(s) predicate (silent no-op on unknown slugs
so a stale frontend chip doesn't break the render)
- ProceedingType.AppealTarget *string field (top-level marker;
NULL on non-appeal proceedings)
- Rule.AppliesToTarget pq.StringArray field (per-row applies-to set)
- CalcOptions.AppealTarget string (engine filter — when set,
keeps only rules whose AppliesToTarget contains the slug)
Engine filter runs after ApplyRuleOverrides but before the rule walk
so the existing condition_expr / spawn / appellant-context machinery
operates on the filtered subset transparently.
paliad-side wiring:
- deadline_rule_service.go: ruleColumns + proceedingTypeColumns
extended to scan the new columns
- handlers/fristenrechner.go: AppealTarget JSON field on the
request payload, threaded into CalcOptions
Frontend (verfahrensablauf surface only):
- Single "Berufung" tile replaces the 3 separate Berufung tiles
- New 5-chip appeal-target row, shown only when upc.apl is picked
- URL state ?target=<slug>; default endentscheidung when none set
- APPELLANT_AXIS_PROCEEDINGS updated: upc.apl.* (3 entries) →
upc.apl (1 entry)
- i18n keys (DE + EN) for the new tile + the 5 chip labels +
the "Worauf richtet sich die Berufung?" / "Appeal against:" prompt
- calculateDeadlines threads appealTarget through to the API
Acceptance:
- go build clean, go test all green (existing test suite — no new
tests on the engine filter as a follow-up; the migration's
sanity-check DO block guards the rule-reassignment count)
- Live audit before drafting confirmed: 3 active UPC appeal
proceeding_types, 16 rules total, primary_party already conforms
to 4-value vocab on all proceeding-bound rules
|
|||
| 5f0a85fa83 |
refactor(litigationplanner): extract Fristen/Verfahrensablauf calc into pkg/litigationplanner (Slice A, t-paliad-298 / m/paliad#124)
Atomic extraction of the deadline-rule compute engine + types from
internal/services into a new pkg/litigationplanner package that paliad
+ youpc.org can both import. No behaviour change — every existing test
passes against the post-move shape.
Package contents (~1850 LoC):
- doc.go package docstring + reuse manifesto
- types.go Rule, ProceedingType, NullableJSON, AdjustmentReason,
HolidayDTO, CalcOptions, CalcRuleParams, Timeline,
TimelineEntry, RuleCalculation*, FristenrechnerType,
ProjectHint, sentinel errors
- catalog.go Catalog interface (proceeding + rule lookups)
- holidays.go HolidayCalendar interface
- courts.go CourtRegistry interface + DefaultsForJurisdiction +
country/regime constants
- expr.go EvalConditionExpr + HasConditionExpr +
ExtractFlagsFromExpr (jsonb gate evaluator)
- durations.go ApplyDuration + AddWorkingDays (pure compute)
- subtrack.go SubTrackRouting + LookupSubTrackRouting registry
- legal_source.go FormatLegalSourceDisplay + BuildLegalSourceURL
- proceeding_mapping.go MapLitigationToFristenrechner + code constants
(CodeUPCInfringement, CodeDEInfringementLG, ...)
- engine.go Calculate + CalculateRule + the trigger-event
branch + applyRuleOverrides (the big move)
paliad side (~1900 LoC net deletion):
- internal/services/fristenrechner.go shrinks from 1505 → ~290 lines
(thin paliad Catalog adapter + type aliases for back-compat).
- internal/models/models.go: DeadlineRule, ProceedingType, NullableJSON
become type aliases to litigationplanner.* — every sqlx scan and
every projection_service caller compiles unchanged.
- internal/services/holidays.go: AdjustmentReason + HolidayDTO become
aliases to lp.* (canonical definitions now in the package).
- internal/services/proceeding_mapping.go: rewritten as thin re-exports
of lp constants + helpers.
- internal/services/deadline_search_service.go: FormatLegalSourceDisplay
+ BuildLegalSourceURL replaced with delegating wrappers to lp.
Catalog interface satisfaction:
- DeadlineRuleService → paliadCatalog adapter (wraps the existing
service, replicates the original SELECT shapes).
- HolidayService → satisfies lp.HolidayCalendar directly (compile-
time assertion at end of fristenrechner.go).
- CourtService → satisfies lp.CourtRegistry directly.
Wire shape is byte-identical. JSON tags on Rule / ProceedingType /
Timeline / TimelineEntry / RuleCalculation match the historical
UIResponse / UIDeadline shape; the frontend reads the same bytes.
Slice B (Catalog interface + paliad loader cleanup) is folded into
this commit since Slice A already needs the interfaces to call
Calculate across the boundary. Slice C (embedded UPC snapshot +
generator) is the next coder shift; the Berufung unification m
called out lands in Slice B/C per head's brief.
Refs: docs/design-litigation-planner-2026-05-26.md
|
|||
| cc13a5b857 |
chore(admin): remove /admin/rules/export page + export-migrations API (t-paliad-297)
Workflow shifted to hand-written numbered migrations; the audit-row SQL
export tool no longer has any consumers. Pure deletion — /admin/rules
and /admin/rules/{id}/edit stay; only the export-to-SQL flow goes.
Deleted:
- frontend/src/admin-rules-export.tsx
- frontend/src/client/admin-rules-export.ts
Removed:
- routes GET /admin/rules/export and GET /admin/api/rules/export-migrations
- handleAdminExportRuleMigrations + handleAdminRulesExportPage
- RuleEditorService.ExportMigrationsSince + ExportResult + sqlEscape helper
- build.ts entries (import, client bundle, dist HTML write)
- Sidebar "Regel-Migrations" nav item + "Migrations exportieren" button on /admin/rules
- all admin.rules.export.* + nav.admin.rules_export + admin.rules.list.export i18n keys (DE+EN)
- .admin-rules-export-* CSS rules (dead after page deletion)
Doc references in design-fristen-phase2-2026-05-15.md and
design-paliad-data-export-2026-05-19.md updated to mark the endpoint as
removed (acceptance #2 requires grep to return zero hits).
|
|||
| abef74fe63 | Merge: t-paliad-296 — sort post-trigger optional events by duration ascending (m/paliad#128) | |||
| 49ddaa4eb8 |
feat(fristenrechner): sort post-trigger events by duration ASC within parent group (t-paliad-296)
Optional events anchored on the same trigger (e.g. the four post-Entscheidung rules in upc.inf.cfi) used to render in catalog sequence_order, so a 2-month rule (R.118.4 Folgeentscheidungen) would precede a 1-month rule (R.151 Kostenentscheidung) chained off the same decision. Now the calculator does a post-evaluation permutation pass that sorts consecutive same-parent rows by duration ascending — days < weeks < months < years, ties broken by duration_value then submission_code. Different trigger groups keep their proceeding-sequence position — the walk only ever permutes rows that already share a parent. Root rules (no parent) are never sorted against each other. Court-set / conditional rows whose date isn't in the duration ladder sort LAST within their group. Verified order against m's report: R.151 cost_app + R.353 rectification (1-month tier) now render before R.220.1 appeal_spawn + R.118.4 cons_orders (2-month tier). Issue: m/paliad#128 |
|||
| f6c8eb5bcf |
fix(projection): conditional label uses trigger_event_id, not parent_id
t-paliad-294 / m/paliad#126. knuth's #121 conditional-rendering defaulted the "abhängig von <parent>" chip to the rule's parent_id display name. For R.262(2) Erwiderung auf Vertraulichkeitsantrag the parent_id resolves to the SoC (Klageerhebung), but the rule's real semantic anchor is the opposing party's confidentiality application (paliad.trigger_events id=25). The chip read "abhängig von Klageerhebung", which is wrong. Fix: when a rule has a non-NULL trigger_event_id, the engine stamps ParentRuleCode / ParentRuleName / ParentRuleNameEN from the trigger_events catalog row instead of from the parent_id chain. The parent_id stays as the calc-time arithmetic anchor — only the user- facing dependency identity shifts. Generalises across every rule with a real trigger_event_id (2 rows in the live corpus today: confidentiality_response and translations_lodge — both relabel correctly). Touches both surfaces in one shot: verfahrensablauf-core's chip ("abhängig von …") and shape-timeline's "Folgt aus …" footer both read from ParentRule*, so no frontend change needed. Tests: extend TestUIDeadline_IsConditional_UncertainAnchors with a DE+EN string-pinning case for R.262(2) plus a generalisation guard for translations_lodge. Negative guard asserts the chip no longer leaks "Klageerhebung" / "Statement of Claim". |
|||
| 293e612582 |
feat(projection): IsConditional for uncertain-anchor rules (t-paliad-289)
Rules anchored on uncertain triggers (R.109 backward-anchor without oral-hearing date; R.118(4) without validity decision; R.262(2) without recorded Vertraulichkeitsantrag) previously rendered concrete dates fabricated off the trigger date. Add IsConditional projection flag so the SmartTimeline + Verfahrensablauf surfaces "abhängig von <parent>" instead of a misleading date. Backend (fristenrechner.go): - Add IsConditional + ParentRuleCode/Name/NameEN to UIDeadline. - Pre-pass populates courtSet from rule.is_court_set=true BEFORE the main loop, so order-of-evaluation in sequence_order no longer matters for the parent-court-set check. Fixes R.109(1) "Antrag auf Simultanübersetzung" (sequence_order=45 < Mündliche Verhandlung's sequence_order=50): the timing='before' backward arithmetic was computing 1 month before the trigger date because the court-set parent hadn't been classified yet. - Set IsConditional=true on every IsCourtSetIndirect branch (catches R.109 backward + R.118(4) cons_orders chain off the decision). - Set IsConditional=true for priority='optional' + primary_party='both' rules whose data-model parent is the trigger anchor (covers R.262(2) confidentiality_response: the data anchors on SoC, but the real trigger is the opposing party's confidentiality motion which may never happen). Suppressed by IsOverridden so user anchors win. Backend (projection_service.go): - Add IsConditional to TimelineEvent + propagate from UIDeadline. - New Status="conditional" for projected rows; clears Date, populates DependsOnRuleCode/Name from UIDeadline.ParentRule* so the row carries the "abhängig von <parent>" payload even when the parent has no computed date for annotateDependsOn to discover. Frontend (verfahrensablauf-core.ts + CSS + i18n): - CalculatedDeadline gains isConditional + parentRule* fields. - deadlineCardHtml renders "abhängig von <parent>" chip with click-to-edit affordance in place of the date column when isConditional=true. IsConditional wins over IsCourtSet for the date column (they overlap; "abhängig von <parent>" names the specific blocker). - .timeline-item--conditional / .fr-col-item--conditional CSS: dotted border + faded text so the conditional state reads at glance. - Replaced escHtml's DOM-backed implementation with a pure-JS regex escape so the module is testable in bun test without jsdom (the old form forced fixtures to leave several fields empty just to avoid the DOM dependency). Tests: - TestApplyLookaheadCap_ConditionalRowsPassThrough: pure-function lock that conditional rows pass through applyLookaheadCap untouched (don't count against ProjectedTotal/Shown, don't get capped). - TestUIDeadline_IsConditional_UncertainAnchors (TEST_DATABASE_URL): asserts R.109(1)/(4), R.118(4) chain, and R.262(2) all render IsConditional=true with empty DueDate + populated ParentRule*; SoD stays non-conditional; override on the oral hearing flips R.109(1) back to concrete date. - 4 new bun tests for the conditional rendering branches in deadlineCardHtml. UX path verified by tests + manual review of the live rule corpus: opening a UPC inf project without oral-hearing date now surfaces R.109(1) + R.109(4) as conditional; recording the Vertraulichkeitsantrag (anchoring R.262(2) via the existing "Datum setzen" flow) flips it back to a concrete date. go build / go test / bun test / bun run build all clean. |
|||
| 07d2eb472c | Merge: t-paliad-287 — submission form revision (Frist drop + grouped sections + Add Party + DB picker) (m/paliad#119) | |||
| d4ed989b8f |
feat(parties): cross-project party search endpoint for submission picker (t-paliad-287)
Adds PartyService.Search returning paliad.parties rows from every project the caller can see, matched by case-insensitive substring on name or representative. Wired via GET /api/parties/search?q=... — used by the submission-draft Add-Party panel's "Aus DB übernehmen" tab. Visibility flows through the same visibilityPredicatePositional helper every project-scoped read uses; invisible projects' parties never surface. Capped at 25 hits per call (no pagination — typical lookup is "the party I'm thinking of by name", not a browse). Result shape carries project_title + project_reference so the picker can disambiguate identically-named parties across cases. |
|||
| 80883eaac5 |
feat(verfahrensablauf): re-surface hidden optional events — show-hidden toggle + un-hide chip (t-paliad-290)
m/paliad#122. atlas's #96 Slice A added per-card 'Überspringen' but no un-skip path — hidden cards just disappeared from the timeline. This adds the missing return path: - CalcOptions.IncludeHidden (default false) tells the calculator to re-surface skipRules entries as faded rows instead of dropping them. When true, the rule renders with UIDeadline.IsHidden=true and the descendant-suppression cascade is bypassed so children compute their dates off the un-suppressed parent. - UIResponse.HiddenCount always reflects the projection's hide count (gate-passed rules whose submission_code is in skipRules) so the "Ausgeblendete (N)" badge stays accurate regardless of toggle state. - /tools/verfahrensablauf gets a "Ausgeblendete anzeigen" checkbox next to the perspective + appellant selectors. URL-driven (?show_hidden=1) so the state is shareable and survives reload. The row hides itself on projections with zero hidden cards. - Hidden cards render via .timeline-item--hidden / .fr-col-item--hidden (opacity 0.55 + dotted border, mirroring the existing --skipped fade) and carry an inline "Wieder einblenden" chip. Clicking the chip removes the skip choice via the page's existing attachEventCardChoices remove callback (URL state + recalc included) and runs through a new delegated handler in event-card-choices.ts. - 3 new i18n keys (DE+EN): choices.show_hidden.label, choices.show_hidden.count, choices.unhide.chip. The skip-choice storage shape (paliad.project_event_choices, atlas's table) is unchanged — un-hide is just a delete of the skip row. Tests: 3 new bun-test cases pin the chip contract (emits on isHidden= true with submission_code, suppressed otherwise); go test ./internal/... + bun run build clean. |
|||
| 4d923562f5 | Merge: t-paliad-283 — /views/any filter-bar Predicates flatten fix (m/paliad#115) | |||
| c70914c2a0 |
fix(filter-bar): flatten FilterSpec.Predicates wire shape (t-paliad-283)
The bar's chip clicks POST a payload shaped as `predicates: {<source>:
<per-source>}` — flat, one entry per data source. Go declared
`Predicates map[DataSource]Predicates` — a doubled-nested wrapper where
each map value was itself a Predicates struct with named per-source
fields. The JSON shape Go expected was
`{"deadline": {"deadline": {"status": [...]}}}`; the shape the bar
emitted was `{"deadline": {"status": [...]}}`. Go silently unmarshalled
the bar's payload as `Predicates{}` (all source fields nil), so every
chip click on /views/any was a server-side no-op — the regression in
#115.
The latent contract bug was present since t-paliad-144 A1 (
|
|||
| 016ac2532a | Merge: t-paliad-282 Slice A — CI/CD pre-deploy gate + snapshot-based migration smoke (m/paliad#114) | |||
| c901293c9c |
feat(cicd): Slice A — pre-deploy gate + role-split migration smoke
Adds .gitea/workflows/test.yaml that gates every push on `go build`, `bun run build`, `go vet`, the migration coordination check, and the role-split end-to-end migration smoke. On push to main + green, calls Dokploy's compose.deploy API and polls /health/ready until 200. t-paliad-282 / m/paliad#114. Design: docs/design-cicd-pre-deploy-gate-2026-05-25.md (inventor shift on mai/cronus/inventor-ci-cd-pre). Catches all three of today's outage classes: brunel (~13:20) slot collision -> TestMigrations_NoDuplicateSlot hermes (~16:05) dropped-col refs -> TestBootSmoke mig 129 (~14:56) 42501 ownership -> TestMigrations_EndToEndAsAppRole Snapshot approach. internal/db/testdata/prod-snapshot.sql is a pg_dump of youpc-supabase paliad schema + applied_migrations rows. CI restores this into a fresh `supabase/postgres:15.8.1.060` (same image, same role topology as prod) and runs ApplyMigrations as the `postgres` role (which is NOT a superuser on supabase/postgres, matching prod). Existing migrations are skipped (already in applied_migrations); only NEW migs from the PR run end-to-end. This sidesteps the fresh-DB idempotence debt in some historical migrations (mig 037 missing pg_trgm, mig 051 inner COMMIT) — those are tracked separately and don't block the gate. Sub-changes: - internal/handlers/handlers.go — new /health/ready endpoint distinct from /healthz. /healthz stays liveness (process alive, no DB); /ready is readiness (DB pool pings within 2 s). Returns 503 when svc or pool is nil (DB-less deploys are intentionally not-ready). svc.Pool added to handlers.Services, wired in cmd/server/main.go. - internal/db/migrate_test.go — TestMigrations_NoDuplicateSlot (pure unit, catches brunel) and TestMigrations_EndToEndAsAppRole (snapshot- gated, catches the 42501 class). - cmd/server/main_smoke_test.go — TestBootSmoke now also asserts /health/ready returns 503 with a nil svc. New TestHealthReady_Live asserts 200 against a live pool. - internal/db/migrations/024_rename_department_columns.up.sql and 027_rename_to_partner_units.up.sql — ALTER INDEX / ALTER POLICY exception handlers now catch undefined_object OR undefined_table OR duplicate_object. Old handler only caught undefined_object; Postgres raises undefined_table when source object never existed, and duplicate_object when destination already exists. The expanded handlers make these migrations truly idempotent across all plausible starting states. - Makefile — verify-mig-app, test-frontend, refresh-snapshot targets. refresh-snapshot pg_dumps youpc-supabase prod (needs PALIAD_PROD_DATABASE_URL), strips pg16 \restrict commands for pg15 restore compat, and filters applied_migrations rows to this branch's max on-disk version. - internal/db/testdata/README.md — explains the snapshot's purpose, refresh procedure, and how to verify locally. - docs/cicd-runner-setup-2026-05-25.md — one-time admin steps for registering a Gitea Actions runner on mriver and wiring DOKPLOY_TOKEN as a repo secret. Documents soft-launch plan per m's Q11.4 (keep Dokploy's autoDeploy=true webhook alive for one week, disable after the workflow has gated 5 successful deploys). Build clean. Full go test ./internal/... ./cmd/... green without TEST_DATABASE_URL. With TEST_DATABASE_URL + TEST_APP_DATABASE_URL set to a supabase/postgres scratch + snapshot restored: TestMigrations_NoDuplicateSlot, TestMigrations_EndToEndAsAppRole, TestBootSmoke, TestHealthReady_Live all pass. Live-DB service tests in internal/services/* fail under supabase/postgres 15.8 with a 42P08 parameter-binding error (unrelated to Slice A — tracked as a follow-up). |
|||
| 0b1653c2bf | Merge: t-paliad-284 — Wave 1 Tier 1 rule additions + Q6 archived cleanup + audit FK fix (mig 132) (m/paliad#116) | |||
| a6cf6ff4c9 |
feat: t-paliad-284 Wave 1 Tier 1 deadline-rule additions (mig 132)
Add 12 Tier 1 procedural deadline rules from curie's audit §10 (docs/research-deadlines-completeness-2026-05-25.md), backfill the UPC R.104/R.105 Interim Conference citation on upc.inf.cfi.interim (m/paliad#116 / m's 2026-05-25 report), and fold in the audit Q6 cleanup of the 40 _archived_litigation.* rows. New rules: T1.1 upc.inf.cfi.cmo_review 15d / R.333.2 T1.2 upc.inf.cfi.confidentiality_response 14d / R.262.2 (trigger 25) T1.3 upc.apl.order.grounds_orders 15d / R.224.2(b) T1.4 upc.apl.order.response_orders 15d / R.235.2 T1.5 upc.inf.cfi.cons_orders 2mo / R.118.4 T1.6 upc.inf.cfi.rectification 1mo / R.353 T1.7 upc.pi.cfi.deficiency 14d / R.207.6(a) T1.8 upc.pi.cfi.merits_start 31d OR 20wd (max) / R.213 + R.198.1 T1.9 upc.inf.cfi.translation_request 1mo BEFORE oral / R.109.1 T1.10 upc.inf.cfi.interpreter_cost 2wk BEFORE oral / R.109.4 T1.11 upc.inf.cfi.translations_lodge 2wk / R.109.5 (trigger 113) T1.12 upc.pi.cfi.response UPDATE: re-anchor on .app, court-set T1.8 uses Wave 2 Slice A primitives (mig 128: working_days unit + combine_op='max'). T1.9/T1.10 use timing='before' with the backward-snap path in deadline_calculator.go. Also drops the deadline_rule_audit.rule_id FK constraint. The mig 079 audit trigger had a latent bug — it could not log DELETEs because the FK rejected the post-delete INSERT (count(*) WHERE action='delete' was 0 across the entire history). Audit tables are append-only history and should not FK-constrain on live entity tables; before_json preserves the full row state. Unblocking this also unblocks the §13b Q6 cleanup. Verified on Supabase: 13 rows present in post-fix shape, all assertions in the DO-block pass, audit log now records 11 creates + 2 updates + 40 deletes for this migration. |
|||
| cb44b3b8cc |
mAi: #117 + #118 - t-paliad-285/-286 UPC dmgs+pi court followup (mig 133)
Adds the post-submission court phase to upc.dmgs.cfi and the appeal route to upc.pi.cfi. The Verfahrensablauf timeline currently stops at the last party submission (dmgs.rejoin / pi.order); without these rows the interim conference / oral hearing / decision / appeal sub-tree never renders, even though atlas's #96 spawn mechanism is in place. Migration 133 (single slot, coordinated with knuth's #116 on 132): Section A — UPC Damages tree end (#117): - upc.dmgs.cfi.interim court-set, R.105 - upc.dmgs.cfi.oral court-set, R.118 / R.250 - upc.dmgs.cfi.decision court-set, R.118 / R.144 - upc.dmgs.cfi.appeal_spawn 2mo, R.220.1(a) / R.224.1(a), spawn → upc.apl.merits Section B — UPC PI appeal route (#118): - upc.pi.cfi.appeal_spawn 2mo, R.220.1(a) / R.224.1(a), spawn → upc.apl.merits PI orders under R.211 dispose of the urgent question and ride the main 2-month track; the 15-day R.220.1(c) order track does not apply. Same shape as mig 095 inf.appeal_spawn and the upc.inf.cfi interim/oral/decision rows from mig 012. Court-set rows reuse the shared interim-conference / oral-hearing / decision concepts. Citations: docs/research-deadlines-completeness-2026-05-25.md §D + Tier 4 (R.144), docs/audit-upc-rop-deadlines-2026-05-08.md §D R.144 + §F R.220.1(a)/R.224.1(a). Per-row RoP citation in the migration header. Idempotent INSERT NOT EXISTS guards per row + post-insert DO block that RAISEs EXCEPTION if any expected row is missing or the spawn shape (is_spawn / spawn_proceeding_type_id / parent_id) is wrong. go build ./... clean, go test ./internal/... clean, bun run build clean. |
|||
| e4c694e01c |
mAi: #108 - t-paliad-276 submission generator language selector (DE/EN)
Per-draft `language` column drives the .docx output language for the
submission generator. The lawyer picks DE or EN on the draft editor's
sidebar; the generator selects the language-matched template variant
(falling back through {code}.{lang} → {code} → _skeleton.{lang} →
_skeleton → letterhead) and resolves language-aware variables
({{procedural_event.name}} → name_de vs name_en).
Schema (mig 130 — bumped from 129 to deconflict with atlas's #96):
- paliad.submission_drafts.language text NOT NULL DEFAULT 'de'
CHECK IN ('de','en'). Existing rows inherit 'de' via the default,
preserving every legacy draft's behaviour byte-for-byte.
Backend (Go):
- SubmissionVarsContext.Lang overrides the user's UI lang. Build()
uses it when set; falls back to user.Lang otherwise — Slice 1's
format-only /generate path keeps working unchanged.
- SubmissionDraftService.BuildRenderBag now threads draft.Language
through. Create/EnsureLatest seed from the UI lang (DE default).
- DraftPatch.Language landed; Update validates and rejects values
outside {de,en}. Project-scoped + global PATCH endpoints both
surface the field.
- resolveSubmissionTemplate(ctx, code, lang) replaces the lang-less
predecessor. Returns the matched tier (per_code_lang / per_code /
skeleton_lang / skeleton / letterhead) so the editor knows whether
to surface the "Fallback: universelles Skelett" notice.
- fileRegistry registers the EN skeleton sibling (`_skeleton.en.docx`)
alongside the DE one; per-code EN variants land in a parallel
submissionTemplateENRegistry (empty for now — EN templates land per
HLC authoring). 404s from Gitea fall through silently.
- /api/projects/{id}/submissions/{code}/generate accepts
`?language=de|en` query override (one-shot path, no draft row to
pull the column from); defaults to the user's UI lang.
Frontend (TS/JSX):
- DE/EN radio above the variables list in the draft editor sidebar.
Switching the radio PATCHes `language` and the server returns the
freshly-resolved bag + preview HTML so the lawyer sees EN values
immediately.
- Fallback notice ("Fallback: universelles Skelett (keine
sprachspezifische Vorlage)") shows when the resolved tier doesn't
match the requested language.
- 4 new i18n keys (DE + EN) + CSS for the toggle.
Tests:
- normalizeDraftLanguage covers DE/EN/case/whitespace/unknown.
- addRuleVars language-pick test pins procedural_event.name and the
rule.name alias to the language-matched value.
- languageFallback truth table covers all 10 (lang × tier) combos.
Build hygiene: go build/vet/test clean; bun run build clean.
|
|||
| c6267e4e6d | Merge: t-paliad-277 — submission party selector + import-from-project (mig 131) (m/paliad#109) | |||
| 4fc3005db8 |
mAi: #109 - t-paliad-277 submission generator party selector + import-from-project
Multi-select party picker on the dedicated submission draft editor —
lawyer picks which of the project's parties to mention in this
specific submission. Adds the t-paliad-277 variable-bag multi-party
shape ({{parties.claimants}}, {{parties.claimant.0.name}}) while
keeping the legacy flat aliases ({{parties.claimant.name}}) for every
existing .docx template authored before the rename.
Surfaces an explicit "Aus Projekt importieren" button + last-imported
timestamp at the top of the variable sidebar so the lawyer can re-pull
project-derived variables (project.*, parties.*, deadline.*,
procedural_event.*, rule.*) when the project data drifts away from the
saved draft overrides. firm.*, today.*, user.* overrides survive the
import — those values aren't sourced from the project record.
Schema: mig 131 adds two columns to paliad.submission_drafts:
- selected_parties uuid[] DEFAULT '{}'::uuid[]
Empty = include every party (legacy default).
Non-empty = restrict to the subset, grouped by role at substitution.
- last_imported_at timestamptz NULL
Bumped each "Aus Projekt importieren" click; surfaced in UI.
Backend:
- SubmissionVarsContext gains SelectedParties — filterPartiesBySelection
restricts the resolved bag before role bucketing.
- addPartyVars emits THREE coexisting forms per role: comma-joined
(parties.claimants), indexed (parties.claimant.0.name), and flat
legacy (parties.claimant.name → first selected claimant). Flat
aliases are kept forever per the issue's backward-compat contract.
- SubmissionDraftService.ImportFromProject strips overrides for
project-derived prefixes and bumps last_imported_at; rejects
project-less drafts (nothing to import from).
- New endpoint POST /api/submission-drafts/{id}/import-from-project.
- DraftPatch + PATCH handlers accept selected_parties.
- submissionDraftView now ships available_parties so the editor can
render the picker without an extra round-trip.
Frontend:
- submission-draft.tsx: new import-row + parties block in the sidebar.
- client/submission-draft.ts: paintImportRow / paintPartyPicker /
onPartySelectionChange / onImportFromProject; group parties by
role bucket (claimant / defendant / other) with DE+EN role-string
matching to mirror the backend bucketing.
- 3 new i18n keys (DE+EN): import.button, parties.title, parties.hint.
- CSS for the picker + import row in global.css.
Tests: 6 new unit tests in submission_vars_parties_test.go covering
the multi-party bag emission, German role-string bucketing, flat-alias
first-of-role resolution, empty-selection-means-all default, non-empty
restriction, and the isProjectDerivedKey policy that powers the
import path.
Build hygiene: go build/vet clean; go test -short ./internal/... pass;
bun run build clean (2876 i18n keys, scan clean).
|
|||
| 1292aa575d | Merge: t-paliad-265 — per-event-card choices Slice A+B (popover + CCR + projection engine, mig 129) (m/paliad#96) | |||
| bf60fc1400 |
feat(t-paliad-265): projection engine + HTTP handlers for per-card choices
m/paliad#96 — slice A engine + slice B engine wired together (per m's Q4 bundling decision in §11 of the design doc). Engine (internal/services/fristenrechner.go): - CalcOptions gains PerCardAppellant map, SkipRules set, IncludeCCRFor set. All three keyed by paliad.deadline_rules.submission_code (same key AnchorOverrides uses). - UIDeadline gains AppellantContext (per-decision pick that propagates to descendants via parent_id chain) + ChoicesOffered (passes the jsonb through to the frontend so the caret renders). - Calculate honours all three: * IncludeCCRFor non-empty → append with_ccr to flag set before gate evaluation (v1 simplification documented in CalcOptions comment; correct for single-CCR-entry-point proceedings). * SkipRules suppression via submission_code match AND parent_id cascade (descendants suppress too — one-pass walk in sequence_order). * AppellantContext: each rule with its own per-card pick stamps its UUID; descendants inherit via parent_id lookup; "" = no override. HTTP: - /api/projects/{id}/event-choices GET / PUT / DELETE — full CRUD with visibility gate, audit-logged via paliad.system_audit_log. - POST /api/tools/fristenrechner accepts either projectId (server pulls choices from project_event_choices) OR inline perCardChoices (unbound /tools/verfahrensablauf surface). Inline wins when both. Services wiring: - EventChoiceService instantiated in cmd/server/main.go; threaded into handlers.dbServices.eventChoice. |
|||
| dc47ea7f43 |
feat(t-paliad-265): migration 129 + EventChoiceService (Slice A foundation)
m/paliad#96 — per-event-card optional choices on the Verfahrensablauf timeline. This commit lands the schema + service layer. Migration 129: - paliad.project_event_choices table (project_id, submission_code, choice_kind ∈ {appellant, include_ccr, skip}, choice_value) with UNIQUE(project_id, submission_code, choice_kind) for idempotent re-pick, RLS via paliad.can_see_project. - paliad.deadline_rules.choices_offered jsonb — opt-in declaration of which choice-kinds each rule offers. Seeded for every decision rule (appellant), every priority='optional' rule (skip), and the two Klageerwiderung rules (upc.inf.cfi.sod + de.inf.lg.erwidg) with include_ccr. Live verification before authoring: - rule_code is NULL on every decision row → submission_code is the join key (matches AnchorOverrides plumbing in fristenrechner.go). - upc.inf.cfi.sod is the UPC Klageerwiderung, not upc.inf.cfi.def (rejected the design doc's first guess; SELECT name ILIKE 'Klageerwiderung' confirmed). Go service: - models.ProjectEventChoice + DeadlineRule.ChoicesOffered. - EventChoiceService: ListForProject / Upsert (with audit-log row to paliad.system_audit_log) / Delete. Pure-helper ToCalcOptionsAddendum + per-kind value validation + unit tests. Design: docs/design-event-card-choices-2026-05-25.md §3 + §6. |
|||
| 930771a898 | Merge: t-paliad-275 — HL-formatted skeleton template with placeholders (m/paliad#107) | |||
| f2fbf93adf |
feat(submissions): HL-formatted skeleton template with placeholders (t-paliad-275)
Adds a firm-formatted Schriftsatz skeleton between the per-submission_code
template and the generic universal skeleton in the fallback chain. Carries
every HL paragraph + character style from the HL Patents Style .dotm
(HLpat-Heading-H1..H5, HLpat-Body-B0, HLpat-Header-Section,
HLpat-Table-Recitals-Party/Details/Roles/Sequencers, HLpat-Signature,
HLpat-Requests-Intro/Level1, HLpat-EvidenceOffering, …) and the firm
letterhead (header logo + firm-address footer), plus the full 48-key
SubmissionVarsService placeholder bag exercised in a real Schriftsatz
layout (rubrum → Betreff → Anträge → Sachverhalt → Rechtsausführungen →
Beweis → Schlussformel) with a locale-aware verification footer covering
every DE/EN alias and the rule.* legacy keys.
Resolved fallback chain after this CL:
1. per-firm per-submission_code template (submissionTemplateRegistry)
2. _firm-skeleton.docx — HL styles + placeholders (NEW)
3. universal _skeleton.docx — placeholders only
4. HL Patents Style.dotm — letterhead only
scripts/gen-hl-skeleton-template/main.go reads the source .dotm,
strips VBA macros + ribbon customizations + glossary parts, patches
[Content_Types].xml and the document rels, and replaces document.xml
with HL-styled paragraphs containing the placeholders. Keeps styles.xml,
theme/, header[12].xml, footer[12].xml, numbering.xml, settings.xml,
fontTable.xml, and media untouched so the firm typography survives.
Template uploaded to HL/mWorkRepo at
6 - material/Templates/Word/Paliad/HLC/_firm-skeleton.docx
(commit 0a41b45, blob SHA 07f7547d).
Verified end-to-end against the in-house renderer with a 48-key sample
project: every placeholder substitutes cleanly, no orphan {{ markers,
no VBA / glossary / customUI leftovers, header/footer rIds resolve.
|
|||
| d4df81e374 |
mAi: #106 - t-paliad-274 — bidirectional draft editor link + click-field-highlights
Extension of #92 (m/paliad/issues/106). Two related polish fixes for the submission draft editor's preview ↔ sidebar wiring. Concern A — link persists after fill (regression coverage + UX visibility) Audited the Go renderer: substituteInTextNodes / substituteAcrossRuns already pass both filled and missing values through htmlPreviewWrapper, so the <span class="draft-var" data-var="…"> wrapping is present for every substituted placeholder regardless of source (resolved bag, lawyer override, missing marker). What looked broken to m was a visibility problem: the always-on rgba(198, 244, 28, 0.12) tint is imperceptible against the serif preview prose, so a filled value reads as plain text and the user concludes "the link is gone". Added TestRenderHTML_WrapsOverriddenValueSameAsResolved that pins the invariant explicitly — an override (project.case_number = "UPC_CFI_ 42/2026") and a resolved value (firm.name = "HLC") both end up in matching draft-var spans. Locks future refactors out of dropping the wrap on either path. CSS rewrite per m's "prose stays clean when not interacting" guidance (issue body): drop the always-on background; on hover of a --has-input span, layer a dotted-underline + brighter lime tint so the click affordance reveals itself. Missing markers carry their own [KEIN WERT: …] / [NO VALUE: …] gap-text and don't need extra visual. Concern B — sidebar-field-focus → preview-occurrence highlight (new) Reverse direction of the click-to-jump from #92. focusin on any .submission-draft-var-input applies .draft-var--active to every matching span in the preview; focusout (or focus shift via Tab) clears them. Sticky-while-focused, not a one-shot flash — the lawyer can scan "where does this variable land in my prose?" while the field stays focused. New CSS class .draft-var--active uses a brighter lime + box-shadow ring so all occurrences pop at once. Handlers are wired in paintVariables and re-applied at the end of both paintVariables AND paintPreview because: - paintVariables runs after autosave and re-creates inputs via innerHTML, so the focusin listener attached to the old input is gone; restoreVarFocus puts focus back programmatically without firing focusin again. We re-apply explicitly to bridge. - paintPreview blows away the preview HTML on every autosave, so any prior --active class is gone too. Re-apply based on the currently-focused sidebar input. Files internal/services/submission_merge_test.go — new regression test frontend/src/client/submission-draft.ts — focus handlers + re-apply frontend/src/styles/global.css — draft-var rewrite, --active Hard rules - .docx export path unchanged (Render passes nil wrap, covered by existing TestRender_DocxOutputUnchangedByPreviewWrap). - Both directions survive autosave-driven preview re-renders (see paintPreview re-apply + paintVariables re-apply). - go build ./... && go test ./internal/... && bun run build all clean. |
|||
| f4dee97493 | hotfix: drop is_optional + condition_flag refs from mig 125 (both dropped in earlier mig; unblock prod) | |||
| 7aed8e4ec5 | Merge: t-paliad-271 — Tier 3 deadline-rule primitives Slice A (working_days + combine_op + before-mode, mig 128) (m/paliad#103) |