fix(validate): scope GE floor-tax gate to margin tags, add regression selftest
Governor-review bounce (PR #7, attempt 2) fix on issue-1: - The floor-tax rule previously triggered on any entry carrying the generic 'ge' tag (tags & {flipping, merchanting, ge}). A training, processing, or procurement entry may legitimately carry 'ge' while its income/cost model is unrelated to flip tax, so it could fail the global gate unless its economics accidentally satisfied the flip equation. - Rule now gates ONLY the unambiguous margin tags (flipping and/or merchanting); the bare 'ge' tag no longer triggers it. - Rule extracted into ge_floor_tax_error() and a --selftest mode added with three regression fixtures, run on every gate invocation: * fractional-tax flipping entry -> must FAIL * non-margin entry with only 'ge' + 'processing' (economics deliberately violate the flip equation) -> must PASS * correctly-floored flipping entry -> must PASS - Demo (old scope): ge+processing fixture with net 2000 vs floored 1900 errored under the old tag set; low-alch-f2p's -112 vs -115 model would also false-positive if it carried 'ge'. Tests: python3 tools/validate.py -> 2 entries, 0 errors, 1 pre-existing warning (researched flag on low-alch-f2p); --selftest -> 3 PASS, 0 errors; git diff --check clean.
This commit is contained in:
parent
864f2c9e1c
commit
f6675ba543
@ -11,15 +11,23 @@ Runs:
|
||||
=> WARNING (entry should be re-verified, not dropped)
|
||||
- `best_practice` lines whose source starts with "researched"
|
||||
=> WARNING (citation TODO — file a follow-up issue)
|
||||
- GE-margin entries (tags include flipping/merchanting): the 2% GE
|
||||
convenience fee is rounded DOWN to whole coins per item, so
|
||||
net_per_cycle MUST equal income - floor(0.02*income) - cost.
|
||||
Fractional-tax arithmetic (0.98*sell - buy) is an error.
|
||||
- GE-margin entries (tags include flipping and/or merchanting —
|
||||
the unambiguous margin signals): the 2% GE convenience fee is
|
||||
rounded DOWN to whole coins per item, so net_per_cycle MUST
|
||||
equal income - floor(0.02*income) - cost. Fractional-tax
|
||||
arithmetic (0.98*sell - buy) is an error.
|
||||
NOTE: the generic `ge` tag alone does NOT gate this rule — a
|
||||
training/processing/procurement entry may legitimately carry
|
||||
`ge` while its income/cost model has nothing to do with flip tax.
|
||||
- Regression fixtures (--selftest): a fractional-tax flipping
|
||||
entry must FAIL; a non-margin entry carrying only `ge` plus a
|
||||
non-flip tag must PASS.
|
||||
3. Global id uniqueness across the whole codex tree.
|
||||
|
||||
Exit codes: 0 = clean (warnings ok), 1 = validation errors.
|
||||
|
||||
Usage: python3 tools/validate.py [--stale-days 30]
|
||||
Usage: python3 tools/validate.py [--stale-days 30] [--selftest]
|
||||
--selftest: run the GE floor-tax regression fixtures only (no tree scan)
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
@ -46,12 +54,82 @@ def load_entries():
|
||||
return sorted(out)
|
||||
|
||||
|
||||
MARGIN_TAGS = {"flipping", "merchanting"}
|
||||
|
||||
|
||||
def ge_floor_tax_error(rel, data):
|
||||
"""GE floor-tax gate for a single entry.
|
||||
|
||||
Applies ONLY to the unambiguous margin tags (flipping/merchanting).
|
||||
The generic `ge` tag is not proof that income_per_cycle is a taxed
|
||||
GE resale: a training/processing/procurement entry may carry `ge`
|
||||
while modeling unrelated income/cost, so it must not be gated by
|
||||
this rule. Returns an error message, or None when the entry is not
|
||||
gated or its floored-tax arithmetic is correct.
|
||||
|
||||
Wiki: "The 2% tax rounds down to the nearest whole number, so items
|
||||
sold for below 50 coins have a tax of less than one coin" — per-item
|
||||
net MUST use the floored tax, never 0.98*sell - buy.
|
||||
"""
|
||||
tags = {str(t).lower() for t in data.get("tags", [])}
|
||||
if not tags & MARGIN_TAGS:
|
||||
return None
|
||||
eco = data.get("economy") or {}
|
||||
if not all(k in eco for k in ("income_per_cycle", "cost_per_cycle", "net_per_cycle")):
|
||||
return None
|
||||
sell, buy = eco["income_per_cycle"], eco["cost_per_cycle"]
|
||||
tax = math.floor(sell * 0.02)
|
||||
expected = sell - tax - buy
|
||||
if eco["net_per_cycle"] != expected:
|
||||
return (
|
||||
f"{rel}: net_per_cycle {eco['net_per_cycle']} != floored-tax model "
|
||||
f"{expected} (sell {sell} - floor({sell}*0.02)={tax} - buy {buy}); "
|
||||
f"GE tax rounds down per item — fractional 0.98*sell - buy is invalid")
|
||||
return None
|
||||
|
||||
|
||||
def run_selftest(verbose=False):
|
||||
"""Regression fixtures for the GE floor-tax gate; returns error strings."""
|
||||
fixtures = [
|
||||
# name, tags, economy, expect_error
|
||||
("fractional-tax flipping entry", ["flipping"],
|
||||
{"income_per_cycle": 244, "cost_per_cycle": 223, "net_per_cycle": 16.12}, True),
|
||||
# 5000 - floor(5000*0.02) - 3000 = 1900 != 2000: this fixture's
|
||||
# economics deliberately do NOT satisfy the flip equation, so the
|
||||
# old `ge`-tag scoping would false-positive on it (2000 != 1900).
|
||||
("non-margin entry with only 'ge' + 'processing'", ["ge", "processing"],
|
||||
{"income_per_cycle": 5000, "cost_per_cycle": 3000, "net_per_cycle": 2000}, False),
|
||||
("correctly-floored flipping entry", ["flipping", "merchanting"],
|
||||
{"income_per_cycle": 244, "cost_per_cycle": 223, "net_per_cycle": 17}, False),
|
||||
]
|
||||
errors = []
|
||||
for name, tags, eco, expect_error in fixtures:
|
||||
rel = f"selftest/{name}"
|
||||
got_error = ge_floor_tax_error(rel, {"tags": tags, "economy": eco}) is not None
|
||||
passed = got_error == expect_error
|
||||
if verbose:
|
||||
print(f"SELFTEST {'PASS' if passed else 'FAIL'} {name}")
|
||||
if not passed:
|
||||
errors.append(
|
||||
f"selftest: '{name}' expected "
|
||||
f"{'an error' if expect_error else 'no error'} but got "
|
||||
f"{'an error' if got_error else 'no error'}")
|
||||
return errors
|
||||
|
||||
|
||||
def main():
|
||||
global STALE_DAYS
|
||||
if "--stale-days" in sys.argv:
|
||||
STALE_DAYS = int(sys.argv[sys.argv.index("--stale-days") + 1])
|
||||
|
||||
errors, warnings = [], []
|
||||
if "--selftest" in sys.argv:
|
||||
errors += run_selftest(verbose=True)
|
||||
print(f"RESULT: selftest, {len(errors)} errors, 0 warnings")
|
||||
return 1 if errors else 0
|
||||
|
||||
# The fix itself is gated: regression fixtures run on every invocation.
|
||||
errors += run_selftest(verbose=False)
|
||||
entries = load_entries()
|
||||
if not entries:
|
||||
print("ERROR: no codex entries found under codex/")
|
||||
@ -112,24 +190,13 @@ def main():
|
||||
if len(a_steps) != len(set(a_steps)):
|
||||
errors.append(f"{rel}: duplicate action step names")
|
||||
|
||||
# GE-margin floor-tax arithmetic. The wiki: "The 2% tax rounds down
|
||||
# to the nearest whole number, so items sold for below 50 coins have
|
||||
# a tax of less than one coin" — per-item net MUST use the floored
|
||||
# tax, never 0.98*sell - buy. Scoped to GE-margin tags so alchemy
|
||||
# (income is alch value, not a GE sale) is unaffected.
|
||||
tags = {str(t).lower() for t in data.get("tags", [])}
|
||||
eco = data.get("economy") or {}
|
||||
if tags & {"flipping", "merchanting", "ge"} and all(
|
||||
k in eco for k in ("income_per_cycle", "cost_per_cycle", "net_per_cycle")
|
||||
):
|
||||
sell, buy = eco["income_per_cycle"], eco["cost_per_cycle"]
|
||||
tax = math.floor(sell * 0.02)
|
||||
expected = sell - tax - buy
|
||||
if eco["net_per_cycle"] != expected:
|
||||
errors.append(
|
||||
f"{rel}: net_per_cycle {eco['net_per_cycle']} != floored-tax model "
|
||||
f"{expected} (sell {sell} - floor({sell}*0.02)={tax} - buy {buy}); "
|
||||
f"GE tax rounds down per item — fractional 0.98*sell - buy is invalid")
|
||||
# GE-margin floor-tax arithmetic (see ge_floor_tax_error): scoped to
|
||||
# the unambiguous margin tags flipping/merchanting. The generic `ge`
|
||||
# tag alone must not gate this rule — alchemy/processing/procurement
|
||||
# entries can carry `ge` with income unrelated to a taxed GE resale.
|
||||
err = ge_floor_tax_error(rel, data)
|
||||
if err:
|
||||
errors.append(err)
|
||||
|
||||
# 3. staleness / research TODOs
|
||||
if data.get("status") == "verified":
|
||||
|
||||
Loading…
Reference in New Issue
Block a user