0263a0e932e2e861907970c90a65971db48e630e
207 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 0263a0e932 |
feat(approvals): t-paliad-216 — server-side hydration for back-link
Server-side additions so /inbox can render the suggest-changes back-link
without an extra client round-trip:
- ApprovalRequestView gains NextRequestID. Hydrated via correlated
subquery on previous_request_id; mig 103's partial index makes the
lookup O(1) per row.
- view_service.go approvalRowSubtitle picks up the changes_requested
case ("Abgelehnt mit Vorschlag von <decider>").
- filter_spec.go validRequestStatuses includes "changes_requested" so
user-views can filter on it.
- handlers/approvals.go isValidInboxStatus accepts "changes_requested"
on the /api/inbox/{mine,pending-mine}?status= query. Test case added
to TestParseInboxFilter_DropsUnknownStatus.
|
|||
| d924ab9743 |
test(approvals): t-paliad-216 SuggestChanges service + handler error mapping
Service-level (real DB, gated on TEST_DATABASE_URL like the rest of the
approval suite):
- HappyPath: OLD row → changes_requested; NEW row pending with
previous_request_id back-pointer; entity reflects counter payload;
two project_events emitted (changes_suggested + requested).
- NoOpRejected: identical counter + empty note → ErrSuggestionRequiresChange.
- NoteOnlyAccepted: identical counter + non-empty note succeeds; entity
keeps the original counter values.
- SelfApprovalBlocked: original requester cannot suggest on their own row.
- RequestNotPending: already-decided row rejects suggest-changes.
- LifecycleInvalid: create-lifecycle pending → ErrSuggestionLifecycleInvalid.
- OriginalRequesterCanApproveCounter: m's Q6 model — after the approver
suggests changes, the ORIGINAL REQUESTER (now no longer the new row's
requested_by) can approve the counter themselves provided their
profession qualifies.
- CounterApproverCannotSelfApprove: 4-Augen still holds — the suggesting
approver cannot approve their own counter (ErrSelfApproval on the new row).
Handler-level (pure-Go, no DB):
- SuggestionRequiresChange400: error code mapping.
- SuggestionLifecycleInvalid400: error code mapping.
|
|||
| 705e1a2e79 |
feat(approvals): t-paliad-216 SuggestChanges service method
ApprovalService.SuggestChanges is the fourth approval action — in one
transaction:
1. Validates the OLD pending row (caller satisfies canApprove,
lifecycle in update/complete only, counter differs from old.payload
OR note is non-empty).
2. Closes the OLD row as 'changes_requested' with decision_note +
counter_payload + decided_by + decided_at + decision_kind.
3. Reverts the entity from old.pre_image (reuses applyRevert — same
code path Reject runs).
4. Runs the deadlock check for the NEW row (excluding the suggesting
caller; original requester is no longer excluded).
5. Re-applies the counter_payload to the entity row (via
applyEntityUpdate, mirroring the write-then-approve write).
6. INSERTs a NEW pending approval_requests row authored by the caller
with previous_request_id pointing back at the OLD row.
7. Marks the entity pending + pending_request_id → new row.
8. Emits two project_events: *_approval_changes_suggested + a fresh
*_approval_requested for the new row.
4-Augen still holds: the suggesting caller is the new row's
requested_by, so self-approval on the new row is blocked by the standard
3-layer guard. The ORIGINAL requester is no longer the requested_by of
the new row — if their profession satisfies the required_role they can
now approve the counter themselves.
Adds:
- const RequestStatusChangesRequested = "changes_requested"
- var ErrSuggestionRequiresChange = "suggestion requires counter diff or note"
- var ErrSuggestionLifecycleInvalid = "suggest is only valid for update/complete"
- models.ApprovalRequest.CounterPayload + PreviousRequestID
- Per-row read paths (getRequestForUpdate, approvalRequestViewColumns)
populate the new columns.
|
|||
| 3677c81fbe |
feat(submissions): t-paliad-215 Slice 1 — template registry + variable bag
TemplateRegistry (services/submission_templates.go) walks the
m-locked Q4 fallback chain — templates/{FIRM_NAME}/{code}.docx →
templates/_base/{code}.docx → templates/_base/{family}.docx →
templates/_base/_skeleton.docx — against the Gitea repo
HL/mWorkRepo. SHA-cache + 5-min refresh check, identical pattern to
internal/handlers/files.go's HL Patents Style proxy. Distinguishes
"no template" (chain fallthrough) from "Gitea down" so the handler
can render different UI for each.
SubmissionVarsService (services/submission_vars.go) assembles the
~30-placeholder bag from project + parties + rule + next-deadline +
user + firm + today. Locale-aware long-date forms (DE + EN) and a
legal_source pretty-printer that rewrites DE.ZPO.276.1 → "§ 276 Abs.
1 ZPO" / "Section 276(1) ZPO" for the prefixes the 254-rule corpus
uses today. Unknown prefixes pass through unchanged.
Visibility inherits from ProjectService.GetByID
(paliad.can_see_project) — unauthorised callers get the same
ErrNotVisible that every project surface returns.
|
|||
| 8ea3509b98 |
feat(submissions): t-paliad-215 Slice 1 — in-house .docx render engine
Pure-Go {{path.dot.notation}} placeholder engine + unit tests
(t-paliad-215, design docs/design-submission-generator-2026-05-19.md
§6). Chosen over github.com/lukasjarosch/go-docx because that library
treats sibling placeholders inside one <w:t> run as nested and
refuses to replace them — patent submissions routinely carry multiple
placeholders per paragraph (party blocks especially), so the library
is a non-starter.
Two-pass strategy preserves run-level formatting on the common path:
1. Pass 1: regex replace inside each <w:t>…</w:t> independently —
no format loss for the 99% case where placeholders are intact.
2. Pass 2: paragraph-level merge for paragraphs that still contain
orphan "{{" or "}}" markers (Word fragmented the placeholder
across runs).
Missing placeholders render [KEIN WERT: <key>] / [NO VALUE: <key>]
markers so the lawyer sees the gap in Word rather than getting a 400.
Tests cover: single-run, multi-per-run (the go-docx failure mode),
cross-run merge, missing-marker (DE+EN), XML escaping of special
chars, non-document zip entries preserved, placeholder regex
grammar.
|
|||
| f9ff7b93e8 |
fix(export): xlsx docProps + pane XML — Excel "repairs required" + wrong Modified date
m hit two bugs opening the Slice 1 export in Excel / Windows:
1. **Excel showed a "Repairs required" prompt** on open. Root cause:
the SetPanes call passed only `{Freeze: true, YSplit: 1}` — the
obvious-but-wrong shape. The resulting <pane> XML missed the
`topLeftCell` and `activePane` attributes that Excel requires for
a frozen-row pane (excelize's parser is permissive on re-read but
Excel is strict). Fix: complete the Panes struct (TopLeftCell="A2",
ActivePane="bottomLeft", Selection on bottomLeft) and surface
SetPanes errors instead of `_ =`-ignoring them.
2. **Windows Explorer / Excel's File→Info showed Modified=2006-09-16
("xuri")** — excelize's hardcoded first-commit defaults. Root cause:
buildXLSX never called SetDocProps so the canned defaults leaked.
Fix: SetDocProps({Created, Modified} = meta.GeneratedAt;
Creator = "Paliad (<firm>)"; Title/Description scoped per export).
3. **Bonus**: the outer-zip entry mtimes were stamped 2000-01-01 (the
deterministic constant) so extracted files showed a Y2K Modified
date in Explorer. Now stamped meta.GeneratedAt, which preserves
determinism within an export (same row state + same GeneratedAt →
same bytes, the actual m's-Q6 contract).
Also: set the active sheet to __meta (index 0) after sheet creation so
a future code path that adds/removes sheets can't leave an out-of-range
active-sheet index that would trip a separate "repairs required" path.
Regression tests in dump_export_test.go pin all three fixes by re-opening
the generated xlsx via excelize.OpenReader and asserting:
- docProps Created/Modified == meta.GeneratedAt (RFC 3339 UTC)
- docProps Creator contains "Paliad"
- xlsx bytes never contain "2006-09-16T00:00:00Z" or "<dc:creator>xuri</dc:creator>"
- sheet2/sheet3 raw XML carries topLeftCell + activePane + state=frozen
- outer-zip entries' Modified is within ±2s of GeneratedAt
- developer hatch: DUMP_EXPORT=1 writes /tmp/paliad-export-debug.{zip,xlsx}
for opening in real Excel.
|
|||
| 28c7215458 |
feat(export): t-paliad-214 Slice 1 backend — personal sync export endpoint + xlsx/json/csv writer
Adds GET /api/me/export streaming a deterministic .zip bundle of the caller's RLS-visible projection (per design §2.3): projects, deadlines, appointments, parties, notes, documents (metadata), audit events, approval requests, checklist instances + personal sidecars (me row, caldav config without ciphertext, views, pins, card layouts, paliadin turns) + reference data (proceeding_types, event_types, deadline_rules, courts, countries, holidays …) + restricted users_referenced sheet. Bundle shape: paliad-export.xlsx + paliad-export.json + per-sheet CSVs (UTF-8 BOM, RFC 4180) + README.txt + __meta.json. Outer zip is byte-deterministic — sorted file list, fixed Modified time on every entry, sorted JSON keys. Two runs at same row-state → identical bytes. ExportService.WritePersonal owns the SQL recipe + column discovery + PII deny-regex (?i)secret|token|password|api[_-]?key|private[_-]?key + per-sheet DropColumns belt-and-braces (e.g. user_caldav_config .password_encrypted explicitly dropped on top of the regex). Audit row written to paliad.system_audit_log before the run, patched with row_counts + file_size_bytes after. Migration 102 creates paliad.system_audit_log (generic event_type + actor_id/email + scope + scope_root + metadata jsonb). Idempotent CREATE TABLE IF NOT EXISTS + indexes; RLS enabled with self-read + admin-read policies. AuditService.ListEntries gains a 6th UNION branch so the new table surfaces on /admin/audit-log. excelize/v2 added to go.mod for xlsx generation. Pure-function tests pin formatCellValue value-coercion, PII regex, CSV quoting + BOM + umlaut survival, JSON shape, meta key order stability, filename slugify, and byte-determinism of the bundle assembly. Design: docs/design-paliad-data-export-2026-05-19.md §7 Slice 1. |
|||
| 4131d2e2a6 |
feat(t-paliad-207): Verfahrensablauf + Fristenrechner polish (jurisdiction prefix, trigger-event, flag rows, rule links, R.19 label)
Five intertwined fixes m surfaced in the interactive session:
1. **Jurisdiction prefix on the picked proceeding** — the collapsed
summary chip and the result header now read "UPC Verletzungsverfahren"
/ "DE Verletzungsklage (LG)" instead of the bare proceeding name.
Disambiguates the 4 redundancies in the corpus once the picker
collapses. Driven by .proceeding-group[data-forum] which is already
on every group.
2. **Trigger Event label = root rule** — step 2's "Auslösendes Ereignis"
line now shows the first event in the proceeding (e.g. Klageerhebung,
Nichtigkeitsklage) instead of the proceeding name. Populated from
the calc response (isRootEvent=true) on every render; em-dash
placeholder while step 3 hasn't rendered yet. lang-change keeps it
coherent.
3. **Flag rows on /tools/verfahrensablauf** — Slice 1 of t-paliad-179
stripped the with_ccr / with_amend / with_cci toggles when it lifted
the shared renderer; they never came back. Lifted the 4 existing
rows from fristenrechner.tsx plus 2 new with_po rows (RoP 19.1
preliminary objection, mig 095) — same wiring + show/hide rules on
both surfaces. with_amend stays nested under with_ccr on upc.inf.cfi
(R.30 only with a CCR).
4. **Rule references → youpc.org/laws links** — new
BuildLegalSourceURL(src) maps the structured legal_source code to
the youpc permalink for the UPC corpus (UPCRoP / UPCA / UPCS today;
39 of 91 active rules carry UPC.RoP.* and now link). DE/EPA/EU
bodies have no youpc home yet and render as plain display text —
filed as m/paliad#39. Wired through UIDeadline.LegalSourceDisplay +
LegalSourceURL so deadlineCardHtml can render <a target="_blank"
rel="noopener"> when the URL is set.
5. **R.19 label: "Vorab-Einrede" → "Einspruch"** — m's correction. DE
only (EN canonical UPC RoP term stays "Preliminary objection").
Client-side change only — i18n + JSX fallbacks. The matching DB
rename on the two rule-name rows folds into joule's broader mig 097
(legal-citation backfill, t-paliad-208 follow-up). The live UPDATE
applied during the session is captured under that audit reason; the
no-op when joule's mig re-applies is harmless.
Build hygiene:
- go build ./... + go vet ./... clean
- new test TestBuildLegalSourceURL covers UPC corpus + DE/EPA/EU
fall-through + edge cases (empty input, malformed source)
- bun run build clean (2417 i18n keys total)
Rebased on origin/main @
|
|||
| a0a3ec32a3 |
fix(mig 098): relax submission_code shape regex to allow digits in suffix
Mig 098 (t-paliad-209, ohm) crash-looped paliad.de prod for ~2h: §6.1 assertion regex `^[a-z_]+\.[a-z_]+\.[a-z_]+\.[a-z_]+(\..*)?$` rejects EPA rule codes that carry the statutory rule number in the suffix — e.g. `epa.opp.boa.r106`, `epa.grant.exa.r71_3`, `epa.opp.opd.r116`, `epa.opp.opd.r79_further`, `epa.opp.boa.entsch2`, `epa.opp.boa.r116`. Migration's UPDATE step succeeds against these rows; the transactional assertion blows them up; rollback leaves the migration tracker dirty at version 98 and the container refuses to start. Patch: allow `[a-z_0-9]` per segment instead of `[a-z_]` in both the SQL assertion (mig 098 §6.1) and the matching Go shape regex (submission_codes_shape_test.go). Same change in both spots so the runtime sanity test stays aligned with the SQL invariant. Manual recovery already applied: forced `paliad.paliad_schema_migrations.version` back to 97 with `dirty=false` so the next deploy retries mig 098 from scratch against the patched file. No data state changed (mig 098 ran inside a transaction and fully rolled back — snapshot table, prefix UPDATE, and column rename all reverted). go build ./... clean. TestProceedingCodeShapeRegexStandalone green. |
|||
| a18b825bee |
feat(t-paliad-207): Verfahrensablauf + Fristenrechner polish (jurisdiction prefix, trigger-event, flag rows, rule links, R.19 label)
Five intertwined fixes m surfaced in the interactive session:
1. **Jurisdiction prefix on the picked proceeding** — the collapsed
summary chip and the result header now read "UPC Verletzungsverfahren"
/ "DE Verletzungsklage (LG)" instead of the bare proceeding name.
Disambiguates the 4 redundancies in the corpus once the picker
collapses. Driven by .proceeding-group[data-forum] which is already
on every group.
2. **Trigger Event label = root rule** — step 2's "Auslösendes Ereignis"
line now shows the first event in the proceeding (e.g. Klageerhebung,
Nichtigkeitsklage) instead of the proceeding name. Populated from
the calc response (isRootEvent=true) on every render; em-dash
placeholder while step 3 hasn't rendered yet. lang-change keeps it
coherent.
3. **Flag rows on /tools/verfahrensablauf** — Slice 1 of t-paliad-179
stripped the with_ccr / with_amend / with_cci toggles when it lifted
the shared renderer; they never came back. Lifted the 4 existing
rows from fristenrechner.tsx plus 2 new with_po rows (RoP 19.1
preliminary objection, mig 095) — same wiring + show/hide rules on
both surfaces. with_amend stays nested under with_ccr on upc.inf.cfi
(R.30 only with a CCR).
4. **Rule references → youpc.org/laws links** — new
BuildLegalSourceURL(src) maps the structured legal_source code to
the youpc permalink for the UPC corpus (UPCRoP / UPCA / UPCS today;
39 of 91 active rules carry UPC.RoP.* and now link). DE/EPA/EU
bodies have no youpc home yet and render as plain display text —
filed as m/paliad#39. Wired through UIDeadline.LegalSourceDisplay +
LegalSourceURL so deadlineCardHtml can render <a target="_blank"
rel="noopener"> when the URL is set.
5. **R.19 label: "Vorab-Einrede" → "Einspruch"** — m's correction. DE
only (EN canonical UPC RoP term stays "Preliminary objection").
Client-side change only — i18n + JSX fallbacks. The matching DB
rename on the two rule-name rows folds into joule's broader mig 097
(legal-citation backfill, t-paliad-208 follow-up). The live UPDATE
applied during the session is captured under that audit reason; the
no-op when joule's mig re-applies is harmless.
Build hygiene:
- go build ./... + go vet ./... clean
- new test TestBuildLegalSourceURL covers UPC corpus + DE/EPA/EU
fall-through + edge cases (empty input, malformed source)
- bun run build clean (2417 i18n keys total)
Rebased on origin/main @
|
|||
| bc5b3557d0 |
feat(t-paliad-209): rename DeadlineRule.Code → SubmissionCode across Go layer
Workstream B Go sweep — matches mig 098. Every place the deadline-rules service reads/writes the per-rule identifier now uses the new column name and the new struct field. Distinct from rule_code (legal citation) and from proceeding_types.code (the proceeding's 3-segment code). Touch points: - models.DeadlineRule.Code → SubmissionCode (db + json tags renamed in lockstep — JSON contract `submission_code` is the new shape). - deadline_rule_service: ruleColumns SELECT list updated. - rule_editor_service: CreateRuleInput.Code → SubmissionCode (json tag too), INSERT + CloneAsDraft SELECT updated. - projection_service: lookupRuleByCode → lookupRuleBySubmissionCode (SQL WHERE clause + error message); every r.Code / parent.Code / rule.Code / first.Code / src.rule.Code read renamed. - fristenrechner: r.Code / prev.Code / rule.Code reads renamed in Calculate (parent-anchor + override-key + computed-by-code map) and in CalculateRule's LocalCode emission; the proceeding-code+submission- code resolver query uses `submission_code = $2`. - event_trigger_service / deadline_calculator: r.Code reads renamed. UIDeadline.Code (the calculator's wire response) is unchanged — that field is a separate API contract pointing at the same value; renaming it would force every frontend deadline-renderer through a contract break that isn't part of this workstream. Test fixtures updated to the new SubmissionCode field name; live-DB tests updated to the post-mig-098 prefixed values (`inf.sod` → `upc.inf.cfi.sod` etc.). New submission_codes_shape_test asserts every active+published row matches the 4+-segment proceeding-prefixed shape (sibling of TestProceedingCodeShape; mirrors mig 098 §6.1). go build ./... clean. go test ./internal/... green. |
|||
| 216abbfc98 |
feat(t-paliad-206): switch Go layer to lowercase dot-form proceeding codes
Sweeps internal/services + internal/handlers + internal/models to use the new proceeding codes landed by mig 096. Stable Code* constants live in internal/services/proceeding_mapping.go so a future rename needs to touch one file. Substantive changes: - proceeding_mapping.go gains ResolveCounterclaimRouting() — the cascade resolver that routes upc.ccr.cfi (illustrative peer) back to upc.inf.cfi with with_ccr=true as default flag (design doc S1). - deadline_search_service.go forum-bucket map updated; upc.ccr.cfi added to upc_cfi since it is a CFI peer. - project_service.go CreateCounterclaim default lookup parameterised so the SQL string carries the constant, not a literal. - proceeding_codes_shape_test.go: new file. Validates the shape regex standalone (always runs) and walks live DB rows asserting every active fristenrechner row matches the new shape + every stable Code* constant resolves to exactly one active row. Comments and test fixtures throughout the Go tree updated to the new shape. Tests pass under `go test ./internal/... -short`. |
|||
| aa82434af9 |
fix(t-paliad-202): grey out inbox actions instead of erroring on illegal click
m's UX bug (2026-05-17, paliad.de prod): clicking Genehmigen/Ablehnen/
Zurückziehen on a row the viewer can't act on alerted ("Eigengenehmigung
nicht zulässig.", "Sie haben nicht die erforderliche Rolle.") after the
POST round-trip. m's ask: "approval that i cannot grant should have the
'Genehmigen' button greyed out... that would be better than showing an
error when I try."
Backend (internal/services/approval_service.go):
- ApprovalRequestView gains viewer_can_approve + viewer_is_requester
booleans. Resolved server-side per caller — false on self-authored rows
(caller == requester), true when the eligibility predicate matches.
- Extract the eligibility EXISTS-block into approvalEligibilitySQL const
and reuse it in ListPendingForApprover (WHERE), PendingCountForUser
(WHERE), and the new viewer_can_approve SELECT expression. Single
source of truth for the gate, identical to canApprove.
- ListPendingForApprover, ListSubmittedByUser, and GetRequest all bind
$1 = callerID so the SELECT computes the flags inline (one query, no
N+1). GetRequest's signature grows a callerID arg; the handler passes
the authenticated user.
Frontend (frontend/src/client/views/shape-list.ts):
- ApprovalDetail picks up the two booleans (optional — falsy is safe:
it disables, never falsely enables).
- approvalActionBtn renders the button as before but flips
btn.disabled + sets a tooltip via disabledReasonFor: approve/reject
share the viewer_can_approve gate (self → self_approval tooltip;
unauthorized → not_authorized); revoke needs viewer_is_requester.
- All three buttons still render on every pending row so users see
what's possible — the disabled+tooltip combo explains what's not.
i18n + CSS:
- 3 new keys × DE/EN: approvals.disabled.{self_approval,
not_authorized,revoke_not_requester}.
- .inbox-row-action:disabled neutralises the .btn-primary/danger/
secondary variant via opacity + not-allowed + muted tokens.
Tests:
- internal/services/approval_service_test.go::TestApprovalService_ViewerFlags
is a 4-case table-driven live-DB test (skips without TEST_DATABASE_URL):
self-authored (false/true), eligible peer (true/false), non-eligible
viewer (false/false), global_admin (true/false). Also asserts the flags
on ListPendingForApprover + ListSubmittedByUser rows.
Defence-in-depth preserved: server still rejects illegal POSTs with the
same error contract, and the alert path stays in inbox.ts for the race
where state changes between render and click.
|
|||
| bdd4999213 |
fix(projects): unbreak Create — drop $1::text reuse + tighten CM CHECK to 6 digits
Two issues m hit and reported in one breath while adding a project:
1. **Internal error on POST /projects** (prod-only, surfaced at 10:23). Both
ProjectService.Create and CreateCounterclaim re-referenced the uuid
parameter `$1` as `$1::text` to fill the path placeholder. Postgres'
planner deduced conflicting types for `$1` (uuid in the id column,
text in the cast) and rejected the prepared statement with 42P08
"inconsistent types deduced for parameter". The path placeholder
value is irrelevant — paliad.projects_sync_path() (BEFORE INSERT
trigger from mig 018/021) always overwrites it from id and parent
path. Fix: replace `$1::text` with a literal '' in both INSERTs,
keeping the parameter list decoupled from the id column's type.
Same comment now anchors the rationale on both call sites.
2. **CM number length — 6 digits, not 7.** m's correction; mig 018's
`^[0-9]{7}$` CHECK on paliad.projects.client_number and
matter_number was wrong. Mig 094 snapshots affected rows to
paliad.projects_pre_094, NULL-s the 3 surviving 7-digit test
values (2 client_numbers, 1 matter_number), then swaps the legacy
`projekte_*_check` constraints from {7} to {6}. Frontend pattern,
maxLength, placeholder, labels, and i18n hint flipped from 7 → 6
on both DE and EN sides; format hint reads CCCCCC.MMMMMM now.
Dry-run against live DB (BEGIN..ROLLBACK):
- Fixed Create SQL: trigger populates path = id::text (36 chars). ✓
- Mig 094: 2 rows snapshotted, 0 clients/matters remain after clear,
0 rows violate the new 6-digit CHECK. ✓
go build, go test ./internal/..., bun run build all clean.
|
|||
| 40e49e87d4 |
refactor(t-paliad-200): Slice 9 follow-up B — retire litigation category from rule corpus
Lorenz's Slice 9 (t-paliad-195) deferred mig 093 because 40 active
paliad.deadline_rules still pointed at the 7 litigation-category
proceeding_types (INF, REV, CCR, APM, APP, AMD, ZPO_CIVIL). Phase 3
Slice 5 (mig 087/088) already retired the category from project-binding;
this migration retires it from the rule corpus.
PLAN CHOICE (audit-gated, paliadin-approved): archive-all-40 rather than
the original re-parent plan. The audit found that 23 of 40 Pipeline-A
rules share their `code` with an existing fristenrechner rule on the
proposed re-parent target (e.g. inf.oral exists on both INF and
UPC_INF). Re-parenting would leave two rules with identical
(proceeding_type_id, code), breaking the implicit per-proceeding
rule_code identity contract keyed off by projection / search /
rule_editor. The fristenrechner rules are clearly the production
version (proper German names, legal_source pinned to UPC.RoP citations,
full bilateral chains, intra-proceeding counterclaim handling); the
Pipeline-A rules are stubs (English-only, mostly NULL legal_source,
duration_value=0 for 28 of 40, no spawn_proceeding_type_id wiring).
Migration 093 sequence (atomic):
1. Snapshot proceeding_types_pre_093 + deadline_rules_pre_093 as
permanent audit anchors.
2. INSERT _archived_litigation pt (category='archived',
is_active=false, jurisdiction='UPC') to home the rules.
3. UPDATE all 40 rules → archive pt + lifecycle_state='archived' +
is_active=false. Captured in paliad.deadline_rule_audit via the
mig 079 trigger.
4. DELETE the 7 litigation rows from paliad.proceeding_types (now
safe — nothing references them).
5. Hard assertions: 0 litigation rows survive, exactly 40 rules on
the archive pt, every snapshot row matches a surviving rule by id.
Critical FK note: deadline_rules.proceeding_type_id is ON DELETE CASCADE
→ proceeding_types(id). A naive DELETE of the 7 litigation rows would
cascade-delete all 40 rules and break the FK from the 1 live deadline
("Lecker Frist", completed) that still references inf.rejoin/INF.
Re-homing the rules before deleting the pt rows is mandatory.
Verified via BEGIN..ROLLBACK against live DB: assertions pass, all 30
intra-litigation parent_id chains preserved, the live deadline FK
stays valid.
Test impact:
internal/services/project_service_test.go:72 used to look up
category='litigation' AND code='INF' to exercise the Slice 5 negative
case. Post-mig-093 that lookup returns NULL. Rewritten to fetch any
category <> 'fristenrechner' row (the _archived_litigation pt is the
canonical post-093 row); defence-in-depth coverage of both the Go
service guard and the mig 088 SQL trigger is preserved.
SURFACED FOR LEGAL REVIEW (4 coverage questions the audit found, to be
triaged as follow-up tasks):
1. inf.prelim (Preliminary Objection, RoP 19, 1 month) — not present
on UPC_INF. Possible coverage gap; legal review to decide whether
to add it to the fristenrechner ruleset.
2. inf.appeal / rev.appeal / ccr.appeal as cross-proceeding spawns
into UPC_APP (2 months, UPC.RoP.220.1) — fristenrechner UPC_APP
currently starts standalone with no spawn from UPC_INF/UPC_REV.
Possible UX gap; Pipeline-A versions had
spawn_proceeding_type_id=NULL so they weren't functional spawns
either.
3. ccr.amend / rev.amend (spawn rules) — superseded by
inf.app_to_amend / rev.app_to_amend on UPC_INF / UPC_REV. Safe to
drop; no action needed.
4. zpo.klage / zpo.vertanz / zpo.klageerw / zpo.berufung — no UPC
analogue; redundant with the DE_INF / DE_INF_OLG / DE_INF_BGH and
DE_NULL / DE_NULL_BGH chains. Safe to drop; no action needed.
Files:
internal/db/migrations/093_retire_litigation_category.up.sql (new)
internal/db/migrations/093_retire_litigation_category.down.sql (new)
internal/services/project_service_test.go (test rewrite)
|
|||
| 29a6b58747 |
refactor(t-paliad-199): Slice 9 follow-up A — drop legacy event_deadlines tables
EventDeadlineService.Calculate now reads source rows from paliad.deadline_rules directly (WHERE trigger_event_id IS NOT NULL), joining via UUID instead of title_de string. The legacy SELECTs against paliad.event_deadlines + paliad.event_deadline_rule_codes are gone. Migration 092: - Snapshots both legacy tables into _pre_092 audit anchors. - Adds paliad.deadline_rules.rule_codes text[] and backfills the 72 multi-code citations from event_deadline_rule_codes via the sequence_order = 1000 + ed.id convention from mig 085 (70 of 77 Pipeline-C deadlines carry codes; 7 are codeless). - Hard assertion ties source-junction-row count to backfilled text[]-element count — any sequence_order mismatch aborts the drop. - Drops the mig 086 read-only trigger (orphan once event_deadlines goes away). - Drops paliad.event_deadlines + paliad.event_deadline_rule_codes. - Final assertion: >=77 active deadline_rules with trigger_event_id NOT NULL — Slice 3 corpus must not have collapsed. - audit_reason wrapper at top so the deadline_rules UPDATE row-trigger records the reason in deadline_rule_audit. Verified via BEGIN..ROLLBACK against the live paliad DB: 72 codes backfilled into 70 rule_codes arrays, multi-code rules (RoP.029.a + RoP.030 for ed_id=6) preserve their ordering, composite rules (combine_op=max) remain intact, both tables drop cleanly, all assertions pass. Parity test rebound to deadline_rules — independent computation still re-runs applyDuration against raw column values for date/composite parity. EventDeadlineResult.ID stays int64 via the sequence_order - 1000 convention so the public /api/tools/event-deadlines wire shape is unchanged. |
|||
| a33060e600 |
feat(t-paliad-197): Slice 2 — project-driven narrowing + cascade auto-walk
Wires the project context into the Determinator row stack so a UPC INF
matter doesn't need to be hand-walked through five obvious cascade picks.
Auto-walk descends single-option chains as `is-prefilled` rows, the inbox
row vanishes for UPC matters (CMS implied), and the first prefilled row
carries the project reference inline ("aus Akte: HL-2024-001").
Backend: `internal/services/proceeding_mapping.go` adds
MapLitigationToFristenrechner — single source of truth for bridging the
litigation conceptual codes (INF / REV / APP / CCR / AMD / APM / OPP) onto
fristenrechner codes (UPC_INF / DE_INF / EPA_OPP / …). Ambiguous combos
(APP+DE, ZPO_CIVIL, AMD+DE) return ok=false; callers degrade to "no
narrowing" instead of guessing. Table-driven test covers every documented
mapping plus the ambiguous-degrade cases.
Frontend: `buildRowStack` filters cascade children by project context
along the proceeding axis (kebab segment lookup against the project's
fristenrechner code); auto-walks while filtered scope narrows to one;
caps depth via `cascadeAutoWalkStopAfter` after an "ändern" on a prefilled
row so the user lands at an active chip set without the auto-walk
re-engaging. Result panel narrows on the post-auto-walk effective slug,
not the URL slug. A one-time inline tooltip ("Diese Schritte ergeben sich
aus Ihrer Akte") surfaces when ≥2 rows render prefilled — dismissal flag
persists in localStorage.
Narrowing is purely additive: an Akte without a fristenrechner code
(11/11 live projects pre-Slice-5 were NULL) degrades to today's
forum-only behaviour. Slice 3 (mobile polish + search relocation) follows.
Refs: docs/design-determinator-row-cascade-2026-05-13.md §10 Slice 2 + §4 + §5.
|
|||
| 7dae9b2216 |
test(t-paliad-195): adapt fixtures + assertions to post-drop shape
Phase 3 Slice 9 test cleanup. Seeds + assertions no longer touch
the legacy columns (mig 091 dropped them).
- projection_service_test.go (Slice 7 fixtures): INSERT seeds
drop the is_mandatory / is_optional columns from the
paliad.deadline_rules column list. Defaults are fine; the
spawn-graph test doesn't read those.
- rule_editor_service_test.go (Slice 11a fixtures): same drop
on the SLICE11A_PREVIEW seed.
- fristenrechner_test.go (Slice 8 wire-shape assertion): drops
the wireFlagsFromPriority round-trip check (the bool pair is
no longer on the wire). The enum-membership invariant
survives. evalConditionExpr table-driven test rewritten —
legacy condition_flag fallback cases removed (the fallback
is gone in Slice 9), pure-jsonb cases retained.
- deadline_rule_service_test.go (Slice 2 backfill integrity):
legacy-pair bucket assertion dropped; the priority-non-NULL
invariant still holds via the CHECK constraint. The
condition_flag cross-check now joins the pre-mig-091 snapshot
when present (a future cleanup slice drops the snapshot
along with this code path).
Build + tests green.
|
|||
| 99a72a744f |
refactor(t-paliad-195): drop legacy fields from Go service surface
Phase 3 Slice 9 Go cleanup. With mig 091's column drops live, the
service layer stops reading + emitting the legacy shape:
- models.DeadlineRule: drop IsMandatory, IsOptional, ConditionFlag,
ConditionRuleID fields. Comment block flags Slice 9 as the
closeout slice.
- DeadlineRuleService.ruleColumns: SELECT no longer enumerates the
dropped columns. The post-Slice-9 schema is the live shape.
- FristenrechnerService.UIDeadline: drops IsMandatory + IsOptional
fields. Frontend reads `priority` directly post-Slice-8; the
legacy emit was kept "for one release" and that release is now.
- evalConditionExpr signature: drops the conditionFlag fallback
param. NULL / "null" expressions return true (unconditional);
the legacy text[] fallback was the only reason for the second
param. New helpers hasConditionExpr + extractFlagsFromExpr fill
the gaps (alt-swap guard + RuleCalculation.FlagsRequired list).
- FristenrechnerService.Calculate + calculateByTriggerEvent +
EventTriggerService.Trigger: switched to the new (single-arg)
evalConditionExpr; alt-swap guard now uses
hasConditionExpr(r.ConditionExpr) instead of the dropped
len(r.ConditionFlag) > 0 check.
- FristenrechnerService.CalculateRule: RuleCalculationRule.IsMandatory
derived from priority via wireFlagsFromPriority (kept for the
result-card panel TS contract). FlagsRequired walks the jsonb
gate tree to enumerate {"flag":"X"} leaves (replaces the
dropped condition_flag enumeration).
- RuleEditorService.Create + CloneAsDraft INSERT statements:
dropped is_mandatory / is_optional / condition_flag from the
column lists. Live shape only.
Test fixtures (projection_service_test.go, rule_editor_service_test.go,
fristenrechner_test.go) all updated to write the live shape on
seed; the evalConditionExpr table-driven test dropped its legacy
fallback cases (the fallback no longer exists) and now exercises
20 pure-jsonb scenarios across AND/OR/NOT compositions.
The deadline_rule_service_test backfill assertion lost its
(is_mandatory, is_optional) bucket cross-check (those columns are
gone); the priority-non-NULL invariant still holds via the CHECK
constraint. condition_flag cross-check now joins the pre-mig-091
snapshot table (when present) instead of the live row.
|
|||
| edc81bbbc2 |
feat(t-paliad-194): AichatPaliadinService + PALIADIN_BACKEND=aichat env gate (m/paliad#38 Phase B)
Adds the Phase B paliad-side migration: a thin HTTP client of the centralized aichat backend shipped in m/mAi#207 Phase A (darwin's mai/darwin/issue-207-aichat branch). Implements the same services.Paliadin interface as LocalPaliadinService / RemotePaliadinService — handler plumbing is unchanged, the cutover is a single env-var flip. internal/services/aichat_paliadin.go (~530 LoC): - POST /chat/turn + POST /chat/reset + GET /chat/health via the aichat JSON envelope (mirrors m/mAi internal/aichat/api/types.go verbatim; no module import to keep paliad self-contained). - Per-turn HS256 JWT mint (uses paliadin_jwt.go from the prior commit) when SUPABASE_JWT_SECRET is configured. Aichat owns file write + cleanup; we just sign and ship. - Service-wide health-gate cache (10 s success window, no failure cache — failures re-probe so recovery surfaces immediately). - Per-user-window primer cache. Pulls up to MaxPrimerTurns prior exchanges from paliad.paliadin_turns and ships them in TurnRequest. Primer so a pane respawn (pane_spawned=true in response) doesn't strand the user with a cold claude. Cleared on ResetSession + pane_spawned response. - Username from email_localpart per m's §13 Q2 pick (sanitized inside aichat). Nil-DB fallback: "user-<uuid8>". - Maps aichat's typed wire errors (auth_failed, persona_unknown, mriver_unreachable, bootstrap_failed, timeout, shim_error) onto paliad's existing audit-row codes — preserves the German i18n table in paliadin.ts unchanged (no new strings needed per design §11). cmd/server/main.go: - PALIADIN_BACKEND env: "aichat" → AichatPaliadinService, anything else → existing remote/local/disabled tree. Default = legacy, so every existing deploy is byte-identical until flipped. - buildAichatPaliadinConfig validates AICHAT_URL + AICHAT_TOKEN at boot; AICHAT_PERSONA defaults to "paliadin". JWT secret threaded in so per-user RLS is on by default. Tests cover constructor defaults, health-gate caching + retry + expiry, ResetSession wiring, error-envelope decoding + classifier, HTTP-layer auth/JSON wiring via a roundTripper, JWT mint integration, TurnContext → meta packing, and the env-gate helper. go test ./... green. NOT self-merged — head owns the merge per task instructions. |
|||
| 08e20883a5 |
feat(t-paliad-194): revive per-turn JWT mint for Paliadin (folded-in t-paliad-156)
Restored from mai/planck/paliadin-per-user-rls (parked, see m/paliad#12 cancel note). The aichat Phase B path (next commit) consumes mintTurnJWT to sign a short-lived HS256 token per turn, scoped to the calling user (sub=userID, role=authenticated, aud=authenticated, iss=paliad/paliadin). Aichat passes the raw token through to the claude pane on mRiver via a per-turn file (managed by aichat's runner, not paliad's transport). The SKILL.md reads it and `SET LOCAL request.jwt.claims = …` before every paliad.* query, which makes RLS evaluate as the user instead of as service role. TTL: 2 min default — covers aichat's 120 s persona timeout + HTTP slack, short enough that a leaked JWT is uninteresting. Each turn mints fresh; no caching. No call sites yet — paliadin_remote.go / paliadin.go are unchanged on this commit. The plumbing arrives with AichatPaliadinService. |
|||
| 1c45c93570 |
feat(t-paliad-192): admin orphan list/resolve endpoints
Slice 11b backend addition for the orphan-resolution flow in the
/admin/rules UI. The Slice 10 fuzzy-match backfill (mig 089) staged
legacy paliad.deadlines rows the matcher could not bind to a unique
deadline_rule into paliad.deadline_rule_backfill_orphans. This adds
the two endpoints the editor needs to surface and resolve them:
GET /admin/api/orphans — unresolved staging rows,
hydrated with the candidate
rule rows in one round-trip.
POST /admin/api/orphans/{id}/resolve — picks a rule_id from the
candidate set, writes it onto
the deadline, and flips
resolved_at + resolved_rule_id
on the staging row in a single
tx.
The methods live on RuleEditorService because they share the same admin
surface and audit semantics; resolved_rule_id + resolved_at on the
staging row is the audit trail (mig 089 COMMENT). reason is captured
into paliad.audit_reason in the same tx so any future audit trigger on
paliad.deadlines picks it up automatically.
Typed errors:
ErrOrphanAlreadyResolved → 409 in handler
ErrOrphanCandidateMismatch → 400 in handler
Route ordering matches Slice 11a's pattern: the static path is
registered alongside the existing /admin/api/rules family inside the
adminGate block in handlers.go.
|
|||
| 936c4967fd |
test(t-paliad-191): rule-editor lifecycle + preview coverage
Live-DB tests (TEST_DATABASE_URL-gated) for Phase 3 Slice 11a:
TestRuleEditorService_Lifecycle — full create→update→publish→archive
→restore round-trip on synthetic fixtures (SLICE11A_TEST_PT
proceeding + rules). Asserts:
1. Create returns lifecycle_state='draft' with published_at=NULL.
2. UpdateDraft on a draft succeeds and lands the patch.
3. CloneAsDraft from a published row creates a new draft with
draft_of pointing at the source.
4. Publish flips draft → published, sets published_at, AND archives
the cloned-from peer (verified by re-reading the peer's
lifecycle_state post-publish).
5. Archive flips published → archived.
6. Restore flips archived → published.
7. ListAudit returns ≥ 3 rows newest-first with non-empty reason
strings (the mig 079 trigger captured them).
8. Empty audit_reason on UpdateDraft → ErrAuditReasonRequired.
9. UpdateDraft on a published row → ErrInvalidLifecycleState.
10. Restore on a non-archived row → ErrInvalidLifecycleState.
TestRuleEditorService_Preview — calculator override hook coverage
(SLICE11A_PREVIEW_PT proceeding + a published rule). Clone the
root rule, patch DurationValue 30 → 60 on the draft, call Preview
at trigger_date=2026-01-15. Asserts:
- Baseline Calculate (no overrides) returns the published rule's
dueDate (~30 days after trigger).
- Preview returns a DIFFERENT dueDate (substitutes the draft's
60-day duration via RuleOverrides) — sanity check that the
override pipeline reached the calculator and shifted the date.
- Both responses are non-empty (the rule is reachable).
Cleanup: WHERE name LIKE 'SLICE11A_TEST_%' / 'SLICE11A_PREVIEW_%'
AND code = 'SLICE11A_TEST_PT' / 'SLICE11A_PREVIEW_PT' so production
rules are untouched. audit_reason set on every seed / cleanup write
so the mig 079 trigger doesn't reject the seed transactions.
Build clean, full local test suite green; this test skips when
TEST_DATABASE_URL is unset.
|
|||
| b21ce6dd7b |
feat(t-paliad-191): RuleEditorService — admin rule lifecycle
Phase 3 Slice 11a (m's Q5 option C: "I need to see these things,
admin only"). RuleEditorService owns the admin-only lifecycle for
paliad.deadline_rules:
Create → INSERT row with lifecycle_state='draft', published_at=NULL.
UpdateDraft → UPDATE WHERE id=$1 AND lifecycle_state='draft'.
Published or archived rows must clone-as-draft first
(ErrInvalidLifecycleState otherwise — 409).
CloneAsDraft → INSERT deep copy of source row (published OR archived)
as a new draft with draft_of pointing at the source.
Lets editors propose changes to live rules without
mutating the live row.
Publish → UPDATE lifecycle_state='published', set published_at.
When draft_of != NULL, also archives the cloned-from
peer so each rule has at most one live row.
Archive → UPDATE lifecycle_state='archived' (allowed from
published OR draft).
Restore → UPDATE lifecycle_state='published' (only from archived).
Preview → Calls FristenrechnerService.Calculate with the draft
as a RuleOverrides entry — pure simulation, no DB
write. If draft_of is set, the override substitutes
for the peer (matching ID); otherwise it's appended.
ListAudit → SELECT paliad.deadline_rule_audit rows for one rule,
newest-first, with offset/limit pagination. Joined
with paliad.users.display_name for the changed_by
column.
ListRules → Admin list view with filters (proceeding_type_id,
trigger_event_id, lifecycle_state, fuzzy q over
name / name_en / rule_code).
ExportMigrationsSince → SQL blob generator for the migration-export
admin flow (Q-H-5 pure SQL format). v1 emits one
statement per audit row in chronological order;
Slice 11b polishes the output (header comment,
collapse consecutive UPDATEs).
Audit-reason invariant: every write method requires a non-empty
reason string. setAuditReasonTx writes it into the session-local
paliad.audit_reason setting in the same transaction as the
INSERT/UPDATE, so mig 079's trigger captures the rationale
forever. Empty reason → ErrAuditReasonRequired (400 in the handler).
Spawn cycle guard: validateSpawnNoCycle pre-checks Create + UpdateDraft
edits that touch spawn_proceeding_type_id against the global rule
graph. Reuses the design §6 cycle-guard semantics — walks the
target proceeding's spawn rules transitively; raises ErrCyclicSpawn
if any reachable proceeding is the source. Slice 7's runtime guard
catches anything this misses; the editor surface catches it at
edit time so the editor sees a clear 409 instead of a silent
projection failure.
Typed errors:
ErrRuleNotFound → 404 in handler
ErrInvalidLifecycleState → 409 in handler
ErrAuditReasonRequired → 400 in handler
ErrInvalidInput → 400 (re-uses the existing services-wide error)
ErrCyclicSpawn → 409 (re-uses Slice 7's typed error)
RuleAuditEntry struct extends models.DeadlineRuleAudit with a
display_name for the admin UI; distinct from services.AuditEntry
(the cross-source union for the site-wide audit panel) so the two
read paths don't conflict.
|
|||
| 358c64d172 |
feat(t-paliad-191): CalcOptions.RuleOverrides + applyRuleOverrides
Phase 3 Slice 11a calculator hook for the rule-editor preview (design §4.5, Q-H-4 option (a)). CalcOptions gains RuleOverrides []models.DeadlineRule. When non-empty, FristenrechnerService.Calculate substitutes any rule with matching .ID in the rule list with the override row, and appends overrides whose ID doesn't match an existing rule (net-new drafts the editor wants to preview). Wired into: - FristenrechnerService.Calculate (proceeding-tree path) - FristenrechnerService.calculateByTriggerEvent (Pipeline-C path) Helper: applyRuleOverrides(src, overrides) — small linear scan since the override slice is 1 row in practice (the draft being previewed). Empty overrides → pass-through (existing behaviour unchanged). No DB writes; pure simulation. The editor's "what would this rule do?" affordance uses this to preview the draft against the rest of the proceeding's rules without mutating the live corpus. |
|||
| d6f5e0c97e |
feat(t-paliad-189): UIResponse emits priority + conditionExpr
Phase 3 Slice 8 wire-shape swap. UIDeadline gains:
- Priority: 4-way enum (mandatory|recommended|optional|informational)
— the authoritative field the frontend reads after Slice 8 to drive
save-modal pre-check + notice-card rendering.
- ConditionExpr: jsonb gate predicate (design §2.4 long form),
emitted verbatim as json.RawMessage so the rule editor (Slice 11)
+ admin surfaces can render the gating shape.
Additivity invariant: the legacy IsMandatory / IsOptional pair stays
populated via wireFlagsFromPriority (mandatory→T/F, optional→T/T,
recommended|informational→F/F). Pre-Slice-8 frontends keep working;
Slice 9 drops the legacy fields once the frontend cutover is verified
in prod.
All three calculator paths populate the new fields:
- FristenrechnerService.Calculate (proceeding-tree, Pipeline A)
- FristenrechnerService.calculateByTriggerEvent (Pipeline C)
- EventTriggerService.Trigger (event-keyed endpoint, Slice 6)
Backend live-DB test asserts:
- Every UPC_INF rule's priority is in the unified enum.
- The wireFlagsFromPriority round-trip holds for every row.
- At least one rule carries a populated conditionExpr (the 17
with_ccr / with_amend / with_cci rules from mig 084).
|
|||
| a55f45ebea |
feat(t-paliad-189): instance_level on project Create/Update
Phase 3 Slice 8 part 1 — wire the instance_level data field (mig 080
column, shipped in Slice 1) through the project service + handler.
- CreateProjectInput / UpdateProjectInput gain InstanceLevel *string.
Empty string is the explicit "clear" sentinel.
- validateInstanceLevel + nullableInstanceLevel helpers mirror the
OurSide pattern. Allowed values per mig 080 CHECK: 'first' |
'appeal' | 'cassation' | NULL.
- Service rejects bad values with ErrInvalidInput (existing handler
error-mapping surfaces this as HTTP 400 with the standard message).
- projectColumns SELECT now includes instance_level so reads
populate the field; Project struct already has the field from
Slice 1.
- handleCreateProject accepts instance_level from the raw map; Update
handler uses the standard JSON decoder into UpdateProjectInput.
Live-DB test exercises:
- Create with instance_level='first' → roundtrips.
- Update to 'appeal' → roundtrips.
- Update to '' → NULL after the trip.
- Update to 'supreme' → ErrInvalidInput.
The DB CHECK on mig 080 is the defence-in-depth backstop should an
SQL-direct INSERT bypass the service.
|
|||
| b64d929586 |
test(t-paliad-188): spawn expansion + cycle guard + multi-spawn
Live-DB test for the Phase 3 Slice 7 spawn wiring. Seeds three
synthetic proceedings (SLICE7_TEST_A/B/C) + rules under them, with
audit-reason wrappers so the mig 079 trigger writes informative
audit rows during seed / cleanup. Three scenarios:
1. A → B single spawn. Expansion emits one spawned-into row whose
RuleCode matches B's root rule. DependsOnRuleCode references
A's spawn rule; DependsOnDate is parsed from the synthetic
UIDeadline date (2026-03-15); Track="spawn" so the frontend
boundary divider lights up. DeadlineRuleID points at B's
root rule UUID.
2. Cycle A → B → A. Adds a spawn rule on B back to A; rerun
expansion → ErrCyclicSpawn surfaces (errors.Is matches). The
visited-set guard catches the second-hop attempt to recurse
into A which is already in the chain. No infinite loop.
3. Multi-spawn defensive. Drops the cycle edge, adds a second
spawn rule on A targeting C. Expansion emits two spawned-into
rows (B's root + C's root); the test asserts both RuleCodes
appear in the output regardless of order.
Cleanup: WHERE name LIKE 'SLICE7_TEST_%' AND code LIKE
'SLICE7_TEST_%' so production rules are untouched. audit_reason
set before every INSERT/DELETE so the mig 079 trigger doesn't
reject the seed transactions.
Build clean, full local test suite green; this test skips when
TEST_DATABASE_URL is unset.
|
|||
| e30bfe89da |
feat(t-paliad-188): cross-proceeding spawn wiring + cycle guard
Phase 3 Slice 7 Step G (design §6). Closes the half-finished
projection_service.go:896-901 spawn-skip from the t-178 audit.
What lands:
- DeadlineRuleService.ListByProceedingTypeIDs(ids): bulk-load
rules for a set of spawn-target proceedings in one round-trip.
Skips hydrateConceptDefaultEventTypes (SmartTimeline doesn't
need concept-default event_types on spawned rows). Pre-sorted
by (proceeding_type_id, sequence_order) so callers pick the
target's root rule via the first slot per proceeding.
- ProjectionService.expandCrossProceedingSpawns: walks the spawn
graph rooted at the project's source proceeding. For each rule
with is_spawn=true AND a non-NULL spawn_proceeding_type_id,
resolves the target proceeding's root rule and emits a
spawned-into TimelineEvent with:
Kind="projected", Track="spawn", Status="predicted",
DependsOnRuleCode=<source.code>, DependsOnRuleName=<source.name>,
DependsOnDate=<source's computed due date when available>.
SpawnLabel on the source rule, if set, is appended to the
target title as "<target name> (<spawn_label>)".
- Cycle guard: visited-set DFS keyed by proceeding_type_id. The
source proceeding is seeded into `visited` before the walk;
when any spawn's target is already in `visited`, the helper
returns ErrCyclicSpawn with rule + proceeding context. The
caller (computeProjections) catches the error and degrades to
"no spawned rows" — better than failing the whole projection.
ProjectionMeta.SpawnCycleDropped surfaces the degradation so
the caller can log + show a "Spawn-Auflösung übersprungen"
banner.
- Recursion: expandCrossProceedingSpawns recurses into the
target proceeding's spawn rules (depth+1) so a chain
A → B → C surfaces every hop. maxSpawnDepth (4) is a safety
belt on top of the visited-set guard.
Live data semantics: the live corpus has 6 active is_spawn=true
rules — AMD.ccr.amend, AMD.rev.amend, APP.ccr.appeal,
APP.inf.appeal, APP.rev.appeal, CCR.ccr.counterclaim. ALL six have
spawn_proceeding_type_id IS NULL today, so the live SmartTimeline
emits zero spawned-into rows. Slice 7 wires the code path; the
backfill of spawn_proceeding_type_id on these 6 rules is a
separate concern (the design doc's mig 093 was deferred — the
litigation-category proceedings these rules sit in were retired
from project-binding in Slice 5).
Calculator stays scoped (Option A, design §6.2): the unified
FristenrechnerService.Calculate does NOT follow spawns. The
SmartTimeline projection service is the sole consumer that chains
across proceedings. UIResponse.Deadlines for a proceeding only
contains rules from that proceeding; spawn resolution happens at
the projection layer.
projection_service.go:896-901 comment updated to reflect the new
post-Slice-7 reality (calculator stays scoped; spawned rules
arrive via expandCrossProceedingSpawns, not via the calculator's
Deadlines list).
|
|||
| 65617a5dcb |
test(t-paliad-187): EventTriggerService integration coverage
Live-DB test (TEST_DATABASE_URL-gated) for the Phase 3 Slice 6
endpoint covering:
1. Missing both event_type_id + concept_id → ErrInvalidInput.
2. Malformed trigger_date → ErrInvalidInput.
3. Unknown event_type_id → ErrInvalidInput.
4. event_type_id only → parity proxy against
EventDeadlineService.Calculate (Slice-3 legacy delegate). Both
code paths share the unified backend post-Slice-4 so the
returned rule-name multiset must be identical. Selects the
test fixture live: ANY event_type with a non-empty
trigger_event_id bridge to active deadline_rules.
5. concept_id only → returns rules linked by concept_id FK.
Picks the concept with the most rules so we exercise the
ordering path (proceeding_type_id NULLS LAST,
sequence_order). Spot-checks each rule's RuleID parses as UUID.
6. event_type_id + concept_id together → UNION dedupe. Today's
corpus has the two paths on disjoint rule sets so the
additive-count assertion holds; if a future seed links a
concept to a Pipeline-C rule, the dedupe branch fires and the
test logs (not fails) the count divergence for review.
7. Perspective filter — locates a concept with both claimant and
defendant rules (skips gracefully when the corpus lacks one)
and asserts the defendant-perspective response omits every
claimant-party rule.
Build clean, full local test suite green; this test skips when
TEST_DATABASE_URL is unset.
|
|||
| 253dc1d1b3 |
feat(t-paliad-187): EventTriggerService.Trigger
Phase 3 Slice 6 (design §5) — service-side implementation of the new
unified event-trigger entry point. Accepts (event_type_id?,
concept_id?, trigger_date, flags?, court_id?, perspective?) and
returns the same UIResponse the proceeding-tree calculator emits.
Rule discovery:
- event_type_id → SELECT paliad.event_types.trigger_event_id →
DeadlineRuleService.ListByTriggerEvent (Pipeline-C path, post-
Slice-3 unified backend).
- concept_id → DeadlineRuleService.ListByConcept (new method on
the rule service: SELECT deadline_rules WHERE concept_id = $1
AND is_active = true). Direct FK lookup; Pipeline-A cascade
leaf semantic.
- Both → UNION deduped by rule.id (seen-set in Go; small rule
sets, no SQL DISTINCT overhead).
- Validation: at least one of the two must be set;
ErrInvalidInput otherwise. Unknown event_type_id also bubbles
as ErrInvalidInput (404-style).
Math reuses the Slice-4 unified helpers verbatim:
- applyDuration(base, value, unit, timing, country, regime, holidays)
- evalConditionExpr(expr, condition_flag, flags) — long-form
jsonb gate with legacy AND-of-array fallback.
- wireFlagsFromPriority(priority) — derives IsMandatory + IsOptional
so the wire shape stays calibrated against /api/tools/fristenrechner.
Composite combine_op (max/min) + legacy alt-swap-on-flag are
applied in the same mutually-exclusive order the proceeding-tree
calculator uses (combine_op IS NULL ⊕ alt-swap-on-flag-met).
matchesPerspective filter is permissive: empty perspective →
pass-through; NULL party → pass-through; only drops on explicit
claimant↔defendant mismatch. Court / both / NULL rules always
render.
is_court_set rules surface IsCourtSet=true and clear the computed
date — matches the proceeding-tree calculator's "wird vom Gericht
bestimmt" rendering.
UIResponse.ProceedingType / ProceedingName stay empty (caller
already has the event-type / concept context); same contract
calculateByTriggerEvent uses.
DeadlineRuleService.ListByConcept: ORDER BY proceeding_type_id NULLS
LAST, sequence_order so a multi-proceeding concept doesn't
interleave its constituent rules in the timeline.
|
|||
| 7afbf52f3e |
test(t-paliad-186): proceeding-type category guard
Live-DB test (TEST_DATABASE_URL-gated) for Phase 3 Slice 5 that
covers the full chain:
1. Migration smoke: post-mig 087, no project points at a
non-fristenrechner-category proceeding_types row.
2. ProjectService.Create with a litigation-category id returns
ErrInvalidProceedingTypeCategory (service-layer guard fires
before any DB write).
3. mig 088 trigger rejects a raw INSERT that bypasses the Go
service — defence-in-depth assertion. Errors on the trigger
raise; test asserts a non-nil error.
4. Fristenrechner-category id (UPC_INF) succeeds. The created
project carries the expected proceeding_type_id.
The four sub-assertions hit each layer of the guard chain (picker
filter → service guard → DB trigger) plus the migration smoke. Any
regression in the chain surfaces here before the deploy.
Tests + main build clean; live test skips when TEST_DATABASE_URL
is unset.
|
|||
| 5b81f2159e |
feat(t-paliad-186): service guard + ?category filter
Phase 3 Slice 5 Go-side: ErrInvalidProceedingTypeCategory typed
error + service-layer validation + handler-level mapping +
listing-side filter.
- services.ErrInvalidProceedingTypeCategory: typed error so
handlers can map to a 400 with a bilingual user-facing message
distinct from generic ErrInvalidInput.
- ProjectService.validateProceedingTypeCategory: looks up the
referenced proceeding_types.category and rejects with the typed
error if it's not 'fristenrechner'. Called from both Create and
Update before any DB write.
- DeadlineRuleService.ListProceedingTypesByCategory: extends the
existing ListProceedingTypes with an optional category filter.
Empty category passes through (legacy callers unaffected).
- GET /api/proceeding-types-db?category=<value>: handler reads the
query param and forwards it to the service. The project-create
/ project-edit pickers pass 'fristenrechner' so users never see
retired litigation codes.
- writeServiceError: maps ErrInvalidProceedingTypeCategory to
HTTP 400 with a bilingual message ("Verfahrenstyp muss ein
Fristenrechner-Typ sein / proceeding type must be a
Fristenrechner type"). Distinct from generic ErrInvalidInput so
the frontend can show a more helpful hint.
Defence-in-depth chain: frontend picker filter → service-layer
validation → DB trigger (mig 088). Each backstops the next.
|
|||
| d7bb238e46 |
test(t-paliad-185): table-driven unit tests for new helpers
Phase 3 Slice 4 test coverage. Adds:
- TestEvalConditionExpr (20 sub-cases): AND/OR/NOT compositions,
single-flag leaf, nested AND-of-OR-and-NOT, empty-args
vacuous-truth semantics, NULL-expr → legacy condition_flag
fallback (preserves the AND-of-flags behaviour for any
pre-Slice-2-style row), malformed JSON / unknown op / malformed
NOT all defensive-true (rule still renders).
- TestWireFlagsFromPriority (6 sub-cases): exhaustive enum +
safe-default for unknown values. Matches the reverse of the
Slice 2 mig 083 backfill mapping.
- TestApplyDuration_Matrix (7 sub-cases): 4 units × multiple
timings × calendar/holiday rollover. Includes the
Thu+1d-over-Tag-der-Arbeit edge that exercises the
weekend+holiday cascade.
Test file housekeeping:
- Drops TestIsCourtDeterminedRule (the function it tested no
longer exists; equivalence is preserved by mig 082's WHERE
predicate and verified by the Slice 2 backfill integrity test).
- Drops the unused models import that becomes orphaned.
- Renames the EventDeadlineService.applyDuration / addWorkingDays
method-receiver tests to call the package-level functions
directly. Same test names + expected dates; only the helper
signature shifted.
- Parity test still calls the same applyDuration body, now via
the unified helper.
Full test suite green locally (live DB tests skip when
TEST_DATABASE_URL is unset, as ever).
|
|||
| 990cc2b797 |
refactor(t-paliad-185): unified calculator (Slice 4 Step D)
Phase 3 Slice 4 Step D (design §3.D, the last foundation slice).
Pure Go — no migrations. Collapses the proceeding-tree + Pipeline-C
calculators onto a single set of unified helpers + reads, all
without changing wire output.
Helpers (package-level in services/fristenrechner.go):
applyDuration(base, value, unit, timing, country, regime, holidays)
→ (raw, adjusted, didAdjust, reason)
Single source-of-truth for date arithmetic. Replaces:
- addDuration (proceeding-tree, no timing / working_days)
- applyDurationOnCalendar (Slice 3 Pipeline-C-only)
- EventDeadlineService.applyDuration / addWorkingDays methods
Handles: timing=before/after, units days/weeks/months/working_days,
weekend + holiday rollover for calendar units. working_days lands
on a working day by construction (no post-rollover).
evalConditionExpr(expr jsonb, conditionFlag []string, flags) bool
Long-form jsonb gate evaluator (design §2.4). Grammar:
leaf: {"flag":"X"}
AND: {"op":"and","args":[<n>...]}
OR: {"op":"or","args":[<n>...]}
NOT: {"op":"not","args":[<one>]}
NULL / empty / "null" → unconditional. Defensive fall-through
on malformed JSON / unknown ops (rule still renders — never
silently drop a deadline). Fallback to condition_flag
AND-semantics when expr is NULL but the legacy column is set
(defensive cover for any row Slice 2 missed).
wireFlagsFromPriority(priority) → (isMandatory, isOptional)
Derives the legacy wire pair from the unified priority enum:
mandatory → (T, F) — statutory must
optional → (T, T) — RoP.151 (opt-in, ☐ pre-unchecked)
recommended → (F, F) — situational filing
informational → (F, F) — never saves today
unknown → (T, F) — safe default
Slice 8 will swap the wire to emit priority directly.
Calculate (proceeding-tree) refactor:
- r.IsCourtSet column read direct, isCourtDeterminedRule() heuristic
function deleted. Slice 2 backfill (mig 082) wrote the column
using the exact heuristic predicate; column-read saves the
per-rule branch test at runtime.
- r.Priority drives the wire IsMandatory / IsOptional pair via
wireFlagsFromPriority. Read of r.IsMandatory / r.IsOptional
columns retained (compat-mode) but never decision-shaping.
- r.ConditionExpr drives the gate; condition_flag is the fallback.
- Added combine_op composite (max/min) branch for proceeding-tree
rules. No live Pipeline-A rules carry combine_op today (it's a
future-friendly column the rule editor will surface); the
branch is reachable but produces zero diffs on the current
corpus.
- timing=before + working_days now usable on proceeding-tree rules
via the unified applyDuration. No live Pipeline-A rules use them.
CalculateRule (single-rule card-click) refactor: same column reads
(IsCourtSet, ConditionExpr, Priority), unified applyDuration.
calculateByTriggerEvent (Pipeline C) refactor: switched to the
unified applyDuration; loses the redundant post-pick reason
recompute (applyDuration now returns reason directly).
EventDeadlineService.Calculate composite-note recompute now calls
the package-level applyDuration instead of the deleted method.
Frontend wire shape stays pixel-identical pre/post-Slice-4. The 17
condition_flag rules in the live corpus continue to gate via the
same (a) leaf or (b) AND-of-args evaluator branches mig 084
produced; jsonb path is exercised first, the array fallback
remains as defensive cover.
|
|||
| 6cddb2e587 |
test(t-paliad-184): 77-row Pipeline-C parity assertion
LOAD-BEARING regression guard for Phase 3 Slice 3. For every distinct
trigger_event_id in paliad.event_deadlines, calls Calculate (now
delegating through FristenrechnerService) AND independently re-runs
the legacy applyDuration math against the source row, asserting:
- count(returned deadlines) == count(active source rows for trigger)
- id, title, titleDE, durationValue, durationUnit, timing all match
- dueDate matches the independently-computed expected date (even
a 1-day diff fails the test — that's the entire point of the
read-only cutover window)
- isComposite matches (CombineOp != nil && alt_* set)
Skipped when TEST_DATABASE_URL is unset, mirroring audit_service_test.go.
Sweep guard: at least 77 rows must have been checked across all
triggers — if the test only walks 0 triggers (e.g. due to a SELECT
glitch), the final tally raises.
Trigger date is an arbitrary working day (2026-01-15) so weekend
rollover noise is minimal; the parity comparison is against an
inline expected value, not a fixed snapshot, so any date that
exercises the calculator works.
|
|||
| 8a814e3442 |
refactor(t-paliad-184): EventDeadlineService.Calculate delegates
Phase 3 Slice 3 service-side rewire. EventDeadlineService.Calculate
now:
1. Looks up trigger event metadata (unchanged — the legacy response
shape still carries TriggerEvent + TriggerDate at the top level).
2. SELECTs source event_deadlines rows for the trigger to recover
(id, duration, alt_*, combine_op, notes_en) — the unified
UIResponse drops those fields. SELECT is still allowed by the
mig 086 read-only trigger; only writes are blocked.
3. Delegates the rule SELECT + math to FristenrechnerService.Calculate
with TriggerEventIDFilter set.
4. Merges the unified result with the source rows (join by Name =
title_de) to produce the legacy EventDeadlineResult shape with
ID, ruleCodes, isComposite, compositeNote intact.
5. Loads rule_codes from event_deadline_rule_codes (also still
readable) by source.id.
Public signature unchanged — /api/tools/event-deadlines callers see
no diff. The legacy applyDuration / addWorkingDays helpers stay on
EventDeadlineService for the pure-Go unit tests + the composite-note
leg-pick that the unified UIDeadline doesn't expose.
main.go wiring: NewEventDeadlineService gains the FristenrechnerService
dependency.
|
|||
| 5f9a8b2ef4 |
feat(t-paliad-184): FristenrechnerService.calculateByTriggerEvent
Phase 3 Slice 3 calculator-side rewire. Adds the Pipeline-C branch
to FristenrechnerService so the unified backend can serve
event-driven deadlines:
- CalcOptions.TriggerEventIDFilter *int64 — when non-nil, Calculate
dispatches to calculateByTriggerEvent (proceedingCode ignored).
- calculateByTriggerEvent — flat-rule calculator: SELECT rules
WHERE trigger_event_id = X, compute each via the new
applyDurationOnCalendar helper (handles timing='before',
working_days, combine_op alt-leg max/min). No parent_id chains,
no flag gating, no IsRootEvent / IsCourtSet semantics — those
are Pipeline-A concerns.
- applyDurationOnCalendar + addWorkingDays — package-level helpers
that the proceeding-tree calculator's existing addDuration
doesn't cover. Slice 4 will fold them into a single unified
helper when the proceeding-tree side also reads timing +
working_days from the unified rule shape.
- DeadlineRuleService.ListByTriggerEvent — SELECT rules scoped to
a single trigger_event_id, ORDER BY sequence_order (preserves
the 1000 + ed.id ordering mig 085 wrote). Skips
hydrateConceptDefaultEventTypes since Pipeline-C rules don't
carry concept_id today.
UIResponse for trigger-event calls returns empty ProceedingType /
ProceedingName — EventDeadlineService owns the trigger metadata in
the legacy CalculateResponse shape. That's a stable contract for
the caller and avoids polluting UIResponse with trigger-event-only
fields.
|
|||
| 32a620b788 |
test(t-paliad-183): assert backfill integrity for Slice 2
Live-DB test (TEST_DATABASE_URL-gated, mirrors Slice 1 pattern)
validating mig 082/083/084 landed correctly:
1. is_court_set matches isCourtDeterminedRule() exactly. Counts
rows where is_court_set != (primary_party='court' OR
event_type IN ('hearing','decision','order')); must be zero.
2. priority is non-NULL everywhere (CHECK guards the schema —
this is belt-and-braces). Buckets by (is_mandatory,
is_optional) and asserts the design §2.3 mapping:
T/F → mandatory; T/T → optional; F/* → recommended.
3. condition_expr translation is complete + non-spurious:
- every non-empty condition_flag has non-NULL condition_expr
- every NULL/empty condition_flag has NULL condition_expr
- single-flag rows: condition_expr ->> 'flag' = condition_flag[1]
- multi-flag rows: condition_expr ->> 'op' = 'and' AND
jsonb_array_length(args) = array_length(condition_flag, 1)
The Slice 1 test's "every row priority='mandatory' && !is_court_set"
assertion is loosened to "priority in enum" + "lifecycle_state='published'"
since Slice 2 backfills now mutate those defaults.
Build clean, full test suite green (live DB tests skip locally).
|
|||
| 1f8230b264 |
feat(t-paliad-182): models + service compat-read for unified rules
Phase 3 Slice 1 Go-side of mig 078–080. Compat-mode reads: the
service selects BOTH the legacy shape (is_mandatory, is_optional,
condition_flag, condition_rule_id) and the new shape (priority,
condition_expr, is_court_set, trigger_event_id,
spawn_proceeding_type_id, combine_op, lifecycle_state, draft_of,
published_at). Existing callers stay on the legacy fields until
Slice 4 cuts the calculator over.
Adds:
- DeadlineRule field block for the nine Phase 3 columns. NULLable
jsonb (condition_expr) uses NullableJSON to dodge the
json.RawMessage NULL-scan trap (see Project.Metadata note from
t-paliad-138 dogfood).
- Project.InstanceLevel *string.
- DeadlineRuleAudit row struct (id, rule_id, changed_by,
changed_at, action, before_json, after_json, reason,
migration_exported).
- ruleColumns const extended to project every new column.
Test (TEST_DATABASE_URL-gated, mirrors audit_service_test.go):
1. ruleColumns SELECT scans cleanly — every new column populates
its Go field.
2. Migration defaults land: priority='mandatory',
is_court_set=false, lifecycle_state='published' on every
pre-Slice-1 row.
3. Audit trigger writes one row on UPDATE WITH paliad.audit_reason
set, captures before+after JSON + reason.
4. Audit trigger RAISES on UPDATE WITHOUT paliad.audit_reason —
Slice 2 backfills fail loudly if they forget to set it.
5. paliad.projects.instance_level accepts NULL + first/appeal/
cassation, rejects 'final'.
Build clean, full test suite green (live DB test skipped locally).
|
|||
| 83a3d27fe0 |
feat(t-paliad-177): ShapeTimeline enum + render_spec wiring
Slice 4 step 1 (faraday-Q7). RenderShape gets a fourth member
ShapeTimeline, AllShapes extends, Validate accepts it. The
companion TimelineConfig struct stores the saved palette / density /
range-preset for a CV-timeline view so re-opening the view restores
the same visual settings — same vocabulary as the standalone
/projects/{id}/chart URL state, just persisted in user_views.render_spec
instead of the URL.
Validator mirrors the frontend's enum guards:
- known palettes (default | kind-coded | track-coded | high-contrast | print)
- known densities (compact | standard | spacious)
- known range presets (1y | 2y | all | custom)
- ISO-date strings length-bounded to 32 chars so a hostile editor
can't bloat the jsonb column.
Tests pin every accept/reject path in TestRenderSpec_TimelineConfigValidates.
Design ref: docs/design-project-chart-2026-05-09.md §11.5 + §14 Q7.
|
|||
| d8f7745f86 |
feat(t-paliad-177): chart export — iCal feed (deadlines+appointments only)
Server-side endpoint GET /api/projects/{id}/timeline.ics returns a
VCALENDAR + one VEVENT per actual deadline (VALUE=DATE all-day) and
appointment (UTC timestamp). Projected / milestone / off_script rows
are deliberately skipped — faraday-Q6 / m's pick: a calendar feed
must never carry predicted dates the user never confirmed, otherwise
Outlook fills with rule_code-derived events that erode trust.
FormatTimelineICS reuses the existing caldav_ical.go escape helpers
and writes through the same canonical UIDs (paliad-deadline-<id> +
paliad-appointment-<id>) so a re-subscribe updates entries instead
of duplicating them. Stable across re-exports = lawyer-safe.
Visibility piggybacks on ProjectionService.For + ProjectService.GetByID
(same gates as the chart page handler). Content-Disposition filename
slugged for portable ASCII so Outlook + Apple Calendar agree.
4 tests pin the contract: only deadline/appointment kinds emit
VEVENTs; undated rows skip cleanly; RFC 5545 §3.3.11 escaping for
; , \ \\n; empty input still produces a valid VCALENDAR.
i18n: 1 new key DE+EN.
Design ref: docs/design-project-chart-2026-05-09.md §7.8.
|
|||
|
|
c2f1c29b10 |
fix(t-paliad-176): FilterBar timeline narrowing + Nur-direkt subtree skip
Two regressions from SmartTimeline Slices 2-4 dogfood @ 2026-05-09: m/paliad#32 — clicking timeline_status / timeline_track / project_event_kind chips changed URL params but the rendered list never narrowed. Two causes: (1) the Verlauf bar mounted only "time" + "project_event_kind" axes — the timeline_status / timeline_track chips never appeared. (2) the customRunner drained predicates into `loadEvents` which writes the legacy `events` array; the SmartTimeline render reads `timelineRows`, so the filter pass was a dead branch. Fix: mount all three axes on the bar; rewrite customRunner to drain state into `verlaufFilters`; renderTimeline applies them client-side via `applyTimelineRowFilters` before handing rows to renderSmartTimeline. project_event_kind is forwarded through the substrate-shaped predicate map (effective.filter.predicates.project_event.event_types); timeline_status / timeline_track sit on raw BarState — the customRunner signature now accepts the BarState snapshot as a second arg so the bar's first run (before the handle is assigned) can read them. Backend adds `ProjectEventType` to TimelineEvent + frontend TimelineEvent — needed so the project_event_kind chip can match against the underlying paliad.project_events.event_type for milestone rows. m/paliad#33 — "Nur direkt" pill flipped subtreeMode and re-fetched the timeline with ?direct_only=true, but ProjectionService.For honoured the flag only at the deadline / appointment / project_events SQL level. CCR sub-project lanes (Slice 3) and child-case lanes (Slice 4) loaded unconditionally, so the "direct" view still showed everything. Fix: `For` short-circuits to `forDirectSelfOnly` whenever DirectOnly is set. Single "self" lane, no CCR / parent_context / child-case aggregation. The level-policy kind/status filter still applies at higher levels so a Patent-level direct view doesn't leak off_script custom milestones the aggregated view filters out. Tests: two new live-DB subtests in TestProjectionService_LevelAggregation_Live pin the contract — Patent direct_only collapses to a single 'self' lane and excludes child-case events; Case-A direct_only excludes the CCR child's milestones (with subtree default still surfacing them). Build: go build/vet/test clean. bun run build clean (2171 keys). |
||
|
|
7da8802f9b |
feat(t-paliad-175): SmartTimeline Slice 4 — backend levelPolicy + lane aggregation + bubble-up
ProjectionService now dispatches on project type per design §5.1:
- Case (and unknown) — full detail flow: parent track + CCR sub-projects
+ parent_context for CCR children. Lanes mirror tracks ("self" +
"counterclaim:<id>" + "parent_context:<id>").
- Patent / Litigation / Client — lane-aggregated: load direct children
matching the axis (cases / patents / litigations), gather subtree
events per lane, apply (kinds, statuses) filter, tag rows with
LaneID = direct-child id. Calculator skipped at higher levels —
predicted future is a Case-level concern.
levelPolicy(projectType) returns the (kinds, statuses, lane_axis)
triple. Patent = deadlines+milestones with done/open/overdue;
Litigation + Client = milestones with done.
metadata.bubble_up on paliad.project_events (no schema change — uses
existing jsonb column) overrides the kind/status filter at higher
levels. Defaults per Q5: counterclaim_created / third_party_intervention
/ scope_change → true; custom_milestone → false (user opts in via
form checkbox). insertCounterclaimEvent now sets bubble_up=true on
both parent + child audit rows so the counterclaim_created milestone
surfaces at Patent / Litigation / Client.
Wire shape changed from []TimelineEvent to envelope {events, lanes} —
lane metadata can ride alongside the rows without exceeding header-
size limits when a Client-level projection has many lanes. Frontend
reads .events for the per-row contract and .lanes for parallel-column
rendering. X-Projection-* headers preserved for Slice 1-3 affordances
(lookahead toggle, track chip).
RecordCustomMilestone gains a bubbleUp bool param; persisted to
metadata.bubble_up only when true (so existing rows-without-it keep
the default-off behaviour).
Tests: TestLevelPolicy locks the triple table; TestRowSurvivesPolicy_
BubbleUpOverridesFilter pins the override contract; TestExtractBubbleUp
covers all per-event-type defaults + explicit override paths;
TestChildTypeForAxis pins the axis → type map. Live integration test
TestProjectionService_LevelAggregation_Live walks the patent-level
fixture: bubbled-up milestone surfaces, regular custom_milestone is
filtered, deadlines surface at Patent level.
Refs: docs/design-smart-timeline-2026-05-08.md §5 + §10 Slice 4
Refs: m/paliad#31, t-paliad-175
|
||
|
|
82888dea78 |
feat(t-paliad-174): SmartTimeline Slice 3 — projection parallel tracks + counterclaim handler
ProjectionService.For now composes multiple tracks instead of a single
"parent" stream. The viewed project always emits Track="parent"; visible
CCR children emit Track="counterclaim:<child_id>"; a project that is
itself a CCR (counterclaim_of != nil) pulls its target's events as
Track="parent_context:<parent_id>" so the lawyer working the CCR sees
the main proceeding without leaving the page (§4.5).
Each track runs the actuals + projection pipeline independently with
its own lookahead cap and dependency annotations against its own
proceeding's rule tree. SubProjectID + SubProjectTitle are populated on
non-parent rows so the frontend can render the sub-project title in the
column sub-header.
ProjectionMeta gains AvailableTracks; the handler surfaces it as the
new X-Projection-Tracks response header (CSV) so the wire shape stays
[]TimelineEvent (frozen since Slice 1).
POST /api/projects/{id}/counterclaim wraps ProjectService.CreateCounterclaim
— accepts proceeding_type_id / flip_our_side / title / case_number,
returns the new project's id + canonical /projects/<id> URL.
Tests: pure-function coverage for derivedCounterclaimOurSide (default
flip + R.49.2.b override + court/both pass-through). Live-DB integration
test covers the four invariants — CreateCounterclaim atomicity (parent
audit + child audit + our_side flip + sibling-under-patent placement),
parent's projection surfaces the counterclaim track, child's projection
surfaces parent_context, two-level CCR chains are rejected by both the
service guard and the schema-level trigger.
|
||
|
|
306bb11618 |
feat(t-paliad-174): SmartTimeline Slice 3 — counterclaim sub-project schema + service
Migration 077 adds paliad.projects.counterclaim_of (nullable FK ON DELETE SET NULL) plus a partial index. A trigger function rejects two-level CCR chains: a project with counterclaim_of NOT NULL cannot be the target of another CCR — UPC practice has no CCR-of-a-CCR shape, so reject it at the schema level rather than defending in the application layer. ProjectService gains LoadCounterclaimChildrenVisible (list visible CCR sub-projects against a parent) and CreateCounterclaim (atomic: project row + creator-as-lead team membership + audit rows on parent AND child). The CCR child is placed as a sibling under the same patent (§4.4), our side flips claimant↔defendant by default with a "Stimmt nicht?" override for the R.49.2.b CCI edge case, and the proceeding type defaults to UPC_REV. Title auto-suggests from the patent ancestor's patent_number when available. Tracker advances 76 → 77. |
||
|
|
331efc8603 |
feat(t-paliad-173): SmartTimeline Slice 2 frontend + #31 layered features
shape-timeline.ts:
- Renders Kind="projected" rows with Status-driven styling: predicted
(faded grey), court_set (dashed border), predicted_overdue (amber
fade with overdue glyph).
- "[Datum setzen]" inline date editor on every projected row with a
rule_code. Submit POSTs /api/projects/{id}/timeline/anchor; 200
triggers onChange (re-fetch + re-render); 409 renders the
predecessor_missing payload as inline error with a "Stattdessen
<predecessor> erfassen" link that scrolls to + opens the parent's
editor.
- "Folgt aus: <Name> (<Code>, <Date|Datum offen>)" footer on every row
with depends_on_rule_code, plus "[Pfad anzeigen]" expander hint.
- "[+ Mehr anzeigen]" / "[− Weniger]" lookahead toggle when backend's
X-Projection-Total header indicates more projections exist beyond
the current cap.
- Status pills on projected rows surface the status nuance next to
the kind chip without overwhelming the title.
projects-detail.ts:
- loadTimeline reads X-Projection-{Total,Lookahead} headers and forwards
them to renderSmartTimeline.
- Lookahead state persisted in localStorage per project (key
`paliad.smarttimeline.lookahead.<id>`).
- Removes the renderEvents() orphan (band-aid from t-paliad-172) and
every call site — renderTimeline is the only project-page render
path now. Aligns with fermat's commit-message hint in
|
||
|
|
85d7dd497c |
feat(t-paliad-173): SmartTimeline Slice 2 backend — projection + anchor + skip + sequence guard
Slice 2 of the SmartTimeline (docs/design-smart-timeline-2026-05-08.md
§6 + §9 + §10) bundled with m/paliad#31's layered requirements:
Migration 076:
- appointments.deadline_rule_id nullable FK to deadline_rules + partial idx
- deadlines.source CHECK widened to include 'anchor' (alongside existing
'manual','fristenrechner','rule','import').
ProjectionService (extended):
- Wires FristenrechnerService + DeadlineRuleService.
- For() now emits Kind="projected" rows for any rule lacking a matching
paliad.deadlines.rule_id / appointments.deadline_rule_id row, with
Status in {predicted | predicted_overdue | court_set}.
- Lookahead cap (default 7, override via ?lookahead=N, max 50): future
predicted rows beyond N are dropped; predicted_overdue + court_set
rows are exempt from the cap (#31 layer 1).
- Dependency annotations DependsOnRuleCode/Date/Name on every row that
carries a DeadlineRuleID, walked from the rule's parent_id chain
(#31 layer 2). Date prefers actuals over projections.
- AnchorOverrides built from completed deadlines (completed_at /
status='completed') + appointments tied via deadline_rule_id.
- triggerDate derives from the proceeding's root rule's anchor when
present, else today() as placeholder.
Anchor write path (POST /api/projects/{id}/timeline/anchor):
- Sequence guard: if rule.parent_id has no anchored actual, return
409 predecessor_missing with the missing rule's code/name DE+EN +
pre-formatted bilingual messages so the frontend can render an
inline error with a "Stattdessen <predecessor> erfassen" link
(#31 layer 3, no confirm-and-write override in v1).
- kind dispatch: rules with event_type IN ('hearing','decision','order')
write paliad.appointments with deadline_rule_id; everything else
writes paliad.deadlines with source='anchor', status='completed',
completed_at=actual_date.
- Idempotent: existing (project_id, rule_id) row PATCHes instead of
inserting (race-safe per design §13).
Skip write path (POST /api/projects/{id}/timeline/skip):
- Writes paliad.project_events with event_type='rule_skipped' +
metadata.rule_code; subsequent reads drop the matching projected
row from the cascade (§6.4).
Handlers expose projection meta via X-Projection-{Has,Total,Shown,Overdue,Lookahead}
headers so the wire shape stays []TimelineEvent (frozen since Slice 1).
|
||
|
|
afd3aab2b2 |
feat(t-paliad-171): SmartTimeline backend skeleton — ProjectionService + /timeline endpoint
Slice 1 of the SmartTimeline (Verlauf-tab redesign). Adds a new service
layer + two HTTP endpoints; no projection logic yet (Slice 2). The wire
shape (TimelineEvent) is frozen so future slices add Kind="projected"
rows additively without breaking the frontend consumer.
ProjectionService.For composes three actuals streams for one project:
- paliad.deadlines → Kind="deadline"
- paliad.appointments → Kind="appointment"
- paliad.project_events with
timeline_kind IS NOT NULL → Kind="milestone"
Visibility goes through the existing inline mirror of
paliad.can_see_project on each underlying service — no new RLS surface.
DirectOnly mirrors the existing "Inkl. Unterprojekte" toggle on
/projects/{id}; IncludeAuditFull broadens project_events to the full
audit log behind the upcoming "Audit-Log anzeigen" header toggle.
ProjectionService.RecordCustomMilestone backs POST /timeline/milestone
("Eigener Meilenstein") — the only write path in Slice 1.
Tests: unit (sort order, status mapping, kind tiebreak — runs by default)
plus a live integration test that seeds one project + dl + appt +
milestone and asserts the merge surfaces all three with the right
ordering. Live test gated on TEST_DATABASE_URL per the existing
convention.
Design ref: docs/design-smart-timeline-2026-05-08.md §2.3 + §9.2 + §10.
|
||
|
|
ebcda13f88 |
feat(t-paliad-170): mount <FilterBar> in /projects/<id> Verlauf tab
Phase 2 slice of the universal-filter migration (Phase 1 was
t-paliad-163 → /inbox; remaining /agenda /events /deadlines
/appointments stay queued).
What ships:
- FilterBar gains two non-invasive options that future surfaces will
also need:
customRunner — bypass the substrate POST and hand the effective
spec to a surface-supplied runner. Required by
surfaces whose data path can't move to the substrate
yet (Verlauf still uses /api/projects/{id}/events for
subtree expansion + cursor pagination, both absent
from the substrate's project_event runner).
timePresets — per-surface override of the time chip cluster, so
backward-looking surfaces can show past_*+all without
forcing forward-looking next_* chips on every host.
systemViewSlug becomes optional; the bar enforces "exactly one of
customRunner | systemViewSlug" at construction.
- project_event_kind axis renderer (was a null stub) — chip cluster
over KnownProjectEventKinds, labels reuse the existing
event.title.<kind> i18n table so the chip text matches the Verlauf
row title for the same kind.
- HorizonPast7d added end-to-end (substrate validate +
computeViewSpecBounds; FilterBar TimeOverlay + parseHorizon; views
TimeHorizon mirror) so the chip value is valid in every layer when a
later SystemView reuses it.
- Verlauf tab on /projects/<id> mounts the bar with
axes=["time","project_event_kind"], timePresets=
["past_7d","past_30d","past_90d","any"], showSaveAsView=false. The
customRunner reads predicates.project_event.event_types + time.horizon
off the effective spec, sets a verlaufFilters global, and routes
through the legacy loadEvents/loadMoreEvents pipeline (which now
applies the filter set client-side and tracks raw cursor IDs so
"Mehr laden" still walks the underlying pagination boundary even when
most rows get filtered out of a page).
- Subtree toggle drives loadEvents through verlaufBar.refresh() so the
current filter state survives the toggle.
URL state reuses the bar's existing keys (?time=past_30d, ?pe_kind=…).
Empty filter → identity passthrough → current behaviour preserved.
Out of scope (deferred to t-paliad-169 SmartTimeline):
- Migrating Verlauf to the substrate (needs scope-with-descendants)
- Past/future split, dated/undated split, source-track facet
Refs m/paliad#23.
|