9b8a865c5feed76c5ffb209862ffe5b9d97bffdc
13 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 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 (
|
|||
| 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.
|
|||
| 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. |
|||
| 293e612582 |
feat(projection): IsConditional for uncertain-anchor rules (t-paliad-289)
Rules anchored on uncertain triggers (R.109 backward-anchor without oral-hearing date; R.118(4) without validity decision; R.262(2) without recorded Vertraulichkeitsantrag) previously rendered concrete dates fabricated off the trigger date. Add IsConditional projection flag so the SmartTimeline + Verfahrensablauf surfaces "abhängig von <parent>" instead of a misleading date. Backend (fristenrechner.go): - Add IsConditional + ParentRuleCode/Name/NameEN to UIDeadline. - Pre-pass populates courtSet from rule.is_court_set=true BEFORE the main loop, so order-of-evaluation in sequence_order no longer matters for the parent-court-set check. Fixes R.109(1) "Antrag auf Simultanübersetzung" (sequence_order=45 < Mündliche Verhandlung's sequence_order=50): the timing='before' backward arithmetic was computing 1 month before the trigger date because the court-set parent hadn't been classified yet. - Set IsConditional=true on every IsCourtSetIndirect branch (catches R.109 backward + R.118(4) cons_orders chain off the decision). - Set IsConditional=true for priority='optional' + primary_party='both' rules whose data-model parent is the trigger anchor (covers R.262(2) confidentiality_response: the data anchors on SoC, but the real trigger is the opposing party's confidentiality motion which may never happen). Suppressed by IsOverridden so user anchors win. Backend (projection_service.go): - Add IsConditional to TimelineEvent + propagate from UIDeadline. - New Status="conditional" for projected rows; clears Date, populates DependsOnRuleCode/Name from UIDeadline.ParentRule* so the row carries the "abhängig von <parent>" payload even when the parent has no computed date for annotateDependsOn to discover. Frontend (verfahrensablauf-core.ts + CSS + i18n): - CalculatedDeadline gains isConditional + parentRule* fields. - deadlineCardHtml renders "abhängig von <parent>" chip with click-to-edit affordance in place of the date column when isConditional=true. IsConditional wins over IsCourtSet for the date column (they overlap; "abhängig von <parent>" names the specific blocker). - .timeline-item--conditional / .fr-col-item--conditional CSS: dotted border + faded text so the conditional state reads at glance. - Replaced escHtml's DOM-backed implementation with a pure-JS regex escape so the module is testable in bun test without jsdom (the old form forced fixtures to leave several fields empty just to avoid the DOM dependency). Tests: - TestApplyLookaheadCap_ConditionalRowsPassThrough: pure-function lock that conditional rows pass through applyLookaheadCap untouched (don't count against ProjectedTotal/Shown, don't get capped). - TestUIDeadline_IsConditional_UncertainAnchors (TEST_DATABASE_URL): asserts R.109(1)/(4), R.118(4) chain, and R.262(2) all render IsConditional=true with empty DueDate + populated ParentRule*; SoD stays non-conditional; override on the oral hearing flips R.109(1) back to concrete date. - 4 new bun tests for the conditional rendering branches in deadlineCardHtml. UX path verified by tests + manual review of the live rule corpus: opening a UPC inf project without oral-hearing date now surfaces R.109(1) + R.109(4) as conditional; recording the Vertraulichkeitsantrag (anchoring R.262(2) via the existing "Datum setzen" flow) flips it back to a concrete date. go build / go test / bun test / bun run build all clean. |
|||
| 3ff1b23238 |
fix(timeline): t-paliad-237 — anchor lookup must traverse linked proceedings
On a CCR sub-project the SmartTimeline renders the parent inf project's
rules in the parent_context lane (correct — the CCR depends on the inf
schedule). Clicking "Datum setzen" on those rows bubbled up as a
generic "Konnte das Datum nicht setzen." because RecordAnchor only
looked up the rule under the CCR's own proceeding_type_id; for an
inf rule like upc.inf.cfi.soc that returned sql.ErrNoRows and dropped
into the catch-all error.
The anchor handler now mirrors the read view's broader rule scope: on
sql.ErrNoRows for a CCR project, we retry the lookup against the
parent project's proceeding_type_id. If the rule is found there, we
reject with a new CrossProceedingAnchorError carrying the parent
project's id + title so the frontend can render a clear DE/EN message
and a clickable link back to the parent ("anchor it on the
infringement proceeding, not the counterclaim"). We deliberately do
NOT auto-route the write across projects — that would silently mutate
the inf project's actuals and is out of scope per the brief.
Genuine "unknown submission_code" failures still surface as
ErrInvalidInput; the predecessor_missing 409 path keeps its existing
shape (the two errors discriminate on the response's `error` field).
Adds a Live-DB integration test that seeds an inf-only rule + a CCR
under a real inf project and verifies all three paths: CCR rejects
cross-proceeding, parent inf project accepts the same code, unknown
codes still report unknown_submission_code.
|
|||
| 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`. |
|||
| 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).
|
|||
|
|
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.
|
||
|
|
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.
|