a4b865d6bdc06325bff9535a511e14e59f222c39
490 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| a4b865d6bd |
fix(builder): initialise scenario sub-arrays + client null-guard (t-paliad-344)
GetScenarioDeep returned nil slices for proceedings/events/shares when a scenario had zero rows, which Go's encoding/json serialises as `null` rather than `[]`. The builder's renderCanvas then unconditionally calls `state.active.proceedings.filter(...)` on a null and dies with `procedures.js:101 TypeError: Cannot read properties of null (reading 'filter')` — every cold-open scenario crashed the page before the empty canvas could render. Backend (root cause): initialise Proceedings / Events / Shares to empty slices in BuilderScenarioDeep before SelectContext, so the wire shape is always arrays. Existing rows still load via SelectContext, which truncates the placeholder and refills from the DB. Frontend (defence in depth): on loadScenario(), normalise each of the three arrays to `[]` if the server response is not an array. Catches a future regression (or an older deployed build) without re-introducing the same crash class. bun build clean, go vet + go test ./... green. |
|||
| a905911cf4 |
fix(deadlines): restore /api/events deadline rail after mig 140 column drop (t-paliad-344)
Two SELECTs still referenced paliad.deadlines.rule_id after mig 140
(Slice B.4) dropped that column in favour of sequencing_rule_id:
- internal/services/deadline_service.go:268 — DeadlineService.
ListVisibleForUser. Powers /api/events?type=deadline (dashboard
deadline rail, /deadlines page, every status bucket). Threw
`pq: column f.rule_id does not exist` on every request → 500
for any authenticated user hitting the dashboard.
- internal/services/projection_service.go:1250 — collectActualsForOverrides.
Same column on `paliad.deadlines d`. Logged once per projection
pass (`ERROR service: projection: deadlines: ...`); aliased the
rename to `rule_id` so the receiving struct tag still scans.
Live container logs confirmed the failure mode — a 60-row burst of
`pq: column f.rule_id does not exist at position 3:36 (42703)` starting
the minute the post-B0 container came up (mig 140 had applied to the
DB but the SELECT still used the dropped name). EXPLAIN against the
live schema after the edit plans cleanly; the LEFT JOIN to
paliad.deadline_rules_unified on sequencing_rule_id was already correct
(only the SELECT projection was stale).
Root cause: mig 140 commit (
|
|||
| 46dc4ec94b |
feat(builder): B2 — multi-triplet stack + spawn nesting + per-event state (m/paliad#153)
Builds on B1 (commit
|
|||
| b78a984a7c |
fix(submission-md): preserve {{...}} placeholders verbatim through inline scanner
The Markdown inline scanner (parseInlineSpans) treats _ and * as
italic delimiters. A placeholder like {{project.case_number}} fed
through the scanner had its underscores consumed as italic markers,
leaving {{project.casenumber}} in the composed OOXML. The v1
placeholder pass then looked up the wrong key, surfacing
[KEIN WERT: project.casenumber] in the preview. The form ↔ preview
highlighting also stopped working because data-var attributes
mismatched between the input (snake_case) and the rendered span
(stripped).
parseInlineSpans now detects {{ at the cursor and skips ahead to
the matching }}, copying the entire placeholder verbatim into the
current text run. Unmatched {{ falls through to the existing
character handling so legal prose with stray braces still renders.
Tests: regression test for underscored keys (single + multiple +
mixed-with-italics), direct guard on parseInlineSpans, and an
italic-around-placeholder structural test.
|
|||
| 0f3c30a647 |
feat(scenario-builder): B0 schema foundation + minimal API (m/paliad#153)
t-paliad-340 — B0 of edison's 7-slice train (PRD §7.1). DB-only: schema + RLS land, dev-only test route exercises the surface, no user-facing change. B1 wires the actual builder UI on top. Migration 157 (additive on the legacy mig-145 scenarios table — 0 rows in prod, safe to relax): - paliad.scenarios gets owner_id / status / origin_project_id / promoted_project_id / stichtag / notes. spec drops NOT NULL and the scenarios_unique_per_scope constraint drops (the builder allows multiple scratch + Unbenanntes Szenario rows per user). - New tables: scenario_proceedings, scenario_events, scenario_shares. - paliad.projects.origin_scenario_id for the promote-to-project audit trail (the FK lands now; the wizard ships in B5). - paliad.can_see_scenario(uuid) STABLE SECURITY DEFINER helper covering owner / share / global_admin / two legacy paths. - Replacement RLS on scenarios + RLS on the three new tables; legacy service + handlers stay live and unchanged. PRD §5.1 deviations called out in the migration header: - proceeding_type_id is integer (live schema), not uuid (PRD draft). - FK target is paliad.users, matching the rest of paliad's schema. Go surface: - ScenarioBuilderService — list/create/get-deep/patch scenarios, add/patch/delete proceedings, add/patch/delete events, add/delete shares. Writes wrap in transactions with set_config( paliad.audit_reason, ..., true) per event_choice_service.go pattern. - /api/builder/scenarios/* — handlers register under a builder/ prefix so the legacy /api/scenarios surface still works. - /dev/scenario-builder — single-page HTML form gated to PaliadinOwnerEmail, exercises the B0 surface without Postman. - Live-DB integration test (TEST_DATABASE_URL gated) covers create + list + deep-get + share + visibility negatives + patch. Audit-first: every DDL block ran clean via BEGIN/ROLLBACK against the live DB before commit; end-to-end sanity (insert chain + CHECK constraints + CASCADE-on-delete) verified via the Supabase MCP. bun build clean. go vet + go test -short ./... green. |
|||
| 39c8ef343b |
feat(procedures): U4 hard-cut legacy URLs + retire dual surfaces (m/paliad#151)
Per m's Q11 divergence in the design (no 2-week dual-ship), this slice flips /tools/fristenrechner and /tools/verfahrensablauf to permanent 301 redirects to /tools/procedures and deletes the legacy frontend pages. Bookmarks resolve via Location preservation of query params; no ?legacy=1 escape, no in-product affordance pointed back at the retired URLs after the merge. Server: - handleFristenrechnerPage + handleVerfahrensablaufPage now 301 to /tools/procedures, carrying any query string through unchanged. - pillDrillURL in deadline_search_service.go retargets to /tools/procedures so freshly indexed search pills land on the new page directly (cached snapshots still work via the 301). Frontend: - Deleted src/fristenrechner.tsx, src/verfahrensablauf.tsx, src/client/fristenrechner.ts. - src/client/verfahrensablauf.ts loses its DOMContentLoaded auto-boot and the now-unused initI18n / initSidebar imports; procedures.ts is the sole caller of initVerfahrensablauf(). - frontend/build.ts drops the legacy entrypoints and renderXxx HTML outputs. - Sidebar.tsx, Header.tsx, index.tsx, paliadin-context.ts repointed to /tools/procedures. - Unused nav.fristenrechner / nav.verfahrensablauf / tools.verfahrensablauf.* i18n keys removed. Tests: - verfahrensablauf_test.go rewritten to assert both legacy URLs return 301 with the correct Location (query string preserved). |
|||
| 60907e7153 |
feat(procedures): U0 skeleton — /tools/procedures page shell (m/paliad#151)
First slice of the unified procedural-events tool train. Ships only the page chrome — route, sidebar/header, filter strip with search box, four entry-mode tabs (Verfahren wählen / Direkt suchen / Geführt / Aus Akte), and the host containers later slices mount their UI into. No data wiring. Per m's decisions (design §11.5): URL is English (/tools/procedures, not /tools/verfahren); all four tabs visible from boot (not a single-default landing); search box lives in the top filter strip and will compose with chip filters once U1+ wire them. U1 fills #procedures-panel-search (Mode A), U2 fills -wizard (Mode B), U3 fills -proceeding + #procedures-output-tree (Verfahrensablauf), U4 hard-cuts /tools/fristenrechner and /tools/verfahrensablauf to 301 redirects and drops the legacy pages. |
|||
| d6a5dedb2b |
feat(deadline-system): P4 (partial) — partial trigger_events deprecation (m/paliad#149)
Phase 2 P4 partial-scope (head approved 2026-05-27 15:24). The full
drop of paliad.trigger_events + the legacy route + 5 read sites is
gated on an editorial backfill that's not in coder scope — 73 active
sequencing_rules carry proceeding_type_id IS NULL and are addressed
ONLY via trigger_event_id today. Dropping anything would break those
73 orphans.
What this lands:
1. Mig 156 — NULL out trigger_event_id on the 2 hybrid rules that
carry BOTH parent_id AND trigger_event_id. Per design §2.1 /
m's Q1, parent_id is the canonical predecessor link; the
hybrid trigger_event_id was redundant. The 2 rules' parent_id
chains keep the live edge. Live-DB verified post-apply: 0
active hybrid rules remain.
2. Deprecation + Link headers on POST /api/tools/event-deadlines
per RFC 8594 / RFC 9745. The route stays functional so the 73
orphans keep working until reparenting lands.
What this does NOT land (gated on editorial):
- DROP TABLE paliad.trigger_events
- DROP COLUMN paliad.sequencing_rules.trigger_event_id
- Remove the legacy /api/tools/event-deadlines handler
- Remove EventDeadlineService + ExportService::1680 sheet
- Remove deadline_rule_service.go:226 label-fallback path
- Remove event_type_service.go:40+414 reads (33 event_types still
reference trigger_event_id)
- Update cmd/gen-upc-snapshot/main.go:185-202 to skip trigger_events
- Drop the sequencing_rules_trigger_event_id_fkey FK
All of the above lands in a follow-up mig once the orphan count
hits zero. Comment to follow on m/paliad#149 with the editorial-
backlog list.
Verified: live-DB pre/post hybrid count (0 active hybrids remain);
mig idempotent; go vet clean.
Design: docs/design-deadline-system-revision-2026-05-27.md §2.1
(parent_id canonical), §3.4 (legacy route fate), §4.3 (table fate),
§5 (slice train P5 row). t-paliad-331.
|
|||
| 9940dd8216 |
feat(deadline-system): P2 — condition_expr write-validator (m/paliad#149)
Phase 2 P2 (design §4.1). Locks the condition_expr grammar to:
CondExpr := { "flag": "<known_flag>" }
| { "op": "and"|"or", "args": [<CondExpr>, ...] }
Where <known_flag> must exist in paliad.scenario_flag_catalog (today:
with_ccr / with_amend / with_cci; editorial adds via the catalog
table as needed).
Wire-time validation in RuleEditorService.Create and UpdateDraft —
the rule editor surfaces a 400 with a friendly message before the row
hits the DB. Empty / JSON null inputs pass through (the "no gate"
shape; stored as NULL column).
The validator:
* walks the JSON tree once, collecting every leaf flag name
* rejects mutually-exclusive shapes (leaf + composite in one node)
* rejects empty args, bad op values, empty flag strings
* does ONE batch lookup of the collected leaf names against the
catalog (regardless of expression depth)
Tests:
* 9 shape-only unit tests covering every reject path (no DB needed)
* TestValidateConditionExpr_LiveCatalog covers 6 good shapes + 2
unknown-flag cases against the live catalog
* TestConditionExpr_AllLiveRowsValidate runs the validator over
every active+published condition_expr in paliad.sequencing_rules
to enforce the §4.1 invariant on every deploy (today's 18 rows
all conform — verified via Supabase MCP pre-flight)
Live-DB tests skip cleanly when TEST_DATABASE_URL is unset (same
posture as sibling live tests in this package).
Design: docs/design-deadline-system-revision-2026-05-27.md §4.1
(grammar formalisation). t-paliad-331.
|
|||
| 3a4e99cb92 |
feat(deadline-system): P1 — upc.apl re-split into merits/cost/order (m/paliad#149)
Phase 2 P1 / m's Q5 divergence (2026-05-27, verbatim):
"Reverse the unification as suggested in 3. They are different
proceedings, I only wanted the approach to be unified in the
'determinator' — but they are actually different proceedings!"
Mig 155 reverts the mig-096 unification:
Before: id=160 upc.apl.unified active (16 rules), id=11/19/20 inactive
After: id=11 upc.apl.merits (7 rules), id=19 upc.apl.cost (2 rules),
id=20 upc.apl.order (7 rules) all active; id=160 inactive
The 16 rules under id=160 split cleanly by event_code prefix; all 10
parent_id edges among them are bucket-local (pre-flight audit), so
the tree shape survives the rebind unchanged.
Spawn FK retarget: pi.cfi.appeal_spawn flips from 11 (merits) → 20
(orders track) per design §3.1 — PI appeals land on orders, not
merits. The inf/rev/dmgs spawns keep target=11 (merits), now active.
Determinator routing layer (proceeding_mapping.go) keeps its single
"Berufung" front door per m's intent — only the data shape changes.
Pre-flight verified: 0 projects bound to id=160, 0 scenarios reference
upc.apl. Zero data migration on the project side.
Tests: lookup_events_test.go assertions on the three appeal_target
buckets updated to the new codes (endentscheidung → upc.apl.merits,
schadensbemessung → upc.apl.merits, bucheinsicht → upc.apl.order).
Same rule set, post-split coordinates.
Snapshot regen (pkg/litigationplanner/embedded/upc/) deferred: the
current snapshot only contains inf+rev so the apl re-split doesn't
shift its contents; regenerating would surface unrelated active PTs
and pollute this slice. Tracked as a follow-up.
Verified: go vet clean, go test ./internal/services/... -run
LookupEvents|proceeding_codes clean.
Design: docs/design-deadline-system-revision-2026-05-27.md §3.1
(re-split mig), §1.3 (spawn graph post-Q5). t-paliad-331.
|
|||
| 3533d79a25 |
feat(deadline-system): S1+S1a — cross-party display + spawn-only picker (m/paliad#149)
Phase 2 S1 + S1a (pre-ratified from t-paliad-327, folded into the
Phase 2 train).
S1 — Cross-party display:
- FristenrechnerService.LookupFollowUps stops filtering by party
server-side; queryFollowUpRows drops the perspective WHERE clause
and returns every published+active child.
- Server now computes is_cross_party per row (true only when
perspective ∈ {claimant,defendant} AND primary_party is the
opposite side; NULL/both/court is never cross-party).
- FollowUpRule wire shape gains the boolean.
- Frontend renderRule adds a "Gegenseitig" badge + is-cross-party
row class (muted styling, disabled checkbox affordance).
- defaultChecked returns false for cross-party rows.
- countSelected + submitWriteBack skip cross-party rows
unconditionally — even if a user manually checks the box, they
describe opposing-side filings and don't belong in our Akte set
(design §2.4 write-back exclusion).
- i18n: deadlines.overhaul.crossparty.badge / .tooltip (DE+EN).
- CSS: .fristen-overhaul-rule-crossparty + .is-cross-party row
modifier.
S1a — Spawn-only picker filter:
- SearchEvents WHERE now adds `sr.is_spawn = false` so spawn rules
(e.g. appeal_spawn, the inf.cfi → upc.apl.merits hop) no longer
surface as picker hits. Spawn rules are consequences, not
triggers — a lawyer searching "Berufung" wants the appeal-tree
root, not the inf.cfi spawn link.
- Terminal leaves (Duplik etc.) stay pickable per design §2.2's
carve-out: their own anchor is non-spawn, so they surface and
render an honest empty follow-up list.
Honest UX: hiding cross-party follow-ups lied about what the
workflow does next (cf. RoP.029.d falling off when perspective=
claimant on def_to_ccr — the workflow continues, just on the
defendant's docket). The fix makes the data legible without
contaminating the write-back path.
Verified: go vet clean, bun build clean, bun test 256/256,
go test ./internal/services/... -run LookupFollowUps... clean.
Design: docs/design-deadline-system-revision-2026-05-27.md §2.4
(cross-party) + §2.2 (spawn-only picker). t-paliad-331.
|
|||
| d36cc9ee15 |
feat(deadline-system): P0 — per-project scenario_flags SSoT (m/paliad#149)
Phase 2 P0 of the deadline + procedural-events revision. Establishes
paliad.projects.scenario_flags (jsonb) + paliad.scenario_flag_catalog as
the single source of truth for per-project scenario state — replacing
the three fragmented stores athena flagged (project_event_choices,
scenarios.spec, DOM-only). All three were empty per the audit so no
data migration is needed.
The jsonb map carries two key shapes:
* named flags (whitelist via scenario_flag_catalog) — today
with_ccr / with_amend / with_cci
* per-rule selection deviations of shape "rule:<uuid>" — wired up
here for validation; the consumer UI lands in P3
Endpoints:
GET /api/projects/{id}/scenario-flags
PATCH /api/projects/{id}/scenario-flags
PATCH semantics: bool = write; null = delete (priority-driven default
returns); missing key = leave alone. The service validates every key
on write (catalog lookup + UUID rule-membership + mandatory-cannot-be-
deselected) before persisting, so a single bad key fails the whole
patch.
Frontend bind: new scenario-flags.ts client module + Mode B's flag
checkboxes (ccr-flag / inf-amend-flag / rev-amend-flag / rev-cci-flag)
now hydrate from / persist to the project's scenario_flags on every
toggle. Kontextfrei (no project) is unchanged. Cross-surface coherence
via a scenario-flag-changed CustomEvent (peer surfaces — Verfahrens-
ablauf strip, Mode B result-view — will subscribe in P3).
Mig 154 is audit-defensive (set_config of paliad.audit_reason); no
audit trigger fires on paliad.projects today but a future one will
inherit the reason. Seeds the three known flags. CHECK constraints
enforce the top-level shape (jsonb_typeof = 'object') and the
catalog key pattern (lowercase, not 'rule:%' prefix).
Verified against the live DB: 18 projects default to '{}', catalog
has 3 rows, applied_migrations advanced to 154.
Design: docs/design-deadline-system-revision-2026-05-27.md §2.3, §2.4a,
§4.1, §5 (P0 row). t-paliad-331.
|
|||
| ba3e0795f8 |
feat(fristenrechner): Slice S6 — drop cascade endpoint, neutralize legacy Pathway B (m/paliad#146)
Cleanup pass per design §7 / S6, executed as a measured first cut
that drops the cascade endpoint + neutralizes the legacy Pathway B
row-stack / cascade init without lifting the entire ~1500 LoC
subtree out of `fristenrechner.ts`. The dead helpers stay for one
follow-up that can lift them safely.
Backend:
* Deleted `internal/handlers/fristenrechner_event_categories.go`.
* Dropped the `GET /api/tools/fristenrechner/event-categories`
route from `handlers.go`. The `EventCategoryService` itself
stays — it still backs the legacy concept-card search's
`?event_category_slug=` filter, which dies in the same
follow-up that removes the concept-card response shape.
* `paliad.event_categories` TABLE is untouched per design §7
(kept for future tools).
Frontend:
* `loadEventCategoryTree()` reduced to a stub returning `[]` — the
endpoint it fetched no longer exists, and no overhaul surface
calls it.
* `initB1Cascade()`, `initForumFilter()`, `initInboxFilter()`
early-return. Their `DOMContentLoaded` registrations stay so
the bundle exports are stable, but no Pathway B cascade /
chip-strip / inbox-channel wiring fires in `?legacy=1` mode.
* The Pathway B markup in `fristenrechner.tsx` stays in place; it
renders inert when a user hits `?legacy=1&path=b`.
* `buildRowStack`, `renderRowStack`, `runB1Search`, and the row-
stack helper functions remain as unreachable code. Removing
them mechanically requires retiring the entire upper-half
Pathway B B2 search wiring (`runSearch` + `renderConceptCard`
+ `renderSearchResults` + `SearchResponse` types) which is
tangled with the legacy concept-card response shape — deferred
to a follow-up that lands together with the backend
concept-card removal.
Verified — bun build clean (2971 i18n keys unchanged), 256
frontend tests pass, go build + vet clean, live-DB tests
(TestListProceedings, TestSearchEvents, TestLookupFollowUps)
still green.
Follow-up scope tracked in design §7 S6 — pending the helper-tree
lift and the legacy concept-card response-shape removal from
/search.
|
|||
| 70985d88b0 |
feat(fristenrechner): Slice S4 — Mode B wizard (m/paliad#146)
Mode B "🧭 Geführt" — the guided 3-5 row wizard defined in
docs/design-fristenrechner-overhaul-2026-05-26.md §3.2. Lands the
user on a single procedural_event (the trigger), then transitions
to the shared §4 result view.
Frontend:
* `fristenrechner-wizard.ts` — row stack with R1..R5:
R1 Was ist passiert? (event_kind, always asked)
R2 Vor welchem Gericht? (jurisdiction, skip if R1 narrows)
R3 In welchem Verfahren? (proceeding_type, auto-skip when
narrowed pool has 1 option)
R4 Welches Schriftstück? (procedural_event, landing)
R5 Welche Seite vertreten Sie? (party, only when follow-ups
differ by primary_party)
Row badges per §11.Q3: R1+R2 = Filter, R3+R4+R5 = Qualifier.
R5 has NO "Beide" option per §11.Q8 — Mode B is the file-mode
where perspective is a qualifier.
* Project prefill — derives R3 + R2 jurisdiction from
project.proceeding_type, R5 from project.our_side. Annotates
pre-filled rows with "aus Akte" tag and implicit rows with
"implizit" tag per §11.Q10 ("erhalten" annotation when a pick is
carried across an upstream change).
* R4-to-result transition — after R4 the wizard fetches /follow-
ups (no dates) to inspect primary_party variance. If both
claimant and defendant rules exist AND R5 isn't already set,
swaps the loading row for the R5 chip picker. Otherwise jumps
straight to mountResultView.
* URL state — `?mode=wizard&kind=…&forum=…&pt=…&r4=…&party=…`
keeps deep-link / back-nav consistent (the launchResult step
sets `event=` so the result view picks up).
* `fristenrechner-result.ts` mountModeShell now dispatches the
"wizard" tab to the wizard module (was a coming-soon
placeholder).
* 18 i18n keys added (DE + EN parity), 145-line CSS block for the
wizard row stack with Filter / Qualifier badge styling and
"aus Akte" annotation chip.
Backend:
* `ProceedingListOptions.EventKind` adds an EXISTS subquery
filter on `paliad.sequencing_rules` ⨯ `paliad.procedural_events`
so Mode B R3 chips only show proceedings whose event roster
contains at least one event of the requested kind (design
§6.3). Endpoint param: `event_kind=` on
/api/tools/proceeding-types.
Test updates:
* `TestListProceedings` switched from SKIP-when-column-missing to
asserting the live filter — mig 153 has landed, `kind` column
is in place. New subtests: kind=proceeding includes
upc.inf.cfi and excludes the phase row upc.cfi.interim;
event_kind=filing narrows to proceedings with filing events.
* `fristenrechner-wizard.test.ts` covers
`followUpsDifferByParty` — the R5 trigger predicate. 7 cases:
asymmetric → true; uniform / both / court / empty → false.
Verified — bun build clean (2971 i18n keys), 256 frontend tests
pass (incl. 7 new), go build + vet clean, live-DB
TestListProceedings passes all 6 subtests against mig 153 data.
|
|||
| 06d6c7540e |
Merge: t-paliad-323 Slice S3 — Fristenrechner Mode A direct search (m/paliad#146)
knuth shipped S3 of the Fristenrechner overhaul (design §3.1, §7-S3):
- New frontend/src/client/fristenrechner-mode-a.ts (507 LoC) — 'Direkt suchen' UI per design §3.1: Filter strip (Forum · Verfahren · Was passierte · Partei) with section-split visual hierarchy per m §11.Q3, free-text search box, ranked result list of procedural_events with click-to-lock-as-trigger.
- Inbox channel as secondary 'Erweitert' chip per §3.3 with CMS→UPC / beA→DE forum nudge.
- Mode tabs pair (Direkt suchen / Geführt) under Step-0 per §11.Q2; wizard tab placeholder until S4.
- Backend ListProceedings(jurisdiction, kind) — kind='proceeding' filter targets mig 153's column (just merged in
|
|||
| 9d688459e3 |
feat(db): mig 153 — proceeding_types kind discriminator + ProjectService hardening
Adds a `kind` column to paliad.proceeding_types (proceeding / phase /
side_action / meta) so the Mode B R3 Fristenrechner wizard, the
projects.proceeding_type_id binding, and the pkg/litigationplanner
snapshot can filter to primary proceedings only.
Implements the ratified design from docs/design-proceeding-types-
taxonomy-2026-05-26.md (m greenlit 2026-05-27 09:57 after the 11-question
AskUserQuestion round-trip).
Mig 153 is purely additive — ADD COLUMN with a safe DEFAULT, UPDATEs
reclassify 23 non-primary rows (4 phase + 10 side_action + 9 meta), and
a BEFORE INSERT/UPDATE trigger on paliad.projects backstops the new
invariant. Pre-mig audit (Supabase MCP, 2026-05-27) confirmed zero
downstream pressure on the 23 reclassified rows.
- internal/db/migrations/153_proceeding_types_kind.up.sql + .down.sql
- snapshot to paliad.proceeding_types_pre_153 in the same TX
- set_config('paliad.audit_reason', …) defensively
- DO-block asserts 23 reclassified rows before the trigger ships
- Q9 carve-out: is_active=false on every phase/side_action/meta row
- new trigger paliad.projects_proceeding_type_kind_check on
paliad.projects.proceeding_type_id
- internal/services/project_service.go
- extend validateProceedingTypeCategory to also enforce
kind='proceeding' AND is_active=true; new typed error
ErrInvalidProceedingTypeKind
- single SELECT picks up category + kind + is_active
- internal/services/project_service_test.go
- TestProjectService_ProceedingTypeKindGuard covers service-layer
rejection, the active-but-non-proceeding edge, mig 153 trigger
backstop, and the kind='proceeding' happy path
- cmd/gen-upc-snapshot/main.go
- filter proceeding_types query to kind='proceeding' for forward-
compat (the embedded UPC snapshot JSON regen requires DATABASE_URL
access and will land in a follow-up; the current placeholder is
already empty of non-primary rows)
t-paliad-325 / m/paliad#147
|
|||
| 2a2c5b8033 |
feat(fristenrechner): Slice S3 — Mode A direct search (m/paliad#146)
Mode A "⚡ Direkt suchen" — the power-user entry path defined in docs/design-fristenrechner-overhaul-2026-05-26.md §3.1. Renders above the §4 result view; clicking a result row locks the trigger event and transitions to the shared result surface from S2. Frontend: * `fristenrechner-mode-a.ts` — filter strip (Forum / Verfahren / Was passierte / Partei) + free-text search input + result list. Section-split visual hierarchy per m §11.Q3: filter chips in a bordered "Filter (eingrenzen)" strip on top, result list below. Inbox channel chip lives behind an "Erweitert" details summary per §3.3; picking CMS / beA auto-nudges the Forum chip. Party chip retains a "Beide" option (Mode A is filter mode per §11.Q8; Mode B drops it in S4). * `fristenrechner-result.ts` — new `mountModeShell(activeTab)` renders the two mode tabs per §11.Q2 and lazy-imports Mode A. Mode B tab is a placeholder until S4 lands. * `fristenrechner.ts` boot — when `?overhaul=1` is set and `?event` is empty, mountModeShell takes over (default tab = search; `?mode= wizard` opens the wizard tab when S4 ships). With `?event=` the flow still jumps straight to the result view. URL state syncs forum / pt / kind / party / q on every chip click. * 28 i18n keys added (DE + EN parity), 310-line CSS block for the mode tabs + Mode A surface. Backend: * New `ProceedingListOptions { Jurisdiction, Kind }` + service method `ListProceedings(ctx, opts)`. Legacy `ListFristenrechnerTypes` keeps the no-filter signature for existing callers. Handler `/api/tools/proceeding-types` accepts `?jurisdiction=` and `?kind=` query params. * `kind=proceeding` filter targets the taxonomy column landed in mig 153 (parallel branch t-paliad-325, m/paliad#147). Sequenced per the taxonomy doc §7 option (c): mig 153 merges before S3 ships to main, so the filter is never false-positive (no phase / side_action / meta rows leak into the chip strip). Verified — bun build clean (2955 i18n keys, data-i18n attributes clean), 249 frontend tests pass, go build + vet clean. New TestListProceedings — 4 PASS (no-filter, jurisdiction=UPC, jurisdiction=DE, ListFristenrechnerTypes alias) + 1 SKIP for the kind=proceeding case that probes the column and skips when mig 153 hasn't landed yet. S1 + S2 live tests still green. |
|||
| 7ea415145f |
feat(fristenrechner): Slice S1 — backend ?kind=events + /follow-ups (m/paliad#146)
Two additive endpoints behind the Fristenrechner overhaul (design §6.1 + §6.2 in docs/design-fristenrechner-overhaul-2026-05-26.md): 1. GET /api/tools/fristenrechner/search?kind=events — returns procedural_events rows directly (not aggregated concept-cards), one hit per (event × proceeding_type) tuple. Trigram-ranked against name / name_en / code. Filters: jurisdiction, proc, event_kind, party. Powers Mode A's result list and Mode B's R4 landing chips. Default search shape unchanged. 2. GET /api/tools/fristenrechner/follow-ups?event=...&trigger_date=... — given a trigger event (by code or uuid) + date, returns the immediate follow-up sequencing rules with computed due dates via litigationplanner.CalculateRule. Each row carries priority / primary_party / is_court_set / is_spawn / has_condition / legal source / spawn target so the result view can group into Mandatory / Recommended / Optional / Conditional with the SPAWNED badge. party=claimant|defendant filters keep "both" rules visible. No schema changes — unified sequencing_rules already has every column needed. Live-DB tests cover the SoC follow-up shape, party narrowing, jurisdiction + event_kind filters, and the unknown- event sentinel. |
|||
| 6acb1167dd |
feat(admin): add proceeding-type column to /admin/procedural-events list (t-paliad-321 / m/paliad#144)
Surfaces the 3-segment proceeding code (e.g. upc.inf.cfi) on the admin
rules list so the 4 legitimately-distinct same-named groups are
visually disambiguated without opening each row's edit page.
Specifically helps with:
- "Antrag auf Patentänderung" × 4 (distinct proceeding_type_ids)
- "Beginn des Hauptsacheverfahrens" × 2
- "Berufungsbegründung-R.220.1" × 2
- "Berufungsschrift-R.220.1" × 2
(The 6× "Mängelbeseitigung / Zahlung" identical clones are dedup'd by
mig 152 in the sibling commit; this column lets m verify the dedupe
landed and confirms the remaining same-named groups are intentional.)
* internal/services/rule_editor_service.go —
- LoadProceedingTypeCodes(ctx, rows) — batch SELECT id, code FROM
paliad.proceeding_types WHERE id = ANY(...) for every distinct
non-NULL proceeding_type_id in rows. Returns id → code map.
Single round-trip, firm-wide reference data (no RLS / visibility
gate). Used only by the LIST endpoint; GetByID etc. don't need it.
* internal/handlers/admin_rules.go —
- adminRuleResponse gains ProceedingTypeCode *string field
(json:"proceeding_type_code,omitempty"). Populated by
wrapRuleListResponse from the id → code map.
- handleAdminListRules calls LoadProceedingTypeCodes after fetching
rows, passes the map to wrapRuleListResponse.
* frontend/src/admin-rules-list.tsx —
- Adds Proceeding column header in position 2 (between Submission
Code and Legal Citation) per paliadin's "Place between submission-
code and the existing columns" spec. Binds to canonical i18n
key admin.procedural_events.col.proceeding (added below).
- Drops the legacy Verfahrenstyp column at position 4 — the new
code-only column at position 2 replaces it; the old column
showed `code · name` which duplicates the new content.
* frontend/src/client/admin-rules-list.ts —
- Rule type gains proceeding_type_code?: string | null.
- New proceedingCodeCell(r) helper: prefers server-side
proceeding_type_code, falls back to dropdown-lookup
proceedingLabel for defense-in-depth on older API responses
(the old behaviour broke for rules whose proceeding_type_id
pointed at non-fristenrechner category proceedings; the new
column never has that bug because the join is server-side).
- Row rendering: new <td class="admin-rules-col-proceeding"><code>
proceedingCodeCell(r) </code></td> in column 2.
* frontend/src/client/i18n.ts —
- admin.procedural_events.col.proceeding alias added for DE +
EN ("Verfahren" / "Proceeding"). Mirror style of the other
canonical aliases from Slice A.
* frontend/src/i18n-keys.ts —
- Generated key union extended with
"admin.procedural_events.col.proceeding".
Build + vet clean. No new SQL — proceeding_types is firm-wide
reference data and the join uses an existing primary key.
|
|||
| 4cd28bc896 |
feat(db): mig 152 — dedupe identical sequencing_rule clones (5 archived) (t-paliad-321 / m/paliad#144 follow-up)
Mig 151 (t-paliad-319) archived 5 of 6 duplicate procedural_events for
"Mängelbeseitigung / Zahlung" and reparented their sequencing_rules
onto the canonical PE. The 6 sequencing_rules themselves were left
active — and they are byte-for-byte clones (proceeding_type_id=NULL,
rule_code=NULL, duration 14d, primary_party=NULL, condition_expr=NULL,
…). The admin shows six indistinguishable rows for one legal concept.
This migration archives 5 of 6, keeping the row with the
lexicographically lowest UUID as canonical.
Pre-write verification (Supabase MCP, 2026-05-26):
- Exactly 1 clone-group surfaces under the full-signature query
(procedural_event_id, proceeding_type_id, rule_code, duration_*,
primary_party, condition_expr::text, trigger_event_id, alt_*,
anchor_alt, combine_op, parent_id, is_spawn, spawn_*):
6 "Mängelbeseitigung / Zahlung" rows.
- 0 paliad.deadlines reference any of the 5 to-be-archived rows
(verified via deadlines.sequencing_rule_id JOIN; rule_id column
was dropped in mig 140 / Slice B.4).
- Other name-duplicates (Antrag auf Patentänderung×4, Beginn des
Hauptsacheverfahrens×2, Berufungsbegründung-R.220.1×2,
Berufungsschrift-R.220.1×2) do NOT collapse under this signature —
their proceeding_type_id / rule_code / duration / primary_party
differ. Legitimately distinct rules per proceeding. This mig
leaves them alone.
Migration shape (mirrors mig 151):
1. Build dedupe mapping (duplicate_id → canonical_id) into a
ROW_NUMBER() OVER (PARTITION BY full-signature ORDER BY
created_at, id::text) TEMP table.
2. PRE NOTICE: surface every clone-group with its canonical + dups
so the deploy log shows what's about to be touched (m may want
to spot-check).
3. Snapshot the duplicates into paliad.sequencing_rules_pre_152
(precedent pre_091/093/095/098/140/151).
4. Reparent paliad.deadlines.sequencing_rule_id duplicate → canonical
BEFORE archiving (defensive no-op today).
5. set_config('paliad.audit_reason', …) — defensive; sequencing_rules
has no audit trigger yet (mig 151 §scope verified), but a future
trigger would inherit the reason automatically.
6. UPDATE sequencing_rules SET is_active=false,
lifecycle_state='archived' WHERE id IN dups.
7. POST assertions: expected archive count met, zero clone groups
remaining in active+published, zero live deadlines pointing at
an archived sequencing_rule. RAISE EXCEPTION on any mismatch.
Down: best-effort revert (flips archived → published from snapshot).
Doesn't undo the deadlines reparent (live data didn't need one;
snapshot doesn't carry pre-state of deadlines).
Build + vet clean. TestMigrations_NoDuplicateSlot passes.
|
|||
| 71e8023784 |
feat(db): mig 151 — dedupe null.* procedural_events (t-paliad-319 / m/paliad#144)
Consolidates 5 name-groups with synthetic null.<8hex> codes (minted by mig 136 from legacy submission_code IS NULL rows) onto a single canonical PE per name. 9 duplicate rows archived (is_active=false, lifecycle_state='archived'), 9 sequencing_rules reparented onto their canonical procedural_event. Worst offender: "Mängelbeseitigung / Zahlung" 6 → 1. Audit-first: per-row RAISE NOTICE before the writes, plus snapshots in paliad.procedural_events_pre_151 and paliad.sequencing_rules_pre_151 (same TX, mirrors precedent pre_091/093/095/098/140). Post-asserts that no name-group still has >1 active+published null.* row and no sr points at an archived PE. Pre-flight schema audit confirmed no audit trigger on procedural_events or sequencing_rules (only INSTEAD OF triggers on deadline_rules_unified, which don't fire on direct table writes), 0 deadlines + 0 draft_of refs to the duplicates, and lifecycle_state has no CHECK constraint blocking 'archived'. .down.sql best-effort restores sr.procedural_event_id and reactivates the archived rows from the snapshot tables. Mig already applied to youpc paliad schema via Supabase MCP within the same TX as the applied_migrations row insert (checksum matches the embedded file); deployed binary will see version 151 as applied. |
|||
| e0a82d9f9e |
fix(mig 140): post-check filters to active+published rows only
The previous post-check compared unfiltered counts (snapshot 493 vs sequencing_rules 231) and false-positived as "dual-write drift". Reality: B.2 dual-write was scoped to is_active=true + lifecycle_state='published' (the read-path universe). Archived + draft rows in deadline_rules were never replicated to sequencing_rules because nothing read them. Patch: filter both counts to active+published before comparison — the invariant B.2 actually maintained. Archived/draft rows survive in deadline_rules_pre_140 for forensic / future-backfill. Third hotfix on mig 140 today (1: missing matview drop; 2: wrong post-check comparand; 3: post-check missing lifecycle filter). The slice itself is sound — every failure was in the verification path, not the data. |
|||
| 026ad2d5ee |
fix(mig 140): POST integrity check compares snapshot to sequencing_rules, not view
The previous post-check compared paliad.deadline_rules_pre_140 row count to paliad.deadline_rules_unified row count and failed with "snapshot has 493 rows, view has 231 rows — drift". That's a false positive: the snapshot has every row (all lifecycle states + is_active), the view filters to is_active+published. They're not supposed to match. The right invariant: snapshot row count == sequencing_rules row count (B.2 dual-write keeps them 1:1 across all lifecycle states). Patched. View count stays in the RAISE NOTICE line as informational. Refs t-paliad-305 / m/paliad#93 Slice B.4 hotfix #2. |
|||
| bd7896ef68 |
feat(submissions): Composer Slice F — section reorder / hide / add custom (m/paliad#141)
The final Composer slice per design doc §12. Lawyer gains full
control over section composition: drag-and-drop reorder, per-section
delete, "+ Add section" picker for custom slugs that don't appear in
the base's default spec. Combined with Slice B's hide toggle, this
closes out the A→F sequence — Composer A→F is complete.
Backend (internal/services/submission_section_service.go, +120 LoC):
- SectionService.Create — adds a new section row to a draft. Validates
section_key + labels + kind (must be prose/requests/evidence).
Auto-assigns next order_index when OrderIndex=0; collisions on
(draft_id, section_key) surface as ErrInvalidInput.
- SectionService.Delete — removes one section by id. Returns
ErrSubmissionSectionNotFound when nothing was deleted.
- SectionService.Reorder — accepts a sequence of section_ids, rewrites
every row's order_index to (1..N)×10 transactionally. Returns the
refreshed list. Sections not present in the sequence are silently
ignored (defensive — partial reorder doesn't lose rows).
Handlers (internal/handlers/submission_sections.go, +180 LoC):
- POST /api/submission-drafts/{draft_id}/sections — owner-scoped via
SubmissionDraftService.Get. 400 on slug collision / invalid kind.
- DELETE /api/submission-drafts/{draft_id}/sections/{section_id} —
owner + section-belongs-to-draft cross-check. 204 on success.
- POST /api/submission-drafts/{draft_id}/sections/reorder — accepts
{"section_order": [uuid, uuid, ...]}; returns refreshed sections list.
Frontend (frontend/src/client/submission-draft.ts, +260 LoC):
- Each section row gains a drag handle (⋮⋮) on the left of the head.
Drag handle is the only draggable element; contentEditable
selections inside the editor body keep working. HTML5 native DnD,
no library.
- Drop-target highlighting via .submission-draft-section--drop-target
(border-top accent). Cleanup on dragend / drop / cancel.
- Per-section "Delete" button next to the existing Hide/Include
toggle. Confirm prompt prevents accidental loss of typed prose.
- "+ Add section" trailing affordance below the section list opens an
inline form (slug + DE label + EN label + kind dropdown). Submit
POSTs to the new endpoint; on success splices the row into
state.view.sections and re-paints.
CSS (frontend/src/styles/global.css, +65 LoC):
- .submission-draft-section-handle (grab cursor + hover background +
active=grabbing).
- .submission-draft-section--dragging / --drop-target visual states.
- .submission-draft-add-section form layout (dashed border + lime
primary submit).
Tests (internal/services/submission_section_slice_f_test.go, NEW,
TEST_DATABASE_URL-gated):
- Create custom section + slug-collision surface as ErrInvalidInput.
- Delete + repeat-delete returns ErrSubmissionSectionNotFound.
- Reorder reverses 10 seeded sections + verifies the resulting
order_index sequence is ascending and matches the input order.
Build hygiene: go build/vet/test -short clean (all packages);
bun run build clean (2906 i18n keys, data-i18n scan clean).
Hard rules honoured:
- NO new migrations (Slice F is pure code on Slice A's schema).
- NO behavior change for pre-Composer drafts (no section rows → no
drag handles to drag).
- {{rule.X}} aliases preserved (custom sections render through the
same composer pipeline as default sections).
- Q2/Q9/Q10 ratifications preserved.
This closes the Composer slice sequence A → F. The full feature set
ratified by m on 2026-05-26 is now in place:
A — base picker + read-only section list (mig 146/147/148)
B — editable prose + anchor-spliced render + MD→OOXML walker
C — building-blocks library + section picker (mig 149)
D — rich prose (headings, lists, blockquote, hyperlinks)
E — specialist bases lg-duesseldorf + upc-formal (mig 150)
F — section reorder / delete / add custom
t-paliad-318 Slice F
|
|||
| 94310ba498 |
feat(submissions): Composer Slice E — specialist bases + base-swap content survival (m/paliad#141)
Two new firm-agnostic base templates + the generic generator that
produced them + a regression test pinning Q10's base-swap-content-
survival contract.
Mig 150: seeds two `submission_bases` rows with firm=NULL.
- lg-duesseldorf — proceeding_family='de.inf.lg'. Conservative
German legal style: Times New Roman 11pt; plain black headings.
Stylemap targets LG-Body / LG-Heading1..3 / LG-ListBullet /
LG-ListNumber / LG-Quote.
- upc-formal — proceeding_family='upc.inf.cfi'. UPC court style:
Calibri 11pt body; UPC-blue (#1F3864) headings; Cambria italic
for blockquotes. Stylemap targets UPC-Body / UPC-Heading1..3 / …
Both rows ship the same 10-section spec.defaults shape as the Slice A
bases (letterhead → signature) with their own seed Markdown.
scripts/gen-submission-base/main.go (NEW, ~240 LoC):
- Generic generator with -preset flag. Two presets baked in
(lg-duesseldorf + upc-formal). Each preset hard-codes typography
(font, sizes, colour) so the lawyer can swap between bases and
see chrome change while section content carries through unchanged.
- Output is byte-reproducible (zip mtime pinned to 2026-05-26 UTC).
- Emits a minimal Composer-mode .docx: [Content_Types].xml,
_rels/.rels, word/_rels/document.xml.rels (empty envelope so the
composer's hyperlink-rels patch from Slice D has somewhere to land),
word/styles.xml (preset's full named-style block + "Hyperlink"
character style for Slice D link runs), word/document.xml (anchor-
only body in §6.1 default section order).
Gitea uploads (via mAi):
- 6 - material/Templates/Word/Paliad/Composer/lg-duesseldorf.docx
blob SHA: 82f57b3cb3b54c755fc5ab36862bfd61b8aaa73e
- 6 - material/Templates/Word/Paliad/Composer/upc-formal.docx
blob SHA: 41b9a388263ccc43ddc28b55caab301a4cf74fe8
These live under Composer/ (not under HLC/) so a future non-HLC
deployment serves the same cross-firm files.
Backend wiring:
- internal/handlers/files.go: two new fileRegistry entries
(composerBaseLGDuesseldorfSlug, composerBaseUPCFormalSlug) +
matching slugs in composerBaseSlugMap so fetchComposerBaseBytes
routes the new catalog rows to the new Gitea objects.
Tests:
- TestComposer_BaseSwapPreservesContent — composes the same draft
against an HLC-style stylemap AND an LG-style stylemap; asserts
(a) content survives both ways, (b) each output carries the
correct stylemap-entry stylenames, (c) neither output leaks the
other's stylenames. Pins Q10's base-swap-survives-content
contract.
Build hygiene: go build/vet/test -short clean (all packages);
bun run build clean.
NOT in scope (Slice E's brief was specialist bases + survival test):
- Generator coverage for HL Patents Style bases — gen-hl-skeleton-
template stays as the per-firm path (it needs the proprietary
.dotm source). gen-submission-base is for firm-agnostic bases.
- LG-Düsseldorf-court-style-guide deep fidelity — the LG preset is
a conservative starting point; admin refines via the admin editor
in a later slice if needed.
- numbering.xml carrying numId=1/2 — Slice D's MD walker emits
visible "• " / "N. " prefixes that don't need numbering.xml;
honours stylemap entry for indentation.
Hard rules honoured:
- Migration purely additive (`ON CONFLICT (slug) DO NOTHING`).
- NO behavior change for pre-Composer drafts.
- NO behavior change for existing hlc-letterhead + neutral seed
rows.
- {{rule.X}} aliases preserved (walker passes placeholders through;
v1 SubmissionRenderer pass substitutes).
- Q10 base-swap-content-survival pinned by new test.
t-paliad-317 Slice E
|
|||
| 5834e3dc66 | Merge: Composer Slice D — rich prose (headings, lists, blockquote, hyperlinks) in MD→OOXML walker (m/paliad#141) | |||
| 677849784c |
feat(submissions): Composer Slice D — rich prose (headings, lists, blockquote, hyperlinks) (m/paliad#141)
Extends the Composer's MD → OOXML walker per the design at
docs/design-submission-generator-v2-2026-05-26.md §12 Slice D from
Slice B's paragraphs + B/I baseline to the full rich-prose feature set:
headings 1-3, bullet + numbered lists, blockquote, inline hyperlinks.
MD walker (internal/services/submission_md.go, +320 / -75 LoC):
- RenderMarkdownToOOXMLWithStyles is the new Slice-D entry point;
RenderMarkdownToOOXML stays as a thin back-compat wrapper.
- splitMarkdownBlocks classifies every line into one of:
paragraph, heading_1/2/3, list_bullet, list_numbered, blockquote.
CommonMark-style 3-space indent tolerance; "N. " and "N) " for
numbered. Blank-line spacing semantics preserved from Slice B.
- renderBlockParagraph applies stylemap[blk.styleKey] (with
fall-back to stylemap["paragraph"]). List blocks emit visible
"• " / "N. " prefix runs so the structure surfaces even if Word
isn't configured with auto-list-numbering — lawyer can apply a
real Word list style post-export. Numbered-list ordinals reset
on every non-list block (so "1. A\nplain\n1. C" renders 1./1.,
not 1./2.).
- parseInlineRuns adds `[label](url)` recognition. Each link gets
routed through the optional HyperlinkAllocator; the walker emits
`<w:hyperlink r:id="{rId}">…runs…</w:hyperlink>` with the
"Hyperlink" character style on each child run. Nil allocator
falls back to plain-text label (URL drops, label survives).
Composer (internal/services/submission_compose.go, +130 / -10 LoC):
- composerLinkAllocator hands the walker fresh rIds (rIdComposer1,
rIdComposer2, …) outside the base's existing namespace; same URL
shared across multiple sections dedupes to one rId.
- patchDocumentXMLRels appends matching <Relationship Type="…/hyperlink"
Target="URL" TargetMode="External"/> entries to
word/_rels/document.xml.rels. Idempotent on rIds already present;
synthesizes a fresh rels part when missing (defensive for stripped
bases). Returns the patched parts slice (caller must overwrite
because append may grow the backing array — fixed in this slice).
- Compose now passes the full stylemap (paragraph + heading_1/2/3 +
list_bullet + list_numbered + blockquote) into the walker, not
just the paragraph-style entry.
Frontend (frontend/src/client/submission-draft.ts):
- Toolbar adds H1/H2/H3 buttons (formatBlock h1/h2/h3), bullet
list, numbered list, blockquote, and a link button that prompts
for a URL + wraps the selection via execCommand("createLink").
- domToMarkdown serializer extends to <h1>/<h2>/<h3>, <ul>/<ol>
with per-item ordinal counter for numbered lists, <blockquote>,
and <a href="…"> → `[label](url)`. Nested <li> handling sits in
the ul/ol branch.
Tests (internal/services/submission_md_test.go, internal/services/
submission_compose_test.go):
- TestRenderMarkdownToOOXML_Heading1 / _Heading2And3 — stylemap
applied.
- _BulletList / _NumberedList / _NumberedListResetsOnNonList —
prefixes + ordinal counter.
- _Blockquote — stylemap applied.
- _Hyperlink — allocator called, w:hyperlink rId wired, Hyperlink
character style on label runs.
- _HyperlinkNilAllocatorFallsBackToPlain — label survives, no
hyperlink tag emitted.
- TestDetectBlockMarker — 13 marker / non-marker cases.
- TestComposer_HeadingsAndLists — end-to-end through Compose with
a multi-construct draft; verifies stylemap presence + content +
ordinal prefixes.
- TestComposer_HyperlinkWiresRels — body has the right
<w:hyperlink r:id="rIdComposer{N}">, document.xml.rels has the
matching <Relationship> rows with External target mode.
- TestComposer_HyperlinkDedupesByURL — two `[label](url)` references
to the same URL share one rId; second allocation gets no new
Relationship row.
Build hygiene: go build/vet/test -short clean (all packages); bun run
build clean (2906 i18n keys).
NOT in scope (Slice D's brief was rich-prose + toolbar):
- Numbering.xml audit on bases — current approach emits visible
"• " / "N. " prefix runs without depending on numbering.xml. A
future slice can swap to `<w:numPr>` if firm-style auto-numbering
becomes a hard requirement.
- DOM-from-Markdown on initial editor paint — the editor still uses
textContent=md, so toolbar-applied formatting reverts to literal
Markdown text after autosave + repaint. Acceptable trade-off for
Slice D; a future polish could parse MD into the DOM on paint.
- Tables, images, footnotes (still design §13 out of scope).
Hard rules honoured:
- NO new migrations (Slice D is pure code).
- NO behavior change for pre-Composer drafts (gate on draft.BaseID
unchanged).
- {{rule.X}} aliases preserved (placeholders pass through the walker
verbatim, get substituted by the v1 SubmissionRenderer pass).
- Q2 ratification preserved (no building_block_id lineage).
- Q9 ratification preserved (4-tier BB visibility from Slice C).
t-paliad-316 Slice D
|
|||
| b27d402156 | Merge: Slice B.6 — /admin/rules → /admin/procedural-events URL rename + 301 redirects + .tsx i18n rebind. #93 slice train concludes (m/paliad#93) | |||
| 6b970da774 |
fix(mig 140): drop+recreate deadline_search matview (was blocking DROP TABLE deadline_rules)
prod-down: mig 140 fails with `cannot drop table deadline_rules because other objects depend on it (2BP01)`. The dependent object is the deadline_search materialized view (mig 077) — curie's brief listed FK re-pointing but missed the matview. Fix: drop the matview before DROP TABLE deadline_rules, recreate it at the end of mig 140 against deadline_rules_unified (same column shape). All 11 indexes restored. REFRESH at end so search keeps working. Single-TX atomicity preserved — if anything past step 6a fails, the whole drop-and-recreate rolls back. The pre_140 snapshot from step 1 remains as the forensic backstop. Refs t-paliad-305 / m/paliad#93 Slice B.4. |
|||
| 9359e99a6b |
feat(handlers,frontend): Slice B.6 — admin URL rename /admin/rules → /admin/procedural-events with 301 redirects + .tsx i18n rebind (t-paliad-305 / m/paliad#93)
Closes the procedural-events rename loop opened by m/paliad#93. The admin surface now lives under its canonical URL; the legacy paths remain reachable for one deprecation cycle via 301 redirects so bookmarks, audit-log entries, and curl scripts keep working. * internal/handlers/handlers.go — - Registers the 12 canonical routes under /admin/procedural-events* (page paths and JSON API). Same handlers — just the new URL slot. - Registers the 12 legacy /admin/rules* routes as 301 redirects. * internal/handlers/admin_rules.go — - redirectToProceduralEvents(dst) — fixed-destination redirect for paths without an {id}. - redirectToProceduralEventEdit — page redirect carrying the {id}. - redirectToProceduralEventAPI(suffix) — JSON API redirect carrying {id} + optional suffix (/clone-as-draft, /publish, /archive, /restore, /audit, /preview). Query string is preserved on every redirect. - All three helpers add the IETF Deprecation header + a Link header pointing at the successor-version path. * frontend internal nav + URL strings — Sidebar.tsx, admin.tsx, admin-rules-list.tsx, admin-rules-edit.tsx, client/admin-rules-list.ts, client/admin-rules-edit.ts: every `/admin/rules*` reference flipped to `/admin/procedural-events*`. In-app navigation now hits the canonical paths directly without a redirect round-trip; external callers keep working via the 301s. * frontend .tsx i18n rebind — 9 admin .tsx i18n bindings rebound to the canonical `admin.procedural_events.*` keys that already exist as aliases in i18n.ts (per Slice A from t-paliad-262). Specifically: admin.rules.list.title → admin.procedural_events.list.title admin.rules.list.heading → admin.procedural_events.list.heading admin.rules.list.new → admin.procedural_events.list.new admin.rules.col.submission_code → admin.procedural_events.col.code admin.rules.edit.title → admin.procedural_events.edit.title admin.rules.edit.breadcrumb → admin.procedural_events.edit.breadcrumb admin.rules.edit.field.submission_code → admin.procedural_events.edit.field.code admin.rules.edit.field.event_type → admin.procedural_events.edit.field.event_kind admin.rules.edit.field.parent → admin.procedural_events.edit.field.parent The remaining ~142 admin.rules.* keys do NOT yet have procedural_events aliases. Migrating them is a follow-up slice — each needs a new alias entry in i18n.ts (DE + EN) before the .tsx reference can be flipped. The 9 keys touched here are the most visible (page titles + edit-page field labels) so the admin UI immediately reads as "Verfahrensschritte" everywhere. * frontend/src/client/i18n.ts header comment updated to reflect that the URL rename has shipped (Slice B.6 done) and to flag the remaining i18n-key migration as the next step. Scope (documented, paliadin authorised): - "go everything" applied: backend routes + frontend nav + .tsx rebind of the 9 keys whose canonical aliases exist. - Full migration of all 142 admin.rules.* keys deferred — would require seeding ~142 new alias entries in i18n.ts (DE + EN) plus another 142 .tsx rebinds. Out of scope for tonight; flag as follow-up `feat(i18n): finish admin.rules.* → admin.procedural_events.* alias migration`. - 12 legacy /admin/rules routes still hit a handler (the redirect helper) — they don't 404 yet. Once a deprecation window passes with no traffic on the old paths, a future slice can drop them outright. Build + vet clean. TestMigrations_NoDuplicateSlot passes. This concludes the m/paliad#93 procedural-events rename slice train (Slices A through B.6). curie stays parked persistently for any follow-up the deploy / monitor cycle surfaces. |
|||
| 2c0efc396c | Merge: Slice B.5 — Go type aliases (SequencingRule = DeadlineRule) + JSON envelope dual-emit + Deprecation headers (m/paliad#93) | |||
| 5c6a0095e3 |
feat(models,services,handlers): Slice B.5 Go rename + JSON envelope dual-emit (t-paliad-305 / m/paliad#93)
Adds the Slice B.5 canonical Go names (SequencingRule, ProceduralEvent,
LegalSource, SequencingRuleService) without breaking any existing
call-site, and dual-emits / dual-accepts the two JSON envelope key
renames on /admin/api/rules with a Deprecation header.
* internal/models/models.go —
- type SequencingRule = DeadlineRule (alias; same struct, same db /
json tags). DeadlineRule remains the underlying type for now —
deferred hard-rename keeps the slice small.
- type ProceduralEvent struct mirroring paliad.procedural_events
(id, code, name, name_en, description, event_kind,
primary_party_default, legal_source_id, concept_id,
lifecycle_state, draft_of, published_at, is_active, timestamps).
Used by future code that needs the PE identity row alone.
- type LegalSource struct mirroring paliad.legal_sources (citation,
jurisdiction, pretty_de / pretty_en — both nullable per mig 136).
* internal/services/deadline_rule_service.go —
- type SequencingRuleService = DeadlineRuleService (alias).
- var NewSequencingRuleService = NewDeadlineRuleService (constructor
alias). Internal callers can adopt either name.
* internal/services/rule_editor_service.go —
- CreateRuleInput gains Code + EventKind fields tagged
json:"code" / json:"event_kind". CoalesceCanonicalKeys() folds
canonical → legacy after json.Decode so the rest of the service
keeps using SubmissionCode / EventType. Canonical wins when
both are sent.
- RulePatch gains EventKind field with the same fold.
* internal/handlers/admin_rules.go —
- adminRuleResponse wraps *models.DeadlineRule and adds Code +
EventKind fields alongside the legacy SubmissionCode /
EventType. Outputs both keys per response for one
deprecation-window slice.
- wrapRuleResponse / wrapRuleListResponse helpers.
- adminRuleDeprecationHeaders emits IETF Deprecation + Link/Sunset
headers on every Rule-bearing response so clients see the
migration signal in transit.
- All 8 Rule-returning handlers (List, Get, Create, Patch, Clone,
Publish, Archive, Restore) now wrap their result and add the
headers.
- Create + Patch handlers call CoalesceCanonicalKeys after decode
so legacy AND canonical request bodies are both accepted.
Scope decisions (documented in commit):
- Type renames use aliases instead of a hard 200-LOC rename. Same
semantics, no call-site churn. A future cleanup slice can flip
the underlying type definitions when convenient.
- ProceduralEvent + LegalSource are NEW structs (not aliases) since
they represent new conceptual rows; no legacy callers exist yet.
- Frontend admin .tsx i18n key rebinds (mentioned in parent task
brief B.5 deliverable list) are deferred — i18n keys themselves
already exist from Slice A (t-paliad-262); rebinding only changes
which key the .tsx file looks up. Pulling this into B.5 ballooned
scope; flagging as a small follow-up slice or B.6 sibling.
- Only /admin/api/rules emits dual keys today. Other handlers that
surface rule rows (Schriftsätze list, deadlines join) continue to
emit the legacy keys via models.DeadlineRule's existing JSON tags
— they're read paths, not the editor surface, and the deprecation
signal is most important where clients write.
Build + vet clean. TestMigrations_NoDuplicateSlot passes.
|
|||
| 6e0961cc30 | Merge: Composer Slice C — building blocks library + section picker (mig 149) (m/paliad#141) | |||
| ee98db94fa |
feat(submissions): Composer Slice C — building blocks library (m/paliad#141)
Per the design at docs/design-submission-generator-v2-2026-05-26.md §8
and the Q2 / Q9 ratifications:
- Q2 (m, 2026-05-26): building blocks are plain text paste sources.
No building_block_id reference is stored on submission_sections.
- Q9 (m, 2026-05-26): four visibility tiers — private / team / firm
/ global.
Schema (mig 149):
- paliad.submission_building_blocks — library catalog. Columns: slug,
firm (NULL = cross-firm), section_key (binds to one section kind),
proceeding_family (NULL = any), title_de/_en + description_de/_en
+ content_md_de/_en, author_id, visibility (CHECK in 4-tier set),
is_published, created_at, updated_at, deleted_at (soft delete).
RLS: coarse-grained SELECT — every authenticated user sees
non-deleted non-private rows + own private rows. Tier-specific
predicate (private/team/firm/global) applied in Go-layer service so
semantics evolve without RLS migrations. Mutations admin-only (no
RLS write paths).
- paliad.submission_building_block_admin_versions — append-only
history per block, retention=20. Admin-side only; NOT referenced
from submission_sections (per Q2's plain-text-paste model). Exists
so accidental delete / overwrite are recoverable.
Backend:
- internal/services/submission_building_block_service.go (~510 LoC):
BuildingBlockService. ListVisible applies tier predicate at query
time (private = author_id match; firm = firm column NULL OR matches
branding.Name; team = author shares a project_team with caller via
paliad.project_teams self-join; global = open). ListAllForAdmin
drops the predicate. Create + Update + SoftDelete + RestoreVersion
all transactional; appendVersionTx writes one audit row +
GC-deletes anything past the retention=20 horizon in the same tx.
InsertIntoSection (the paste mechanic) clones content_md_<lang>
into the section row with a "\n\n" separator if section already has
content. NO building_block_id stamped per Q2.
- internal/handlers/submission_building_blocks.go (~480 LoC): nine
handlers split between the lawyer-facing picker (list, insert) and
the admin editor (list, get, create, update, delete, list-versions,
restore-version, page). buildingBlockUpdateInput uses presence-
tracking UnmarshalJSON for the four nullable fields (firm,
proceeding_family, description_de/_en) so PATCH can distinguish
"no change" from "set to null".
- Routes registered: lawyer-facing under /api/submission-building-blocks,
admin-gated under /api/admin/submission-building-blocks/* and
/admin/submission-building-blocks (page).
- Wiring: handlers.Services + dbServices + cmd/server/main.go all
gain SubmissionBuildingBlock. NewBuildingBlockService takes the
branding.Name firm hint for the visibility predicate.
Frontend:
- frontend/src/admin-submission-building-blocks.tsx (~85 LoC):
three-pane admin shell (list / editor / version log) registered
in build.ts.
- frontend/src/client/admin-submission-building-blocks.ts (~370
LoC): admin client — list paint, edit form (slug + firm +
section_key + proceeding_family + title/desc/content per lang +
visibility radio + is_published toggle), per-block version log
with restore button. Bilingual labels.
- frontend/src/client/submission-draft.ts: per-section "+ Baustein"
button on the Composer editor toolbar (Slice B substrate gets one
more affordance). openBlockPicker opens a modal filtered to the
section's section_key, 200ms-debounced search by free text against
title/description/content. Click a hit → POST insert-into-section
→ section row's content_md_<lang> gains the block's content
appended at the end (Q2's plain-text paste semantic, no lineage).
- ~240 LoC of CSS: modal overlay + picker rows with tier-colored
visibility chips + admin editor 3-pane grid + form rows + version
list.
- 12 new i18n keys × 2 langs (admin.building_blocks.*).
Tests:
- TestValidVisibility (8 cases including case-sensitivity + empty).
- TestAppendBlockContent (8 cases covering empty-existing / empty-
addition / whitespace-only / trailing newline collapse).
- TestBuildingBlockVisibilityConstants pins the 4 string literals
against drift (RLS predicate + DB CHECK depend on them).
Build hygiene: go build/vet/test -short clean; bun run build clean
(2906 i18n keys, data-i18n scan clean).
Hard rules per ratifications honoured:
- Q2: no building_block_id lineage on sections (paste is plain text).
- Q9: 4 visibility tiers (private/team/firm/global).
- NO behavior change for pre-Composer drafts (the picker just doesn't
show — section list is hidden for base_id NULL drafts).
- {{rule.X}} aliases preserved (block content goes through the same
v1 placeholder pass on export as section prose).
NOT in scope per Slice C brief:
- User-authored private blocks (Slice C ships admin curation only;
any-user create is a follow-up).
- Tier promotion review workflow (admin sets tier directly today).
- Per-section "where is this block used" reverse lookup (no lineage
to query).
- Slice D's rich-prose features (headings, lists, blockquote) still
Slice D's job; this Slice doesn't extend the MD walker.
t-paliad-315 Slice C
|
|||
| 987db27831 | Merge: t-paliad-305 — Slice B.4 destructive drop: paliad.deadline_rules retired, INSTEAD OF triggers on view (mig 140, snapshot pre_140 same-TX) (m/paliad#93) | |||
| 1129baba7a |
feat(db,services): Slice B.4 destructive drop — paliad.deadline_rules retired, INSTEAD OF triggers on view route writes (mig 140, t-paliad-305 / m/paliad#93)
Drops the legacy paliad.deadline_rules table after 3 weeks of dual-write
shadowing (mig 136 → B.2 dual-write → B.3 read cutover via view). The
new tables — paliad.procedural_events, paliad.sequencing_rules,
paliad.legal_sources — are the sole source of truth from this commit
forward.
Pre-flip drift verified clean against prod:
deadline_rules=231 == sequencing_rules=231 == procedural_events=231
legal_sources=87
missing_sr=0, orphaned_sr=0, mismatched_lifecycle=0
* internal/db/migrations/140_drop_deadline_rules.up.sql (new) —
Single TX, audit-first:
1. CREATE TABLE paliad.deadline_rules_pre_140 AS TABLE paliad.deadline_rules
(precedent migs 091/093/095/098 — snapshot in same TX as destructive op).
2. Final reconciliation UPDATE on paliad.deadlines (no-op when
drift is already 0; defensive against last-minute writes).
3. DROP TRIGGER deadline_rules_audit_aiud.
4. Re-point FKs to sequencing_rules:
- paliad.appointments.deadline_rule_id → paliad.sequencing_rules(id)
- paliad.deadline_rule_backfill_orphans.resolved_rule_id → paliad.sequencing_rules(id)
(the id values are identical — sr.id inherited dr.id at mig 136.)
5. DROP COLUMN paliad.deadlines.rule_id.
6. DROP TABLE paliad.deadline_rules.
7. CREATE INSTEAD OF INSERT + INSTEAD OF UPDATE triggers on
paliad.deadline_rules_unified. Triggers route writes into the
three new tables in the same TX, preserving the legacy column
shape on the wire so RuleEditorService SQL only needs a
table-name swap, not a structural rewrite. Synthetic-code mint
expression is byte-identical to mig 136 + the B.2 dual-write
helper. POST assertions confirm the table is gone, the column
is gone, and the snapshot matches.
Trigger design notes (1:N caveat documented in-trigger):
- PE identity columns (code, name, name_en, description, event_kind,
primary_party_default, legal_source_id, concept_id) mirror from
the writing sequencing-rule.
- PE lifecycle columns (lifecycle_state, published_at, is_active)
deliberately do NOT mirror — a draft sequencing-rule cloned from
a published source shares the source's PE; we don't want the
clone's 'draft' lifecycle to leak back onto the source's PE.
Practical bound today (1:1 corpus); explicit comment in-trigger
for the eventual 1:N pattern.
* internal/db/migrations/140_drop_deadline_rules.down.sql (new) —
Best-effort restore from the snapshot. Triggers / indexes /
CHECK constraints from historical migrations are NOT replayed;
operator must reapply 078/079/091/095/098/122/128/134/135 to
bring the restored table to working shape. The down path is for
catastrophic recovery, not casual revert.
* internal/services/rule_editor_service.go —
Six syncDualWriteFromDeadlineRule(...) calls removed (the
INSTEAD OF triggers now do the fan-out). Five
INSERT/UPDATE paliad.deadline_rules statements (Create,
UpdateDraft, CloneAsDraft INSERT+SELECT, Publish, peer-archive,
flipLifecycle) renamed to paliad.deadline_rules_unified —
trigger handles the routing.
* internal/services/rule_editor_orphans.go — ResolveOrphan no
longer writes deadlines.rule_id (column dropped). Sets
sequencing_rule_id directly + derives procedural_event_id from
the matching sequencing_rules row in the same UPDATE statement.
* internal/services/deadline_service.go — deadlineColumns now
lists sequencing_rule_id (Deadline.RuleID still binds to it via
the db tag rename below). Update path's appendSet("rule_id",…)
flipped to appendSet("sequencing_rule_id",…) and post-write
derivation moved to the renamed syncDeadlineProceduralEventID
helper.
* internal/services/projection_service.go,
internal/services/submission_vars.go — `WHERE rule_id = $X`
reads on paliad.deadlines flipped to sequencing_rule_id.
* internal/models/models.go — Deadline.RuleID db tag changed from
"rule_id" to "sequencing_rule_id". Field name + JSON name kept
for backward compat with the frontend and existing Go callers;
semantic value is identical (same UUID).
* internal/services/dual_write.go — Massively trimmed.
Removed: syncDualWriteFromDeadlineRule, syncDeadlineDualLinks,
CheckDualWriteDrift, DualWriteDriftReport, HasDrift,
StartDualWriteDriftCheckLoop. All referenced
paliad.deadline_rules which no longer exists.
Kept (renamed): syncDeadlineProceduralEventID — derives
procedural_event_id from sequencing_rule_id after any
DeadlineService.Update that touched the back-link.
* cmd/server/main.go — Removed the StartDualWriteDriftCheckLoop
bootstrap call (and its `time` import that only that call
needed). Comment notes the retirement.
* internal/services/dual_write_test.go — Removed the final
CheckDualWriteDrift assertion in
TestDualWrite_RuleEditorLifecycle (function deleted). The
per-step asserts against procedural_events / sequencing_rules
/ legal_sources cover the same contract by direct query.
Hard rules followed:
- Audit-first: snapshot precedes destructive ops in the same TX.
- No silent data loss: pre-drop drift was zero; snapshot captures
the final state; FK re-points use identical UUIDs.
- INSTEAD OF triggers documented in mig 140 — single source of
truth for the legacy→new mapping.
- Down migration is honest about its scope (catastrophic recovery
only).
Build + vet clean. TestMigrations_NoDuplicateSlot passes. Live-DB
tests skipped (no TEST_DATABASE_URL in this env) — they'll exercise
the full mig 140 + INSTEAD OF triggers in CI.
|
|||
| f963b0df34 |
feat(submissions): Composer Slice B — editable prose sections + anchor-spliced render (m/paliad#141)
The "Composer actually works" milestone per the design at
docs/design-submission-generator-v2-2026-05-26.md §12 Slice B. Builds on
Slice A's substrate (submission_bases, submission_sections, base_id on
drafts); no new migrations needed.
Backend additions:
- internal/services/submission_md.go (~240 LoC): Markdown → OOXML
walker. Per the head's Slice B brief, scope is paragraphs +
bold/italic + blank-line spacing. Placeholders pass through
unchanged for the v1 substitution pass. CRLF normalisation; nested
formatting (***bold-italic***); two delimiter forms (* and _);
XML-escaping for &/</>; explicit empty-paragraph emit so blank
lines round-trip. 12 unit tests.
- internal/services/submission_compose.go (~470 LoC): SubmissionComposer
service. Pipeline: ConvertDotmToDocx pre-pass → extract
word/document.xml → render each included section's content_md_<lang>
→ splice via {{#section:KEY}}/{{/section:KEY}} anchor pairs in
the body → strip anchors for excluded sections → append unanchored
sections before <w:sectPr> → repack zip → run v1 placeholder pass.
RE2-friendly anchor scanner walks markers in body-order and matches
open/close pairs with a stack (handles unbalanced anchors
defensively). 6 unit tests covering anchor-mode splice,
append-mode-no-anchors, excluded-section drop, placeholder
resolution, lang column pick, order_index ASC.
- internal/services/submission_section_service.go: SectionPatch +
Update method. Six optional fields (content_md_de/en, included,
label_de/en, order_index). Sentinel ErrSubmissionSectionNotFound on
RLS-filtered miss.
- internal/handlers/submission_sections.go (NEW, ~150 LoC):
PATCH /api/submission-drafts/{draft_id}/sections/{section_id}.
Owner-scoped via SubmissionDraftService.Get; section-belongs-to-draft
cross-check. 404 on both missing-draft and section-belongs-elsewhere
paths.
- internal/handlers/files.go: fetchComposerBaseBytes + composerBaseSlugMap
reuse the existing Gitea proxy cache for base .docx bytes. hlc-letterhead
→ existing firmSkeletonSubmissionSlug, neutral → existing
skeletonSubmissionSlug.
- internal/handlers/submission_drafts.go: exportSubmissionDraft helper
branches on draft.BaseID. When set AND base + bytes + sections all
resolve → Composer pipeline. Else v1 fallback render path stays.
Audit metadata jsonb gains "composer": true + "base_id" flag when
composer was used.
Wiring:
- handlers.Services gains SubmissionComposer.
- dbServices.submissionComposer wired from svc.SubmissionComposer.
- main.go instantiates NewSubmissionComposer with the existing
SubmissionRenderer (so the {{rule.X}} alias contract stays preserved
inside section content).
Frontend additions (~400 LoC):
- client/submission-draft.ts: paintSectionList rewritten to render a
contentEditable per included section with a per-section B/I
toolbar. Per-section autosave debounced 500ms; mousedown handlers on
toolbar buttons preserve editor focus mid-command. domToMarkdown
walks the contentEditable's DOM tree back to Markdown source-of-
truth (b/strong → **…**, i/em → *…*, div/p → paragraph break, br
→ newline). Updated state.view.sections in-place on PATCH success
without re-painting (avoids focus-stealing on every keystroke);
re-paints only on structural changes (included toggle, label edits,
order changes).
- client/submission-draft.ts: onSectionToggleIncluded hides/shows a
section via PATCH. flushSectionAutosave on blur force-flushes
pending edits so leaving an editor doesn't strand unsynced changes.
- styles/global.css: editor surface (contentEditable area with focus
ring + placeholder), toolbar buttons (B/I 1.8rem squares),
per-section "Hide"/"Include" toggle in the head row.
- Updated i18n hint copy: "Inhalt pro Abschnitt — Autosave nach
500ms. Letztes Layout in Word."
Templates regenerated on Gitea:
- _skeleton.docx → composer-mode body (anchors only): blob SHA
ac0cdeaf49f7cd417ec143e2319ffbb02ec65644.
- _firm-skeleton.docx → composer-mode body (anchors only, preserves
sectPr → firm header/footer rIds): blob SHA
f1e9a9fb9a29ca01bf7bee709a45c5dda2a8e317.
- Both uploaded as mAi via --netrc-file ~/.netrc-mai.
- gen-skeleton-submission-template script gains an -anchors flag
(default true) so future regens emit composer-ready bodies. The
_firm-skeleton.docx regen was done via a one-off /tmp helper since
the gen-hl-skeleton-template script requires the proprietary .dotm
source which lives in HL/mWorkRepo; extending that script to accept
an existing .docx as input is a follow-up cleanup.
Build hygiene: go build/vet/test -short ./internal/... ./cmd/... all
clean; bun run build clean (2900 i18n keys, data-i18n scan clean).
NO behavior change for pre-Composer drafts (base_id NULL → v1
fallback render path stays compiled in). NO migrations needed in this
slice — sections were already in the schema from Slice A; only
content_md_de/en UPDATEs happen via the new PATCH endpoint.
Hard rules per Q2/Q10 ratification still honoured:
- No building_block_id lineage (Slice C territory; Q2).
- Caption/letterhead/signature are regular prose sections, seeded from
base spec; lawyer can edit/hide freely (Q10).
- {{rule.X}} aliases preserved (renderer pass unchanged).
NOT in scope per Slice B brief:
- Headings 1–3, lists, blockquote (Slice D's MD walker extension).
- Building blocks library (Slice C).
- Reorder / add-custom-section (Slice F).
- Auto-upgrade of pre-Composer drafts (Slice C — explicitly NOT in
this slice per head's brief msg #2393).
t-paliad-313 Slice B
|
|||
| 6cd340300b | Merge: t-paliad-313 — Composer Slice A: base picker + read-only section list (migs 146/147/148) (m/paliad#141) | |||
| 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
|
|||
| e2969fc358 |
feat(submissions): Composer Slice A — base picker + read-only section list (m/paliad#141)
The first slice of the Submission generator v2 ("Composer") per the
design at docs/design-submission-generator-v2-2026-05-26.md §12 Slice A.
Ships the base concept + per-draft section seeding end-to-end with NO
change to the .docx render path — v1 export still works exactly as
today.
Schema (mig 146/147/148):
- paliad.submission_bases — catalog table; one row per template base
(slug, firm, proceeding_family, label_de/en, gitea_path, section_spec
jsonb, is_default_for[]). RLS: wide-open SELECT for authenticated
users, mutations admin-only (handler-enforced, no RLS write paths).
Seeded with 2 rows: hlc-letterhead → _firm-skeleton.docx; neutral →
_skeleton.docx. Each section_spec carries the 10-section default
(letterhead, caption, introduction, requests, facts, legal_argument,
evidence, exhibits, closing, signature) with bilingual labels +
bag-driven seed Markdown for caption/letterhead/signature.
- paliad.submission_drafts gains base_id (FK SET NULL, optional) +
composer_meta jsonb (default '{}'). Purely additive; pre-Composer
drafts keep base_id NULL → v1 fallback render path stays active.
- paliad.submission_sections — per-draft section rows (draft_id,
section_key, order_index, kind ∈ {prose,requests,evidence},
label_de/en, included, content_md_de/en). RLS mirrors
submission_drafts (owner-scoped + can_see_project, four policies).
Backend:
- BaseService (read-only Slice A): List + GetByID + GetBySlug +
GetDefaultForCode (firm/family fallback chain).
- SectionService: ListForDraft + Get + SeedFromSpec (transactional
multi-INSERT).
- SubmissionDraftService.AttachComposer wires both; Create resolves
the firm default base and seeds base_id + section rows in one tx.
Composer wiring is additive — when bases==nil the service stays
v1-shaped.
- Update accepts BaseID **uuid.UUID (set / clear / no-change).
- submissionDraftView gains BaseID, ComposerMeta, Sections fields.
- Routes: GET /api/submission-bases (catalog list). PATCH endpoints
on both project-scoped and global drafts accept "base_id".
Frontend:
- submission-draft.tsx: base picker dropdown above language toggle
(hidden until catalog loads); section-list pane above the preview
(hidden when no rows).
- client/submission-draft.ts: loadBases() parallel-fetches on boot;
paintBasePicker rebuilds <option> list on every paint; onBaseChange
PATCHes base_id and repaints; paintSectionList renders each section
read-only (label + kind chip + excluded badge + Markdown body).
- Per the brief: NO auto-upgrade of existing 11 drafts (that's Slice C).
Pre-Composer drafts get the picker (catalog still loads) but the
section pane stays hidden until they pick a base on a new draft.
Tests:
- TestFamilyOfCode + TestBaseSectionSpec_DecodeShape + _EmptyDecode
(pure unit, no DB).
- TestComposerSeedFlow (live, TEST_DATABASE_URL-gated): asserts mig 146
seeded 10 default sections on both bases; GetDefaultForCode picks
hlc-letterhead for HLC/de.inf.lg.erwidg; new draft via Create seeds
base_id + 10 section rows in tx with ascending order_index and
bilingual labels populated.
NO behavior change to .docx export — the v1 path stays sole render
path this slice. Composer's anchor-based assembly engine + MD→OOXML
walker land in Slice B.
Build hygiene: go build/vet/test -short clean; bun run build clean
(2900 i18n keys, data-i18n scan clean).
t-paliad-313
|
|||
| 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) |