Initial PuGa toolkit: data layer, econ, depth-aware scan, state sync, plan push

- puga/: cached FIO + PRUNplanner clients, market view with order-book walk,
  econ formulas ported from PRUNplanner (tested against its suite and live FIO),
  saturation model v1 (reviewed by Opus)
- tools/: scan (depth-aware), price, book, chain, state sync, plan_push
  (dry run default, [PuGa]-prefixed plans only), legacy prun_scan/prun_cxarb
- docs/: mechanics (PRUNplanner is source of truth), roadmap, decisions,
  saturation design, archived handoff
- secrets stay in .env (gitignored); ref/ holds PRUNplanner source (ignored)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 23:00:21 +02:00
co-authored by Claude Sonnet 5
commit 7a538cb300
35 changed files with 1692 additions and 0 deletions
Executable
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Make-vs-buy cost tree for a material at one exchange, plus sourcing depth for every raw input.
Cost to make = inputs at min(buy, make) per output unit; wages/capex excluded unless --wages.
tools/chain.py DEC # AI1
tools/chain.py BHP --depth 3 --qty 13
"""
import argparse, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from puga import config, econ, fio, market, saturation as sat
def main():
ap = argparse.ArgumentParser()
ap.add_argument("ticker")
ap.add_argument("--cx", default=config.DEFAULT_CX)
ap.add_argument("--depth", type=int, default=2, help="levels of make-vs-buy expansion")
ap.add_argument("--qty", type=float, default=1.0, help="units/day you would make (sourcing check)")
ap.add_argument("--wages", action="store_true", help="add wage cost per output unit (both luxuries at ask)")
ap.add_argument("--cogc")
a = ap.parse_args()
snap = market.snapshot()
Q = lambda t: snap.get((t, a.cx))
blds = {b["Ticker"]: b for b in fio.buildings()}
by_out = {}
for r in fio.recipes():
for o in r["Outputs"]:
by_out.setdefault(o["Ticker"], []).append(r)
def wage_per_day(b):
tot = 0.0
for t in econ.TIERS:
n = b[t.capitalize() + "s"]
if n:
tot += n * sum(need / 100 * (Q(tk).ask if Q(tk) and Q(tk).ask else 0) for tk, need, _, _ in econ.CONSUMPTION[t])
return tot
def buy(t):
q = Q(t)
return q.ask if q and q.ask else None
memo = {}
def make_cost(t, depth, stack=()):
"""(best unit cost, how). Compares buy vs best recipe."""
key = (t, depth)
if key in memo:
return memo[key]
b = buy(t)
best = (b if b is not None else float("inf"), "buy" if b is not None else "n/a", None)
if depth > 0 and t not in stack:
for r in by_out.get(t, []):
bld = blds.get(r["BuildingTicker"])
if not bld:
continue
cost = 0.0
for i in r["Inputs"]:
c, _, _ = make_cost(i["Ticker"], depth - 1, stack + (t,))
cost += i["Amount"] * c
outs = sum(o["Amount"] for o in r["Outputs"] if o["Ticker"] == t)
if outs == 0:
continue
if a.wages:
eff, _ = econ.building_efficiency({x.lower() + "s": bld[x + "s"] for x in ("Pioneer", "Settler", "Technician", "Engineer", "Scientist")},
{x: 1.0 for x in econ.TIERS}, expertise=bld["Expertise"] or None, cogc=a.cogc)
cost += wage_per_day(bld) * (r["TimeMs"] / 86400e3) / eff
unit = cost / outs
if unit < best[0]:
best = (unit, f"make {r['BuildingTicker']} {r['TimeMs']/3.6e6:.1f}h", r)
memo[key] = best
return best
def show(t, depth, qty, indent=0):
c, how, r = make_cost(t, depth)
q = Q(t)
pad = " " * indent
mk = f"ask {q.ask:.0f} bid {q.bid:.0f} sup {q.supply:.0f} dem {q.demand:.0f} flow {sat.tref(q.traded7, q.traded30):.1f}/d" if q and q.ask and q.bid else "no market"
print(f"{pad}{t:5} x{qty:<8.1f} best {c:9.0f}/u via {how:18} | {mk}")
if r and how.startswith("make") and depth > 0:
outs = sum(o["Amount"] for o in r["Outputs"] if o["Ticker"] == t)
for i in r["Inputs"]:
show(i["Ticker"], depth - 1, qty * i["Amount"] / outs, indent + 1)
show(a.ticker.upper(), a.depth, a.qty)
print("\n# sourcing check for buy-leaves at", a.qty, "output units/day: ask-walk price for one day of inputs")
q0 = Q(a.ticker.upper())
_, _, r = make_cost(a.ticker.upper(), a.depth)
if r:
outs = sum(o["Amount"] for o in r["Outputs"] if o["Ticker"] == a.ticker.upper())
for i in r["Inputs"]:
need = a.qty * i["Amount"] / outs
w = market.walk(i["Ticker"], a.cx, need, "buy")
x = Q(i["Ticker"])
days = x.supply / need if x and need else 0
print(f" {i['Ticker']:5} need {need:8.1f}/d ask {x.ask if x else None} walked avg {w['avg'] and round(w['avg'])} short={w['short']} standing supply = {days:.0f} days of your use, market flow {sat.tref(x.traded7, x.traded30):.1f}/d")
if __name__ == "__main__":
main()