feat(forecasting): activate §25.3 cannibalization timing axis (#1169) #1176

Merged
bot-backend merged 1 commit from feat/cannibalization-timing-1169 into main 2026-06-09 07:11:00 +00:00
Collaborator

Summary

Activates the §25.3 cannibalization timing axis (the deferred follow-up from #1169 PR2). _build_cannibalization already had candidate_release_month plumbed but unfed; this derives it from the §25.1 Launch Window peak-deficit horizon and threads it in.

  • candidate_release_month = date.today() + launch_window_horizon_months (first-of-month) — when the recommended project would actually hit the market.
  • compute_special_indices now computes Launch Window once and reuses it (injected back as a no-op closure) — no double-compute of the expensive demand/supply forecast.
  • Graceful: Launch Window unavailable → candidate_release_month=None → timing axis excluded (None-not-0, not fabricated); cannibalization still scores on class+price+geo.
  • _add_months helper: pure stdlib (no dateutil), year-boundary safe (Oct+6→Apr next year, Dec+1→Jan), first-of-month.
  • Deterministic (§16): date.today() read once, everything downstream pure.

§25.3 axes now: class + price + timing + geo active; unit-mix remains phase-2 (needs a project-level recommended mix).

Test plan

  • uv run pytest tests/services/forecasting/test_special_indices.py -q168 passed (+18: _add_months incl. year-boundary, horizon extraction, candidate-month derivation, timing-contributes-when-LW-resolves, near>far, timing-excluded-when-LW-absent, determinism)
  • ruff clean; mypy special_indices.py → 0 errors in file (pre-existing baseline elsewhere out of scope)
  • code-reviewer APPROVE (no double-compute / no fabricated-timing / deterministic — all verified)
  • no dep changes; scope = special_indices.py + its test only.

Activation note

Fully lights up once OWN_DEVELOPER_IDS is set (own-portfolio populated) — until then cannibalization runs the labelled proxy. The timing axis logic is unit-test-verified now.

Refs #1169

## Summary Activates the §25.3 cannibalization **timing axis** (the deferred follow-up from #1169 PR2). `_build_cannibalization` already had `candidate_release_month` plumbed but unfed; this derives it from the **§25.1 Launch Window** peak-deficit horizon and threads it in. - `candidate_release_month = date.today() + launch_window_horizon_months` (first-of-month) — when the recommended project would actually hit the market. - `compute_special_indices` now computes Launch Window **once** and reuses it (injected back as a no-op closure) — no double-compute of the expensive demand/supply forecast. - **Graceful**: Launch Window unavailable → `candidate_release_month=None` → timing axis excluded (None-not-0, not fabricated); cannibalization still scores on class+price+geo. - `_add_months` helper: pure stdlib (no dateutil), year-boundary safe (Oct+6→Apr next year, Dec+1→Jan), first-of-month. - **Deterministic** (§16): `date.today()` read once, everything downstream pure. §25.3 axes now: **class + price + timing + geo active**; unit-mix remains phase-2 (needs a project-level recommended mix). ## Test plan - [x] `uv run pytest tests/services/forecasting/test_special_indices.py -q` → **168 passed** (+18: `_add_months` incl. year-boundary, horizon extraction, candidate-month derivation, timing-contributes-when-LW-resolves, near>far, timing-excluded-when-LW-absent, determinism) - [x] `ruff` clean; `mypy special_indices.py` → 0 errors in file (pre-existing baseline elsewhere out of scope) - [x] code-reviewer ✅ APPROVE (no double-compute / no fabricated-timing / deterministic — all verified) - no dep changes; scope = special_indices.py + its test only. ## Activation note Fully lights up once `OWN_DEVELOPER_IDS` is set (own-portfolio populated) — until then cannibalization runs the labelled proxy. The timing axis logic is unit-test-verified now. Refs #1169
bot-backend added 1 commit 2026-06-09 06:51:28 +00:00
feat(forecasting): activate §25.3 cannibalization timing axis from launch window (#1169)
All checks were successful
CI / frontend-tests (pull_request) Has been skipped
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 5s
CI / backend-tests (push) Successful in 6m25s
CI / backend-tests (pull_request) Successful in 6m31s
60c82b2053
Derive candidate_release_month = report-as-of (date.today()) + §25.1 Launch
Window peak-deficit horizon, threaded into _build_cannibalization so the timing
overlap axis activates against own-portfolio release_month (near-in-time own
projects raise cannibalization risk). Launch Window now computed once in
compute_special_indices and reused (no double-compute). Launch Window
unavailable -> candidate_release_month None -> timing axis gracefully excluded
(None-not-0); cannibalization still scores on class/price/geo. Adds stdlib
_add_months helper (year-boundary safe, no new dep). Deterministic. 168 tests.

§25.3 now: class+price+timing+geo active; unit-mix remains phase-2.

Refs #1169
bot-backend merged commit cd48a095c0 into main 2026-06-09 07:11:00 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1176
No description provided.