feat(routeforge): refine duplicate detection
Bundles three completed OpenSpec changes:
routeforge-duplicate-detection-refinement:
- StrictDuplicateKey value object owns the 5-tuple (bundle_id,
airline_id, flight_number, route_code, route_leg). L4 / L5 / L12 /
DuplicateChecker delegate; no more dup'd dupKey() / normalize().
- L5 severity flipped WARNING -> ERROR for same-bundle full-tuple
matches. Scoped to enabled = true AND owner_type IS NULL AND same
bundle_id.
- L12 new WARNING for cross-bundle airline+flight collisions.
Catalog grows 12 -> 13 rules.
- DuplicateChecker (the typing-time /check-duplicates endpoint)
classifies results as same_bundle / error vs cross_bundle / warning,
passes existing_bundle_id + existing_bundle_name to the UI.
- New migration adds flights._dup_key generated column + UNIQUE
index. STORED on MySQL / MariaDB / PostgreSQL; VIRTUAL on SQLite
(ALTER TABLE quirk). Driver-switched expression.
- Migration first canonicalizes route_code / route_leg storage
('' / '0' / 0 -> NULL), then auto-disables pre-existing dup rows
via a single window-function pass (lowest id per cluster kept),
with activity-log entries per disabled row.
- Flight model gains routeCode() / routeLeg() Attribute mutators
for canonical NULL on set. Legacy 'route_leg' => 'integer' cast
removed.
- README MySQL min bumped 5.7 -> 8.0 (window functions needed
by the dedup migration).
- Frontend types updated: DuplicateMatch now carries severity,
kind, existing_bundle_id, existing_bundle_name.
routeforge-page-boot-via-api:
- /boot endpoint + RouteForgeBootResource serving SPA bootstrap
envelope (CSRF, user, airlines, routes, config, translations).
- Frontend boot.ts state init, BootError component.
routeforge-controller-thin:
- Extract AirlineStatsService, CommitInputFactory,
LintContextFactory from the controller.
- New BundlesRequest + RouteForgeBundleResource.
Misc cleanup:
- Support\Geo utility extracted from generator.ts logic.
- BaseRouteForgeBatchRequest validation polish.
Verified: 670 pest tests pass (2682 assertions). PHPStan clean on
touched paths. Pint clean (--dirty). OpenSpec validate --strict
green.