# Phase 3 — Timing & penalty tables **Status:** ⬜ Not started — co-developed with [[processor]] Phase 2 **Outcome:** The schema half of the paired schema/code work that produces real timing results. Adds `entry_segment_starts`, `entry_crossings`, `entry_penalties`, `stage_results`, and `penalty_formulas` collections. Penalty evaluator registry ships on the [[processor]] side; the rule numeric values ship here. After Phase 3, operators can review computed penalties, override them, and publish official stage results. ## Why this is co-developed - **Schema and writer must land together.** `entry_crossings` rows are written by [[processor]] Phase 2; defining the collection without the writer is dead weight, defining the writer without the collection is broken code. Land both in the same release window. - **Penalty formula seeding is event-specific.** Rally Albania 2026's SLZ brackets come from the Supplementary Regulation (published 60 days before the event per regs §1.8). Phase 3 needs that data, or a reasonable placeholder, before the rally. - **Snapshot pattern requires care.** `entry_penalties` snapshots its inputs (peak speed, count, formula values). The schema must capture this faithfully so the recompute strategy in [[processor]] works. ## Tasks (sketched, not detailed) | # | Task | Notes | |---|------|-------| | 3.1 | `penalty_formulas` collection | Per [[directus-schema-draft]]: `event_id`, `belongs_to_type`, `belongs_to_id`, `type`, `offence_min`, `offence_max`, `operator`, `penalty`, `retroactive`, `enabled`. Both bracket-style and flat-style coexist. | | 3.2 | `entry_segment_starts` collection | `entry_id`, `segment_id`, `start_position`, `target_at`, `manual_override`. Materialized at stage open. | | 3.3 | `entry_crossings` collection | Per-position-derived crossing events. Idempotent on `(entry_id, geofence_id, ts)`. Written by [[processor]]. | | 3.4 | `entry_penalties` collection | `entry_id`, `type`, `formula_id`, `formula_snapshot` (JSONB), `inputs` (JSONB), `seconds`, `evaluated_at`, `recomputed_at`, `manual_override`. Snapshot inputs and rule rows for cheap recompute. | | 3.5 | `stage_results` collection | `entry_id`, `stage_id`, `clean_time`, `penalty_seconds`, `total_time`, `position_in_class`, `position_overall`, `published_at`. The next-stage seeding input is `clean_time`. | | 3.6 | Custom interface for penalty review | Operator-facing panel showing all `entry_penalties` rows for a stage, with diff between auto-computed and manual override. Likely a custom extension (Phase 5 territory). Phase 3 produces the schema; the UI follows. | | 3.7 | Snapshot regeneration + CI verification | All new collections in the snapshot; CI dry-run still passes. | | 3.8 | Rally Albania SLZ bracket seed | Once the Supplementary Regulation is published, seed `penalty_formulas` rows with the actual brackets. Single SQL/Directus-API script. | ## Open questions blocking task-level detail 1. Does `entry_crossings` need PostGIS metadata on each row (e.g. the exact geometry-relative position), or just `(entry_id, geofence_id, ts, kind)`? Default: minimal, the position is in `positions` and the join key gets you back to it. 2. Where does `position_in_class` and `position_overall` get computed — DB view, materialized view, or [[processor]]-written column? Trade-off: view is simpler but slower; column is faster but needs invalidation. 3. Penalty review workflow UX — is the operator approving each row individually, or bulk-approving the auto-computed set with manual exceptions? Drives whether `manual_override` is a single bool or a richer state.