Commit Graph

3 Commits

Author SHA1 Message Date
mAi
39353d49ed fix(litigationplanner): UPC vacations no longer block deadlines (align with paliad t-paliad-121)
youpc.org/deadlines was rolling a deadline "from 2027-01-02 (UPC Winter
Vacation)" — i.e. across the UPC judicial vacation as if it were a
public holiday. Paliad-side t-paliad-121 already decided vacations are
informational only (the Court keeps running through them, RoP / UPC AC
decision-on-judicial-vacation 2023-05-26), and `HolidayService.Is
NonWorkingDay` in `internal/services/holidays.go` is correct. The
embedded snapshot consumed by youpc.org via Go-module replace had
drifted: `pkg/litigationplanner/embedded/upc/holidays.go:74` blocked on
both `isClosure()` AND `isVacation()`.

This commit aligns the embedded calendar with the paliad-side semantics
and ships a fresh holiday set so the existing 2026/2027 fix actually
takes effect downstream.

Code changes (`holidays.go`):

- `IsNonWorkingDay`: drop the `|| h.isVacation()` branch — only weekends
  and `isClosure()` rows trigger the roll. Godoc rewritten to mirror
  the paliad-side rationale (Court keeps operating, RoP cites,
  vacation rows kept for informational labels).
- `isClosure()`: accept both `"public_holiday"` and `"closure"`. Live
  paliad DB rows use the `public_holiday` value; the placeholder
  snapshot shipped with the original Slice C used `closure` as a
  hand-crafted synonym. Reconciles with
  `internal/services/holidays.go:132` which already does the same
  union. Required to make the regenerated JSON (full of
  `public_holiday`) keep blocking DE national holidays after the
  regeneration in this commit.
- Type-level godoc updated: `SnapshotHolidayCalendar` now documents
  vacation-is-informational, and the `AdjustForNonWorkingDaysWithReason`
  precedence note explains that `vacation` kind only fires when a
  vacation row overlaps a weekend or closure that's already doing the
  rolling.

Data refresh (`holidays.json`):

- Regenerated from paliad prod (postgres @ 100.99.98.201:11833,
  paliad schema). 55 rows for 2026 + 2027: 22 DE public_holiday +
  33 UPC vacation (25 Summer Vacation Jul 27–Aug 28, 8 Winter
  Vacation Dec 24/28–31 + Jan 4–6). The previous placeholder shipped
  only 5 rows (3 Sommerpause + Neujahr + Tag der Arbeit, no Winter
  Vacation at all) — which is why a date landing in late Dec / early
  Jan landed inside an unmodeled gap on the consumer side.
- `meta.json` bumped: version → `2026-05-27-1-holidays-only`,
  `holiday_count` 5 → 55, `source_db_label` flags that only
  holidays.json was refreshed (see friction note below).

Regression test (`snapshot_test.go::TestSnapshotHolidayCalendar`):

- 2026-08-04 (Tue, UPC Summer Vacation) — `IsNonWorkingDay` must be
  false; `AdjustForNonWorkingDays` must NOT mutate the date.
- 2027-01-02 (Sat, m's flagged scenario) — must roll forward through
  Sat/Sun, then STOP on Mon 2027-01-04 (UPC Winter Vacation, no longer
  blocking). Pre-fix this rolled all the way to Thu 2027-01-07.

Cross-repo: youpc.org imports `pkg/litigationplanner` via Go-module
replace; the regenerated snapshot ships on its next rebuild. No
separate youpc.org commit needed — paliad is the source of truth.

Friction note: `cmd/gen-upc-snapshot/main.go` itself is incompatible
with the current paliad schema. Migration 140 (`140_drop_deadline_rules`)
dropped `paliad.deadline_rules`, but the generator still SELECTs from
it (main.go ~L162). Running the tool against prod fails on the rules
step. I bypassed the broken path and generated `holidays.json` directly
from the DB via psql + jq (same JSON shape that `EmbeddedHoliday`
expects, nulls filtered for `omitempty`). The other snapshot files
(rules.json, proceeding_types.json, trigger_events.json, courts.json)
remain at their pre-existing placeholder state — re-flagged in
meta.json's `source_db_label`. Refitting the generator for the post-
mig-140 schema is a separate task.

go vet + go test ./... clean (256+ Go tests pass, including the new
regression cases).
2026-05-27 15:04:05 +02:00
mAi
cd5f752a0e feat(litigationplanner): scenarios — paliad.scenarios jsonb table + Catalog API + engine adapter (Slice D, t-paliad-306, m/paliad#124 §5)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
A scenario is a named composition of existing proceedings + flags +
per-card choices + anchor dates. Users compose, they don't author —
spec references existing rules by submission_code; never creates new
rules. Per m's 2026-05-26 AskUserQuestion picks (doc commit 6e58595):
  Q1 composition: primary + spawned (v1); multi-proceeding peer
                  compose is the v2 goal (spec.proceedings[] array)
  Q2 scope:       per-project + abstract (project_id NULL = abstract)
  Q3 trigger:     per-anchor overrides over one base date
  Q4 storage:     NEW paliad.scenarios table with jsonb spec
                  (NOT a project_event_choices column extension)

Migration 145 — additive only. Pre-flight coordination check:
  - On-disk max: 138 (Berufung backfill, just merged).
  - Live DB tracker: 106 (significantly behind — many migs pending
    deploy).
  - curie's #93 B.2-B.6 migs not pushed yet — reserved 139-143 + 144
    as buffer; claimed 145 as the safe minimum that won't collide.
  - paliad.scenarios has audit_reason NOT applicable (no audit
    trigger on the table); updated_at trigger added on the table
    itself.
  - paliad.projects gains active_scenario_id uuid NULL FK with ON
    DELETE SET NULL (mig 134 lesson — no updated_at clauses on
    proceeding_types-style assumptions).

Schema:
  paliad.scenarios (
    id uuid pk,
    project_id uuid NULL FK → projects(id) ON DELETE CASCADE,
    name text NOT NULL CHECK char_length > 0,
    description text NULL,
    spec jsonb NOT NULL CHECK jsonb_typeof = 'object',
    created_by uuid NULL FK → users(id) ON DELETE SET NULL,
    created_at + updated_at timestamptz,
    UNIQUE NULLS NOT DISTINCT (project_id, created_by, name)
  );
  paliad.projects.active_scenario_id uuid NULL FK;
  RLS: project-scoped → can_see_project; abstract → created_by = auth.uid();
  Trigger: scenarios_touch_updated_at_trg.

pkg/litigationplanner additions:
  - Scenario struct (db + json tags)
  - ScenarioSpec / ScenarioProceeding / ScenarioCardChoice — parsed
    view of the jsonb (version-1 today, v2 multi-peer-ready)
  - ParseSpec(raw) + ScenarioSpec.PrimaryProceeding() + CalcOptionsFromSpec()
  - ScenarioFilter + Catalog.LoadScenarios + Catalog.MatchScenario
  - CalculateFromScenario(scenario, catalog, holidays, courts) — high-
    level engine entry: parses spec → builds CalcOptions → delegates
    to Calculate
  - Sentinel errors: ErrUnknownScenario, ErrInvalidScenario,
    ErrScenarioNoPrimary

paliadCatalog impl:
  - LoadScenarios with progressively-built WHERE clauses (project-id
    filter, abstract-for-user filter, or all)
  - MatchScenario by id — returns ErrUnknownScenario on not-found
  - Services connection bypasses RLS; ScenarioService enforces
    visibility at the application layer (mirrors EventChoiceService
    pattern from t-paliad-265)

SnapshotCatalog impl (embedded/upc):
  - LoadScenarios returns empty slice (no scenarios in the snapshot)
  - MatchScenario returns ErrUnknownScenario

internal/services/scenario_service.go:
  - Create / Get / ListForProject / ListAbstractForUser / Patch /
    SetActive / Delete with visibility checks
  - validateSpec checks version, base_trigger_date format, every
    proceedings[*].code resolves to an active paliad.proceeding_types
    row, every appeal_target is valid, every anchor_overrides date
    parses, every role ∈ {primary, peer}
  - SetActive validates the scenario belongs to the requested project
    (a scenario from a different project can't be active here)
  - Returns ErrScenarioNotVisible for failed visibility checks

REST endpoints (registered in handlers.go):
  GET    /api/scenarios?project=<id>             — list project's
  GET    /api/scenarios?abstract=true            — list user's abstract
  GET    /api/scenarios/{id}                     — one
  POST   /api/scenarios                          — create
  PATCH  /api/scenarios/{id}                     — partial update
  DELETE /api/scenarios/{id}                     — remove
  PUT    /api/projects/{id}/active-scenario      — set / clear active

Handler error mapping:
  - ErrUnknownScenario / ErrScenarioNotVisible → 404
  - ErrInvalidInput / ErrInvalidScenario / ErrScenarioNoPrimary → 400
  - everything else → 500

Tests:
  - pkg/litigationplanner/scenarios_test.go: ParseSpec roundtrip
    (well-formed + unknown version + malformed json),
    PrimaryProceeding zero/multi/single, CalcOptionsFromSpec full
    unpack, trigger_date_override path, no-base-trigger safety check.
    8 cases total, all DB-free.

Wired in cmd/server/main.go alongside EventChoice — same pattern,
nil-safe when DATABASE_URL is unset (handlers 503 in that mode).

Acceptance:
  - go build ./... clean
  - go test ./... all green (incl. new scenarios tests)
  - Pre-flight audit confirmed mig 145 number is safe vs curie's
    pending B.2-B.6 range
2026-05-26 17:48:56 +02:00
mAi
ce28ea972e feat(litigationplanner): embedded UPC snapshot + generator (Slice C, m/paliad#124 §19)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
Lays the foundation for youpc.org's cross-repo integration: an
in-package UPC subset of paliad's deadline corpus, embedded as JSON,
that any consumer can use to run the litigationplanner engine without
DB access.

Generator (cmd/gen-upc-snapshot):
  - Reads paliad's live DB (DATABASE_URL), applies pending migrations
    to match schema HEAD, SELECTs the UPC subset
    (proceeding_types WHERE jurisdiction='UPC' AND is_active=true,
    deadline_rules WHERE lifecycle_state='published' AND is_active=true
    on those proceedings, referenced trigger_events, DE+UPC holidays,
    UPC courts).
  - Writes pretty-printed JSON to
    pkg/litigationplanner/embedded/upc/{proceeding_types, rules,
    trigger_events, holidays, courts, meta}.json.
  - Idempotent — same DB state → same output (modulo
    meta.generated_at + auto-versioned suffix).
  - Date-stamped versioning (YYYY-MM-DD-N) with same-day suffix bump.
  - Operator runbook in cmd/gen-upc-snapshot/README.md.

Embedded subpackage (pkg/litigationplanner/embedded/upc/):
  - embed.go    — //go:embed *.json + LoadMeta()
  - snapshot.go — SnapshotCatalog (full lp.Catalog impl: LoadProceeding
    / LoadProceedingByID / LoadRuleByID / LoadRuleByCode /
    LoadRulesByTriggerEvent / LoadTriggerEventsByIDs / LookupEvents);
    O(1) map lookups; LookupEvents linear over the < 100-row UPC corpus.
  - holidays.go — SnapshotHolidayCalendar implementing lp.HolidayCalendar
    (IsNonWorkingDay / Adjust* with structured AdjustmentReason).
  - courts.go   — SnapshotCourtRegistry implementing lp.CourtRegistry.
  - Compile-time assertions (_ lp.X = (*Snapshot*)(nil)) catch
    interface drift.

Wire-up for consumers:
  cat, _ := upc.NewCatalog()
  hc, _  := upc.NewHolidayCalendar()
  cr, _  := upc.NewCourtRegistry()
  timeline, _ := lp.Calculate(ctx, "upc.inf.cfi", "2026-05-26",
                              lp.CalcOptions{}, cat, hc, cr)

Tests (snapshot_test.go, all DB-free):
  - meta parses cleanly, non-zero counts
  - LoadProceeding(upc.inf.cfi) returns expected proc + rules
  - LoadProceeding(unknown) returns ErrUnknownProceedingType
  - LookupEvents(Jurisdiction:UPC, all-following) covers corpus
  - LookupEvents(party=defendant, next) scopes anchors correctly
  - engine end-to-end via lp.Calculate against the embedded snapshot
  - holiday calendar (weekends, DE closures, UPC vacation block)
  - court registry (empty courtID fallback, known + unknown court)

Placeholder data shipped (2 proceedings, 2 rules, 5 holidays, 2
courts) so tests run without a live DB. Operator regenerates against
prod via `make snapshot-upc` once migrations 134 (B1) and 135 (B3)
have landed on prod — see cmd/gen-upc-snapshot/README.md for the
runbook. The placeholder's meta.version is suffixed `-placeholder`
to make the regeneration delta obvious.

Makefile target:
  make snapshot-upc — wraps the generator + reruns the snapshot tests

Design (§19 of docs/design-litigation-planner-2026-05-26.md):
  - Embedding format: go:embed JSON (diff-friendly, no compile coupling)
  - Generator entry: cmd/gen-upc-snapshot/main.go (idiomatic Go cmd path)
  - Versioning: meta.json carries semver + generated_at + paliad_commit
  - Regeneration: manual via Make target or `go generate`; no CI cron in v1
  - Out of scope: snapshot signing, DE/EPA/DPMA snapshots, snapshot
    diff tooling

Acceptance:
  - go build clean, go test all green (incl. 6 new tests in
    pkg/litigationplanner/embedded/upc, all DB-free)
  - SnapshotCatalog passes the compile-time lp.Catalog assertion
  - Generator binary builds + runs (Idempotence verified by re-running
    against the same source data)
2026-05-26 15:11:07 +02:00