Skip to content

L1: month-turn engine (#612)#636

Merged
PipFoweraker merged 6 commits into
mainfrom
l1-month-turn-engine
Jul 14, 2026
Merged

L1: month-turn engine (#612)#636
PipFoweraker merged 6 commits into
mainfrom
l1-month-turn-engine

Conversation

@PipFoweraker

@PipFoweraker PipFoweraker commented Jul 13, 2026

Copy link
Copy Markdown
Owner

L1 — Month-turn engine (#612)

Implements the ADR-0009 plan-months / two-speeds turn structure plus the workshop #3 spec addendum. The month is the plan cadence; the day-turn is demoted to a resolution tick beneath it (badge = calendar date; days-survived scoring stays fine-grained). Built as a layer over the existing day-tick engine rather than a re-grain — the sim substrate, RNG stream, and recorded replays are untouched, so this stays green and non-degrading.

What landed (per SCOPE item)

  1. Month = plan turn, day = resolution tick. Clock gains month helpers (month_index / is_month_boundary / month_ordinal / month_label). The HUD badge retires Week 12 | … | Day 3/5 (which hung attention on the day tick, violating the guard rule) for the plan-month date badge. Also fixes the latent leap-day infinite loop (Build lane L0 · Pre-L1 stabilization: one GameManager, one clock, split turn monoliths (behavior-preserving) #620 note 2) that would have hung on Feb-29 dates across the 2017–2040 window. Guard rule honoured: no routine decision hangs on the day tick.
  2. Plan phase — Attention economy. New MonthPlan: founder currency Attention (~20/month, Balance.attention.per_month, not hardcoded); explicit reserve that evaporates crisply at month end (no banking); queued strategic actions with durations (nothing strategic resolves instantly). Attention is introduced alongside the legacy AP pool (L2 owns deleting AP + migrating cost dicts — seam left clean, no big-bang). Staff actions remain per-person/separate.
  3. Event delivery tiers. EventTiers classifies every genre ambient / feed / window with source_id provenance on feed items; only windows demand a decision. WindowResolver implements the costed menu HANDLE-from-reserve / HANDLE-by-cannibalizing / DEFER (mints a Ledger entry, deferrable class only) / IGNORE (list price); unignorable flag supported; unanswered windows auto-resolve as IGNORE + a mild, data-driven rep penalty. Window demand budget 2–3/month → 5–6 endgame (Balance-driven) enforced in the controller.
  4. Day-tick playback + auto-pause-on-window. MonthController advances ticks through TurnManager, routes events by tier, pauses on windows, resets the plan phase at month boundaries, and releases duration-elapsed WIP.
  5. Save/load survives all of it. MonthPlan serialized into GameState.to_dict/from_dict; a pause→save→load smoke test proves an open window + reserve + WIP round-trip and playback resumes.
  6. Replay artifact. record_window_response bumps the schema with payment_source; the league (month) id is stamped beside (seed, game_version). Format tag held at v1 (additive; unknown log keys are ignored by the v1 simulator, so pre-L1 artifacts stay verifiable).

Out of scope (left clean): ADR-0015 doom-function migration is Lane 2 — this resolver/schema never reads/writes doom directly (routes through option effects + ledger factories only). The full new plan scene UI (explicit reserve dial, four-verb window menu incl. DEFER) remains attrition work — but the month loop is now reachable from the playable scene (see below).

Playable month path (playtest follow-up)

Pip's first playtest hit the gap: the engine existed but End Turn still called end_turn() (single day-step) — zero L1 behavior reachable. Fixed (e4cd408):

  • End Turn / Commit Plan now call GameManager.end_month(): commit queued actions as the month plan → implicit reserve = all unspent Attention (v1 default; the plan screen makes it a dial) → execute the open plan turn → day-tick playback (visible date advance, day_tick_seconds pacing).
  • Auto-pause-on-window: windows present through the existing event_dialog (the L10 extraction built for exactly this), AP pre-stripped so displayed costs match what resolution charges. Choices route through MonthController.resolve_current_window_option: the event's ignore option → IGNORE; any other option → HANDLE paid reserve-first, then by cannibalizingpayment_source still lands in the replay artifact. (The explicit four-verb menu incl. DEFER is the plan screen's job.)
  • Month boundary → review dialog → next plan phase. The boundary tick is held open as the new plan phase (month_open_pending) — never auto-executed, or its consequence steps would double-run when the plan commits. A plain month-review dialog closes into planning. Loop: plan → play → review → plan.
  • Old day-step is DEV-ONLY: it survives solely as the DEV MODE overlay's "Day step (dev — old path)" button; end_turn() semantics unchanged (tests pin it). Guard rule holds: only windows pause the day ticks.
  • Feed items surface as log lines with source_id; ambient stays silent.

Honest build badge (playtest follow-up)

The baked build_stamp.txt (fd60eb6 · 2026-07-11) was stale on every branch and cost the playtest session (a4e7661). Dev checkouts now read live git HEAD at runtime (git -C <repo-root> rev-parse, gated on .git existing so exported builds never shell out; cached; worktree-safe): badge reads DEV BUILD v0.11.0 · l1-month-turn-engine@a4e7661 · live. Exported/no-git builds fall back to the baked stamp explicitly marked (stamp) — no badge form can pass as something it isn't, so two checkouts can never show the same badge silently. A stale stamp prints a console note.

Window-drop fix (balance-sweep finding)

4269c0b: MonthController.resolve_current_window() (the four-verb path) popped the window off the queue before WindowResolver.resolve validated payment and never re-pushed on failure — handle_reserve with an empty reserve silently dropped the window (no effect, no charge, no window). Latent in the shipped v1 dialog (which routes through the correctly-guarded option path) but fatal to the future plan-screen UI. Fixed by mirroring the option path's guard — peek, resolve, pop only on success; skip_current_window hardened with the same shape. Regression test: failed handle_reserve leaves the window queued (and in the serialized pause point), then resolves via handle_cannibalize.

Local GUT results

CI's test gate is hollow (#629) — these are real local runs (Godot 4.5.1, after a headless --import pass):

  • Full tests/unit/ run: 403/403 passing, 0 failures, 39 GUT scripts (the 40th unit file, test_verification_determinism.gd, extends Node — a pre-existing non-GUT script — so no Test suite: 14 order-dependent failures in full local run (singleton leakage), invisible to CI #590 hidden parse-failures).
  • New L1 suites (49 tests): all green. test_clock_month (leap-day + month helpers), test_month_plan (Attention/reserve/durations/serialization), test_event_tiers, test_window_resolver, test_month_controller (demand budget, auto-pause, boundary hold-open, option→verb mapping, headless playable-loop smoke), test_month_save_load (pause→save→load), test_month_button_path (a headless smoke driving a FULL month through the exact End-Turn-button API: plan commit → ticks → auto-pause → review → next plan phase).
  • Touched regression suites green: test_game_manager 35/35 (end_turn semantics pinned + unchanged), test_replay_verification, test_save_load_roundtrip, test_game_state, test_events, test_engine_determinism, test_salary_cadence, test_liability_ledger, test_dev_build_indicator (+2 badge-honesty tests).

Schema decisions to eyeball

  • Attention alongside AP, not replacing it — L1 introduces the new currency; L2 deletes the pool. Keeps blast radius sane.
  • Event schema additions: delivery_tier, event_class, source_id, unignorable, expiry_turns, and a window{ attention_cost, handle_option, ignore_option, defer{factory,amount} } block. Un-annotated legacy popups default to window (behaviour-preserving).
  • Window options strip legacy action_points costs — Attention is the window decision currency.
  • League id placeholder = YYYY-MM of the run start month, pending the ADR-0016 pipeline.
  • months_per_turn() stays 1.0 (turn = workday) — the month is a layer, not a re-grain; this keeps risk/doom calibration and replays intact. Noted in-code for L2.

🤖 Generated with Claude Code

PipFoweraker and others added 3 commits July 13, 2026 15:34
…ar badge

ADR-0009 #612: introduce the MONTH as the plan cadence over the day-grained
resolution tick (turn stays a workday; badge = calendar date, days-survived
scoring unchanged). New MonthPlan (RefCounted): founder currency Attention
(~20/month, Balance-driven), explicit reserve that evaporates crisply at month
end (no banking), and duration-bearing queued strategic actions (nothing
strategic resolves instantly). Composed onto GameState + serialized (L7).

- Clock: month_index/is_month_boundary/month_ordinal/month_label helpers;
  annual_to_per_month; fix the L0 latent leap-day infinite loop (#620 note 2 —
  a date landing exactly on Feb 29 hung the rollover) across the 2017-2040 window.
- HUD badge: retire 'Week 12 | ... | Day 3/5' (hung attention on the day tick,
  violating the guard rule) for the plan-month date badge.
- Balance: attention.per_month, events.window_demand_budget(+endgame),
  unanswered_window_rep_penalty, delivery-tier/class defaults.

Attention is introduced ALONGSIDE the legacy AP pool (L2 deletes AP and migrates
cost dicts) — seam left clean. 20 new GUT tests (MonthPlan + Clock month/leap).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…schema bump

Workshop#3 addendum #1-2, ADR-0009 §3, ADR-0012. The #630 flood fix made structural:
every event genre is classified ambient / feed / window; only WINDOWS demand a
decision, so the flood ceiling becomes a demand budget, not an information budget.

- EventTiers: delivery-tier + event-class classifier (un-snoozable/deferrable/
  standing/no-action), source_id provenance, legal-response derivation, and a
  partition() that splits a fired-event stream into the three tiers.
- WindowResolver: the costed menu — HANDLE-from-reserve / HANDLE-by-cannibalizing
  (delays/kills planned WIP) / DEFER (mints a Ledger entry, deferrable class only)
  / IGNORE (list price). Unanswered windows auto-resolve as IGNORE + a mild,
  data-driven rep penalty; unignorable windows refuse DEFER and auto-ignore.
  Attention (via MonthPlan) is the window currency; legacy AP costs stripped.
  Routes doom only through option effects + ledger factories — ADR-0015 seam clean.
- VerificationTracker: record_window_response appends {t,k:'w',ev,resp,pay} — the
  schema bump carrying payment_source. Replay artifact stamps the ADR-0016 league
  id beside seed+version. Format tag held at v1 (additive; unknown keys ignored).

15 new GUT tests. No regressions (70/70 touched suites).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e-load

ADR-0009 UI/acceptance: MonthController drives day-tick playback within a plan
month, delegating the heavy sim to TurnManager and PAUSING on any window that
demands a decision (auto-pause-on-window). Ambient/feed events never interrupt.

- Window DEMAND budget enforced here (workshop#3 addendum #1): only N windows/month
  demand a decision (2-3 early, 5-6 endgame, Balance-driven); excess window-tier
  events downgrade to the feed. The structural #630 fix.
- Month boundaries open a fresh plan phase: new Attention grant, crisp reserve
  evaporation, budget reset, duration-elapsed strategic WIP released (L2 seam).
- Unanswered windows auto-resolve as IGNORE + mild rep penalty; unignorable refuse.
- Open windows mirror into serialized pending_events so pause->save->load captures
  the pause point; rehydrate_from_state re-enters it after load.
- GameManager: thin plan-layer API (get_month_plan / set_attention_reserve /
  queue_strategic_action / resolve_window) + replay league stamp.

9 new GUT tests incl. a headless playable-month-loop smoke and pause->save->load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

PipFoweraker and others added 2 commits July 13, 2026 21:36
…he month out

Playtest follow-up: the month engine existed but was unreachable — main_ui's End
Turn still called end_turn() (single day-step), so the playtest exercised zero L1
behavior. The button now drives the month loop end to end:

- GameManager.end_month(): commit queued actions as the month plan, default the
  reserve to all unspent Attention (implicit v1 — the plan screen makes it a dial),
  execute the open plan turn, then hand control to MonthController playback.
- Day ticks advance visibly (day_tick_seconds, test-tunable); windows auto-pause
  and present through the EXISTING event_dialog (its extraction docstring promised
  exactly this reuse) with AP pre-stripped so displayed costs match what resolution
  charges (Attention). resolve_event routes paused-window choices through
  MonthController.resolve_current_window_option: the event's ignore option maps to
  IGNORE; any other option is a HANDLE paid reserve-first then by cannibalizing —
  payment_source still lands in the replay artifact.
- Month boundary: the boundary tick is HELD OPEN as the new plan phase
  (month_open_pending) — never auto-executed, or its consequence steps would
  double-run when the plan commits. A plain month-review dialog (synthetic event,
  intercepted in resolve_event) closes into planning.
- Feed items surface as log lines; ambient stays silent. Guard rule holds: only
  windows pause the day ticks.
- Old single day-step survives ONLY behind the DEV MODE overlay (relabelled
  'Day step (dev — old path)'); end_turn() itself is unchanged (tests pin it).
- Save loaded mid-pause: controller rehydrates; answering the window resumes
  playback (resolve_event branch not gated on playback_active).

Tests: new test_month_button_path.gd — headless smoke driving a FULL month via the
exact button API (plan commit -> ticks -> auto-pause -> review -> next plan phase),
with a doom clamp so pre-rebalance drift can't kill the run before the boundary.
+2 controller tests (boundary hold-open, option->verb mapping). Full unit suite:
402/402 passing, 39 GUT scripts (no #590 hidden parse failures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… marked

The baked build_stamp.txt (fd60eb6 · 2026-07-11) was two days stale on every
branch and cost a playtest session to build confusion. Dev checkouts now read the
REAL identity at runtime: BuildInfo.get_live_git_stamp() shells git -C <repo-root>
rev-parse (gated on a .git dir/file existing, so exported builds never try; cached
after one probe; worktrees resolve correctly since git handles the .git file).

Badge forms, each self-identifying:
- dev checkout:   'DEV BUILD v0.11.0 · l1-month-turn-engine@e4cd408 · live'
- exported/no git: '... · fd60eb6 · 2026-07-11 (stamp)'
- never stamped:  '... · unstamped'

Acceptance: two different checkouts can never show the same badge silently — live
HEAD differs per checkout, and a stamped badge visibly declares it is a stamp. A
stale stamp additionally prints a console note (print, not push_warning: in a dev
checkout the stamp is stale after every commit by construction; GUT/CI must not
count that as an engine error).

+2 badge tests (live identity present in a git checkout; every badge form declares
its provenance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…it open

Balance-sweep finding: MonthController.resolve_current_window() (the four-verb
path) popped the window off the queue BEFORE WindowResolver.resolve validated
payment, and never re-pushed on failure — handle_reserve with an empty reserve
silently dropped the window (no effect, no charge, no window, playback stuck
paused on an empty queue). Latent in the shipped v1 dialog (which routes through
resolve_current_window_option, whose guard is correct) but fatal to the future
plan-screen UI.

Mirror the option path's guard: peek the head, resolve, pop only on success.
skip_current_window hardened with the same shape (its unignorable pre-check made
it safe today; the pop-before-validate trap is now gone everywhere).

Regression test: handle_reserve with empty reserve -> fails, window still queued
AND still in the serialized pause point, then resolves via handle_cannibalize.
Full unit suite: 403/403 passing, 39 GUT scripts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PipFoweraker
PipFoweraker merged commit bb38fcf into main Jul 14, 2026
15 of 16 checks passed
@PipFoweraker
PipFoweraker deleted the l1-month-turn-engine branch July 14, 2026 01:23
PipFoweraker added a commit that referenced this pull request Jul 14, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PipFoweraker added a commit that referenced this pull request Jul 14, 2026
* feat(L1): month plan layer — Attention economy, crisp reserve, calendar badge

ADR-0009 #612: introduce the MONTH as the plan cadence over the day-grained
resolution tick (turn stays a workday; badge = calendar date, days-survived
scoring unchanged). New MonthPlan (RefCounted): founder currency Attention
(~20/month, Balance-driven), explicit reserve that evaporates crisply at month
end (no banking), and duration-bearing queued strategic actions (nothing
strategic resolves instantly). Composed onto GameState + serialized (L7).

- Clock: month_index/is_month_boundary/month_ordinal/month_label helpers;
  annual_to_per_month; fix the L0 latent leap-day infinite loop (#620 note 2 —
  a date landing exactly on Feb 29 hung the rollover) across the 2017-2040 window.
- HUD badge: retire 'Week 12 | ... | Day 3/5' (hung attention on the day tick,
  violating the guard rule) for the plan-month date badge.
- Balance: attention.per_month, events.window_demand_budget(+endgame),
  unanswered_window_rep_penalty, delivery-tier/class defaults.

Attention is introduced ALONGSIDE the legacy AP pool (L2 deletes AP and migrates
cost dicts) — seam left clean. 20 new GUT tests (MonthPlan + Clock month/leap).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(L1): event delivery tiers + response-window resolution + replay schema bump

Workshop#3 addendum #1-2, ADR-0009 §3, ADR-0012. The #630 flood fix made structural:
every event genre is classified ambient / feed / window; only WINDOWS demand a
decision, so the flood ceiling becomes a demand budget, not an information budget.

- EventTiers: delivery-tier + event-class classifier (un-snoozable/deferrable/
  standing/no-action), source_id provenance, legal-response derivation, and a
  partition() that splits a fired-event stream into the three tiers.
- WindowResolver: the costed menu — HANDLE-from-reserve / HANDLE-by-cannibalizing
  (delays/kills planned WIP) / DEFER (mints a Ledger entry, deferrable class only)
  / IGNORE (list price). Unanswered windows auto-resolve as IGNORE + a mild,
  data-driven rep penalty; unignorable windows refuse DEFER and auto-ignore.
  Attention (via MonthPlan) is the window currency; legacy AP costs stripped.
  Routes doom only through option effects + ledger factories — ADR-0015 seam clean.
- VerificationTracker: record_window_response appends {t,k:'w',ev,resp,pay} — the
  schema bump carrying payment_source. Replay artifact stamps the ADR-0016 league
  id beside seed+version. Format tag held at v1 (additive; unknown keys ignored).

15 new GUT tests. No regressions (70/70 touched suites).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(L1): month-loop playback controller + plan-layer API + pause-save-load

ADR-0009 UI/acceptance: MonthController drives day-tick playback within a plan
month, delegating the heavy sim to TurnManager and PAUSING on any window that
demands a decision (auto-pause-on-window). Ambient/feed events never interrupt.

- Window DEMAND budget enforced here (workshop#3 addendum #1): only N windows/month
  demand a decision (2-3 early, 5-6 endgame, Balance-driven); excess window-tier
  events downgrade to the feed. The structural #630 fix.
- Month boundaries open a fresh plan phase: new Attention grant, crisp reserve
  evaporation, budget reset, duration-elapsed strategic WIP released (L2 seam).
- Unanswered windows auto-resolve as IGNORE + mild rep penalty; unignorable refuse.
- Open windows mirror into serialized pending_events so pause->save->load captures
  the pause point; rehydrate_from_state re-enters it after load.
- GameManager: thin plan-layer API (get_month_plan / set_attention_reserve /
  queue_strategic_action / resolve_window) + replay league stamp.

9 new GUT tests incl. a headless playable-month-loop smoke and pause->save->load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(L1): playable month path — End Turn commits the plan and plays the month out

Playtest follow-up: the month engine existed but was unreachable — main_ui's End
Turn still called end_turn() (single day-step), so the playtest exercised zero L1
behavior. The button now drives the month loop end to end:

- GameManager.end_month(): commit queued actions as the month plan, default the
  reserve to all unspent Attention (implicit v1 — the plan screen makes it a dial),
  execute the open plan turn, then hand control to MonthController playback.
- Day ticks advance visibly (day_tick_seconds, test-tunable); windows auto-pause
  and present through the EXISTING event_dialog (its extraction docstring promised
  exactly this reuse) with AP pre-stripped so displayed costs match what resolution
  charges (Attention). resolve_event routes paused-window choices through
  MonthController.resolve_current_window_option: the event's ignore option maps to
  IGNORE; any other option is a HANDLE paid reserve-first then by cannibalizing —
  payment_source still lands in the replay artifact.
- Month boundary: the boundary tick is HELD OPEN as the new plan phase
  (month_open_pending) — never auto-executed, or its consequence steps would
  double-run when the plan commits. A plain month-review dialog (synthetic event,
  intercepted in resolve_event) closes into planning.
- Feed items surface as log lines; ambient stays silent. Guard rule holds: only
  windows pause the day ticks.
- Old single day-step survives ONLY behind the DEV MODE overlay (relabelled
  'Day step (dev — old path)'); end_turn() itself is unchanged (tests pin it).
- Save loaded mid-pause: controller rehydrates; answering the window resumes
  playback (resolve_event branch not gated on playback_active).

Tests: new test_month_button_path.gd — headless smoke driving a FULL month via the
exact button API (plan commit -> ticks -> auto-pause -> review -> next plan phase),
with a doom clamp so pre-rebalance drift can't kill the run before the boundary.
+2 controller tests (boundary hold-open, option->verb mapping). Full unit suite:
402/402 passing, 39 GUT scripts (no #590 hidden parse failures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(dev): honest build badge — live git HEAD in dev, stamped fallback marked

The baked build_stamp.txt (fd60eb6 · 2026-07-11) was two days stale on every
branch and cost a playtest session to build confusion. Dev checkouts now read the
REAL identity at runtime: BuildInfo.get_live_git_stamp() shells git -C <repo-root>
rev-parse (gated on a .git dir/file existing, so exported builds never try; cached
after one probe; worktrees resolve correctly since git handles the .git file).

Badge forms, each self-identifying:
- dev checkout:   'DEV BUILD v0.11.0 · l1-month-turn-engine@e4cd408 · live'
- exported/no git: '... · fd60eb6 · 2026-07-11 (stamp)'
- never stamped:  '... · unstamped'

Acceptance: two different checkouts can never show the same badge silently — live
HEAD differs per checkout, and a stamped badge visibly declares it is a stamp. A
stale stamp additionally prints a console note (print, not push_warning: in a dev
checkout the stamp is stale after every commit by construction; GUT/CI must not
count that as an engine error).

+2 badge tests (live identity present in a git checkout; every badge form declares
its provenance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* balance: L1 month-cycle sweep — data + memo (no constants changed)

Structured bot sweep over the new L1 month cycle (PR #636). 72 deterministic
runs across 6 plan/response policies through the real MonthController playback
(not clamped). Adds the harness + the balance memo + raw per-run CSV. Changes
no balance constants — measures and recommends.

Headline: no policy (incl. do-nothing) survives even the first plan-month —
all runs die of doom at workday-tick 2-15 while a month is ~23 ticks. Dominant
doom source is rival-lab pressure billed per day-tick (sized for the pre-L1
strategic turn, now billing ~23x/month) — the re-denomination pass ADR-0009
flagged is still open. DEFER can add +99..+143 doom in one tick via a
desperation_payroll ledger bill (8/8 ledger-rooted deaths, corrected
classifier). Reserve is inert (Attention ~6x oversupplied vs window demand).

Also documents a latent #636 wiring bug (not fixed): resolve_current_window
pops the window before validating payment, silently dropping it on a failed
reserve payment; resolve_current_window_option guards correctly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* balance: L1 recalibration — dials 1-4 applied + iteration log

Re-denominates the per-day-tick doom/ledger pressures that were sized for the
pre-L1 strategic turn (ADR-0009) so the month cadence is survivable. After 5
measured 72-run sweeps: do_nothing 14 mo (target 12-18), greedy dies
fastest-but-not-instantly, the ledger bites legibly over months (not a one-tick
guillotine), and every policy still dies (max 28 mo — ADR-0002).

Two load-bearing findings surfaced and are documented for Pip's veto:
- The ledger's doom teeth were INERT: _step_resolve_doom overwrites
  state.doom = doom_system.current_doom every tick, clobbering the ledger's
  direct doom writes. Routed ledger doom through doom_system so DEFER's cost
  actually lands (capped per bill + rolled over months = loan semantics).
- Safety's beats-passive margin (~1.1x, target 1.5-2.5x) is capped by the bot's
  monthly-overhiring bankruptcy — a money/salary-economy effect outside dials 1-4.

Dials: (1) rival + player-researcher/base/unproductive doom re-denomination;
(2) start doom 50->20; (3) DEFER/ledger priced as a loan — per-bill doom+rep
caps with month-spread rollover, ledger fuses/interest/exposure re-denominated;
(4) momentum re-tune. Dial 5 (Attention) untouched (out of scope).

Harness: MAX_MONTHS 60->420 (ADR-0002 detection), m1-m6 slope/CSV columns,
MORTALITY_CHECK. Stale magic-number assertions in doom_system/ledger_actions/
death_attribution reworked to read Balance (tuning-robust).

Full memo + per-iteration log + re-narrated lived experiences:
docs/balance/L1_CALIBRATION_2026-07-14.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(balance): refresh L1 calibration memo + CSV to post-merge numbers

Regenerated the 72-run sweep on the merge with main (event-outcome fixes +
window-pop guard). do_nothing 14 mo (unchanged), safety_lean 20 / reserve_heavy
19 (main's event economy lifted safety from the pre-merge 1.1x to ~1.4x — a
near-miss on 1.5-2.5x, still bankruptcy/variance-bound: a +25% safety-coefficient
probe did not move the median). Mortality max 45 mo, PASS. Narratives re-narrated
from the merged runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* balance: T9 floor — no standard run completes ruin before month 6

Pip's T9 ruling (#638 review): the early game must be nearly unlosable w.r.t.
compounding debt — ruin can be SET UP early but not COMPLETED early. Levers
were dial-3 completion territory + remaining per-tick cadence artifacts, never
ambient doom (do_nothing stays 14 mo, byte-identical).

- risk.trigger_scale_per_tick (NEW, 0.045): risk-pool trigger probability
  pool/100 was per DAY-TICK — a pool of 40 fired ~9 events/month. Re-denominated
  so pool N reads ~N%/MONTH. Threshold crossings stay guaranteed one-shots.
  (This was greedy_overcommit's hidden +2-doom drip; 5 -> 7 mo.)
- ledger fuses stretched (loan 44->77, desperation 33->55, strings 66->88 ticks)
  + interest 0.012->0.009; funding_strings principal x0.15->x0.10;
  doom_per_unpaid_1000 1.5->0.9.
- per-bill caps 15->10 doom AND rep (the monthly Ponzi-collapse loan PAIR now
  lands +20, not +30); expose.rep_per_1000 4.0->2.5 (the last sub-6 run was a
  rep-collapse via secret exposures).
- ledger.rollover_fuse_ticks (NEW, 8): residual doom beyond the cap bleeds
  every ~9 ticks instead of every 2 — a matured mega-debt takes months to
  finish you, forgives nothing (ADR-0002 intact).

Momentum (dial 4, ruled a low-commitment SWITCH): doom.momentum_enabled +
doom.momentum_weight Balance keys; accumulators keep ticking while disabled so
mid-run toggles behave.

Final: every standard policy survives >=6 full months (loan_desperation 6-7,
greedy 6-13); do_nothing 14; mortality max 45 mo, 0 immortal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: save/load float determinism + pre-L1 simulator caps; memo rewrite per #638 rulings

Save/load determinism (found by the newly-real CI gate, #640):
- Godot's JSON float parse is not correctly-rounded — full-precision floats can
  come back from a save 1 ulp off. Probe-verified: 0.009/0.015/0.045 corrupt,
  0.008/0.0025/0.25 survive. Ledger interest rates moved to round-trip-safe
  decimals (0.009 -> 0.008) and principals quantized to whole units at mint +
  each compound step (integer-valued doubles round-trip exactly; self-healing).
  Gotcha documented in defaults.json + memo open question.

Pre-L1 cap relics (T9-adjacent cadence artifacts):
- ReplaySimulator/BaselineSimulator MAX_TURNS 200 -> 10000: 200 DAY-TICKS is
  ~9 fiction-months, so post-recalibration the verifier would truncate — and
  reject — every honest long run, and the baseline score truncated every game.
- test_replay_verification: scripted runs must actually reach game_over
  (ADR-0006 score is defined at game over); max_turns 40 -> 500.
- test_game_state: start-doom + clamp assertions read Balance (dial 2).

Memo rewritten per the #638 review rulings: T1-T9 each carry reasoned context;
T6 plain-language explainer (what DEFER minted, why +99-143 was an accounting
collision, what cap+rollover means in play, why 10-15); momentum explainer box
+ switch semantics; ledger doom-routing approval recorded with the
ADR-0005/ADR-0015 one-authority rationale; Legacy-variables shopping list
(15 items) for the migration lane; T9 iteration log; final tables + narratives
re-narrated from final-constants runs.

All three CI gates pass locally: quick 333/0, integration 14/0, simulation 93/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
PipFoweraker added a commit that referenced this pull request Jul 15, 2026
Verified current main (post #636/#638/#639/#640/#641/#643 merge stack)
launches clean headless, plays a full month through the End Turn ->
MonthController path, and passes the fast unit gate CI runs
(345 tests, 0 failures, 36/36 files). Nine-stream doom, finance offers,
and flight recorder all pass their unit suites with no wiring gaps
found. No code changes needed -- green light for tomorrow.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
PipFoweraker added a commit that referenced this pull request Jul 15, 2026
Developer-facing architecture map (audience: future junior dev). Maps each
major system to its code and deciding ADR(s), with honest built/partial/designed
status. Preserves the prior funder pitch at ARCHITECTURE_FUNDERS.md.

Written against the L1 wave (month engine #636, nine-stream doom #643, finance
#641, calibration #638, honest CI #640). Surfaces coherence-check findings in §7.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant