diff --git a/codex/economy/low-alch-members.yaml b/codex/economy/low-alch-members.yaml index cc3b7c7..261f77d 100644 --- a/codex/economy/low-alch-members.yaml +++ b/codex/economy/low-alch-members.yaml @@ -3,8 +3,8 @@ title: Low-Level Alchemy (Members) domain: economy tags: [alching, magic-training, members, loss-training] status: verified -version: 5 # 5: bounce-fix 4 2026-08-28 (owner guidance 10325) — complete GE acquisition path (open -> place -> fill -> collect) for initial buy AND every restock (bank-stock fast path only when the bank holds stock); fail-closed staff equip (bounded retries, no fire-rune fallback); capital model (cash vs supplies, staff cost, max-fill worst case, 6-batch loss reserve, float); monotonic break/restock counters (breaks 550/1,100/1,650 independent of restocks 901/1,802) + validator regression; startup-vs-steady-state rates over a 2 h session horizon -updated: "2026-08-28" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date +version: 6 # 6: bounce-fix 5 2026-08-29 (owner guidance for governor 10361) — loss-reserve stop fixed to 'initial capital - total assets > 162,990' everywhere + validator boundary fixtures (below/at/above/profit) + realized GP/hr bounds derived from declared fill-price scenarios (A live-low -3.00/cast, B max allowed +5% -30.15/cast): gp low -29,463 / high -1,758. # 5: bounce-fix 4 2026-08-28 (owner guidance 10325) — complete GE acquisition path (open -> place -> fill -> collect) for initial buy AND every restock (bank-stock fast path only when the bank holds stock); fail-closed staff equip (bounded retries, no fire-rune fallback); capital model (cash vs supplies, staff cost, max-fill worst case, 6-batch loss reserve, float); monotonic break/restock counters (breaks 550/1,100/1,650 independent of restocks 901/1,802) + validator regression; startup-vs-steady-state rates over a 2 h session horizon +updated: "2026-08-29" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date summary: > Magic training from level 21 by low-alching members items with the lowest @@ -13,7 +13,10 @@ summary: > (mapping lowalch != null, members=true, live GE prices 2026-08-27). At current prices the method burns a small amount of GP per cast for 31 XP — run it for XP, not GP. Least-loss liquid anchor at last check: rune - javelin tips (-3/cast). Model pins (owner guidance 2026-08-28): one + javelin tips (live-low net -3/cast; realized loss band -1,758 to -29,463 + gp/hr across the declared live-low and max-allowed +5% fill scenarios — + XP is fill-independent at 23,000/hr session-realized midpoint). Model + pins (owner guidance 2026-08-28): one iteration = one cast; breaks every 400-700 COMPLETED CASTS (cast-count based, independent of bank visits — midpoint example: 550, 1,100, 1,650, ...); restock when candidate or nature drops below the numeric @@ -68,8 +71,8 @@ economy: cost_per_cycle: 543 # javelin tips buy 403 + nature rune 140 (staff of fire, live low 2026-08-27) income_per_cycle: 540 # lowalch of rune javelin tips (40% of value 1350) net_per_cycle: -3 - gp_per_hour_low: -2930 # session-realized fast bound: 977.2 casts/hr x -3 = -2,932 (3.4 s cycle, 300 s startup, 2 h horizon) - gp_per_hour_high: -1760 # session-realized slow bound: 585.9 casts/hr x -3 = -1,758 (5.2 s cycle, 600 s startup, 2 h horizon) + gp_per_hour_low: -29463 # WORST-CASE realized: max-allowed fill scenario B (-30.15/cast) x session-fast 977.2 casts/hr = -29,462.6 (3.4 s cycle, 300 s startup, 2 h horizon) — see FILL-PRICE SCENARIOS below + gp_per_hour_high: -1758 # BEST-CASE realized: live-low fill scenario A (-3.00/cast) x session-slow 585.9 casts/hr = -1,757.7 (5.2 s cycle, 600 s startup, 2 h horizon) — see FILL-PRICE SCENARIOS below xp_per_hour: 23000 # session-realized midpoint: 741.8 casts/hr x 31 = 22,995 (pin 23,000) # ---- rate model (pinned; owner guidance 2026-08-28) ---- # LOOP (idealised, zero overhead — ceiling reference ONLY, never the @@ -87,14 +90,30 @@ economy: # SESSION-REALIZED (2 h horizon = 7,200 s; one-time startup 300-600 s, # mid 450 s -> 450/7200 = 6.25 %): realized = steady-state x (1 - # startup/session): fast 977.2, mid 741.8, slow 585.9 casts/hr. - # xp mid = 741.8 x 31 = 22,995 -> pin 23,000; gp = casts/hr x -3: - # low -2,932 (fast), high -1,758 (slow), mid -2,225. + # xp mid = 741.8 x 31 = 22,995 -> pin 23,000. REALIZED GP/hr derives from + # the FILL-PRICE SCENARIOS below (the SAME ledger as the batch economics): + # worst = -29,463 = scenario B x session-fast 977.2 (-30.15 x 977.2 = -29,462.6) + # best = -1,758 = scenario A x session-slow 585.9 (-3.00 x 585.9 = -1,757.7) + # midpoints: scenario A x 741.8 = -2,225; scenario B x 741.8 = -22,365. # BATCH ECONOMICS (901 casts; deposits 901 x 540 = 486,540 gp per batch): # input cost at live low: 901 x 543 = 489,243 -> net -2,703/batch # input cost at MAX fill (live low + 5 %): 901 x 570.15 = 513,705 -> net # -27,165/batch (worst case). Reserve = 6 worst-case batches (Magic # 21 -> 55 = ~5,300 casts = ~6 batches) = 162,990 gp; the capital stop # checks TOTAL ASSETS, not raw cash (see accounts + stop_conditions). + # FILL-PRICE SCENARIOS (one ledger for capital, batch economics, AND the + # realized GP bounds — every number below comes from the same scenario): + # A live-low fill: candidate 403 + nature 140 = 543/cast vs + # 540 lowalch -> net -3.00/cast (the anchor) + # B max allowed fill (+5%): candidate 403x1.05 = 423.15 + nature + # 140x1.05 = 147 -> 570.15/cast vs 540 lowalch + # -> net -30.15/cast (the executable buy policy + # permits live low + 5%; this scenario, not the + # -3 live-low margin, bounds the downside) + # gp_per_hour_low = -29,463 | gp_per_hour_high = -1,758 derive from these + # scenarios at the session rates above; the batch ledger uses the same + # 543 / 570.15 per-cast costs (901-cast batch: -2,703 live-low, -27,165 + # max fill), so capital accounting and GP bounds can never diverge. supplies: - item: rune javelin tips qty_per_cycle: 1 @@ -363,7 +382,7 @@ run_control: - "magic level >= target (55 -> hand off to HLA)" - "session elapsed >= 2 h (session horizon — startup is amortised over this window in the rate model)" - "total assets (cash + supplies at current buy cost) < 575,150 — cannot fund the next batch at max fill (570,150) plus staff replacement (5,000); assets, NOT raw cash: cash that became supplies is not a loss" - - "session net loss (total assets - initial capital) > 162,990 — the 6-batch worst-case loss reserve is exhausted" + - "session net loss: initial capital - total assets > 162,990 — a loss makes 'initial capital - total assets' POSITIVE, so this strict-greater comparison fires on losses and never on profits; 162,990 = the 6-batch worst-case reserve (exactly AT the boundary it does NOT fire — strict >)" - "3 consecutive failed casts (client issue — do not loop)" - "3 consecutive no-fill GE rounds (initial acquisition or restock) with no fallback candidate available" - "staff of fire equip failed after bounded retries — FAIL CLOSED (log ERROR, alert, stop)" @@ -406,8 +425,9 @@ run_control: # 1,802 (901-cadence, 1,802 = 901 x 2). The sequences 550/1,100/1,650/2,200 # and 901/1,802/2,703 interleave independently. # Step 6 — continue until a stop_condition fires: Magic >= 55 (HLA handoff), - # the 2 h session horizon, the capital stops (assets floor 575,150 or net - # loss > 162,990), 3 failed casts, or 3 consecutive no-fill GE rounds. + # the 2 h session horizon, the capital stops (assets floor 575,150, or + # loss = initial capital - total assets > 162,990), 3 failed casts, or 3 + # consecutive no-fill GE rounds. # ============================================================================================ risk: @@ -436,7 +456,11 @@ risk: # (a8); 3 consecutive failed casts -> failed (stop, log ERROR sentinel, alert). # - Capital: the stop checks TOTAL ASSETS (cash + supplies at current buy cost), and # both capital stops are assets-based (see stop_conditions) — cash converted into -# supplies is not a loss and never fires a stop by itself. +# supplies is not a loss and never fires a stop by itself. Loss direction is pinned +# everywhere as 'initial capital - total assets > 162,990': a loss makes that +# difference positive, so the stop fires on losses and never on profits (the +# reversed sign form can never exceed the threshold on a loss and is rejected by +# the validator regression; boundary fixtures: below/at/above + profit side). # - Explicit completion/stop: stop_conditions above (Magic 55 HLA handoff, 2 h session # horizon, assets floor 575,150, loss reserve 162,990, repeated failures, fail-closed # staff equip). diff --git a/tools/validate.py b/tools/validate.py index 0068a6f..d5b74e3 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -72,6 +72,77 @@ def alch_worked_example_regression(rel, data, raw_text): return errs +def _loss_reserve_stop(initial_capital, total_assets, reserve): + """Loss-reserve stop comparator in the pinned direction (owner guidance + 2026-08-29, governor review 10361, PR #8): stop fires only when the + loss EXCEEDS the reserve, i.e. when (initial capital - total assets) + > reserve. A loss makes the LHS positive; a profit (total assets > + initial capital) makes it negative and can never fire this stop. The + reversed 'total assets - initial capital' form is invalid because it + is negative exactly when a loss exists.""" + return (initial_capital - total_assets) > reserve + + +def alch_accounting_regression(rel, data, raw_text): + """Loss-reserve + fill-price accounting regression (owner guidance + 2026-08-29 for governor review 10361, PR #8): the low-alch-members + entry must express the loss-reserve stop in the correct direction + EVERYWHERE ('initial capital - total assets > 162,990'), the reversed + sign form must never reappear, and the realized GP/hour bounds must be + derived from the declared fill-price scenarios (A live-low -3.00/cast + vs B max allowed +5% fill -30.15/cast), exposing the +5% fill loss + instead of recycling the live-low margin. Includes below/at/above + boundary fixtures and a profit-side negative regression for the + comparator itself. Scoped to the members low-alchemy entry only.""" + if data.get("id") != "low-alch-members": + return [] + errs = [] + + # 1. comparator boundary fixtures (pure function, entry's own constants) + INIT, RESERVE = 1_308_290, 162_990 # entry capital / 6-batch reserve + fixtures = [ + ("below (loss 162,989)", INIT - 162_989, False), + ("at boundary (loss 162,990, strict >)", INIT - 162_990, False), + ("above (loss 162,991)", INIT - 162_991, True), + ("profit side (assets 50,000 above initial)", INIT + 50_000, False), + ("breakeven (assets == initial)", INIT, False), + ] + for label, total_assets, expect in fixtures: + got = _loss_reserve_stop(INIT, total_assets, RESERVE) + if got != expect: + errs.append(f"{rel}: loss-reserve comparator fixture '{label}' " + f"returned {got}, expected {expect}") + + # 2. entry direction: correct form present, reversed form absent + if "initial capital - total assets > 162,990" not in raw_text: + errs.append(f"{rel}: loss-reserve stop must be expressed as " + f"'initial capital - total assets > 162,990' everywhere") + if "total assets - initial capital" in raw_text: + errs.append(f"{rel}: loss-reserve stop uses the reversed sign form — " + f"on a loss it is negative and can never exceed the " + f"reserve; use 'initial capital - total assets > 162,990'") + + # 3. realized GP bounds must come from the fill-price scenarios + eco = data.get("economy", {}) + if eco.get("gp_per_hour_low") != -29463: + errs.append(f"{rel}: gp_per_hour_low must be -29,463 (max-fill " + f"scenario B -30.15/cast x session-fast 977.2 casts/hr " + f"= -29,462.6), got {eco.get('gp_per_hour_low')}") + if eco.get("gp_per_hour_high") != -1758: + errs.append(f"{rel}: gp_per_hour_high must be -1,758 (live-low " + f"scenario A -3.00/cast x session-slow 585.9 casts/hr " + f"= -1,757.7), got {eco.get('gp_per_hour_high')}") + for pin, label in (("-30.15", "max-fill per-cast loss -30.15"), + ("570.15", "max-fill per-cast cost 570.15"), + ("423.15", "max-fill candidate price 423.15")): + if pin not in raw_text: + errs.append(f"{rel}: economy FILL-PRICE SCENARIOS missing {label} " + f"({pin}) — the +5% accepted-fill scenario must be " + f"declared so the realized GP bounds cannot silently " + f"revert to the live-low-only margin") + return errs + + def main(): global STALE_DAYS if "--stale-days" in sys.argv: @@ -134,6 +205,10 @@ def main(): # monotonic break/restock counters, scoped to the one-cast model errors += alch_worked_example_regression(rel, data, path.read_text()) + # loss-reserve direction + fill-price GP bound regression (owner + # guidance 2026-08-29 for governor 10361), scoped to low-alch-members + errors += alch_accounting_regression(rel, data, path.read_text()) + # action id/step uniqueness a_ids = [a["id"] for a in data["actions"]] a_steps = [a["step"] for a in data["actions"]]