fix(flip-candidates): release the physical GE slot after a completed flip via closed -> free (governor 10471)

A successful sell followed sell-filled-awaiting-collect -> closed, and
'closed' had no outgoing transition while the scheduler left closed
slots untouched — every GE slot could perform ONE flip and then became
permanently unusable (no rotations, breaks, or longer sessions).

Fix (finding-by-finding, governor comment 10471):
1. 'closed' is no longer a sink. The completed cycle's ledger is
   reconciled and ARCHIVED at close, and the physical slot is released
   to free via the explicit archival transition closed -> free
   (flip-candidates.yaml slot_transitions, whens, scheduler_iteration,
   a9 archive step, summary, risk, sources). The scheduler pass
   dispatches the release (SLOT_DISPATCH['closed'] = 'release'), so a
   closed slot is only ever transient.
2. A deliberately closed cancelled sell releases identically:
   sell-cancelled-awaiting-collect -> closed -> free (whens updated).
3. Committed two-rotation regression (validator, wired into
   ge_lifecycle_errors for every flipping entry): slot_reuse_violations
   walks one physical slot through free -> buy-open ->
   buy-filled-awaiting-collect -> ready-to-sell -> sell-open ->
   sell-filled-awaiting-collect -> closed -> free -> buy-open (second
   buy in the SAME slot), validating every ledger frame against both
   conservation gates; archived_ledger_preserved_after_second_buy
   proves the rotation-1 archived ledger survives rotation 2 bit-for-bit
   (no loss, duplication, or mutation). Negative fixtures prove a
   closed sink or a closed that does not release to free FAILS.
4. ge_lifecycle_errors now requires closed == ['free'] exactly and
   rejects any machine where a state has no transitions.
5. Worked example: t5/t8 walk the release, t8b runs rotation 2 in the
   same physical slots.

All prior fixtures preserved (tax x4, async-slot scheduler,
collection-box, limit-window, side-aware conservation x25) — now 35
selftest checks.

Evidence: python3 tools/validate.py -> 2 entries, 0 errors, 1
pre-existing warning; --selftest -> 35 PASS, 0 errors; git diff
--check clean.
This commit is contained in:
Claude 2026-08-29 03:11:20 -05:00
parent 76f8e5ad27
commit 671d025d9f
3 changed files with 237 additions and 38 deletions

View File

@ -3,7 +3,7 @@ title: GE Flipping (margin trading)
domain: economy
tags: [flipping, merchanting, ge, money-making, hybrid]
status: verified
version: 5 # 5: bounce-fix 6 2026-08-28 (governor comment 10390) — the 4h buy-limit reset NEVER cancels an offer (offer lifetime = independent quote_age_policy: stale edge or strategy reallocation only); explicit buy-filled-awaiting-collect / sell-filled-awaiting-collect states so completed offers must pass through the GE collection box (ge_collect) before reuse or closing; SIDE-AWARE per-slot ledgers (buy: quoted units = filled + still_open + cancelled_unfilled, acquired items = filled ONLY, cash = spent + still_committed + refunded; sell: quoted = sold + still_open + returned_unsold) with committed negative fixtures for phantom refunded buy units, a full buy sold before collection, and a full sell closed before proceeds collection. Capped/floored tax model + its 4 selftest fixtures and the asynchronous scheduler behavior preserved (owner decisions 2026-08-28).
version: 6 # 6: bounce-fix 7 2026-08-29 (governor comment 10471) — 'closed' is no longer a sink: after a completed sell's proceeds are collected and the ledger is reconciled + ARCHIVED, the physical GE slot is released to free via the explicit archival transition `closed -> free` (a deliberately closed cancelled sell releases the same way), and the committed two-rotation regression proves one slot completes buy/collect/sell/collect, archives the first ledger, and opens a SECOND buy in the same slot without losing, duplicating, or mutating the archived record. Previous (v5, bounce-fix 6): the 4h buy-limit reset NEVER cancels an offer (offer lifetime = independent quote_age_policy: stale edge or strategy reallocation only); explicit buy-filled-awaiting-collect / sell-filled-awaiting-collect states so completed offers must pass through the GE collection box (ge_collect) before reuse or closing; SIDE-AWARE per-slot ledgers (buy: quoted units = filled + still_open + cancelled_unfilled, acquired items = filled ONLY, cash = spent + still_committed + refunded; sell: quoted = sold + still_open + returned_unsold) with committed negative fixtures for phantom refunded buy units, a full buy sold before collection, and a full sell closed before proceeds collection. Capped/floored tax model + its 4 selftest fixtures and the asynchronous scheduler behavior preserved (owner decisions 2026-08-28).
updated: "2026-08-28" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date
summary: >-
@ -21,7 +21,11 @@ summary: >-
global scheduler pass that acts only on dispatchable slots and never
closes or advances unrelated ones; completed offers (full buys and full
sells) land in the GE COLLECTION BOX and must pass through ge_collect +
ledger reconciliation before assets are reused or the slot closes; the
ledger reconciliation before assets are reused or the slot closes;
'closed' is NOT a sink — the physical GE slot is released to free via
the archival transition `closed -> free` after the completed cycle's
ledger is reconciled + archived, so every slot runs flip after flip
through rotations and breaks (two-rotation reuse regression); the
4h buy-limit ledger (limit_used / first_fill_ts / limit_resets_at /
remaining) is DISTINCT from offer expiry — a limit-window reset NEVER
cancels an offer and cancellation NEVER renews the limit; ge_cancel
@ -85,12 +89,16 @@ best_practice:
why: "A slot's offer fills on its own timeline (partial fills keep resting). The
global scheduler pass acts only on dispatchable slots (free -> quote,
*-awaiting-collect -> ge_collect, ready-to-sell -> sell, ready-to-requote ->
requote) and leaves resting buy-open / sell-open / closed slots untouched — one
slot's cancel or close never advances an unrelated slot. A FULLY FILLED offer
never sells or closes directly: it first enters an explicit
buy-/sell-filled-awaiting-collect state because its assets sit in the GE
collection box until ge_collect (finding 2, governor 10390)."
source: "governor review comment 10323 + 10390 (2026-08-28); OSRS wiki — Grand Exchange / Bank (collection box); docs/action-grammar.md"
requote, closed -> free archival release) and leaves resting buy-open /
sell-open slots untouched — one slot's cancel or close never advances an
unrelated slot. A FULLY FILLED offer never sells or closes directly: it
first enters an explicit buy-/sell-filled-awaiting-collect state because
its assets sit in the GE collection box until ge_collect (finding 2,
governor 10390). A 'closed' slot is only ever transient: the completed
cycle's ledger is archived at close and the pass releases the physical
slot to free (closed -> free), so the same slot runs the next flip —
never one flip and done (governor 10471)."
source: "governor review comments 10323 + 10390 + 10471 (2026-08-28/29); OSRS wiki — Grand Exchange / Bank (collection box); docs/action-grammar.md"
- name: keep a per-slot offer ledger; ge_cancel -> collection box -> ge_collect -> reconcile, never strand units
why: "Every slot tracks its side-aware ledger: a BUY quote partitions units into
filled + still_open + cancelled_unfilled with acquired items = filled ONLY (the
@ -176,7 +184,7 @@ slot_state_machine:
sell-filled-awaiting-collect: [closed]
sell-cancelled-awaiting-collect: [ready-to-requote, closed]
ready-to-requote: [buy-open, sell-open, free]
closed: []
closed: [free] # archival release — the physical slot is reusable for the next flip (governor 10471)
slot_transition_whens:
- "free -> buy-open: scheduler pass quotes a resting buy at the low edge (qty = min(remaining 4h limit, slot capital, capital available))"
- "buy-open -> buy-filled-awaiting-collect: offer FULLY filled (filled == quoted) — the units sit in the GE COLLECTION BOX until ge_collect; the slot may NOT sell them directly (governor 10390 finding 2)"
@ -189,9 +197,10 @@ slot_state_machine:
- "ready-to-sell -> sell-open: scheduler pass quotes a resting sell at the high edge for the units in hand"
- "sell-open -> sell-filled-awaiting-collect: sell offer FULLY filled (sold == quoted) — the proceeds sit in the GE COLLECTION BOX until ge_collect; the slot may NOT close directly (governor 10390 finding 2)"
- "sell-open -> sell-cancelled-awaiting-collect: quote_age_policy abort — scanned edge stale or strategy reallocation; ge_cancel: unsold units + proceeds received so far move to the collection box (never to inventory)"
- "sell-filled-awaiting-collect -> closed: ge_collect withdrawn the proceeds; ledger reconciled — slot freed"
- "sell-filled-awaiting-collect -> closed: ge_collect withdrawn the proceeds; ledger reconciled and ARCHIVED — the archived cycle record is immutable; the physical slot is released at the next dispatch by the closed-to-free archival transition (governor 10471)"
- "sell-cancelled-awaiting-collect -> ready-to-requote: ge_collect withdrawn; requote the sell at the fresh high edge or the current band"
- "sell-cancelled-awaiting-collect -> closed: ge_collect withdrawn; slot closed (exit taken)"
- "sell-cancelled-awaiting-collect -> closed: ge_collect withdrawn; ledger archived, deliberately closed (exit taken) — the physical slot releases to free exactly like a completed sell (governor 10471)"
- "closed -> free: ARCHIVAL RELEASE — the completed cycle's ledger stays archived (immutable, conserved), and the physical GE slot returns to free to place the next flip's buy (two-rotation reuse regression)"
- "ready-to-requote -> buy-open: requoted buy placed (buy_capacity_ok: limit_used + qty <= limit until limit_resets_at)"
- "ready-to-requote -> sell-open: requoted sell placed at the fresh high edge"
- "ready-to-requote -> free: slot abandoned and released"
@ -201,11 +210,15 @@ slot_state_machine:
awaiting-collect / buy-cancelled-awaiting-collect /
sell-filled-awaiting-collect / sell-cancelled-awaiting-collect ->
ge_collect, ready-to-sell -> sell (ge_sell), ready-to-requote ->
requote. Resting buy-open / sell-open / closed slots produce NO action
and are never closed, advanced, re-quoted or touched by the pass. A
pass therefore advances only the slots whose sub-state is dispatchable,
and a completed offer always passes through its *-filled-awaiting-
collect state (ge_collect) before its assets are reused.
requote, closed -> free (archival release: the completed cycle's
ledger was archived at close; the pass returns the physical slot to
free so the next flip can begin). Resting buy-open / sell-open slots
produce NO action and are never closed, advanced, re-quoted or touched
by the pass. A pass therefore advances only the slots whose sub-state
is dispatchable, a completed offer always passes through its
*-filled-awaiting-collect state (ge_collect) before its assets are
reused, and a closed slot is only ever transient — released to free
rather than left to strand after one flip (governor 10471).
quote_age_policy: >-
Offer lifetime is INDEPENDENT of the 4h buy-limit ledger. Resting
offers are re-checked at the 600 s cadence and kept resting while the
@ -249,14 +262,22 @@ slot_state_machine:
gates: a buy-filled-awaiting-collect slot has NOTHING sold yet (a full
buy cannot sell before ge_collect) and a sell-filled-awaiting-collect
slot holds its proceeds in the box; a 'closed' slot has no uncollected
proceeds or cancelled/returned units. Committed validator fixtures —
proceeds or cancelled/returned units — it holds ONLY the archived
(immutable) cycle record, and the physical slot is released to free
via the closed -> free archival transition so the SAME slot can run
the next flip (governor 10471; two-rotation reuse regression: the
archived rotation-1 ledger survives rotation 2 bit-for-bit).
Committed validator fixtures —
including the negative fixtures (phantom refunded buy units; a full
buy sold before collection; a full sell closed before proceeds
collection) — prove the invariants and the scheduler dispatch.
collection; a 'closed' sink that strands the slot after one flip) —
prove the invariants and the scheduler dispatch.
break_resume: >-
Breaks are taken only when no slot is dispatchable: every slot is
resting (buy-open / sell-open), free, or collected (closed) — never
mid-collect, mid-place, or in an *-awaiting-collect state. Full state
resting (buy-open / sell-open) or free — never mid-collect, mid-place,
or in an *-awaiting-collect state. A closed slot is only ever
transient (archived and released to free by the pass), so a break
never catches the activity between archive and release; full state
(offers server-side + the persisted per-slot record) survives the
pause; resume by re-opening the GE, re-checking offers, re-running the
scheduler pass and continuing each slot from its persisted sub-state.
@ -268,7 +289,7 @@ state_machine:
terminal: [complete, failed]
states:
idle:
goal: "be logged in at a bank with a GE booth, capital >= floor, every slot's ledger closed (sub-state free or closed, all buckets zero)"
goal: "be logged in at a bank with a GE booth, capital >= floor, every slot's ledger closed (sub-state free — a closed slot is transient and released to free by the pass — all live buckets zero)"
next: [scan, complete]
scan:
goal: "read the current high/low band for every candidate; compute net-positive edges with floored+capped tax (a3)"
@ -312,7 +333,7 @@ state_machine:
when: "a slot filled and proceeds/results are ready to collect"
to: collect
- from: collect
when: "only resting or closed slots remain; cadence continues"
when: "only resting slots remain (closed slots are released to free by the same pass); cadence continues"
to: wait
- from: collect
when: "session GP target reached"
@ -424,6 +445,7 @@ actions:
withdraw: "from the GE collection box: bought (filled) items, refunded CASH, unsold units, sale proceeds — per slot"
reconcile: "SIDE-AWARE per-slot ledger conservation check BEFORE any slot reuse — buy: quoted == filled + still_open + cancelled_unfilled and cash_quoted == spent + still_committed + refunded; sell: quoted == sold + still_open + returned_unsold"
decide: "buy-filled/buy-cancelled -> ready-to-sell (sell edge positive) | ready-to-requote (buy margin positive and remaining limit > 0) | free (release); sell-filled/sell-cancelled -> closed | ready-to-requote"
archive: "at 'closed' the completed cycle's ledger is reconciled and ARCHIVED (immutable); the pass then releases the physical slot to free (closed -> free) so the SAME slot places the next flip's buy — the two-rotation reuse regression proves the archived record survives rotation 2 (governor 10471)"
log: "RESULT item, qty, net gp per slot (floored+capped tax)"
on_fail: alert
anti_ban:
@ -435,7 +457,7 @@ actions:
target: "current location"
params:
duration: "[60, 240] seconds of idle AFK per break (matches run_control.break.for)"
safe: "only when every slot is resting (buy-open / sell-open) or collected (closed) — resting offers persist server-side at their quoted edges and the per-slot record persists locally; never break mid-collect (coins in hand) or mid-place (offer half-configured)"
safe: "only when every slot is resting (buy-open / sell-open) or free — resting offers persist server-side at their quoted edges and the per-slot record persists locally; a closed slot is transient (archived + released to free by the pass), so breaks never catch the activity between archive and release; never break mid-collect (coins in hand) or mid-place (offer half-configured)"
resume: "re-open GE (a2), re-check offer state (a6), re-run the scheduler pass (schedule), continue each slot from its persisted sub-state (a4/a7/a8/a9)"
on_fail: skip
@ -496,8 +518,12 @@ run_control:
#
# t5 pass 4: iron sell-filled-awaiting-collect -> ge_collect (a9): withdraw
# 1,570,000; tax floor(157 x 0.02) = 3/item -> net (157 - 3 - 145) x 10,000
# = 90,000. proceeds_in_box -> cash_collected; ledger reconciled;
# sub-state -> closed, slot freed. Conservation: quoted 10,000 == sold 10,000.
# = 90,000. proceeds_in_box -> cash_collected; ledger reconciled and
# ARCHIVED (the immutable cycle record: quoted 10,000 == sold 10,000);
# sub-state -> closed, and the SAME pass's archival dispatch releases the
# physical slot: closed -> free (governor 10471). The slot can place the
# next flip's buy immediately — it is never stranded after one flip.
# Conservation: quoted 10,000 == sold 10,000, zero proceeds left uncollected.
#
# t6 LIMIT RESET, NOT AN OFFER EVENT (finding 1): arrowtips reaches
# limit_resets_at with 6,000 still open on the resting offer. The window
@ -516,7 +542,19 @@ run_control:
# t8 pass 6: arrowtips ready-to-sell -> sell-open 10,000 @ 244 -> full fill ->
# sell-filled-awaiting-collect -> pass 7 ge_collect: proceeds 2,440,000;
# tax floor(244 x 0.02) = 4/item -> net (244 - 4 - 223) x 10,000 = 170,000
# (matches the 170,000 4h/slot table row) -> closed.
# (matches the 170,000 4h/slot table row) -> closed (ledger archived) ->
# closed -> free: the arrowtips physical slot is released for the next flip.
#
# t8b SECOND ROTATION in the SAME physical slot (governor 10471 regression):
# the iron slot released at t5 and the arrowtips slot released at t8 are
# free and dispatchable again — pass 8 quotes a NEW arrowtips buy in the
# iron slot (free -> buy-open, a4; buy_capacity_ok re-checked against the
# CURRENT window's limit_ledger). Rotation 1's archived ledgers (iron
# closed record, arrowtips closed record) remain archived and bit-for-bit
# untouched — no loss, duplication, or mutation — while the live ledger is
# a fresh rotation-2 record (quoted 10,000, cash_quoted 2,230,000,
# still_open 10,000). This is the committed slot_reuse regression: a slot
# completes one flip, is released, and runs the next one.
#
# t9 STALE-EDGE recovery (cosmic, finding 1 cancel trigger): at a cadence check
# the cosmic LOW edge has risen 122 -> 126: net 128 - floor(128 x 0.02)=2 -
@ -566,6 +604,7 @@ risk:
- "ge_cancel NEVER returns items/coins directly to inventory: everything lands in the GE collection box. BUY cancels return filled items + refunded CASH (the unfilled quantity is cash, never 'refunded units'); SELL cancels return unsold units + proceeds received. Reuse a slot only after ge_collect + side-aware ledger reconciliation (conservation invariants)."
- "The 4h buy limit is independent of offer life: cancelling does not renew it and the reset does not cancel offers. A slot at limit_used == limit cannot place a new/requoted BUY until limit_resets_at (first_fill_ts + 4h); its resting offer is unaffected."
- "Breaks are safe only when no slot is dispatchable (resting offers or post-collect points, see a10 and break_resume); state = GE offers + the persisted per-slot record, which survive the pause."
- "A completed cycle NEVER strands a slot: after ge_collect the ledger is reconciled + archived at 'closed' and the scheduler pass releases the physical slot to free (closed -> free archival release), so every slot keeps running flip after flip. Never model a slot as one-flip-then-done (governor 10471)."
sources:
- "OSRS wiki — Grand Exchange (2% convenience fee since 2025-05-29, capped 5M/item, sub-50gp effectively untaxed, tax-exempt low-value items; buying rate restricted per 4h window starting on the FIRST purchase of the window; connected limits; verified 2026-08-27)"
@ -576,7 +615,8 @@ sources:
- "prices.runescape.wiki/api/v1/osrs/latest (high/low price band snapshot 2026-08-27)"
- "governor review comment 10323 on PR #7 (2026-08-28): per-slot asynchronous sub-state machine + scheduler pass (finding 3); ge_cancel -> collection box + ge_collect + reconciliation (finding 2); buy-limit ledger distinct from offer expiry, cancellation never renews (finding 1); conservation invariants (finding 4); typed ceiling vs documented realized scenarios (finding 5); explicit scheduler iteration + persisted break/resume, no slot downgrade (finding 6)"
- "governor review comment 10390 on PR #7 (2026-08-28): the 4h limit reset never cancels offers — resting offers remain open while the margin is positive (finding 1); filled offers must pass through explicit *-filled-awaiting-collect states and ge_collect before reuse/close (finding 2); side-aware ledgers kill the phantom-units error (finding 3); side-aware conservation with negative fixtures, including sale-before-collection and close-before-proceeds-collection (finding 4)"
- "owner decisions 2026-08-28 (guidance comments 10264, 10271, 10334): capped/floored global tax formula; complete lifecycle contract; authorisation to keep repairing this same branch/PR"
- "governor review comment 10471 on PR #7 (2026-08-29): a completed flip must NOT permanently strand the GE slot — after proceeds are collected and the completed ledger is archived/reconciled the physical slot returns to free via the archival transition closed -> free (also for a deliberately closed cancelled sell); committed two-rotation regression (buy/collect/sell/collect -> archive -> second buy in the same slot) with the archived ledger preserved"
- "owner authorisations 2026-08-28/29 (guidance comments 10264, 10271, 10334, 10527/10528): capped/floored global tax formula; complete lifecycle contract; continued full-lifecycle repair on the same branch/PR (retry-budget superseded)"
- "docs/action-grammar.md (ge_cancel/ge_collect rows, 2026-08-28) + schemas/entry.schema.json (slot_state_machine, ceiling_gp_per_hour)"
verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing); prices last re-fetched live 2026-08-27

View File

@ -70,7 +70,7 @@ Legend for the mapping table:
| `ge_sell` | sell `qty` of `item`, confirm | — | — | — |
| `ge_cancel` | abort an offer (quote_age_policy only — stale edge or strategy reallocation, never a 4h limit-window reset): BUY — filled items + refunded CASH (unspent remainder) enter the GE COLLECTION BOX; SELL — unsold units + proceeds received so far enter the box; NEVER directly to inventory; the 4h buy limit is NOT renewed | — | — | — |
| `ge_offers_done` | predicate: no pending buy/sell offers | — | — | — |
| `ge_collect` | withdraw from the GE collection box: bought (filled) items, refunded CASH, unsold units, sale proceeds; reconcile the SIDE-AWARE per-slot ledger (buy: quoted = filled + still_open + cancelled_unfilled; sell: quoted = sold + still_open + returned_unsold) before any slot reuse | — | — | — |
| `ge_collect` | withdraw from the GE collection box: bought (filled) items, refunded CASH, unsold units, sale proceeds; reconcile the SIDE-AWARE per-slot ledger (buy: quoted = filled + still_open + cancelled_unfilled; sell: quoted = sold + still_open + returned_unsold) before any slot reuse; a completed cycle closes with the ledger reconciled + ARCHIVED and the same pass releases the physical slot to free (`closed -> free` archival transition), so the slot immediately runs the next flip (governor 10471) | — | — | — |
| `ge_check_price` | read current buy/sell price of an item | — | — | — |
## F. Combat

View File

@ -38,6 +38,13 @@ Runs:
refunded units; sell: quoted = sold + still_open + returned_unsold)
with phase gates (a full buy cannot sell before collection, a full
sell cannot close before proceeds collection);
closed has ONE outgoing transition — the archival release
``closed -> free`` — so a completed cycle's physical GE slot
returns to free for the next flip instead of stranding forever
(governor 10471); the two-rotation reuse regression proves a slot
completes buy/collect/sell/collect, archives the first ledger, and
places a second buy in the same slot without loss, duplication, or
mutation of the archived record;
economy.ceiling_gp_per_hour is the TYPED economic ceiling and
0 <= gp_per_hour_low <= gp_per_hour_high <= ceiling.
- Regression fixtures (--selftest): a fractional-tax flipping
@ -133,7 +140,7 @@ SLOT_SUB_STATES = [
"sell-filled-awaiting-collect", # sell FULLY filled — proceeds sit in the box until ge_collect
"sell-cancelled-awaiting-collect", # aborted sell: proceeds + unsold units in the box
"ready-to-requote", # collected units/refunds; requote or abandon
"closed", # slot finished, ledger reconciled, nothing held
"closed", # completed cycle: ledger reconciled + ARCHIVED; the physical slot is released to free via the closed -> free archival transition (governor 10471)
]
# Dispatch table: the pure action a slot in a given sub-state needs this pass.
SLOT_DISPATCH = {
@ -146,7 +153,7 @@ SLOT_DISPATCH = {
"sell-filled-awaiting-collect": "collect",
"sell-cancelled-awaiting-collect": "collect",
"ready-to-requote": "requote",
"closed": None,
"closed": "release", # archival release: closed -> free (governor 10471)
}
@ -155,7 +162,12 @@ def scheduler_dispatch(sub_state):
A None (resting) slot is left completely untouched — the pass must not
close, advance, or mutate it. This is the property the worked-example
regression proves.
regression proves. A 'closed' slot is NOT resting: its completed
cycle's ledger was archived at ge_collect time and the pass releases
the physical slot to free ('release' = the closed -> free archival
transition), so the slot can run another flip instead of stranding
forever (governor 10471). Resting states are buy-open / sell-open
only.
"""
return SLOT_DISPATCH.get(sub_state)
@ -334,6 +346,120 @@ def buy_capacity_ok(limit_used, limit, qty_requested):
return limit_used + qty_requested <= limit
# Deterministic ledger frames for ONE physical GE slot across TWO full
# flips (governor 10471): the fixture walks a complete buy/collect/
# sell/collect cycle, archives the first ledger at `closed`, releases the
# physical slot to `free`, and opens a SECOND buy in the SAME slot. Every
# frame must satisfy both conservation checks; the archived first-cycle
# record must survive rotation 2 untouched (no loss, duplication, or
# mutation). Prices: Rune arrowtips buy 223 / sell 244 / tax
# min(floor(244*0.02), 5M) = 4 / proceeds 2,440,000 (snapshot 2026-08-27,
# matches the worked example).
_CYCLE_ITEM = "Rune arrowtips"
_CYCLE_FRAMES = [
# (sub_state, ledger) — ledger is side-aware and conservation-clean.
("free", {"side": "buy", "quoted": {}, "filled": {}, "still_open": {},
"cancelled_unfilled": {}, "cash_quoted": 0, "spent": 0,
"still_committed": 0, "refunded": 0}),
("buy-open", {"side": "buy", "quoted": {_CYCLE_ITEM: 10_000},
"filled": {}, "still_open": {_CYCLE_ITEM: 10_000},
"cancelled_unfilled": {}, "cash_quoted": 2_230_000,
"spent": 0, "still_committed": 2_230_000, "refunded": 0}),
("buy-filled-awaiting-collect",
{"side": "buy", "quoted": {_CYCLE_ITEM: 10_000},
"filled": {_CYCLE_ITEM: 10_000}, "still_open": {},
"cancelled_unfilled": {}, "cash_quoted": 2_230_000,
"spent": 2_230_000, "still_committed": 0, "refunded": 0}),
("ready-to-sell",
{"side": "buy", "quoted": {_CYCLE_ITEM: 10_000},
"filled": {_CYCLE_ITEM: 10_000}, "still_open": {},
"cancelled_unfilled": {}, "cash_quoted": 2_230_000,
"spent": 2_230_000, "still_committed": 0, "refunded": 0}),
("sell-open", {"side": "sell", "quoted": {_CYCLE_ITEM: 10_000},
"sold": {}, "still_open": {_CYCLE_ITEM: 10_000},
"returned_unsold": {}, "proceeds_in_box": 0,
"cash_collected": 0}),
("sell-filled-awaiting-collect",
{"side": "sell", "quoted": {_CYCLE_ITEM: 10_000},
"sold": {_CYCLE_ITEM: 10_000}, "still_open": {},
"returned_unsold": {}, "proceeds_in_box": 2_440_000,
"cash_collected": 0}),
("closed", {"side": "sell", "quoted": {_CYCLE_ITEM: 10_000},
"sold": {_CYCLE_ITEM: 10_000}, "still_open": {},
"returned_unsold": {}, "proceeds_in_box": 0,
"cash_collected": 2_440_000}), # ARCHIVED here
("free", {"side": "buy", "quoted": {}, "filled": {}, "still_open": {},
"cancelled_unfilled": {}, "cash_quoted": 0, "spent": 0,
"still_committed": 0, "refunded": 0}), # released: physical slot reusable
# rotation 2: the SAME physical slot opens a second buy (ledger reset;
# the archived rotation-1 record above must remain untouched).
("buy-open", {"side": "buy", "quoted": {_CYCLE_ITEM: 10_000},
"filled": {}, "still_open": {_CYCLE_ITEM: 10_000},
"cancelled_unfilled": {}, "cash_quoted": 2_230_000,
"spent": 0, "still_committed": 2_230_000, "refunded": 0}),
]
# The canonical complete flip cycle: rotation 1 (buy/collect/sell/collect)
# plus the archival release and the start of rotation 2.
_CYCLE_PATH = [f[0] for f in _CYCLE_FRAMES]
# Index of the `closed` frame (archived ledger) and the frames around it.
_CLOSED_IDX = _CYCLE_PATH.index("closed")
def slot_reuse_violations(transitions):
"""Two-rotation slot-reuse regression (governor 10471).
Walks ``_CYCLE_PATH`` through the entry's ``slot_transitions`` and
validates every ledger frame against both conservation gates. A
``closed`` state with no outgoing transition (or one that does not
release to ``free``) strands the physical GE slot after one flip —
the exact defect this regression exists to forbid. Errors are
returned as a list of strings.
"""
out = []
for a, b in zip(_CYCLE_PATH, _CYCLE_PATH[1:]):
if b not in (transitions.get(a) or []):
out.append(
f"slot cycle: transition {a} -> {b} is missing — a completed "
f"flip must end by releasing the physical slot to free "
f"(archival release closed -> free), otherwise the slot "
f"performs one flip and then becomes permanently unusable")
for state, record in _CYCLE_FRAMES:
rec = dict(record, sub_state=state)
con = conservation_violations(rec)
if con:
out.append(f"slot cycle: conservation fails at '{state}': {con}")
ph = slot_phase_violations(rec)
if ph:
out.append(f"slot cycle: phase gate fails at '{state}': {ph}")
return out
def archived_ledger_preserved_after_second_buy():
"""The rotation-1 archived ledger survives rotation 2 intact.
``closed`` archives a deep copy of the completed cycle's ledger; when
the same physical slot is released to free and opens a second buy, the
archived record must be bit-for-bit unchanged (no loss, duplication,
or mutation) while the live ledger is a fresh empty record.
"""
archived = json.loads(json.dumps(_CYCLE_FRAMES[_CLOSED_IDX][1]))
archived_state = _CYCLE_FRAMES[_CLOSED_IDX][0]
live = {k: (dict(v) if isinstance(v, dict) else v)
for k, v in _CYCLE_FRAMES[-1][1].items()} # rotation-2 buy-open ledger
if archived != _CYCLE_FRAMES[_CLOSED_IDX][1]:
return ["slot cycle: archived rotation-1 ledger was MUTATED by rotation 2"]
if archived_state != "closed":
return [f"slot cycle: unexpected archived state {archived_state!r}"]
# The live ledger at rotation-2 buy-open must be a fresh record, not a
# copy of the archived one: quoted cash/units belong to rotation 2.
if live.get("cash_quoted") != 2_230_000 or live.get("proceeds_in_box"):
return ["slot cycle: rotation-2 live ledger is not a fresh record"]
return []
def ge_lifecycle_errors(rel, data):
"""GE-flip lifecycle gates (flipping/merchanting entries only):
@ -342,6 +468,9 @@ def ge_lifecycle_errors(rel, data):
- the slot model is per-slot asynchronous: slot_state_machine must
declare exactly the canonical sub-states, valid transitions, a
scheduler_iteration, persisted_state, conservation and break_resume;
'closed' is NOT a sink: it must release the physical slot via the
archival transition closed -> free, and the committed two-rotation
reuse regression (slot_reuse_violations) must pass;
- rates: ceiling_gp_per_hour is the typed economic ceiling and
0 <= gp_per_hour_low <= gp_per_hour_high <= ceiling.
"""
@ -392,11 +521,16 @@ def ge_lifecycle_errors(rel, data):
errors.append(f"{rel}: slot transition '{from_s}' -> unknown "
f"sub-state '{to_s}'")
for state in SLOT_SUB_STATES:
if state != "closed" and not trans.get(state):
if not trans.get(state):
errors.append(f"{rel}: slot sub-state '{state}' has no transitions "
f"(closed is the only sink)")
if trans.get("closed"):
errors.append(f"{rel}: 'closed' is a sink — it must have no outgoing transitions")
f"— every state must be able to advance; a closed "
f"slot must release the physical slot via the "
f"archival closed -> free transition (governor 10471)")
if list(trans.get("closed") or []) != ["free"]:
errors.append(f"{rel}: 'closed' must have exactly the archival release "
f"['free'] — a closed slot with no outgoing transition "
f"performs one flip and then becomes permanently "
f"unusable (governor 10471)")
whens = [str(w) for w in ssm.get("slot_transition_whens", []) or []]
for w in whens:
arrow = w.split("->")
@ -408,6 +542,10 @@ def ge_lifecycle_errors(rel, data):
"limit_ledger", "conservation", "break_resume"):
if not str(ssm.get(field, "")).strip():
errors.append(f"{rel}: slot_state_machine.{field} must be declared")
# Two-rotation reuse regression (governor 10471): every flipping entry's
# encoded machine must complete buy/collect/sell/collect, release the
# physical slot to free, and support a second buy in the same slot.
errors += [f"{rel}: {e}" for e in slot_reuse_violations(trans)]
return errors
@ -574,7 +712,7 @@ def run_selftest(verbose=False):
"sell-filled-awaiting-collect": ["closed"],
"sell-cancelled-awaiting-collect": ["ready-to-requote", "closed"],
"ready-to-requote": ["buy-open", "sell-open", "free"],
"closed": [],
"closed": ["free"], # archival release: physical slot reusable (governor 10471)
},
"slot_transition_whens": [
"free -> buy-open: quote placed",
@ -588,9 +726,10 @@ def run_selftest(verbose=False):
"ready-to-sell -> sell-open: sell offer placed",
"sell-open -> sell-filled-awaiting-collect: sell FULLY filled — proceeds sit in the box until ge_collect",
"sell-open -> sell-cancelled-awaiting-collect: abort on stale edge or strategy reallocation",
"sell-filled-awaiting-collect -> closed: proceeds collected and ledger reconciled",
"sell-filled-awaiting-collect -> closed: proceeds collected and ledger reconciled + ARCHIVED",
"sell-cancelled-awaiting-collect -> ready-to-requote: collected, requote",
"sell-cancelled-awaiting-collect -> closed: collected, slot closed",
"sell-cancelled-awaiting-collect -> closed: collected, ledger archived, slot closed (exit taken)",
"closed -> free: archival release — completed cycle's ledger stays archived; the physical GE slot is reusable for the next flip",
"ready-to-requote -> buy-open: requoted buy",
"ready-to-requote -> sell-open: requoted sell",
"ready-to-requote -> free: abandoned",
@ -630,6 +769,26 @@ def run_selftest(verbose=False):
slot_transition_whens=ssm_ok["slot_transition_whens"][:4])))
check("flip lifecycle: a sub-state without transitions (ready-to-sell missing) is rejected",
any("no transitions" in e for e in errs))
errs = ge_lifecycle_errors("bad5.yaml", dict(flip_ok, slot_state_machine=dict(
ssm_ok, slot_transitions=dict(ssm_ok["slot_transitions"], closed=[]))))
check("flip lifecycle: a 'closed' sink (no archival release) is rejected — the physical slot would perform one flip and strand",
any("archival release" in e for e in errs))
# --- slot reuse through rotations (governor 10471) ---
check("slot cycle: the archival release closed -> free is a dispatched scheduler action",
scheduler_dispatch("closed") == "release")
check("slot reuse: one full buy/collect/sell/collect lifecycle completes and the SAME physical slot opens a second buy (two-rotation walk passes)",
slot_reuse_violations(ssm_ok["slot_transitions"]) == [])
check("slot reuse: the rotation-1 archived ledger survives rotation 2 intact (no loss, duplication, or mutation)",
archived_ledger_preserved_after_second_buy() == [])
sink_trans = dict(ssm_ok["slot_transitions"], closed=[])
check("slot reuse: a machine whose closed has no outgoing transition FAILS the two-rotation regression (one flip, then permanently unusable)",
any("permanently unusable" in v
for v in slot_reuse_violations(sink_trans)))
closed_no_free = dict(ssm_ok["slot_transitions"], closed=["ready-to-sell"])
check("slot reuse: a closed that does not release to free FAILS the two-rotation regression",
any("closed -> free" in v
for v in slot_reuse_violations(closed_no_free)))
return errors