diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md
index 0b0af89..a27d65e 100644
--- a/docs/CURRENT-STATE.md
+++ b/docs/CURRENT-STATE.md
@@ -1,21 +1,31 @@
# Current State
-**As of:** 2026-07-28 · **Branch:** `fix/m0-audit-corrections` (branched from reviewed PR-#1 head `beb9289`)
-**Milestone:** M0 — Headless Service Lab. **Status:** bounded audit-correction pass COMPLETE; awaiting
-independent re-review + owner-run human playtests.
+**As of:** 2026-07-29 · **Branch:** `fix/m0-price-forecast-integrity` (continues the audit-correction line)
+**Milestone:** M0 — Headless Service Lab. **Status:** SECOND bounded correction (strategy integrity +
+forecast) COMPLETE; awaiting independent re-review + owner-run human playtests.
-**Independent audit** (PR #1: Conditional-Pass / Defer) found 3 High + 5 Medium. The bounded correction pass
-addressed them (no M1, no new features):
-- **H1** causal autopsy now attributes the primary loss by measured lost contribution (two-tier report),
- removing the 88%-util flip; validated against covers-served response.
-- **H2** menu-coherence lever defuses the discovered dominant hybrid (goldens byte-identical); dominance is
- now SEARCHED. Residual **NEW-1** (uniform-overprice exploit) disclosed, deferred to M0.5.
-- **H3** forecast de-biased (demand-opportunity vs expected-completed vs low-confidence range); band
- coverage 0% -> 61% (stated 55%).
-- **M4** exact integer-cents CLI price parsing; **M5** direct FIFO + labor tests; **M6** no-float guard now
- scans static fields + properties; **M7** cross-OS determinism claims corrected to "verified same-env only."
+**Second correction (2026-07-29)** closed the two findings the first pass left open, inside M0:
+- **H2 / NEW-1 (strategy integrity) — RESOLVED.** Credible single-service **price elasticity** (`PriceModel`)
+ anchors willingness-to-pay to each dish's own suggested price and makes overpricing cost real demand this
+ service. A **frontier search** shows **no cross-market dominator**: the value lunch's best plan is
+ fair-priced/high-capacity while the enthusiast's is premium/lean — distinct, opposed regimes, so no single
+ plan wins everywhere. Premium pricing stays viable but **contextual** (Premium Craft wins only enthusiast,
+ restoring hypothesis H1). See `docs/design/PRICING-CONTRACT.md`, DECISION-LOG D-020/D-022/D-023/D-024.
+- **H3 (forecast) — CORRECTED.** Per-station kitchen bottleneck + over-acceptance waste stop the forecast
+ from **reversing** the seat decision (was 46→66 seats: forecast +$483 / actual −$377). Held-out
+ seat-direction agreement **11/12**; band coverage 66%/73% vs stated 55%. See
+ `docs/design/FORECAST-CONTRACT.md`, DECISION-LOG D-021.
+- Golden checksums were **deliberately re-baselined** (prices recentered ~+40%, sensitivities retuned); this
+ is the golden policy working as intended, documented in D-022 — not a silent change.
-See `../reports/m0/M0-CORRECTION-REPORT.md` and `../reports/m0/PRE-FIX-AUDIT-EVIDENCE.md`.
+**First correction (2026-07-28)** — the audit (PR #1: Conditional-Pass / Defer) found 3 High + 5 Medium.
+**H1** causal autopsy attributes the primary loss by measured lost contribution (removed the 88%-util flip).
+**H2** menu-coherence lever defused the value-chassis hybrid. **H3** forecast de-biased. **M4** exact
+integer-cents parsing; **M5** FIFO + labor tests; **M6** widened no-float guard; **M7** determinism claims
+corrected to same-env-only.
+
+See `../reports/m0/M0-CORRECTION-2-REPORT.md`, `../reports/m0/M0-CORRECTION-REPORT.md`,
+`../reports/m0/PRE-FIX-2-EVIDENCE.md`, `../reports/m0/PRE-FIX-AUDIT-EVIDENCE.md`.
## What is complete
- Repository initialized; full structure; license; determinism/architecture/design/product/risk/commercial/
@@ -25,22 +35,29 @@ See `../reports/m0/M0-CORRECTION-REPORT.md` and `../reports/m0/PRE-FIX-AUDIT-EVI
FNV-1a state checksum. Fixtures: 3 segments, 12 recipes, 8 employees, 3 market scenarios, 9 named strategies.
- **CLI** (`src/RestaurantSim.Cli`): inspect → plan → forecast → commit → autopsy → revise → run again.
- **Harness** (`src/RestaurantSim.Harness`): distribution/dominance/determinism analysis → `reports/`.
-- **Tests:** 74 passing across 3 projects (invariants, determinism, golden scenarios, balance properties).
+- **Tests:** 117 passing across 3 projects (invariants, determinism, golden scenarios, balance properties).
- **Evidence** (200 seeds/cell) committed under `reports/`.
## Latest test status
-`dotnet test` → **103 passed, 0 failed** (Core 54, Determinism 31, Scenario 18) after the correction pass.
-New tests lock each fix: attribution (5), forecast calibration (3), dominance regression (2), FIFO order,
-labor value, exact price parse, widened no-float guard. Re-run to confirm.
+`dotnet test` → **117 passed, 0 failed** (Core 54, Determinism 31, Scenario 32) after the second correction.
+The second pass adds `PriceElasticityTests` (7: WTP/resistance smoothness, overprice unprofitability,
+per-dish anchoring, segment ordering, dominator beaten), `ForecastDirectionTests` (4: seat/cook/pricing
+directionality), and `DominanceFrontierTests` (3: opposed regimes, no near-optimal-everywhere plan, the
+searched champion is not a dominator), plus updated calibration and re-baselined goldens. Re-run to confirm.
## Latest determinism status
Harness determinism check: **PASS** for the sampled matrix (same seed ⇒ identical checksum). Golden
checksums locked in `tests/RestaurantSim.Scenario.Tests`. Cross-OS CI comparison is an OPEN item.
## Balance status
-Distinct winning strategies across 3 markets: **2 of 3** → no single dominant strategy. Poor strategies all
-post negative medians and are legibly diagnosed. See `reports/balance/distribution.md` and
-`docs/design/M0-BALANCE-HYPOTHESES.md`.
+Among the nine named strategies, distinct winners across 3 markets: **2 of 3** → no single dominant strategy.
+The rigorous check is a **frontier search**: the best generalist plan is **~$1170 below the per-market
+frontier in its worst market**, and the per-market optima are distinct, structurally-opposed regimes (value
+lunch → fair-priced/high-capacity; enthusiast → premium/lean) — so **no cross-market dominator** (the first
+pass's residual NEW-1 exploit is resolved, not deferred). Caveat (disclosed): the named strategies are
+illustrative and under-optimized versus the frontier; enriching them and reviewing labor/throughput economics
+are M0.5 items (DECISION-LOG D-024). See `reports/balance/distribution.md`,
+`reports/balance/dominance-search.md`, and `docs/design/M0-BALANCE-HYPOTHESES.md`.
## What is partially complete / not started
- **Human playtests:** NOT gathered. The Builder cannot fabricate them. Plan/script/consent are ready in
@@ -51,8 +68,13 @@ post negative medians and are legibly diagnosed. See `reports/balance/distributi
## Known defects / caveats
- None blocking. Known limitation (by design, not a defect): in a single service with no repeat visits,
- throughput dominates profit and quality has limited economic teeth — see DECISION-LOG D-012. This is a
- finding for M1, not a bug.
+ price and quality have no *durable* teeth — a customer can decline an overpriced meal this service (now
+ modeled), but there is no remembered reputation across services. Single-service elasticity closes the
+ in-service exploit; durable price/quality feedback is an M1 system (reputation/repeat visits). See
+ DECISION-LOG D-012 / D-020.
+- The forecast's expected-covers point estimate is a coarse pre-service approximation (per-station cap) and
+ can under-predict for some plans; it is directionally correct and honestly banded, not exact. See
+ `docs/design/FORECAST-CONTRACT.md`.
## Scope audit
Clean. No item from the M0 hard non-goals list was built (no graphics/engine/pathfinding/inventory/
diff --git a/docs/DECISION-LOG.md b/docs/DECISION-LOG.md
index 8c873b1..6de74ac 100644
--- a/docs/DECISION-LOG.md
+++ b/docs/DECISION-LOG.md
@@ -5,13 +5,79 @@ consequences, owner, and conditions to revisit. Newest first.
---
+### 2026-07-29 · D-024 · Dominance is tested against a searched FRONTIER, not the named strategies
+- **Status:** locked · **Owner:** Builder
+- **Decision:** The dominance search now builds a strong per-market **frontier** (random search + a principled
+ archetype sweep, 80 seeds/market) and asks whether any single plan is within $150 of the frontier in ALL
+ three markets. It no longer merely checks whether a candidate beats the nine **named** strategies.
+- **Why:** an independent adversarial verifier showed the old test was low-power and used a weak bar — the
+ named strategies are under-optimized (e.g. Focused Value makes ~$450 in the lunch rush where a fair-priced,
+ fully-staffed Burger+Ribeye+Fish plan makes ~$2350). A plan can beat every named winner without being a
+ cross-market optimum, so "beats all named winners" is the wrong dominance signal.
+- **Result:** No cross-market dominator. The best generalist plan is **~$1170 below the frontier in its worst
+ market**; the per-market optima are distinct regimes (lunch: fair/high-capacity; enthusiast: premium/lean).
+ Locked in `DominanceFrontierTests`; reported in `reports/balance/dominance-search.md`.
+- **Follow-ups (M0.5, disclosed not hidden):** (a) the named strategy set should be enriched with
+ frontier-competitive archetypes so the distribution reflects skilled play; (b) lean strategies underperform
+ heavily — labor/throughput economics may over-reward staffing up and deserve a calibration review.
+
+### 2026-07-29 · D-023 · Premium is now genuinely contextual: it wins only the enthusiast market
+- **Status:** locked · **Owner:** Builder
+- **Decision:** After single-service price elasticity, Premium Craft wins **only** the enthusiast evening and
+ loses both the value lunch rush (−$151 median) and the social dinner to Focused Value. This restores the
+ pre-registered hypothesis H1 (a value lunch rush rewards a lean value operation and punishes premium
+ pricing). Distinct winners across the three markets stay at **2/3**; no single strategy dominates.
+- **Consequence:** The lunch rush is a **thin market** where only one strategy (Focused Value) clears the
+ +$150 "profitable" bar. That is realistic for a price-led, high-volume market, and is a within-market
+ observation, not a violation of the cross-market non-dominance bar. Locked as `Premium_is_not_always_best`
+ and the lunch-winner assertion in `ScenarioTests`. See [[D-020]], `M0-BALANCE-HYPOTHESES.md`.
+
+### 2026-07-29 · D-022 · Suggested prices recentered and segment sensitivities retuned; goldens re-baselined
+- **Status:** locked · **Owner:** Builder
+- **Decision:** All 12 recipe suggested prices were recentered (~+40%) to give the WTP model credible absolute
+ anchors, and segment price sensitivities were retuned (Value 8000→10000, Social 5000→5500, Enthusiast
+ 3500→3000) so the three segments span a clear elasticity range. Because the simulated order mix changes,
+ the three **golden checksums were deliberately re-baselined** (Focused Value/lunch `0x290EB112568926A4`,
+ Premium Craft/enthusiast `0xC9AEF12F9CD876DA`, Balanced Competent/social `0x8BC9E9401D27E91B`).
+- **Rationale:** This is the golden policy working as intended — a checksum changes when behavior
+ intentionally changes, documented here rather than preserved by contorting the model. The elasticity
+ constant `PriceResistScaleBp` itself does **not** change any golden (no golden plan prices above its
+ segment's WTP), so the re-baseline is attributable solely to the price/sensitivity recentering.
+- **Revisit:** M0.5 economic calibration.
+
+### 2026-07-29 · D-021 · Second correction: forecast per-station bottleneck + over-acceptance waste (H3)
+- **Status:** locked · **Owner:** Builder
+- **Decision:** The forecast's kitchen capacity is now the **per-station** bottleneck (min over stations),
+ realization is keyed off **kitchen** throughput only, and covers seated past the kitchen wall incur an
+ **over-acceptance waste** cost. Together these make seats past the kitchen bind add zero/negative predicted
+ contribution, so the forecast no longer **reverses** the seat decision (was: 46→66 seats forecast +$483 /
+ actual −$377). Held-out seat-direction agreement **11/12**; band coverage 66% in-harness / 73% held-out vs
+ a stated 55%. Forecaster stays pre-service only; the sim and checksum are untouched. See
+ `FORECAST-CONTRACT.md`, `reports/balance/forecast-calibration.md`.
+
+### 2026-07-29 · D-020 · Second correction: single-service price elasticity resolves NEW-1 (H2)
+- **Status:** locked · **Owner:** Builder · **Supersedes:** [[D-019]] deferral
+- **Decision:** Willingness-to-pay is anchored to each dish's **own** calibrated suggested price (not the menu
+ median), widened by segment tolerance and dish quality; above WTP, order probability decays **smoothly**
+ (hyperbolic), scaled by segment price sensitivity. No hard cap, no name branch, no single-threshold cliff,
+ no fixture-targeted penalty. `PriceResistScaleBp = 450` is chosen from an elasticity target (a maximally
+ price-sensitive diner retains ~1/3 of demand at ~8% over WTP), not from the dominance result — every value
+ in [350,600] already defeats the dominator.
+- **Result:** The uniformly-overpriced coherent menu is defeated, and pricing becomes context-dependent:
+ a **frontier search** (see [[D-024]]) finds the value lunch's best plan is FAIR-priced/high-capacity while
+ the enthusiast's best plan is premium/lean — distinct, structurally-opposed regimes, so **no single plan
+ is near-optimal in all three markets**. Premium pricing stays viable **contextually** (wins the enthusiast
+ market). Goldens byte-identical to before this constant. See `PRICING-CONTRACT.md`, `PriceModel.cs`.
+- **Revisit:** M1 adds reputation/repeat-visits, which give price/quality *durable* teeth beyond one service.
+
### 2026-07-28 · D-019 · NEW-1 residual dominator (uniform overprice) disclosed, deferred to M0.5
-- **Status:** open finding · **Owner:** owners
-- **Decision:** The dominance search still finds one cross-market dominator: a uniformly-overpriced coherent
+- **Status:** RESOLVED by [[D-020]] (fixed in-M0, not deferred) · **Owner:** owners
+- **Decision:** The dominance search found one cross-market dominator: a uniformly-overpriced coherent
menu. It stems from M0 having no repeat-visit/reputation teeth on price/quality (same root as [[D-012]]).
- Strengthening price elasticity would change the Focused Value golden, so it is NOT fixed in the bounded
- pass. Disclosed in `reports/balance/dominance-search.md`; recommended for M0.5 calibration.
-- **Revisit:** M0.5 (price elasticity) or M1 (reputation/repeat visits).
+ The first bounded pass judged that strengthening price elasticity would change the Focused Value golden, so
+ it deferred the fix. The **second** correction (D-020) implemented credible single-service elasticity and
+ resolved it inside M0. Disclosed in `reports/balance/dominance-search.md`.
+- **Revisit:** M1 (reputation/repeat visits) for *durable* price/quality teeth beyond one service.
### 2026-07-28 · D-018 · Audit correction: docs corrected to "same-environment determinism verified"
- **Status:** locked · **Decision:** DETERMINISM-CONTRACT and ADR-002 no longer assert cross-OS byte-identity
diff --git a/docs/NEXT-ACTION.md b/docs/NEXT-ACTION.md
index 82bd542..726c388 100644
--- a/docs/NEXT-ACTION.md
+++ b/docs/NEXT-ACTION.md
@@ -3,17 +3,24 @@
> **FIRST: re-confirm the repository's actual current state before trusting this document.**
> Run `git status`, `git log --oneline -8`, `dotnet test`, and
> `dotnet run -c Release --project src/RestaurantSim.Harness -- --seeds 200 --out /tmp/verify`.
-> Confirm 74 tests pass, determinism reports PASS, and distinct winners = 2/3. Any commit hash or number in
-> a doc is a timestamp, not a contract — verify against reality first.
+> Confirm 117 tests pass, determinism reports PASS, distinct winners = 2/3, and the dominance search reports
+> no cross-market dominator (best generalist well below frontier in its worst market). Any commit hash or
+> number in a doc is a timestamp, not a contract — verify against reality first.
## The single next authorized action
-**Independent reviewer RE-CHECKS the bounded correction pass** (branch `fix/m0-audit-corrections`), then
-owners run the human playtests. Re-check specifically: (1) the causal autopsy no longer flips on the 88%
-util boundary and attributes the primary loss correctly (see `AttributionTests` + `PRE-FIX-AUDIT-EVIDENCE`);
-(2) the forecast band coverage meets its stated confidence and labels are honest; (3) the coherence lever
-kept the three golden checksums byte-identical and the hybrid is no longer a cross-market dominator; (4) the
-**NEW-1** residual (uniform-overprice exploit) is disclosed, not hidden, and the owner accepts deferring it
-to M0.5; (5) M4/M5/M6/M7 fixes; (6) determinism, accounting, and scope remain intact. Then:
+**Independent reviewer RE-CHECKS the second correction** (branch `fix/m0-price-forecast-integrity`), then
+owners run the human playtests. Re-check specifically: (1) **price elasticity is principled, not a
+fixture-targeted patch** — WTP is anchored to each dish's own suggested price (not the menu median), the
+curve is smooth (no cliff), there is no hard cap or name branch, and `PriceResistScaleBp` is justified by an
+elasticity target rather than the dominance outcome (see `PRICING-CONTRACT.md`, `PriceElasticityTests`);
+(2) **no cross-market dominator** reproduces under the reviewer's own FRONTIER search (not just a comparison
+to the under-optimized named strategies) — the per-market optima are distinct regimes and the uniformly-
+overpriced menu is genuinely beaten while premium stays viable in the enthusiast market; (3) **the forecast no longer
+reverses the seat decision** — seats past the kitchen wall do not raise the prediction, and seat-direction
+agreement holds on held-out seeds (see `FORECAST-CONTRACT.md`, `ForecastDirectionTests`,
+`forecast-calibration.md`); (4) the **golden re-baseline** is the deliberate, documented consequence of the
+price recentering (D-022), not an accident; (5) the first correction's fixes (H1, M4–M7) and determinism,
+accounting, and scope all remain intact. Then:
1. **Independent reviewer** (not the Builder): clone fresh, run the tests and the harness with their own
tooling, try to break determinism, and confirm M0 stayed in scope (nothing from the non-goals list, no
diff --git a/docs/design/FORECAST-CONTRACT.md b/docs/design/FORECAST-CONTRACT.md
new file mode 100644
index 0000000..e7da59b
--- /dev/null
+++ b/docs/design/FORECAST-CONTRACT.md
@@ -0,0 +1,110 @@
+# Forecast Contract (pre-service prediction)
+
+**Status:** Locked for M0. Implemented by `RestaurantSim.Core/Forecast.cs` and the `Forecast*` constants in
+`Tuning.cs`. Materially revised by the **second bounded correction** (audit finding H3). The forecast is
+computed from pre-commit information only, stored immutably, and compared against the actual; it is never
+recomputed with post-service knowledge.
+
+> The forecast is **advisory** and never feeds the simulation or the checksum. It exists so the player can
+> reason about a plan before committing, and so forecast-vs-actual gaps are legible.
+
+## Why this was revised
+
+The audit found the forecast **reversed a real decision**: raising seats from 46 to 66 on a kitchen-bound
+plan raised the *forecast* by ~$483 while the *actual* fell by ~$377. The cause was a kitchen capacity
+estimate **averaged across stations**, which never saw the true station wall (e.g. two mains sharing one
+grill), so it read too high, never bound, and made extra seats look free. This contract makes the forecast
+model the binding constraint and the cost of over-seating, so its directionality matches reality.
+
+## What it outputs (`ForecastSnapshot`)
+
+1. **Demand opportunity** — the average-capacity, zero-loss ceiling: `min(attempted covers, kitchen cap,
+ seat cap)`. The best case if every seated party completed. *Not* what completes.
+2. **Expected covers** — demand opportunity × **realization** (peaking + walkouts + comps). The honest point
+ estimate.
+3. **Expected contribution** + a **low/high band** — revenue − ingredients − labor − overhead −
+ over-acceptance waste, with a wide, downward-skewed interval.
+
+## The three fixes that restored directionality
+
+### 1. Per-station kitchen bottleneck (not an average)
+
+Kitchen cover capacity is the **minimum over stations** of `stationWork / workPerCover`, where each station's
+work is its assigned crew's speed × station-fit × service-minutes × `SlotsPerCook`, and `workPerCover` is
+that station's expected active-minutes per cover (course-probability weighted, complexity-loaded). An empty
+station is not a constraint. This is the same station model the simulator uses, so the two cannot drift.
+
+*Effect:* two mains on one grill now shows the grill as the wall, so the kitchen cap binds at the real
+number instead of an inflated average.
+
+### 2. Realization keyed off kitchen throughput only
+
+`RealizationBp` (fraction of the ceiling that completes) is keyed on **peak-minute cover demand vs the
+kitchen's per-minute capacity** — *not* `min(kitchen, seat)`. Peaking is a kitchen phenomenon, so adding
+seats past the kitchen wall cannot raise realization or expected completed covers.
+
+```
+realizationBp = clamp(BaseBp - peakOverPct * PeakPenaltyBp, FloorBp, BaseBp)
+```
+
+### 3. Over-acceptance waste
+
+Seats above the kitchen wall create covers that get seated but cannot be served; their ingredients are sunk
+for no revenue. Modeled explicitly, and *only when demand actually pressures the kitchen* (slack demand
+wastes nothing):
+
+```
+overSeats = max(0, seatCoverCap - kitchenCoverCap)
+demandPressureBp = clamp(attemptedCovers / kitchenCoverCap, 0, 10000)
+wasteCovers = overSeats * demandPressureBp / 10000
+overAcceptCost = wasteCovers * ingPerCover * ForecastOverAcceptWasteBp / 10000
+```
+
+*Effect:* more seats than the kitchen can serve **reduce** predicted contribution; fewer seats, when
+kitchen-bound, **raise** it — the direction the actual moves.
+
+## The contribution band
+
+Contribution swings with operating leverage, so the band width scales with expected **revenue** (a stabler
+base than near-zero contribution), is skewed low (`ForecastBandDownBp` > `ForecastBandUpBp`), and carries a
+fixed buffer (fixed overhead) so near-zero contributions still get a real interval. The stated confidence is
+the **measured** held-out coverage, deliberately quoted *below* what the band actually achieves — honest, not
+aspirational.
+
+## Constants (`Tuning.cs`)
+
+| Constant | Value | Meaning |
+|---|--:|---|
+| `ForecastRealizationBaseBp` | 9000 | realization when the peak fits capacity |
+| `ForecastRealizationFloorBp` | 4200 | realization floor under heavy oversubscription |
+| `ForecastRealizationPeakPenaltyBp` | 42 | realization lost per 1% the peak exceeds capacity |
+| `ForecastOverAcceptWasteBp` | 8000 | fraction of a wasted cover's ingredient charged when seats exceed the kitchen |
+| `ForecastBandDownBp` | 5800 | downside band as a fraction of revenue |
+| `ForecastBandUpBp` | 4800 | upside band as a fraction of revenue |
+| `ForecastStatedConfidenceBp` | 5500 | stated confidence (below measured held-out coverage) |
+| `ForecastAvgDwellMin` | 34 | assumed party dwell for the seat-turn estimate |
+| `SlotsPerCook` | 4 | concurrent covers a cook advances (shared with the sim) |
+
+## Directional guarantees (locked as `ForecastDirectionTests`)
+
+1. Adding seats **past the kitchen wall** does not raise predicted contribution.
+2. Adding a **needed cook** at the bottleneck station raises predicted contribution.
+3. Adding seats **when seating is the bind** does not lower predicted contribution.
+4. **Extreme overpricing** lowers predicted completed covers (the forecast shares `PriceModel`).
+
+## Measured calibration (held-out seeds, separate from tuning)
+
+- **Seat-direction agreement (forecast vs actual): 11/12** across markets and seat levels — the forecast now
+ moves the same direction as reality on the capacity decision it used to reverse.
+- **Band coverage: 66%** in-harness / **73%** held-out overall (Value/named 64%, adversarial 71%, capacity
+ 82%, price 87%) against a **stated 55%** — the band is honestly conservative in every plan class.
+- **Expected-covers error** on the auditor's headline case (Focused Value / lunch) is within ~40% of the
+ actual mean, versus the old ~135-vs-98 ceiling; the per-station cap is a coarse pre-service approximation,
+ so the point estimate can under-predict but no longer over-predicts the ceiling. See
+ `reports/balance/forecast-calibration.md`.
+
+## Out of scope (deliberately)
+
+No multi-service learning, no Bayesian update from prior actuals, no confidence that narrows with experience,
+no per-station queue simulation in the forecast. The forecast stays a single-shot analytic approximation; its
+job is to be **directionally correct and honestly bounded**, not exact.
diff --git a/docs/design/M0-BALANCE-HYPOTHESES.md b/docs/design/M0-BALANCE-HYPOTHESES.md
index 4626ecc..df4af5a 100644
--- a/docs/design/M0-BALANCE-HYPOTHESES.md
+++ b/docs/design/M0-BALANCE-HYPOTHESES.md
@@ -79,3 +79,57 @@ The independent audit refuted the bare claim "no dominant strategy." Two correct
incoherence exploit is fixed, and one price-elasticity exploit remains — a manifestation of the disclosed
no-repeat-visit M0 boundary, deferred to M0.5.* This is a fair basis for the M0 gate; it is not the
unqualified "dominance ruled out."
+
+---
+
+## Second-correction update (strategy-integrity + forecast, 2026-07-29)
+
+The residual **NEW-1** exploit above is now **resolved inside M0**, not deferred, and the forecast that used
+to reverse the seat decision is corrected. See `PRICING-CONTRACT.md`, `FORECAST-CONTRACT.md`, DECISION-LOG
+D-020..D-023.
+
+- **NEW-1 fixed with credible single-service price elasticity.** Willingness-to-pay is anchored to each
+ dish's **own** suggested price (not the menu median, which the uniform-overprice menu hid behind), so
+ overpricing every dish now costs real demand in the current service. The uniformly-overpriced coherent menu
+ is regression-locked as beaten in `PriceElasticityTests`.
+- **No cross-market dominator — tested against a searched FRONTIER, not the named strategies.** An independent
+ verifier correctly showed that "beats every named market winner" is a weak signal: the named strategies are
+ under-optimized (Focused Value makes ~$450 in the lunch rush where a fair-priced, fully-staffed
+ Burger+Ribeye+Fish plan makes ~$2350). The strengthened search builds a strong per-market frontier and asks
+ whether any single plan is near-optimal in ALL three markets. It is not: the best generalist is **~$1170
+ below the frontier in its worst market**, and the per-market optima are distinct, structurally-opposed
+ regimes (lunch → fair-priced/high-capacity; enthusiast → premium/lean). Locked in `DominanceFrontierTests`,
+ reported in `reports/balance/dominance-search.md`. See DECISION-LOG D-024.
+- **Premium stays viable but becomes CONTEXTUAL.** Premium Craft now wins **only** the enthusiast evening and
+ **loses the value lunch rush and the social dinner** to Focused Value. This *restores* pre-registered **H1**
+ (the value lunch punishes premium), which the first correction's coarser model had let slip.
+
+Winners by market (200 seeds/cell, `reports/balance/distribution.md`), winner **bold**:
+
+| Strategy | lunch | social | enthusiast |
+|---|--:|--:|--:|
+| Focused Value | **+$453** | **+$1091** | +$671 |
+| Premium Craft | −$151 | +$618 | **+$2149** |
+| Balanced Competent | −$108 | +$692 | +$1255 |
+| Broad Menu | −$142 | +$384 | +$1392 |
+| Overcapacity / Understaffed / Overpriced / Station Bottleneck / Intentionally Bad | all negative | mostly negative | mostly negative |
+
+- **Distinct winners: 2/3** (Focused Value, Premium Craft) — no single dominant strategy, unchanged.
+- **Context clearly changes the winner:** value pricing wins the two price-led markets; premium craft wins
+ the one quality-led market. "Premium is not always best" and "the highest prices are not automatically
+ best" now hold *strictly* — premium wins exactly the one market it fits.
+- **The lunch rush is a thin market:** only Focused Value clears the +$150 profitable bar there. That is
+ realistic for a high-volume, price-sensitive crowd and is a within-market observation, not a breach of the
+ cross-market non-dominance bar.
+
+**Forecast integrity.** The forecast now models the per-station kitchen wall and the waste from seating past
+it, so raising seats past the kitchen bind no longer *raises* the prediction. Held-out **seat-direction
+agreement is 11/12**; contribution-band coverage is 66% in-harness / 73% held-out against a stated 55%.
+
+**Updated honest claim:** *No single plan is near-optimal across all three markets — the best generalist is
+~$1170 below the per-market frontier in its worst market, and the per-market optima are distinct,
+structurally-opposed regimes. Premium pricing is viable but contextual (wins only where it fits); value/fair
+pricing wins the price-led markets; poor strategies fail legibly; and the forecast is directionally correct
+on the capacity decision it used to reverse.* Disclosed caveats for M0.5: the named strategy set is
+illustrative and under-optimized versus the frontier, and lean strategies underperform enough that
+labor/throughput economics deserve a calibration review (DECISION-LOG D-024).
diff --git a/docs/design/PRICING-CONTRACT.md b/docs/design/PRICING-CONTRACT.md
new file mode 100644
index 0000000..7396880
--- /dev/null
+++ b/docs/design/PRICING-CONTRACT.md
@@ -0,0 +1,109 @@
+# Pricing Contract (single-service price elasticity)
+
+**Status:** Locked for M0. Implemented by `RestaurantSim.Core/PriceModel.cs` and the `Price*` constants in
+`Tuning.cs`. Added by the **second bounded correction** (audit finding H2/NEW-1). This contract governs how
+a customer decides, *within one service*, whether a dish is worth its price.
+
+> **Scope boundary (authorized behavior).** A customer deciding **not to order** an overpriced dish during
+> the current service is in scope. Any *persistent* consequence of that choice (a remembered grudge, a
+> reputation hit, fewer visits next time) is **out of M0 scope** and is deliberately not modeled here. Price
+> elasticity in M0 changes only *this* service's order mix, nothing durable. See DECISION-LOG D-012 / D-020.
+
+## Why this exists
+
+The independent re-review found that a **uniformly overpriced coherent menu** (every dish ~2.5x its
+suggested price) dominated all three markets. The prior menu-*coherence* lever could not catch it: the menu's
+internal median was normal, so no dish looked like an outlier. Demand was effectively price-inelastic, so
+raising every price was free money. This contract makes overpricing cost real demand *in the current
+service*, without a hard cap, a name branch, or a fixture-targeted penalty.
+
+## The model
+
+### 1. Willingness-to-pay (WTP) — anchored to the dish, not the menu
+
+```
+premiumRoom = 10000 - segment.PriceSensitivityBp // value≈0, social=4500, enthusiast=7000
+toleranceBp = 10000
+ + PriceToleranceBaseBp * premiumRoom / 10000 // low-sensitivity segments tolerate more
+ + max(0, QualityCeiling - PriceQualityRefLevel) * premiumRoom / PriceQualityToleranceDiv
+WTP(dish, segment) = dish.SuggestedPriceCents * toleranceBp / 10000
+```
+
+WTP is anchored to each **dish's calibrated suggested price**, never to the menu median. That is the whole
+point: a uniformly overpriced menu has a normal internal median but every dish is far above its own WTP, so
+every dish is resisted. A genuinely premium dish (high `QualityCeiling`) earns a higher WTP **for the
+segments that value quality** (large `premiumRoom`), which is why premium pricing stays legitimate.
+
+- **Value Lunch** (sensitivity 10000): `premiumRoom = 0` ⇒ tolerance = 1.00 ⇒ **WTP = suggested price
+ exactly**. Value diners grant no premium room.
+- **Food Enthusiast** (sensitivity 3000): `premiumRoom = 7000` ⇒ large base tolerance **plus** a quality
+ bonus ⇒ WTP well above suggested for high-quality dishes.
+
+### 2. Resistance above WTP — smooth, hyperbolic, segment-scaled
+
+```
+if price <= WTP: ResistBp = 10000 // full demand at or below WTP
+else:
+ over = price - WTP
+ denom = WTP + over * segment.PriceSensitivityBp / PriceResistScaleBp
+ ResistBp = clamp(10000 * WTP / denom, PriceResistanceFloorBp, 10000)
+```
+
+`ResistBp` is the fraction of demand (basis points) that still orders at that price. It is continuous and
+monotone, steepest just above WTP and flattening toward a small floor — **a smooth curve, never a cliff.**
+The steepest single 1%-of-WTP price step drops resistance by ~18 points for the most price-sensitive
+segment; a true discontinuity would be ~90. Steepness scales with the segment's own price sensitivity, so
+value diners react sharply and enthusiasts gently.
+
+**Retention at a glance (fraction of demand kept, `PriceResistScaleBp = 450`):**
+
+| price / WTP | Value (10000) | Social (5500) | Enthusiast (3000) |
+|---|--:|--:|--:|
+| ≤ 1.0x | 100% | 100% | 100% |
+| 1.25x | 15% | 24% | 37% |
+| 1.5x | 8% | 14% | 23% |
+| 2.0x | 4% | 7% | 13% |
+| 3.0x | 3% (floor) | 3% (floor) | 6% |
+
+## Constants (`Tuning.cs`)
+
+| Constant | Value | Meaning |
+|---|--:|---|
+| `PriceToleranceBaseBp` | 6000 | premium-room → base WTP tolerance above suggested price |
+| `PriceQualityRefLevel` | 650 | dish quality above this widens WTP |
+| `PriceQualityToleranceDiv` | 650 | divisor scaling the quality tolerance bonus |
+| `PriceResistScaleBp` | 450 | lower ⇒ steeper resistance above WTP |
+| `PriceResistanceFloorBp` | 300 | demand asymptote (never quite zero) |
+
+**On `PriceResistScaleBp = 450`.** Every value in `[350, 600]` already defeats the uniform-overprice
+dominator, so *dominance does not pin this constant*. It is set from an elasticity target: a maximally
+price-sensitive diner retains ~1/3 of demand at ~8% over WTP. At that steepness premium pricing becomes
+genuinely **contextual** — it wins the enthusiast evening it fits and correctly loses the value lunch rush.
+Softer values (≥ 500) let a premium menu skim the value market's minority and win the lunch rush, which is
+premium being non-contextual; steeper values (≤ 400) push the near-WTP step needlessly high. See
+DECISION-LOG D-020.
+
+## Where it is used (and where it is not)
+
+- **`Demand.ConversionBp`** multiplies segment conversion by `ResistBp(typicalMainPrice, Wtp(topDish, seg))`.
+- **`Simulator` dish choice** weights each dish by `ResistBp(price, Wtp(dish, seg))` so overpriced dishes
+ are ordered less *within* a menu.
+- It does **not** touch labor, ingredients, the forecast's cost side, satisfaction, or any persistent state.
+ It is a pure, deterministic, integer function of `(price, WTP, sensitivity)`.
+
+## Invariants (locked as tests in `PriceElasticityTests`)
+
+1. At or below WTP, resistance is exactly 10000 (premium dishes at suggested price face no penalty).
+2. Extreme uniform overpricing (3x) earns **less** than moderate pricing (1.25x) in every market.
+3. The profit-maximising markup is **moderate** (≤ 1.75x), not the top of the range.
+4. The uniformly overpriced coherent menu **no longer wins all three markets**.
+5. Value diners keep **less** demand than enthusiasts at the same relative overprice.
+6. Resistance is **smooth** — no 1% step approaches a discontinuity.
+7. A cheap filler dish **cannot** rescue an overpriced main (resistance is per-dish, not menu-median).
+
+## What this deliberately does not do
+
+No hard/global price cap. No branching on dish, recipe, or strategy name. No single-threshold cliff. No
+fixture-targeted penalty. No reputation, memory, or repeat-visit effect. The menu-median **coherence** lever
+from the first correction (`Tuning.CoherenceWeightBp`) is retained for the value-chassis-plus-anchor case it
+was built for, but it is no longer the primary defense — per-dish WTP resistance is.
diff --git a/reports/balance/distribution.md b/reports/balance/distribution.md
index 07edf66..b46c30b 100644
--- a/reports/balance/distribution.md
+++ b/reports/balance/distribution.md
@@ -10,15 +10,15 @@ _Value-heavy, high-volume, price-sensitive, small parties, early peak. Rewards f
| Strategy | median contrib | p10 | p90 | loss% | avg covers | avg sat | avg ticket | win% |
|---|--:|--:|--:|--:|--:|--:|--:|--:|
-| Focused Value | $304.15 | $29.25 | $551.95 | 8% | 93 | 531 | 16m | 74% |
-| Premium Craft | -$51.88 | -$387.76 | $325.66 | 56% | 21 | 767 | 15m | 16% |
-| Broad Menu | -$282.00 | -$761.50 | $142.50 | 78% | 82 | 559 | 20m | 7% |
-| Overcapacity | -$789.00 | -$929.00 | -$662.00 | 100% | 22 | 537 | 20m | 0% |
-| Understaffed | -$528.00 | -$615.00 | -$448.50 | 100% | 12 | 506 | 22m | 0% |
-| Balanced Competent | -$408.00 | -$726.00 | -$1.00 | 90% | 66 | 570 | 20m | 1% |
-| Overpriced Weak Execution | -$302.80 | -$534.80 | $20.20 | 87% | 7 | 397 | 27m | 1% |
-| Station Bottleneck | -$597.00 | -$707.50 | -$497.00 | 100% | 15 | 591 | 18m | 0% |
-| Intentionally Bad | -$391.20 | -$441.00 | -$325.20 | 100% | 5 | 528 | 20m | 0% |
+| Focused Value | $452.55 | $105.10 | $881.45 | 3% | 76 | 519 | 16m | 68% |
+| Premium Craft | -$150.90 | -$605.36 | $512.30 | 62% | 25 | 611 | 20m | 10% |
+| Broad Menu | -$142.00 | -$732.00 | $486.50 | 61% | 65 | 528 | 21m | 12% |
+| Overcapacity | -$687.50 | -$867.50 | -$515.00 | 100% | 20 | 521 | 20m | 0% |
+| Understaffed | -$501.00 | -$601.50 | -$392.00 | 100% | 10 | 485 | 23m | 0% |
+| Balanced Competent | -$108.00 | -$528.00 | $360.50 | 58% | 58 | 546 | 21m | 3% |
+| Overpriced Weak Execution | -$236.00 | -$540.70 | $375.00 | 69% | 8 | 382 | 28m | 7% |
+| Station Bottleneck | -$547.50 | -$686.00 | -$416.00 | 100% | 12 | 569 | 19m | 0% |
+| Intentionally Bad | -$140.00 | -$284.50 | -$7.50 | 91% | 7 | 568 | 19m | 0% |
**Winner:** Focused Value · **profitable strategies (median >= +$150):** 1
@@ -28,17 +28,17 @@ _Mid-budget social diners in groups, multi-course, moderate patience. Rewards a
| Strategy | median contrib | p10 | p90 | loss% | avg covers | avg sat | avg ticket | win% |
|---|--:|--:|--:|--:|--:|--:|--:|--:|
-| Focused Value | $519.50 | $114.85 | $723.05 | 3% | 101 | 582 | 18m | 42% |
-| Premium Craft | $361.90 | -$538.82 | $1334.24 | 31% | 58 | 639 | 24m | 42% |
-| Broad Menu | $93.00 | -$600.00 | $706.50 | 42% | 95 | 581 | 23m | 10% |
-| Overcapacity | -$572.50 | -$843.50 | -$326.00 | 100% | 37 | 578 | 21m | 0% |
-| Understaffed | -$480.00 | -$607.50 | -$354.50 | 100% | 17 | 514 | 25m | 0% |
-| Balanced Competent | $42.00 | -$540.50 | $655.50 | 48% | 87 | 609 | 21m | 6% |
-| Overpriced Weak Execution | -$899.10 | -$1077.60 | -$694.20 | 100% | 5 | 374 | 29m | 0% |
-| Station Bottleneck | -$626.00 | -$807.50 | -$402.00 | 100% | 21 | 624 | 20m | 0% |
-| Intentionally Bad | -$394.60 | -$488.80 | -$293.80 | 100% | 7 | 517 | 23m | 0% |
-
-**Winner:** Focused Value · **profitable strategies (median >= +$150):** 2
+| Focused Value | $1091.35 | $499.90 | $1456.15 | 0% | 99 | 574 | 18m | 39% |
+| Premium Craft | $618.36 | -$309.10 | $1917.08 | 17% | 52 | 586 | 24m | 34% |
+| Broad Menu | $383.50 | -$546.00 | $1300.50 | 32% | 81 | 552 | 24m | 8% |
+| Overcapacity | -$326.50 | -$679.00 | $100.00 | 83% | 36 | 567 | 21m | 0% |
+| Understaffed | -$402.00 | -$555.50 | -$209.50 | 99% | 17 | 506 | 25m | 0% |
+| Balanced Competent | $691.50 | -$91.00 | $1536.50 | 14% | 84 | 593 | 22m | 17% |
+| Overpriced Weak Execution | -$862.70 | -$1063.40 | -$569.60 | 99% | 6 | 359 | 29m | 0% |
+| Station Bottleneck | -$501.50 | -$703.50 | -$199.00 | 98% | 19 | 605 | 21m | 0% |
+| Intentionally Bad | -$204.50 | -$393.00 | $65.50 | 84% | 11 | 530 | 24m | 0% |
+
+**Winner:** Focused Value · **profitable strategies (median >= +$150):** 4
## Destination Enthusiast Evening (`enthusiast-evening`)
@@ -46,17 +46,17 @@ _High-budget enthusiasts, lower volume, high quality expectations, patient. Rewa
| Strategy | median contrib | p10 | p90 | loss% | avg covers | avg sat | avg ticket | win% |
|---|--:|--:|--:|--:|--:|--:|--:|--:|
-| Focused Value | $246.95 | $80.95 | $411.30 | 2% | 63 | 633 | 13m | 0% |
-| Premium Craft | $1031.68 | $161.14 | $1754.74 | 6% | 68 | 612 | 24m | 74% |
-| Broad Menu | $547.00 | -$38.00 | $945.50 | 12% | 88 | 587 | 23m | 11% |
-| Overcapacity | -$20.50 | -$424.00 | $486.50 | 52% | 51 | 561 | 22m | 0% |
-| Understaffed | -$336.00 | -$498.50 | -$171.00 | 98% | 22 | 466 | 27m | 0% |
-| Balanced Competent | $484.50 | $191.50 | $743.00 | 0% | 78 | 642 | 18m | 13% |
-| Overpriced Weak Execution | -$851.80 | -$1053.60 | -$580.00 | 99% | 7 | 315 | 30m | 0% |
-| Station Bottleneck | -$394.50 | -$666.00 | -$123.50 | 97% | 27 | 581 | 22m | 0% |
-| Intentionally Bad | -$319.40 | -$442.40 | -$170.80 | 100% | 10 | 469 | 24m | 0% |
-
-**Winner:** Premium Craft · **profitable strategies (median >= +$150):** 4
+| Focused Value | $670.70 | $387.40 | $927.50 | 0% | 63 | 629 | 13m | 0% |
+| Premium Craft | $2149.38 | $878.82 | $3140.88 | 2% | 68 | 571 | 25m | 71% |
+| Broad Menu | $1391.50 | $481.50 | $2104.50 | 3% | 86 | 558 | 24m | 12% |
+| Overcapacity | $386.50 | -$112.00 | $1112.50 | 16% | 51 | 556 | 22m | 0% |
+| Understaffed | -$217.50 | -$420.00 | $14.50 | 86% | 22 | 465 | 27m | 0% |
+| Balanced Competent | $1254.50 | $814.00 | $1696.00 | 0% | 79 | 632 | 18m | 16% |
+| Overpriced Weak Execution | -$748.20 | -$978.90 | -$391.00 | 98% | 7 | 309 | 30m | 0% |
+| Station Bottleneck | -$144.50 | -$521.50 | $214.50 | 69% | 26 | 571 | 22m | 0% |
+| Intentionally Bad | -$170.50 | -$345.50 | $108.50 | 79% | 12 | 469 | 25m | 0% |
+
+**Winner:** Premium Craft · **profitable strategies (median >= +$150):** 5
## Dominance check
diff --git a/reports/balance/dominance-search.md b/reports/balance/dominance-search.md
index dd4f156..299f688 100644
--- a/reports/balance/dominance-search.md
+++ b/reports/balance/dominance-search.md
@@ -1,17 +1,31 @@
-# M0 Dominance Search Report
+# M0 Cross-Market Dominance Search
-Closes audit finding H2: the old 'no dominant strategy' claim only compared 9 hand-authored plans.
-This bounded random search over the DECISION SPACE (menu subset, prices, staffing, seats) looks for
-any configuration that beats the best NAMED strategy in ALL three markets by a material margin.
+Question: does a SINGLE plan sit near the best-achievable contribution in ALL three markets
+(a cross-market dominator), or are the per-market optima DISTINCT plans (healthy context-dependence)?
-Searched **500** random configurations x 40 seeds. Named-best medians: lunch-rush $236.70, social-dinner $542.25, enthusiast-evening $1054.92.
+Searched **3290** configurations (4x800 random + archetype sweep) x 80 seeds each, held-out base 900000.
-**Configurations that dominate all 3 markets by >= $150.00: 1.**
+## Per-market frontier (best contribution found) — the plans are DISTINCT
-Best such config (min cross-market edge $340.46): menu[Classic Burger@$26.55, Roast Chicken@$52.14, Seared Scallops@$41.04, Veggie Bowl@$29.54] seats 36, 8 staff
+| Market | Best contribution | Winning plan |
+|---|--:|---|
+| lunch-rush | $2349.50 | menu[Classic Burger@$21.00, Ribeye Steak@$59.00, Fish & Chips@$25.00, Ice Cream@$10.00] seats 55, 8 staff |
+| social-dinner | $5005.00 | menu[Ribeye Steak@$73.75, Fish & Chips@$31.25, Ice Cream@$10.00] seats 40, 8 staff |
+| enthusiast-evening | $4657.00 | menu[Ribeye Steak@$88.50, Fish & Chips@$37.50, Ice Cream@$10.00] seats 40, 8 staff |
-> KNOWN RESIDUAL (audit NEW-1): the menu-coherence lever fixes the value-chassis-plus-premium-anchor
-> exploit, but a UNIFORMLY-OVERPRICED COHERENT menu can still dominate because M0 has no repeat-visit
-> or reputation feedback, so quality/price expectations have no economic teeth within a single service.
-> This is the disclosed throughput-vs-quality M0 boundary (DECISION-LOG D-012); giving price a
-> countervailing cost is an M0.5/M1 concern, not something to hack in this bounded pass. NOT hidden.
+## Best generalist (the single plan closest to winning everywhere)
+
+Plan: menu[Classic Burger@$26.25, Ribeye Steak@$73.75, Fish & Chips@$31.25, Ice Cream@$10.00] seats 40, 8 staff
+- lunch-rush: $1180.00 (frontier $2349.50)
+- social-dinner: $4280.25 (frontier $5005.00)
+- enthusiast-evening: $3549.25 (frontier $4657.00)
+- **worst-market deficit to frontier: -$1169.50**
+
+> **No cross-market dominator found.** The best generalist is -$1169.50 below the frontier in its worst market — far outside the $150.00 near-optimal band. The per-market optima are distinct, structurally-opposed regimes (fair-priced / high-capacity for the value lunch vs premium / lean for the enthusiast evening), so context genuinely changes the best strategy.
+
+## Note on the NAMED strategies
+The nine named strategies are illustrative archetypes, not frontier-optimal. A searched plan can
+beat every named market winner without being a cross-market dominator (it merely out-optimizes the
+hand-authored baselines). That is why this report tests against the searched frontier, not the named set.
+
+Named-best medians (for reference): lunch-rush $452.55, social-dinner $1091.35, enthusiast-evening $2149.38.
diff --git a/reports/balance/forecast-calibration.md b/reports/balance/forecast-calibration.md
index 45a4884..3ad9cef 100644
--- a/reports/balance/forecast-calibration.md
+++ b/reports/balance/forecast-calibration.md
@@ -7,8 +7,8 @@ range. Stated confidence: **55%**. Seeds/cell: 200.
| Scenario | contribution band coverage | median |covers bias| |
|---|--:|--:|
-| lunch-rush | 1000/1800 = 55% | 126% |
-| social-dinner | 984/1800 = 54% | 112% |
-| enthusiast-evening | 1342/1800 = 74% | 29% |
+| lunch-rush | 1240/1800 = 68% | 55% |
+| social-dinner | 1218/1800 = 67% | 29% |
+| enthusiast-evening | 1122/1800 = 62% | 17% |
-**Aggregate band coverage: 3326/5400 = 61%** (was ~0% pre-fix; meets the stated 55% confidence).
+**Aggregate band coverage: 3580/5400 = 66%** (was ~0% pre-fix; meets the stated 55% confidence).
diff --git a/reports/determinism/checksums.md b/reports/determinism/checksums.md
index 9a23f3f..500faaf 100644
--- a/reports/determinism/checksums.md
+++ b/reports/determinism/checksums.md
@@ -5,14 +5,14 @@ A management sim that promises "same seed reproduces the same result" must pass
| Scenario | Strategy | Seed | Checksum run 1 | Checksum run 2 | Match |
|---|---|--:|---|---|:--:|
-| lunch-rush | Focused Value | 700042 | `F07AA46F729A0A8F` | `F07AA46F729A0A8F` | ✅ |
-| lunch-rush | Premium Craft | 700042 | `A0D653B17FF192D8` | `A0D653B17FF192D8` | ✅ |
-| lunch-rush | Broad Menu | 700042 | `1607D833572605AB` | `1607D833572605AB` | ✅ |
-| social-dinner | Focused Value | 700042 | `54F356FF43370823` | `54F356FF43370823` | ✅ |
-| social-dinner | Premium Craft | 700042 | `CC04BB7D20A81AEE` | `CC04BB7D20A81AEE` | ✅ |
-| social-dinner | Broad Menu | 700042 | `7CBDD747FEEBB6EC` | `7CBDD747FEEBB6EC` | ✅ |
-| enthusiast-evening | Focused Value | 700042 | `94FEB3F3011C1E36` | `94FEB3F3011C1E36` | ✅ |
-| enthusiast-evening | Premium Craft | 700042 | `24EE4A83E7436797` | `24EE4A83E7436797` | ✅ |
-| enthusiast-evening | Broad Menu | 700042 | `2B5ABA477A42D072` | `2B5ABA477A42D072` | ✅ |
+| lunch-rush | Focused Value | 700042 | `290EB112568926A4` | `290EB112568926A4` | ✅ |
+| lunch-rush | Premium Craft | 700042 | `63E95A99F2C13357` | `63E95A99F2C13357` | ✅ |
+| lunch-rush | Broad Menu | 700042 | `55DFC522B0BD4341` | `55DFC522B0BD4341` | ✅ |
+| social-dinner | Focused Value | 700042 | `ACCE23DA358F613A` | `ACCE23DA358F613A` | ✅ |
+| social-dinner | Premium Craft | 700042 | `14AF2AF9B113A2B9` | `14AF2AF9B113A2B9` | ✅ |
+| social-dinner | Broad Menu | 700042 | `9835ED1C0E64FA04` | `9835ED1C0E64FA04` | ✅ |
+| enthusiast-evening | Focused Value | 700042 | `D56A79E4252970E5` | `D56A79E4252970E5` | ✅ |
+| enthusiast-evening | Premium Craft | 700042 | `C9AEF12F9CD876DA` | `C9AEF12F9CD876DA` | ✅ |
+| enthusiast-evening | Broad Menu | 700042 | `854CEB075DABF14E` | `854CEB075DABF14E` | ✅ |
**All checksums matched: determinism holds for the sampled matrix.**
diff --git a/reports/m0/M0-CORRECTION-2-REPORT.md b/reports/m0/M0-CORRECTION-2-REPORT.md
new file mode 100644
index 0000000..64ce8c0
--- /dev/null
+++ b/reports/m0/M0-CORRECTION-2-REPORT.md
@@ -0,0 +1,166 @@
+# M0 Second Correction — Strategy Integrity & Forecast — Builder's Report
+
+**Branch:** `fix/m0-price-forecast-integrity` · **Date:** 2026-07-29 · **Scope:** the two findings the first
+correction left open (H2/NEW-1 strategy integrity, H3 forecast). Not M0.5, not M1, not a general balance pass.
+
+---
+
+## A. Reviewed baseline
+
+Branched from the first-correction line. The re-review had left two open questions:
+- **H2 / NEW-1:** a uniformly-overpriced coherent menu robustly dominated all three markets because demand was
+ grossly price-inelastic. The first pass disclosed it and deferred it to M0.5.
+- **H3:** the forecast reversed the seat-capacity decision (46→66 seats: forecast +$483, actual −$377).
+
+The re-review's charge: implement credible single-service price elasticity that removes the overpricing
+dominance while preserving premium pricing as a viable contextual strategy, and correct the forecast so seats
+past the kitchen bind add zero/negative contribution. No arbitrary anti-exploit patch, no hard price cap, no
+name/fixture branch, no single-threshold cliff.
+
+## B. Investigation and independent verification
+
+Phase-B investigation was conducted as direct instrumented probing (documented in `PRE-FIX-2-EVIDENCE.md` and
+this report), not as four separate read-only sub-agents. **After** the work was implemented and green, three
+**independent adversarial verification agents** re-ran the claims from scratch (their own scratch code against
+the built Core, no reuse of the harness):
+1. **Price-elasticity verifier** — reproduce dominance, WTP anchoring, smoothness, premium viability.
+2. **Forecast verifier** — reproduce the seat-direction fix and held-out calibration.
+3. **Scope/determinism/accounting verifier** — tests, determinism, no-float, accounting, non-goals, git.
+
+Verifiers 2 and 3 returned clean (all VERIFIED / PASS with honest caveats). **Verifier 1 refuted the original
+"0 dominators" claim** — and it was right: see §E. That refutation directly drove the frontier-based
+strengthening of the dominance methodology. This is the correction working as intended: an independent check
+caught a weak test before the gate, not after.
+
+## C. Price-elasticity model (H2 / NEW-1)
+
+New `RestaurantSim.Core/PriceModel.cs`. Full contract in `docs/design/PRICING-CONTRACT.md`.
+
+- **Willingness-to-pay is anchored to each dish's OWN suggested price** (verified: `Wtp` has no menu
+ parameter — it structurally cannot read a median), widened by segment tolerance (low price-sensitivity →
+ more room) and dish quality. A uniformly-overpriced menu can no longer hide behind a normal internal median.
+- **Resistance above WTP is a smooth hyperbolic decay**, steepness scaled by the segment's price sensitivity,
+ floored at 3% (asymptote). No cliff: the steepest 1%-of-WTP step drops resistance ~18 points (a
+ discontinuity would be ~90). No hard cap. No branch on recipe id or strategy name (independently verified).
+- **`PriceResistScaleBp = 450`** is chosen from an elasticity target (a maximally price-sensitive diner retains
+ ~1/3 of demand at ~8% over WTP), not from the dominance outcome — every value in [350, 600] already defeats
+ the uniform-overprice menu, so dominance does not pin the constant.
+- Retention (fraction of demand kept): at 1.25×/1.5×/2× WTP a value diner keeps 15%/8%/4%, an enthusiast
+ 37%/23%/13%. Premium dishes at or below WTP face **zero** resistance.
+
+## D. Forecast correction (H3)
+
+`RestaurantSim.Core/Forecast.cs`. Full contract in `docs/design/FORECAST-CONTRACT.md`.
+
+1. **Per-station kitchen bottleneck** — kitchen cover capacity is the minimum over stations of
+ `stationWork / workPerCover`, not an average, so two mains on one grill now shows the grill as the wall.
+2. **Realization keyed off kitchen throughput only** (not `min(kitchen, seat)`), so seats past the kitchen
+ wall cannot raise expected completed covers.
+3. **Over-acceptance waste** — seats above the kitchen wall create sunk-ingredient covers, charged in
+ proportion to the seat surplus and demand pressure (slack demand wastes nothing).
+
+Effect: seats past the kitchen bind now add zero/negative predicted contribution; the 46→66 reversal is gone
+(forecast −$373, tracking the actual down). Independently reproduced by verifier 2 (0/3 past-wall reversals).
+
+## E. Strategy-space evidence (the frontier pivot)
+
+**What the first "0 dominators" claim got wrong.** It compared searched candidates only to the nine **named**
+strategies, at 40 seeds. But the named strategies are under-optimized: Focused Value earns ~$450 in the lunch
+rush where a fair-priced, fully-staffed Burger+Ribeye+Fish plan earns ~$2350. Verifier 1 found a plan that
+beats every named market winner — correctly refuting "0 dominators" **as stated** — but that plan is not near
+the true frontier; it merely out-optimizes weak baselines.
+
+**The corrected, honest test (`reports/balance/dominance-search.md`, `DominanceFrontierTests`).** Build a
+strong per-market frontier (random search + a principled archetype sweep, 80 seeds/market), then ask whether
+any single plan is within $150 of the frontier in ALL three markets.
+
+| Market | Frontier (best found) | Winning regime |
+|---|--:|---|
+| lunch-rush | ~$2350 | Burger+Ribeye+Fish @ **fair** prices, 55 seats |
+| social-dinner | ~$5005 | Ribeye+Fish @ 1.25×, 40 seats |
+| enthusiast-evening | ~$4657 | Ribeye+Fish @ 1.5×, 40 seats |
+
+The best **generalist** plan is **~$1170 below the frontier in its worst market** — far outside the $150
+near-optimal band. The per-market optima are distinct, **structurally-opposed** regimes (fair-priced /
+high-capacity for the value lunch vs premium / lean for the enthusiast evening): a plan cannot be
+simultaneously fair-and-premium, value-main-and-not, 55-and-40 seats. **There is no cross-market dominator.**
+This is the property the M0 question actually asks for, now shown by a strong test rather than a weak one.
+
+Among the named strategies, distinct winners across the three markets stay at **2/3** (Focused Value wins
+lunch and social; Premium Craft wins only the enthusiast evening — restoring pre-registered hypothesis H1).
+
+**Disclosed residuals (M0.5, not hidden):** (a) the named strategy set is illustrative and should be enriched
+with frontier-competitive archetypes; (b) lean strategies underperform heavily, so labor/throughput economics
+may over-reward staffing up and deserve a calibration review.
+
+## F. Forecast calibration evidence
+
+Measured on **held-out seeds** (base separate from tuning), across all named strategies and adversarial /
+capacity / price plan classes:
+- **Contribution-band coverage: 66% in-harness / 73% held-out** overall (named 64%, adversarial 71%, capacity
+ 82%, price 87%) against a **stated 55%** — honestly conservative in every plan class. (Per-strategy coverage
+ is uneven — legibly-failing strategies under-cover — so 55% is stated as an aggregate, and labeled as such.)
+- **Seat-direction agreement (forecast vs actual): 11/12** held-out; an independent 84-pair sweep found 92.9%,
+ with every disagreement at the low-seat *seat-bound* transition, not past the kitchen wall.
+
+## G. Golden re-baseline (deliberate, documented)
+
+To give the WTP model credible absolute anchors, all 12 suggested prices were recentered (~+40%) and segment
+sensitivities retuned (Value 8000→10000, Social 5000→5500, Enthusiast 3500→3000). Because the simulated order
+mix changes, the three golden checksums were **deliberately re-baselined** (Focused Value/lunch
+`0x290EB112568926A4`, Premium Craft/enthusiast `0xC9AEF12F9CD876DA`, Balanced Competent/social
+`0x8BC9E9401D27E91B`). This is the golden policy working as intended (DECISION-LOG D-022). The elasticity
+constant itself changes no golden (no golden plan prices above its segment's WTP), verified by holding the
+checksums fixed while sweeping the constant.
+
+## H. Technical verification
+
+- **Tests: 117 pass, 0 fail** (Core 54, Determinism 31, Scenario 32), identical across two runs. New:
+ `PriceElasticityTests` (7), `ForecastDirectionTests` (4), `DominanceFrontierTests` (3), plus updated
+ calibration and re-baselined goldens.
+- **Determinism:** harness double-run byte-identical (example checksum `8BC9E9401D27E91B`); golden checksum
+ test passes.
+- **No float / wall-clock / System.Random in Core:** guard tests pass; `PriceModel` is integer-only. Verified.
+- **Accounting:** `contribution = revenue − ingredients − labor − overhead (− over-accept waste)`; hand-checked
+ to the cent by verifier 3.
+
+## I. Scope audit
+
+Clean. Nothing from the M0 non-goals list was built (no renderer/engine, pathfinding, inventory/suppliers,
+recipe-editor, competitors, campaign, save-system, reputation/repeat-visits, multi-service state). `PriceModel`
+is single-service by design (its docstring disclaims persistent state). No binary assets committed. This is a
+correction pass on the existing M0 core; no M1 work.
+
+## J. Pass conditions (§17) — self-assessment
+
+| # | Condition (as understood) | Verdict |
+|---|---|---|
+| 1 | Uniform overpricing no longer dominates any market | PASS |
+| 2 | No cross-market dominator (tested against a strong searched frontier) | PASS |
+| 3 | Premium pricing remains viable contextually (wins the market it fits) | PASS |
+| 4 | Elasticity is principled (WTP anchored to per-dish suggested price, quality/segment-adjusted) | PASS |
+| 5 | Resistance is smooth — no cliff | PASS |
+| 6 | Name-agnostic — no branch on recipe/strategy name | PASS (independently verified) |
+| 7 | No hard global price cap | PASS |
+| 8 | No fixture-targeted penalty | PASS |
+| 9 | WTP does not rely only on the menu median | PASS (per-dish) |
+| 10 | Seats past the kitchen bind add zero/negative forecast contribution | PASS |
+| 11 | Forecast models capacity constraints (per-station bottleneck) | PASS |
+| 12 | Forecast recalibrated on competent AND failure-mode plans, held-out | PASS |
+| 13 | Forecast directionality validated across basic decisions (seats/cook/pricing) | PASS |
+| 14 | Determinism preserved; golden changes documented, not silent | PASS |
+
+## K. Gate recommendation and required reviewer action
+
+**Builder recommendation: technical Conditional-Pass, pending the owner-run human playtests.** Both audit
+findings are resolved: single-service price elasticity removes the overpricing dominance and makes pricing
+genuinely context-dependent; the forecast no longer reverses the seat decision. The central strategy property
+— no cross-market dominator, context changes the best plan — now holds under a rigorous frontier test rather
+than a weak named-strategy comparison. Two calibration residuals (named-set enrichment; labor/throughput
+economics) are disclosed for M0.5, not hidden.
+
+**Required reviewer action** (branch `fix/m0-price-forecast-integrity`, do not merge): reproduce the frontier
+result with your own search (confirm no single plan is near-optimal in all three markets), confirm the
+elasticity is principled and cliff-free, re-run the forecast seat-direction and held-out calibration, and
+confirm the golden re-baseline is the documented consequence of the price recentering. Then the owners run the
+human playtests (`docs/playtests/`). The Builder does not declare M1 readiness and has not begun M1.
diff --git a/reports/m0/PRE-FIX-2-EVIDENCE.md b/reports/m0/PRE-FIX-2-EVIDENCE.md
new file mode 100644
index 0000000..003ee36
--- /dev/null
+++ b/reports/m0/PRE-FIX-2-EVIDENCE.md
@@ -0,0 +1,48 @@
+# Pre-Fix Evidence — Second M0 Correction (price elasticity + forecast capacity)
+
+Reproductions of the two remaining High findings from the focused independent re-review of PR #2
+(comment 5110095931), captured **before** this correction so the defects stay reproducible and the fix
+is checkable. Baseline commit: `88c1c0b`. Held-out seeds (not used to tune anything): 3,000,000 / 3,500,000.
+
+## Baseline technical state (to preserve)
+- **Tests:** 103 passing (Core 54, Determinism 31, Scenario 18).
+- **Golden checksums:** Focused Value/lunch `F07AA46F729A0A8F`, Premium Craft/enthusiast `24EE4A83E7436797`,
+ Balanced Competent/social `7C4703D4A5375AF5`.
+- **Committed dominance search:** reports 1 all-3-market dominator (500 configs × 40 seeds, acceptance fixed).
+
+## H2 / NEW-1 — price inelasticity (the residual dominator)
+**Price sweep** — scale ALL prices of a coherent balanced menu; median contribution (150 held-out seeds).
+The defect: contribution keeps rising to ~2.5–3× before finally falling at 4×, i.e. overpricing is optimal.
+
+| market | ×0.5 | ×1.0 | ×1.5 | ×2.0 | ×2.5 | ×3.0 | ×4.0 |
+|---|--:|--:|--:|--:|--:|--:|--:|
+| lunch-rush | −$1096 | −$434 | **+$92** | −$20 | +$58 | +$30 | −$226 |
+| social-dinner | −$1000 | −$2 | +$1100 | +$2485 | **+$2981** | +$2930 | +$1477 |
+| enthusiast-evening | −$501 | +$475 | +$1401 | +$2097 | +$2585 | **+$2772** | +$2344 |
+
+**Residual dominator** (single fixed complete plan: `Burger@$26.55, Chicken@$52.14, Scallops@$41.04,
+Veggie@$29.54`, seats 36, 8 staff) vs the best NAMED strategy per market (200 held-out seeds):
+
+| market | dominator | named-best | edge |
+|---|--:|--:|--:|
+| lunch-rush | $530.32 | $321.95 | **+$208 (+65%)** |
+| social-dinner | $2090.80 | $495.65 | **+$1595 (+322%)** |
+| enthusiast-evening | $1769.19 | $1094.42 | **+$675 (+62%)** |
+
+Root cause: `DemandModel.ConversionBp` and `ServiceSimulator.PickBest` both use
+`priceFit = clamp(Bp − overBp·sensitivity/Bp, floor, Bp)` with `overBp = price/budgetShare − 1`. A $52
+main for social (57% over budget) still keeps ~71% order weight — far too inelastic. There is no
+absolute willingness-to-pay ceiling and no competing "decline to order" outcome, so raising all prices
+increases contribution.
+
+## H3 — forecast reverses the seat-capacity decision
+Balanced plan, seats 46→66, forecast vs actual mean contribution delta (300 held-out seeds):
+
+| market | forecast Δ | actual Δ | |
+|---|--:|--:|--|
+| lunch-rush | +$482.96 | −$376.69 | **SIGN REVERSED** |
+| social-dinner | +$87.14 | −$164.57 | **SIGN REVERSED** |
+
+Root cause: `Forecaster.Compute` uses `demandOpportunity = min(attempted, kitchenCoverCap, seatCoverCap)`;
+adding seats raises `seatCoverCap`, so expected contribution rises even when the kitchen is the binding
+constraint and the extra seated covers actually walk out / get comped.
diff --git a/reports/m0/example-forecast-vs-actual.txt b/reports/m0/example-forecast-vs-actual.txt
index bbb4e1e..ceef7cb 100644
--- a/reports/m0/example-forecast-vs-actual.txt
+++ b/reports/m0/example-forecast-vs-actual.txt
@@ -1,12 +1,12 @@
PLAN: Balanced Competent
Seats: 46 Walk-in acceptance: 100%
Menu:
- [Starter] House Salad $8.00 (station Cold, 4m, cost $2.50)
- [Main ] Classic Burger $15.00 (station Grill, 7m, cost $4.50)
- [Main ] Roast Chicken $22.00 (station Grill, 12m, cost $5.50)
- [Main ] Fish & Chips $18.00 (station Saute, 8m, cost $5.00)
- [Dessert] Ice Cream $7.00 (station Pastry, 3m, cost $1.50)
- [Dessert] Cheese Plate $16.00 (station Cold, 4m, cost $6.00)
+ [Starter] House Salad $11.00 (station Cold, 4m, cost $2.50)
+ [Main ] Classic Burger $21.00 (station Grill, 7m, cost $4.50)
+ [Main ] Roast Chicken $31.00 (station Grill, 12m, cost $5.50)
+ [Main ] Fish & Chips $25.00 (station Saute, 8m, cost $5.00)
+ [Dessert] Ice Cream $10.00 (station Pastry, 3m, cost $1.50)
+ [Dessert] Cheese Plate $22.50 (station Cold, 4m, cost $6.00)
Crew:
Nina -> Saute
Owen -> Grill
@@ -17,14 +17,14 @@ PLAN: Balanced Competent
Tom -> FrontOfHouse
FORECAST (committed before service; immutable):
- Demand opportunity: 139 covers (best case if all completed)
- Expected covers: 117 (completed, after peaking & walkouts)
- Expected revenue: $2757.35
- Expected contribution: $1069.86 low-confidence range [-$679.40 .. $1916.92] (55% confidence)
+ Demand opportunity: 141 covers (best case if all completed)
+ Expected covers: 91 (completed, after peaking & walkouts)
+ Expected revenue: $2713.32
+ Expected contribution: $907.87 low-confidence range [-$815.86 .. $2285.26] (55% confidence)
Key assumptions:
- - Conversion of attempted visits: 61% (menu fit & pricing vs this market).
- - Demand opportunity (best case): ~139 covers if every seated party completed; binding ceiling = attempted demand (market/pricing).
- - Realization: ~84% complete after peaking & walkouts -> ~117 EXPECTED covers (check/cover $27.65).
+ - Conversion of attempted visits: 62% (menu fit & pricing vs this market).
+ - Demand opportunity (best case): ~141 covers if every seated party completed; binding ceiling = attempted demand (market/pricing).
+ - Realization: ~64% complete after peaking & walkouts -> ~91 EXPECTED covers (check/cover $38.67).
- Menu complexity load: +7% ticket work.
- Confidence is deliberately LOW: pre-service cannot see execution failures, patience walkouts, or peak-minute crowding; the range is wide and skewed low.
@@ -32,60 +32,60 @@ SERVICE LOG (sampled):
t= 0 seated= 0 queue= 0 cooking= 0 tickets[Cold:0 Saute:0 Grill:0 Pastry:0] rev=$0.00
t= 15 seated= 3 queue= 0 cooking= 2 tickets[Cold:1 Saute:0 Grill:0 Pastry:0] rev=$0.00
t= 30 seated= 6 queue= 0 cooking= 4 tickets[Cold:0 Saute:0 Grill:0 Pastry:0] rev=$0.00
- t= 45 seated=11 queue= 0 cooking= 5 tickets[Cold:0 Saute:0 Grill:0 Pastry:0] rev=$81.00
- t= 60 seated=15 queue= 0 cooking=10 tickets[Cold:0 Saute:1 Grill:7 Pastry:0] rev=$339.00
- t= 75 seated=18 queue= 0 cooking=13 tickets[Cold:2 Saute:0 Grill:6 Pastry:0] rev=$533.00
- t= 90 seated=19 queue= 0 cooking=10 tickets[Cold:1 Saute:2 Grill:5 Pastry:0] rev=$761.00
- t=105 seated=17 queue= 2 cooking=10 tickets[Cold:4 Saute:2 Grill:8 Pastry:0] rev=$1085.00
- t=120 seated=18 queue= 5 cooking=12 tickets[Cold:8 Saute:0 Grill:10 Pastry:0] rev=$1260.00
- t=135 seated=20 queue= 4 cooking=14 tickets[Cold:8 Saute:0 Grill:11 Pastry:0] rev=$1511.00
- t=150 seated=20 queue= 2 cooking=15 tickets[Cold:8 Saute:0 Grill:14 Pastry:0] rev=$1734.00
- t=165 seated=17 queue= 0 cooking=13 tickets[Cold:1 Saute:0 Grill:9 Pastry:0] rev=$1940.00
- t=180 seated= 9 queue= 0 cooking= 7 tickets[Cold:1 Saute:0 Grill:4 Pastry:0] rev=$2193.00
- t=195 seated= 4 queue= 0 cooking= 2 tickets[Cold:0 Saute:0 Grill:0 Pastry:0] rev=$2247.00
- t=210 seated= 1 queue= 0 cooking= 0 tickets[Cold:0 Saute:0 Grill:0 Pastry:0] rev=$2427.00
+ t= 45 seated=11 queue= 0 cooking= 5 tickets[Cold:0 Saute:0 Grill:0 Pastry:0] rev=$113.00
+ t= 60 seated=15 queue= 0 cooking=10 tickets[Cold:0 Saute:2 Grill:7 Pastry:0] rev=$473.00
+ t= 75 seated=18 queue= 0 cooking=13 tickets[Cold:2 Saute:0 Grill:6 Pastry:0] rev=$744.50
+ t= 90 seated=19 queue= 0 cooking=12 tickets[Cold:6 Saute:3 Grill:9 Pastry:0] rev=$1076.50
+ t=105 seated=17 queue= 1 cooking=12 tickets[Cold:11 Saute:3 Grill:10 Pastry:0] rev=$1518.50
+ t=120 seated=18 queue= 5 cooking=12 tickets[Cold:11 Saute:1 Grill:10 Pastry:0] rev=$1646.00
+ t=135 seated=20 queue= 6 cooking=16 tickets[Cold:13 Saute:0 Grill:13 Pastry:0] rev=$1944.50
+ t=150 seated=20 queue= 0 cooking=14 tickets[Cold:13 Saute:0 Grill:15 Pastry:0] rev=$2211.50
+ t=165 seated=21 queue= 0 cooking=14 tickets[Cold:9 Saute:1 Grill:10 Pastry:0] rev=$2257.50
+ t=180 seated=11 queue= 0 cooking= 7 tickets[Cold:5 Saute:0 Grill:6 Pastry:0] rev=$2574.00
+ t=195 seated= 5 queue= 0 cooking= 3 tickets[Cold:0 Saute:0 Grill:0 Pastry:0] rev=$2782.50
+ t=210 seated= 3 queue= 0 cooking= 0 tickets[Cold:0 Saute:0 Grill:0 Pastry:0] rev=$2956.50
====== POST-SERVICE AUTOPSY [social-dinner] Balanced Competent seed 700042 ======
FORECAST vs ACTUAL:
- Covers: forecast 117 actual 123
- Contribution: forecast $1069.86 actual $546.50 diff -$523.36
+ Covers: forecast 91 actual 113
+ Contribution: forecast $907.87 actual $1242.00 diff $334.13
DEMAND FUNNEL:
- Attempted parties (after market/pricing): 69
- Served parties: 53 covers: 123
- Lost to seating/capacity: 1 walked out waiting: 15 no acceptable dish: 0
+ Attempted parties (after market/pricing): 70
+ Served parties: 50 covers: 113
+ Lost to seating/capacity: 1 walked out waiting: 19 no acceptable dish: 0
ECONOMY:
- Revenue: $2524.00
- Ingredients: - $1222.50
+ Revenue: $3297.00
+ Ingredients: - $1300.00
Labor: - $605.00
Fixed overhead:- $150.00
- CONTRIBUTION: $546.50 (profit)
+ CONTRIBUTION: $1242.00 (profit)
-SERVICE: avg ticket 23 min service failures 95 menu-complexity load +7% overall satisfaction 600/1000
+SERVICE: avg ticket 25 min service failures 108 menu-complexity load +7% overall satisfaction 581/1000
BY DISH:
dish ord dlv fail qual revenue contrib
- House Salad 78 62 26 377 $360.00 $165.00
- Classic Burger 46 33 16 399 $315.00 $108.00
- Roast Chicken 55 41 21 493 $550.00 $247.50
- Fish & Chips 55 49 3 595 $864.00 $589.00
- Ice Cream 30 24 5 518 $147.00 $102.00
- Cheese Plate 33 25 9 618 $288.00 $90.00
+ House Salad 80 60 26 329 $429.00 $229.00
+ Classic Burger 42 31 18 345 $378.00 $189.00
+ Roast Chicken 59 39 23 497 $775.00 $450.50
+ Fish & Chips 60 43 3 570 $1050.00 $750.00
+ Ice Cream 23 18 3 550 $170.00 $135.50
+ Cheese Plate 42 31 16 613 $495.00 $243.00
BY SEGMENT (satisfaction 0-1000; F=food W=wait S=service V=value):
- Value Lunch parties 10 covers 18 sat 449 (F488 W380 S571 V627)
- Social Dinner parties 48 covers 82 sat 512 (F456 W478 S636 V692)
- Food Enthusiast parties 10 covers 23 sat 474 (F378 W609 S731 V655)
+ Value Lunch parties 9 covers 8 sat 325 (F328 W297 S399 V392)
+ Social Dinner parties 50 covers 82 sat 475 (F430 W446 S617 V589)
+ Food Enthusiast parties 10 covers 23 sat 463 (F373 W563 S721 V648)
BY STATION (utilization / peak queue / dishes / staff):
- Cold util 64% peakQ 13 dishes 111 staff 1
- Saute util 64% peakQ 5 dishes 55 staff 1
- Grill util 76% peakQ 17 dishes 101 staff 2
- Pastry util 14% peakQ 1 dishes 30 staff 1
+ Cold util 68% peakQ 17 dishes 122 staff 1
+ Saute util 67% peakQ 5 dishes 60 staff 1
+ Grill util 74% peakQ 18 dishes 101 staff 2
+ Pastry util 10% peakQ 0 dishes 23 staff 1
CAUSAL SUMMARY:
- Primary loss: the kitchen could not keep pace — 15 parties walked out waiting and 56 dishes were comped (~$965.52 of lost contribution). The Grill station ran at 76% (peak queue 17) was the tightest point.
- Most profitable dish: Fish & Chips Least useful dish: Cheese Plate Busiest station: Grill (a condition, not necessarily the primary loss)
- checksum: 7C4703D4A5375AF5
+ Primary loss: the kitchen could not keep pace — 19 parties walked out waiting and 59 dishes were comped (~$1703.02 of lost contribution). The Grill station ran at 74% (peak queue 18) was the tightest point.
+ Most profitable dish: Fish & Chips Least useful dish: Ice Cream Busiest station: Grill (a condition, not necessarily the primary loss)
+ checksum: 8BC9E9401D27E91B
diff --git a/reports/m0/harness-console-summary.txt b/reports/m0/harness-console-summary.txt
index 95489d6..139ce41 100644
--- a/reports/m0/harness-console-summary.txt
+++ b/reports/m0/harness-console-summary.txt
@@ -2,10 +2,10 @@ M0 harness: 9 strategies x 3 scenarios x 200 seeds = 5400 services
=== SUMMARY ===
lunch-rush winner: Focused Value viable strategies: 1
- social-dinner winner: Focused Value viable strategies: 2
- enthusiast-evening winner: Premium Craft viable strategies: 4
+ social-dinner winner: Focused Value viable strategies: 4
+ enthusiast-evening winner: Premium Craft viable strategies: 5
distinct winners across scenarios: 2/3 no single dominant strategy
- determinism: PASS example seed 700042 checksum 7C4703D4A5375AF5
- forecast band coverage: 61% (stated 55%)
- dominance search: 1 config(s) dominate all 3 markets by >= $150.00 (see dominance-search.md)
+ determinism: PASS example seed 700042 checksum 8BC9E9401D27E91B
+ forecast band coverage: 66% (stated 55%)
+ dominance search: 0 config(s) dominate all 3 markets by >= $150.00 (see dominance-search.md)
reports written under: reports/
diff --git a/src/RestaurantSim.Core/Demand.cs b/src/RestaurantSim.Core/Demand.cs
index 03991d4..ce30b53 100644
--- a/src/RestaurantSim.Core/Demand.cs
+++ b/src/RestaurantSim.Core/Demand.cs
@@ -17,12 +17,11 @@ public static int ConversionBp(M0World world, MarketScenario sc, ServicePlan pla
{
var seg = world.Segment((SegmentId)s);
int bestAppeal = mains.Max(r => r.Appeal[s]);
- long budgetMain = seg.BudgetPerCoverCents * 6 / 10;
var top = mains.OrderByDescending(r => r.Appeal[s]).ThenBy(r => r.Id).First();
long typicalMainPrice = menu[top.Id];
int appealFactor = FixedMath.Clamp(bestAppeal * 10, 2000, 10000);
- int overBp = (int)Math.Max(0, FixedMath.MulDivRound(typicalMainPrice, FixedMath.Bp, Math.Max(1, budgetMain)) - FixedMath.Bp);
- int priceFactor = FixedMath.Clamp(FixedMath.Bp - overBp * seg.PriceSensitivityBp / FixedMath.Bp, 800, 10000);
+ // absolute price resistance vs the segment's willingness to pay for its favourite main
+ int priceFactor = PriceModel.ResistBp(typicalMainPrice, PriceModel.Wtp(top, seg), seg.PriceSensitivityBp);
int segConv = (int)FixedMath.MulDivRound(9000L * appealFactor / FixedMath.Bp * priceFactor, 1, FixedMath.Bp);
conv += (long)sc.SegmentMixBp[s] * segConv;
}
diff --git a/src/RestaurantSim.Core/Forecast.cs b/src/RestaurantSim.Core/Forecast.cs
index fdfcc19..7ae3bb2 100644
--- a/src/RestaurantSim.Core/Forecast.cs
+++ b/src/RestaurantSim.Core/Forecast.cs
@@ -36,17 +36,32 @@ public static ForecastSnapshot Compute(M0World world, MarketScenario sc, Service
int avgPrep = menuSize == 0 ? 0 : (int)(recipes.Sum(r => (long)r.PrepComplexity) / menuSize);
int complexityBp = Tuning.ComplexityBp(menuSize, avgPrep);
- // kitchen cover capacity
- long kitchenWorkCapacity = 0; // bp-minutes over the service
+ // Kitchen cover capacity = the PER-STATION bottleneck, not an average over all stations. The
+ // previous averaged cap never saw the true station wall (e.g. two mains on the grill), so it read
+ // too high and never bound, which let extra seats look free. (audit H3 forecast capacity fix)
+ int numMains = mains.Count, numStarters = starters.Count, numDesserts = desserts.Count;
+ long kitchenCoverCap = long.MaxValue;
for (int i = 0; i < Stations.Count; i++)
{
+ long stationWork = 0; // bp-minutes available at this station over the service
foreach (var e in KitchenCrew(world, plan, (StationId)i))
- kitchenWorkCapacity += (long)sc.ServiceMinutes * e.SpeedBp * e.StationFit[i] / 1000 * Tuning.SlotsPerCook;
+ stationWork += (long)sc.ServiceMinutes * e.SpeedBp * e.StationFit[i] / 1000 * Tuning.SlotsPerCook;
+ long workPerCover = 0; // expected bp-minutes of THIS station's work per cover
+ foreach (var r in recipes.Where(r => r.Station == (StationId)i))
+ {
+ int probBp = r.Course == Course.Main ? FixedMath.Bp / Math.Max(1, numMains)
+ : r.Course == Course.Starter ? (int)starterProb / Math.Max(1, numStarters)
+ : (int)dessertProb / Math.Max(1, numDesserts);
+ long dishWork = (long)r.ActiveMinutes * FixedMath.Bp * (FixedMath.Bp + complexityBp) / FixedMath.Bp;
+ workPerCover += FixedMath.MulDivRound(dishWork, probBp, FixedMath.Bp);
+ }
+ if (workPerCover > 0) // an empty station is not a constraint
+ {
+ long stationCoverCap = stationWork / workPerCover;
+ if (stationCoverCap < kitchenCoverCap) kitchenCoverCap = stationCoverCap;
+ }
}
- int avgActive = recipes.Count == 0 ? 10 : (int)(recipes.Sum(r => (long)r.ActiveMinutes) / recipes.Count);
- long workPerDish = (long)avgActive * FixedMath.Bp * (FixedMath.Bp + complexityBp) / FixedMath.Bp;
- long dishCapacity = workPerDish == 0 ? 0 : kitchenWorkCapacity / workPerDish;
- long kitchenCoverCap = dishesPerCoverBp == 0 ? 0 : dishCapacity * FixedMath.Bp / dishesPerCoverBp;
+ if (kitchenCoverCap == long.MaxValue) kitchenCoverCap = 0; // no staffed station with dishes
// seat cover capacity
int avgDwell = Tuning.ForecastAvgDwellMin;
@@ -55,9 +70,10 @@ public static ForecastSnapshot Compute(M0World world, MarketScenario sc, Service
// "Demand opportunity" is the average-capacity, zero-loss ceiling. It is NOT what completes.
long demandOpportunity = Math.Max(0, Math.Min(attemptedCovers, Math.Min(kitchenCoverCap, seatCoverCap)));
- // Realization: peaking (arrivals cluster at the peak) plus walkouts/comps mean fewer covers
- // actually complete than the average-capacity ceiling. This de-biases the point estimate.
- int realizationBp = RealizationBp(sc, conversionBp, meanSizeX10, kitchenCoverCap, seatCoverCap);
+ // Realization: peaking + walkouts/comps mean fewer covers complete than the ceiling. It is keyed
+ // off KITCHEN throughput ONLY (not min(kitchen,seat)), so adding seats past the kitchen wall cannot
+ // raise realization or expected completed covers. (audit H3 seat-direction fix)
+ int realizationBp = RealizationBp(sc, conversionBp, meanSizeX10, kitchenCoverCap);
long expectedCompleted = FixedMath.MulDivRound(demandOpportunity, realizationBp, FixedMath.Bp);
// average check per cover
@@ -76,7 +92,7 @@ public static ForecastSnapshot Compute(M0World world, MarketScenario sc, Service
int dishesPerCoverBpLocal = dishesPerCoverBp;
long expectedDishes = FixedMath.MulDivRound(expectedCompleted, dishesPerCoverBpLocal, FixedMath.Bp);
int avgKitchenRelBp = AvgKitchenReliabilityBp(world, plan);
- int overPct = PeakOverPct(sc, conversionBp, meanSizeX10, kitchenCoverCap, seatCoverCap);
+ int overPct = PeakOverPct(sc, conversionBp, meanSizeX10, kitchenCoverCap);
int compRateBp = FixedMath.Clamp((FixedMath.Bp - avgKitchenRelBp) + complexityBp / 2 + overPct * 18, 0, 3200);
long expectedComps = FixedMath.MulDivRound(expectedDishes, compRateBp, FixedMath.Bp);
long avgDishPrice = dishesPerCoverBpLocal == 0 ? 0 : FixedMath.MulDivRound(checkPerCover, FixedMath.Bp, dishesPerCoverBpLocal);
@@ -86,7 +102,18 @@ public static ForecastSnapshot Compute(M0World world, MarketScenario sc, Service
var ingredient = new Money(expectedCompleted * ingPerCover);
Money labor = Money.Zero;
foreach (var (empId, asg) in plan.Assignments) if (asg != Assignment.Off) labor += new Money(world.Employee(empId).WageCents);
- var contribution = revenue - ingredient - labor - world.FixedOverheadPerService;
+
+ // Over-acceptance waste: covers that get SEATED but exceed what the kitchen can serve walk out after
+ // their dishes are started, so their ingredients are sunk for no revenue. This makes MORE seats than
+ // the kitchen can serve REDUCE contribution (and fewer seats, when kitchen-bound, raise it). (audit H3)
+ // Seats above the kitchen wall create peak crowding proportional to the seat surplus, but only when
+ // demand actually pressures the kitchen (slack demand wastes nothing). So more seats -> more waste,
+ // fewer seats -> less, even when total attempted demand is unchanged.
+ long overSeats = Math.Max(0, seatCoverCap - kitchenCoverCap);
+ int demandPressureBp = (int)FixedMath.Clamp(FixedMath.MulDivRound(attemptedCovers, FixedMath.Bp, Math.Max(1, kitchenCoverCap)), 0, FixedMath.Bp);
+ long wasteCovers = FixedMath.MulDivRound(overSeats, demandPressureBp, FixedMath.Bp);
+ var overAcceptCost = new Money(FixedMath.MulDivRound(wasteCovers * ingPerCover, Tuning.ForecastOverAcceptWasteBp, FixedMath.Bp));
+ var contribution = revenue - ingredient - labor - world.FixedOverheadPerService - overAcceptCost;
// Honest, downward-skewed contribution band. Contribution swings with operating leverage, so the
// band width scales with expected REVENUE (a more stable base than near-zero contribution), is
@@ -120,21 +147,21 @@ public static ForecastSnapshot Compute(M0World world, MarketScenario sc, Service
/// of peak-minute cover demand to the binding per-minute capacity. Pure function of numeric inputs;
/// no branching on scenario or strategy names.
///
- public static int RealizationBp(MarketScenario sc, int conversionBp, int meanSizeX10, long kitchenCoverCap, long seatCoverCap)
+ public static int RealizationBp(MarketScenario sc, int conversionBp, int meanSizeX10, long kitchenCoverCap)
{
- int overPct = PeakOverPct(sc, conversionBp, meanSizeX10, kitchenCoverCap, seatCoverCap);
+ int overPct = PeakOverPct(sc, conversionBp, meanSizeX10, kitchenCoverCap);
return FixedMath.Clamp(Tuning.ForecastRealizationBaseBp - overPct * Tuning.ForecastRealizationPeakPenaltyBp,
Tuning.ForecastRealizationFloorBp, Tuning.ForecastRealizationBaseBp);
}
/// How many percent the peak-minute cover demand exceeds the binding per-minute capacity (0 if it fits).
- private static int PeakOverPct(MarketScenario sc, int conversionBp, int meanSizeX10, long kitchenCoverCap, long seatCoverCap)
+ private static int PeakOverPct(MarketScenario sc, int conversionBp, int meanSizeX10, long kitchenCoverCap)
{
var curve = DemandModel.ArrivalCurve(sc, conversionBp);
int peakIntensityMilli = 0;
foreach (var v in curve) if (v > peakIntensityMilli) peakIntensityMilli = v;
long peakCoversPerMinX1000 = (long)peakIntensityMilli * meanSizeX10 / 10;
- long bindingPerMinX1000 = Math.Min(kitchenCoverCap, seatCoverCap) * 1000 / Math.Max(1, sc.ServiceMinutes);
+ long bindingPerMinX1000 = kitchenCoverCap * 1000 / Math.Max(1, sc.ServiceMinutes);
if (bindingPerMinX1000 <= 0) return 100;
int rhoBp = (int)FixedMath.MulDivRound(peakCoversPerMinX1000, FixedMath.Bp, bindingPerMinX1000);
return Math.Max(0, (rhoBp - FixedMath.Bp) / 100);
diff --git a/src/RestaurantSim.Core/M0Content.cs b/src/RestaurantSim.Core/M0Content.cs
index 5d17c2d..0f33738 100644
--- a/src/RestaurantSim.Core/M0Content.cs
+++ b/src/RestaurantSim.Core/M0Content.cs
@@ -19,26 +19,28 @@ public static class M0Content
public static IReadOnlyList Segments() => new[]
{
// budget seatPat foodPat qExp priceSens atmos startBp destBp failSens novelty WFood WWait WSvc WVal
- new SegmentDef(SegmentId.ValueLunch, "Value Lunch", 2200, 12, 24, 520, 8000, 300, 1500, 1000, 500, 300, 25, 35, 15, 25),
- new SegmentDef(SegmentId.SocialDinner, "Social Dinner", 5500, 20, 34, 640, 5000, 700, 5500, 5000, 700, 550, 28, 18, 30, 24),
- new SegmentDef(SegmentId.FoodEnthusiast,"Food Enthusiast",8500, 25, 42, 820, 3500, 600, 6500, 5500, 850, 750, 45, 12, 18, 25),
+ new SegmentDef(SegmentId.ValueLunch, "Value Lunch", 2200, 12, 24, 520, 10000, 300, 1500, 1000, 500, 300, 25, 35, 15, 25),
+ new SegmentDef(SegmentId.SocialDinner, "Social Dinner", 5500, 20, 34, 640, 5500, 700, 5500, 5000, 700, 550, 28, 18, 30, 24),
+ new SegmentDef(SegmentId.FoodEnthusiast,"Food Enthusiast",8500, 25, 42, 820, 3000, 600, 6500, 5500, 850, 750, 45, 12, 18, 25),
};
public static IReadOnlyList Recipes() => new[]
{
// id name course station ing qCeil diff act prep hold price appeal[VL,SD,FE]
- new RecipeDef(1, "House Salad", Course.Starter, StationId.Cold, 250, 620, 200, 4, 150, 15, 800, new[]{600,520,400}),
- new RecipeDef(2, "Soup of the Day", Course.Starter, StationId.Cold, 200, 640, 260, 5, 200, 20, 900, new[]{520,560,480}),
- new RecipeDef(3, "Seared Scallops", Course.Starter, StationId.Saute, 750, 900, 720, 9, 500, 6, 1800, new[]{200,620,880}),
- new RecipeDef(4, "Classic Burger", Course.Main, StationId.Grill, 450, 680, 260, 7, 200, 12, 1500, new[]{820,600,360}),
- new RecipeDef(5, "Roast Chicken", Course.Main, StationId.Grill, 550, 760, 420, 12, 300, 18, 2200, new[]{560,720,560}),
- new RecipeDef(6, "Ribeye Steak", Course.Main, StationId.Grill, 1200, 900, 620, 11, 250, 10, 4200, new[]{240,760,820}),
- new RecipeDef(7, "Mushroom Risotto", Course.Main, StationId.Saute, 400, 820, 640, 14, 650, 8, 2400, new[]{360,640,720}),
- new RecipeDef(8, "Fish & Chips", Course.Main, StationId.Saute, 500, 700, 360, 8, 250, 10, 1800, new[]{700,560,420}),
- new RecipeDef(9, "Veggie Bowl", Course.Main, StationId.Cold, 300, 640, 220, 5, 200, 20, 1400, new[]{480,460,360}),
- new RecipeDef(10, "Ice Cream", Course.Dessert, StationId.Pastry, 150, 600, 150, 3, 100, 25, 700, new[]{520,520,360}),
- new RecipeDef(11, "Chocolate Fondant", Course.Dessert, StationId.Pastry, 350, 900, 760, 10, 600, 6, 1400, new[]{300,640,820}),
- new RecipeDef(12, "Cheese Plate", Course.Dessert, StationId.Cold, 600, 780, 200, 4, 300, 30, 1600, new[]{220,560,700}),
+ // Suggested prices recalibrated so a menu priced at ~suggested is genuinely profitable (they were
+ // ~40% too low, which the new price elasticity exposed). Willingness-to-pay scales with these.
+ new RecipeDef(1, "House Salad", Course.Starter, StationId.Cold, 250, 620, 200, 4, 150, 15, 1100, new[]{600,520,400}),
+ new RecipeDef(2, "Soup of the Day", Course.Starter, StationId.Cold, 200, 640, 260, 5, 200, 20, 1250, new[]{520,560,480}),
+ new RecipeDef(3, "Seared Scallops", Course.Starter, StationId.Saute, 750, 900, 720, 9, 500, 6, 2500, new[]{200,620,880}),
+ new RecipeDef(4, "Classic Burger", Course.Main, StationId.Grill, 450, 680, 260, 7, 200, 12, 2100, new[]{820,600,360}),
+ new RecipeDef(5, "Roast Chicken", Course.Main, StationId.Grill, 550, 760, 420, 12, 300, 18, 3100, new[]{560,720,560}),
+ new RecipeDef(6, "Ribeye Steak", Course.Main, StationId.Grill, 1200, 900, 620, 11, 250, 10, 5900, new[]{240,760,820}),
+ new RecipeDef(7, "Mushroom Risotto", Course.Main, StationId.Saute, 400, 820, 640, 14, 650, 8, 3350, new[]{360,640,720}),
+ new RecipeDef(8, "Fish & Chips", Course.Main, StationId.Saute, 500, 700, 360, 8, 250, 10, 2500, new[]{700,560,420}),
+ new RecipeDef(9, "Veggie Bowl", Course.Main, StationId.Cold, 300, 640, 220, 5, 200, 20, 2000, new[]{480,460,360}),
+ new RecipeDef(10, "Ice Cream", Course.Dessert, StationId.Pastry, 150, 600, 150, 3, 100, 25, 1000, new[]{520,520,360}),
+ new RecipeDef(11, "Chocolate Fondant", Course.Dessert, StationId.Pastry, 350, 900, 760, 10, 600, 6, 2000, new[]{300,640,820}),
+ new RecipeDef(12, "Cheese Plate", Course.Dessert, StationId.Cold, 600, 780, 200, 4, 300, 30, 2250, new[]{220,560,700}),
};
public static IReadOnlyList Employees() => new[]
diff --git a/src/RestaurantSim.Core/PriceModel.cs b/src/RestaurantSim.Core/PriceModel.cs
new file mode 100644
index 0000000..804794b
--- /dev/null
+++ b/src/RestaurantSim.Core/PriceModel.cs
@@ -0,0 +1,40 @@
+namespace RestaurantSim.Core;
+
+///
+/// Single-service price elasticity (audit second correction, H2/NEW-1). A customer decides, DURING this
+/// service, whether a dish is worth its price. Willingness-to-pay is anchored to the dish's calibrated
+/// SUGGESTED price (so a uniformly overpriced menu cannot hide behind its own median), widened by segment
+/// tolerance and dish quality (premium dishes justify premium prices for quality-valuing segments). Above
+/// WTP, order probability falls SMOOTHLY (hyperbolic), scaled by segment price sensitivity. No hard cap, no
+/// name branching, no single-threshold cliff. Integer / deterministic.
+///
+public static class PriceModel
+{
+ /// What this segment will pay for this dish before price resistance begins (cents).
+ public static long Wtp(RecipeDef r, SegmentDef seg)
+ {
+ // Low price-sensitivity segments tolerate higher prices; high-quality dishes widen tolerance,
+ // and that quality headroom counts for more to low-sensitivity (quality-valuing) segments.
+ int premiumRoom = FixedMath.Bp - seg.PriceSensitivityBp; // enthusiast big, value small
+ int toleranceBp = FixedMath.Bp
+ + (int)FixedMath.MulDivRound(premiumRoom, Tuning.PriceToleranceBaseBp, FixedMath.Bp)
+ + (int)FixedMath.MulDivRound((long)Math.Max(0, r.QualityCeiling - Tuning.PriceQualityRefLevel) * premiumRoom, 1, Tuning.PriceQualityToleranceDiv);
+ return FixedMath.MulDivRound(r.SuggestedPriceCents, toleranceBp, FixedMath.Bp);
+ }
+
+ ///
+ /// Order-weight multiplier in basis points (10000 = full demand at or below WTP), decaying smoothly
+ /// toward the floor as price exceeds WTP. Hyperbolic so overpricing is increasingly punished without a
+ /// cliff: at 1.25x WTP a value diner keeps ~15% and a low-sensitivity enthusiast ~37%; by 2x WTP the
+ /// value diner is near the floor (~4%) while the enthusiast still keeps ~13% (all via segment sensitivity).
+ ///
+ public static int ResistBp(long priceCents, long wtpCents, int sensitivityBp)
+ {
+ if (wtpCents <= 0) return FixedMath.Bp;
+ long over = priceCents - wtpCents;
+ if (over <= 0) return FixedMath.Bp;
+ long denom = wtpCents + FixedMath.MulDivRound(over, sensitivityBp, Tuning.PriceResistScaleBp);
+ return (int)FixedMath.Clamp(FixedMath.MulDivRound(FixedMath.Bp, wtpCents, Math.Max(1, denom)),
+ Tuning.PriceResistanceFloorBp, FixedMath.Bp);
+ }
+}
diff --git a/src/RestaurantSim.Core/Simulator.cs b/src/RestaurantSim.Core/Simulator.cs
index b309e2d..40f3ec9 100644
--- a/src/RestaurantSim.Core/Simulator.cs
+++ b/src/RestaurantSim.Core/Simulator.cs
@@ -314,19 +314,19 @@ private void OrderDishes(M0World world, ServicePlan plan, Dictionary
for (int cover = 0; cover < p.Size; cover++)
{
// main (always attempted)
- var main = PickBest(byCourse, Course.Main, seg, menu, 60, ref choice, menuMedianPrice);
+ var main = PickBest(byCourse, Course.Main, seg, menu, ref choice, menuMedianPrice);
if (main == null) continue; // this cover finds no acceptable main
AddDish(world, p, main, menu, ref nextDishId, stations, complexityPenaltyBp);
// starter
if (choice.Chance(seg.StarterProbBp, FixedMath.Bp))
{
- var st = PickBest(byCourse, Course.Starter, seg, menu, 20, ref choice, menuMedianPrice);
+ var st = PickBest(byCourse, Course.Starter, seg, menu, ref choice, menuMedianPrice);
if (st != null) AddDish(world, p, st, menu, ref nextDishId, stations, complexityPenaltyBp);
}
// dessert
if (choice.Chance(seg.DessertProbBp, FixedMath.Bp))
{
- var de = PickBest(byCourse, Course.Dessert, seg, menu, 20, ref choice, menuMedianPrice);
+ var de = PickBest(byCourse, Course.Dessert, seg, menu, ref choice, menuMedianPrice);
if (de != null) AddDish(world, p, de, menu, ref nextDishId, stations, complexityPenaltyBp);
}
}
@@ -344,10 +344,9 @@ private static long MedianPrice(IReadOnlyList