Fix.generatePackage (lib/game/seed.ts) drew a flat 6–72 h expiry window (priority ×0.5 → 3–36 h) independent of leg distance, so some jobs' deadlines were shorter than the flight itself and could never be completed (legs up to MAX_JOB_LEG_KM = 9000 km; fastest catalog plane ~920 km/h ≈ 10 h for a max leg). The drawn window is now clamped UP after special scaling:
deadline ≥ now + travelDurationMs(legKm, FASTEST_CATALOG_SPEED_KMH) + EXPIRY_BUFFER_MS (flat 1 h buffer), reusing the existing travelDurationMs from lib/sim/movement.ts. New data-driven constant FASTEST_CATALOG_SPEED_KMH in lib/game/catalog.ts (max of PLANE_TYPES speeds — a job must be finishable with the best available aircraft, not the starter's 510 km/h). The window is a single uniform draw, so clamping up covers both requirements: short legs are byte-identical to before, and when the raw draw falls below the floor the deadline lands exactly on it — always ≤ the scaled ceiling for any pair of airports on Earth (max leg ~20000 km → floor ≈ 22.8 h < the 36 h priority ceiling), so no separate ceiling raise is needed. Forced deadlineOffsetMs skips the floor, same as it skips special scaling.
Tests. Unit +5 in lib/game/seed.test.ts (new "expiry floor" block): constants pinned; seeded sweep over 200 unforced packages on the real dataset asserting the invariant for every one; farthest within-cap pair (~9000 km) forced with priority and null across 50 seeds each — invariant holds everywhere and at least one package per special lands exactly on the floor (deterministic, proves the clamp engages); short legs pinned byte-identical to a replay of the pre-v0.54.0 draw order (no economy regression); forced offsets still skip the floor. E2E +1 in e2e/expiry.spec.ts: existing POST /api/test/create-package hook forces EDDB→KSMF (~9000 km) with special: 'priority'; 10 packages created, every one asserted to expire ≥ travel time at max speed + buffer (and ≤ the 36 h priority ceiling), then all ignored for cleanup — no new server hooks.
Local verification: tsc --noEmit clean; vitest 364/364 in 30 files (baseline 359); playwright --list compiles, 59 tests in 15 files.
Closes #119
**Fix.** `generatePackage` (lib/game/seed.ts) drew a flat 6–72 h expiry window (priority ×0.5 → 3–36 h) independent of leg distance, so some jobs' deadlines were shorter than the flight itself and could never be completed (legs up to MAX_JOB_LEG_KM = 9000 km; fastest catalog plane ~920 km/h ≈ 10 h for a max leg). The drawn window is now clamped UP after special scaling:
`deadline ≥ now + travelDurationMs(legKm, FASTEST_CATALOG_SPEED_KMH) + EXPIRY_BUFFER_MS` (flat 1 h buffer), reusing the existing `travelDurationMs` from lib/sim/movement.ts. New data-driven constant `FASTEST_CATALOG_SPEED_KMH` in lib/game/catalog.ts (max of PLANE_TYPES speeds — a job must be finishable with the best available aircraft, not the starter's 510 km/h). The window is a single uniform draw, so clamping up covers both requirements: short legs are byte-identical to before, and when the raw draw falls below the floor the deadline lands exactly on it — always ≤ the scaled ceiling for any pair of airports on Earth (max leg ~20000 km → floor ≈ 22.8 h < the 36 h priority ceiling), so no separate ceiling raise is needed. Forced `deadlineOffsetMs` skips the floor, same as it skips special scaling.
**Tests.** Unit +5 in lib/game/seed.test.ts (new "expiry floor" block): constants pinned; seeded sweep over 200 unforced packages on the real dataset asserting the invariant for every one; farthest within-cap pair (~9000 km) forced with priority and null across 50 seeds each — invariant holds everywhere and at least one package per special lands exactly on the floor (deterministic, proves the clamp engages); short legs pinned byte-identical to a replay of the pre-v0.54.0 draw order (no economy regression); forced offsets still skip the floor. E2E +1 in e2e/expiry.spec.ts: existing `POST /api/test/create-package` hook forces EDDB→KSMF (~9000 km) with `special: 'priority'`; 10 packages created, every one asserted to expire ≥ travel time at max speed + buffer (and ≤ the 36 h priority ceiling), then all ignored for cleanup — no new server hooks.
Local verification: `tsc --noEmit` clean; vitest 364/364 in 30 files (baseline 359); playwright --list compiles, 59 tests in 15 files.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #119
Fix.
generatePackage(lib/game/seed.ts) drew a flat 6–72 h expiry window (priority ×0.5 → 3–36 h) independent of leg distance, so some jobs' deadlines were shorter than the flight itself and could never be completed (legs up to MAX_JOB_LEG_KM = 9000 km; fastest catalog plane ~920 km/h ≈ 10 h for a max leg). The drawn window is now clamped UP after special scaling:deadline ≥ now + travelDurationMs(legKm, FASTEST_CATALOG_SPEED_KMH) + EXPIRY_BUFFER_MS(flat 1 h buffer), reusing the existingtravelDurationMsfrom lib/sim/movement.ts. New data-driven constantFASTEST_CATALOG_SPEED_KMHin lib/game/catalog.ts (max of PLANE_TYPES speeds — a job must be finishable with the best available aircraft, not the starter's 510 km/h). The window is a single uniform draw, so clamping up covers both requirements: short legs are byte-identical to before, and when the raw draw falls below the floor the deadline lands exactly on it — always ≤ the scaled ceiling for any pair of airports on Earth (max leg ~20000 km → floor ≈ 22.8 h < the 36 h priority ceiling), so no separate ceiling raise is needed. ForceddeadlineOffsetMsskips the floor, same as it skips special scaling.Tests. Unit +5 in lib/game/seed.test.ts (new "expiry floor" block): constants pinned; seeded sweep over 200 unforced packages on the real dataset asserting the invariant for every one; farthest within-cap pair (~9000 km) forced with priority and null across 50 seeds each — invariant holds everywhere and at least one package per special lands exactly on the floor (deterministic, proves the clamp engages); short legs pinned byte-identical to a replay of the pre-v0.54.0 draw order (no economy regression); forced offsets still skip the floor. E2E +1 in e2e/expiry.spec.ts: existing
POST /api/test/create-packagehook forces EDDB→KSMF (~9000 km) withspecial: 'priority'; 10 packages created, every one asserted to expire ≥ travel time at max speed + buffer (and ≤ the 36 h priority ceiling), then all ignored for cleanup — no new server hooks.Local verification:
tsc --noEmitclean; vitest 364/364 in 30 files (baseline 359); playwright --list compiles, 59 tests in 15 files.