From 986fc1a6a89233f537a93eec4d6584d726fcdbd6 Mon Sep 17 00:00:00 2001 From: Vale Date: Thu, 27 Aug 2026 02:59:55 -0500 Subject: [PATCH] Codex skeleton: schema contract, action grammar, data-sources rules, JSON schema, validator, golden example (low-alch-f2p, wiki+price verified), README --- README.md | 69 +++++++++- codex/economy/low-alch-f2p.yaml | 155 ++++++++++++++++++++++ docs/action-grammar.md | 140 ++++++++++++++++++++ docs/data-sources.md | 64 +++++++++ docs/schema.md | 224 ++++++++++++++++++++++++++++++++ schemas/entry.schema.json | 220 +++++++++++++++++++++++++++++++ tools/validate.py | 138 ++++++++++++++++++++ 7 files changed, 1008 insertions(+), 2 deletions(-) create mode 100644 codex/economy/low-alch-f2p.yaml create mode 100644 docs/action-grammar.md create mode 100644 docs/data-sources.md create mode 100644 docs/schema.md create mode 100644 schemas/entry.schema.json create mode 100644 tools/validate.py diff --git a/README.md b/README.md index 08829e1..cf343cc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,68 @@ -# osrs-codex +# OSRS Codex -Client-agnostic source of truth for everything botable in OSRS: activities, ordering, actions, requirements. AI agents translate entries into client APIs. \ No newline at end of file +**One source of truth for everything that can be botted in OSRS.** + +Every bot-able activity — alching, mining, slayer, bosses, trading — is +described here **once**, in a strict client-agnostic YAML structure: +requirements, execution order (state machine), the exact action sequence, +economics, best-in-class setup, and anti-ban notes. + +AI agents building scripts do **not** re-learn the game. They read a codex +entry and *translate* it into the target client API +(DreamBot / Microbot / Storm / RuneLite plugins / …). The game knowledge +lives in this repo; the client glue lives in whatever script is being +written. + +``` +client-agnostic client-specific (NOT in this repo) ++----------------+ +-------------------+ +| codex entry | translate | DreamBot Java | +| (YAML, strict) | ---------> | Microbot JS | +| actions: verbs | | RuneLite plugin | ++----------------+ +-------------------+ +``` + +## Layout + +``` +codex//.yaml # one entry per activity + economy/ trading, alching, flipping + training/ skill grinds (magic, mining, woodcutting, ...) + combat/ bosses + combat-slayers/ + quests/ quest entries (referenced by requires_quest) + transport/ teleport/teleport-reach recipes (shared by many entries) + meta/ account rotation, world hopping, break scheduling +docs/schema.md # the entry contract (read this first) +docs/action-grammar.md # the universal verb vocabulary +docs/data-sources.md # where every fact must come from + how to verify +schemas/entry.schema.json +tools/validate.py # the gate: python3 tools/validate.py (must exit 0) +``` + +## The rules (short version) + +1. **One entry = one activity.** State machine + action list, both mandatory. +2. **Every fact carries a source.** Wiki-cited or `researched ` + (a TODO: follow-up issue must land the proper citation). Live prices + are re-verified against `prices.runescape.wiki` on every bump. +3. **Best-in-class setup is mandatory** (`best_practice`). Top-operator + execution: staff of fire for alchemy, noted items, shortest routes. + Translators adopt it by default; deviating requires `improvised: true`. +4. **Anti-ban is first-class.** Every action declares delay bounds and + movement style; entries carry `risk.ban_level` + notes. +5. **The gate must pass** before a PR merges: `python3 tools/validate.py`. +6. **Never mix in client knowledge.** No DreamBot/Microbot/Storm API names + in entries. Translations live in script repos, referencing the entry id. + +## Entry lifecycle + +`draft` → `verified` (all facts sourced, prices re-checked, validator +clean) → `stale` (facts > 30d old or prices moved materially). Stale +entries are skipped by translators unless explicitly allowed. + +## Contributing + +This repo is **issue-driven**: all content is specified in Gitea issues +(acceptance criteria + source URLs + expected output path), implemented in +branches, and gated by `tools/validate.py` in CI. No direct pushes. diff --git a/codex/economy/low-alch-f2p.yaml b/codex/economy/low-alch-f2p.yaml new file mode 100644 index 0000000..44cbb9a --- /dev/null +++ b/codex/economy/low-alch-f2p.yaml @@ -0,0 +1,155 @@ +id: low-alch-f2p +title: Low-Level Alchemy (F2P) +domain: economy +tags: [alching, magic-training, f2p, loss-training] +status: verified +version: 1 +updated: "2026-08-27" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date + +summary: > + Magic training from level 21 by low-alching F2P items with the highest + lowalch-value among affordable, high-limit items. At current prices it is + a LOSS activity (money in exchange for Magic XP); run it only for XP. + +accounts: + membership: f2p + min_level: 21 # Magic 21 to cast Low Level Alchemy (wiki-verified) + requires_quest: [] + requires_other: + - "Staff of Fire equipped (removes 3 fire runes from the cost)" + - "A bank with GE access within reach" + +best_practice: + equipment: + - item: staff of fire + why: "unlimited fire runes — LA rune cost drops from 3 fire + 1 nature to 1 nature per cast (~15gp/cycle saved)" + source: "OSRS wiki — Staff of Fire; Low Level Alchemy (Cost)" + techniques: + - name: cast on noted (stacked) items + why: "one inventory slot, minimal mouse travel between spell icon and item" + source: "OSRS wiki — Low Level Alchemy (Usage: casting alchemy spells on noted items)" + - name: bank at a GE-adjacent bank + why: "supply restock without a second travel leg" + source: "researched 2026-08-27 (standard operator practice; wiki LA Cost section corroborates rune economy)" + +economy: + # Verified 2026-08-27 against prices.runescape.wiki (live 'latest') + # + OSRS wiki Low_Level_Alchemy infobox. + item: Gold necklace # example anchor; rank by cheapest candidate live + item_id: 1654 + cost_per_cycle: 292 # necklace buy 148 + nature rune 144 (staff of fire) + income_per_cycle: 180 # lowalch of gold necklace (40% of value 450) + net_per_cycle: -112 + gp_per_hour_low: -250000 # negative: this burns GP + gp_per_hour_high: -200000 + xp_per_hour: 55800 # 31 xp/cast (wiki) @ ~1800 casts/hr (3-tick, wiki) + supplies: + - item: gold necklace + qty_per_cycle: 1 + source: ge + ge_limit: 18000 # mapping 'limit', wiki-verified + - item: nature rune + qty_per_cycle: 1 + source: ge + ge_limit: 12500 + profit_drivers: + - "NONE at current prices — XP is the product, not GP" + - "Pick the candidate with the least-negative net each run (live rank)" + +state_machine: + initial: idle + terminal: [complete, failed] + states: + idle: + goal: reach the bank + have supplies in inventory + next: [casting, bank_supplies] + casting: + goal: cast Low Level Alchemy on the current candidate item + next: [bank_supplies, idle, complete, failed] + bank_supplies: + goal: bank the produced coins, withdraw fresh candidate + runes + next: [casting, idle] + complete: + goal: reached the run's XP/level target + failed: + goal: hard failure — stop, log ERROR sentinel, alert + transitions: + - from: casting + when: "candidate item count < buffer OR nature rune count < buffer" + to: bank_supplies + - from: idle + when: "magic level >= target (default 54, hand off to HLA at 55)" + to: complete + +actions: + - id: a1 + step: reach-bank + verb: move_to + target: bank + params: + location: "nearest bank with GE" + method: [teleport, walk] + on_fail: retry + anti_ban: + delay: [400, 1600] + + - id: a2 + step: buy-supplies + verb: ge_buy + target: grand-exchange + params: + item: "current candidate item" + qty: "fill to buffer" + max_price: "live low + 5%" + on_fail: retry + anti_ban: + delay: [1200, 3000] + + - id: a3 + step: cast-alch + verb: use_item_on + target: "candidate item in inventory" + params: + spell: "Low Level Alchemy" + item: "cheapest live candidate in inventory" + on: "itself" + on_fail: retry + anti_ban: + delay: [1600, 3400] # 3-tick cast = ~5.4s game; stay under it, humanised + + - id: a4 + step: bank-loop + verb: bank_inventory + target: bank + params: + deposit: [all coins, all products] + withdraw: "candidate item + nature runes up to buffer" + on_fail: alert + +run_control: + iteration: "one cast" + max_iterations: null + break: + every: [8, 14] # bank cycles (breaks land on bank visits) + for: [45, 120] # seconds of idle AFK + stop_conditions: + - "magic level >= target" + - "gp under floor" + - "3 consecutive failed casts (client issue — do not loop)" + +risk: + ban_level: low + notes: + - "High cast cadence is the main detection vector; keep anti_ban delays." + - "GE buy bursts: pace offers, respect the 4h limit window." + +sources: + - "OSRS wiki — Low Level Alchemy (level 21, 31 xp/cast, 3-tick, F2P; infobox 2026-08-27)" + - "prices.runescape.wiki mapping (id 1654 gold necklace: members=false, limit 18000, lowalch 180, highalch 270, value 450; id 561 nature rune; id 554 fire rune)" + - "prices.runescape.wiki latest 2026-08-27 (necklace buy 148, nature buy 144, fire buy 5)" +verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing) + +related: + - high-alch-p2p + - f2p-alch-catalog + - mule-topup diff --git a/docs/action-grammar.md b/docs/action-grammar.md new file mode 100644 index 0000000..234e520 --- /dev/null +++ b/docs/action-grammar.md @@ -0,0 +1,140 @@ +# OSRS Codex — Universal Action Grammar + +The action grammar is the **bridge** between codex entries (client-agnostic) +and real scripts (client-specific). An entry never names a client API; it +uses a **verb** from this file. Each agent maintains the mapping table for +its own client. + +A `verb` is one primitive, atomic capability. Compound behaviour is built by +chaining verbs in the entry's `actions[]` list and wiring the state machine. + +Legend for the mapping table: +- `DB` = DreamBot (tblkit / OSBot-style Java) +- `MB` = Microbot +- `RL` = RuneLite plugin +- `—` = not expressible as a single primitive; entry should decompose. + +> Columns are filled by each client owner as their client support lands. +> `—` is a legal value: it means "decompose into smaller verbs." + +--- + +## A. World / presence + +| verb | meaning | DB | MB | RL | +|-----------------|------------------------------------------------|--------------|-------------|----| +| `login` | authenticate the account | — | — | — | +| `select_world` | join a specific world by id/type | — | — | — | +| `hop_world` | switch worlds (F2P or P2P hop) | — | — | — | +| `is_logged_in` | predicate: authenticated & in-game | — | — | — | + +## B. Movement + +| verb | meaning | DB | MB | RL | +|-----------------|--------------------------------------------------|----|----|----| +| `move_to` | get to a named location (teleport-or-walk) | — | — | — | +| `walk_to` | path-walk to a tile | — | — | — | +| `teleport` | use a teleport (spell/item/scroll/portal) | — | — | — | +| `enter_region` | assert we are inside a named area (predicate) | — | — | — | +| `open_door` | open/lock an interactable door | — | — | — | + +## C. Interaction (the core) + +| verb | meaning | DB | MB | RL | +|--------------------|----------------------------------------------------|----|----|----| +| `use_item_on` | use inventory item `X` on target `Y` (or itself) | — | — | — | +| `click_object` | left-click a world object (NPC/NPC tile/thing) | — | — | — | +| `right_click` | click a specific menu option on a target | — | — | — | +| `talk_to` | open an NPC dialogue and pick a line | — | — | — | +| `click_button` | click a UI widget by stable reference | — | — | — | +| `menu_option` | select from an in-game menu (F-keys, menus) | — | — | — | + +## D. Inventory / bank + +| verb | meaning | DB | MB | RL | +|--------------------|----------------------------------------------------|----|----|----| +| `bank_inventory` | open bank, deposit/withdraw per params, close | — | — | — | +| `deposit_all` | deposit every bankable item | — | — | — | +| `withdraw` | withdraw a named item up to a quantity | — | — | — | +| `has_item` | predicate: inventory holds item ≥ qty | — | — | — | +| `count_item` | read inventory quantity of an item | — | — | — | +| `move_to_inv` | move item from bank/ground into inventory | — | — | — | +| `sort_inventory` | reorder inventory into a known layout | — | — | — | + +## E. Exchange (Grand Exchange) + +| verb | meaning | DB | MB | RL | +|--------------------|----------------------------------------------------|----|----|----| +| `ge_open` | open GE at a bank | — | — | — | +| `ge_buy` | buy `qty` of `item` at offer price, paced | — | — | — | +| `ge_sell` | sell `qty` of `item`, confirm | — | — | — | +| `ge_offers_done` | predicate: no pending buy/sell offers | — | — | — | +| `ge_collect` | collect finished GE transactions | — | — | — | +| `ge_check_price` | read current buy/sell price of an item | — | — | — | + +## F. Combat + +| verb | meaning | DB | MB | RL | +|--------------------|----------------------------------------------------|----|----|----| +| `attack` | start/continue attacking a target | — | — | — | +| `prayer` | toggle a prayer on/off | — | — | — | +| `buff` | apply a buff item/potion | — | — | — | +| `eat` | heal when HP < threshold | — | — | — | +| `retreat` | disengage and reposition | — | — | — | +| `is_dead` | predicate: account is dead | — | — | — | +| `respawn` | return to a spawn/bank after death | — | — | — | + +## G. Observation (predicates) + +Predicates return a boolean and drive `state_machine.transitions`. They are +**free** (no anti-ban cost) and may be polled every tick. + +| verb | meaning | +|------------------|------------------------------------------| +| `is_at` | at a named location | +| `has_item` | inventory holds item ≥ qty | +| `item_count` | quantity of an item in inv/bank | +| `level_is` | a skill is at/above a level | +| `xp_is` | a skill's xp at/above a threshold | +| `hp_is` | HP below a threshold | +| `is_dead` | dead | +| `ge_clear` | no pending GE offers | +| `bank_open` | a bank is open | +| `quest_state` | a quest is started/completed | +| `npc_present` | a named NPC is in range/visible | + +## H. Housekeeping / safety + +| verb | meaning | DB | MB | RL | +|-----------------|--------------------------------------------------|----|----|----| +| `log` | write a sentinel log line (RESULT/DONE/ERROR) | — | — | — | +| `alert` | raise a humanised alert (operator inbox) | — | — | — | +| `sleep` | humanised pause `delay:[min,max]` | — | — | — | +| `human_move` | anti-ban mouse wander / cursor nudge | — | — | — | +| `take_break` | scheduled AFK pause (run_control.break) | — | — | — | + +--- + +## Parameter conventions + +`params` are key/value and client-agnostic. Recognised keys: + +- `location` — a **named** place. Must resolve to a real, named OSRS + location (never a bare coordinate in the entry; coordinates live in the + client mapping). +- `item` / `on` — canonical item names (exact wiki names). +- `qty`, `buffer` — counts. +- `method` — ordered preference list for how to achieve the verb + (e.g. `[teleport, walk]`); the client tries in order. +- `menu`, `option` — the exact right-click/option string. +- `price` / `max_price` — GP ceiling for a GE buy (anti-loss). +- `threshold` — a level/xp/hp cutoff for predicates and `eat`. + +## Anti-ban hints (optional, per action) + +- `delay: [min, max]` — randomised ms to sleep after the action. +- `mouse` — free-text humanisation directive (wander, jitter, bezier). +- `jitter` — randomise click offset in px. + +These are **hints**, not guarantees. The translating agent may strengthen +them, never weaken a codex-mandated value. diff --git a/docs/data-sources.md b/docs/data-sources.md new file mode 100644 index 0000000..885e071 --- /dev/null +++ b/docs/data-sources.md @@ -0,0 +1,64 @@ +# OSRS Codex — Data Sources & Verification Rules + +Every codex entry must carry `sources:` and `verified:` (date). Facts are +classified, and each class has a **required** verification path. An entry +whose facts fail verification stays `status: draft` and blocks merge. + +## Fact classes + +| class | examples | required source | +|-------|----------|-----------------| +| **item-ids** | item names, wiki ids | `prices.runescape.wiki/api/v1/osrs/mapping` | +| **alch values** | lowalch / highalch / value | same mapping endpoint; assert `lowalch == highalch*2//3` | +| **ge limits** | buy limit / 4h | mapping `limit` **and** wiki `Module:Exchange/` Lua field `limit` — both must agree | +| **membership** | F2P vs members | mapping `members` **and** item page `members = No/Yes` | +| **live prices** | buy/sell price | `prices.runescape.wiki/api/v1/osrs/latest` → `low` (buy); re-verify on every `status: verified` bump | +| **xp / mechanics** | XP per action, level reqs, cast times | OSRS wiki article infobox (oldschool.runescape.wiki/w/) | +| **locations / NPC dialogue** | bank names, menu option strings | OSRS wiki + in-game observation (flag `observed: date`) | +| **best-practice setup** | equipment/technique choices (staff of fire, noted items, best prayer) | OSRS wiki page, or `researched ` (follow-up citation issue) | + +## Authoritative endpoints (all require a descriptive User-Agent or 403) + +``` +mapping : https://prices.runescape.wiki/api/v1/osrs/mapping (JSON array) +latest : https://prices.runescape.wiki/api/v1/osrs/latest ({"data":{id:{low,high}}}) +exchange: https://oldschool.runescape.wiki/api.php?action=query&titles=Module:Exchange/&prop=revisions&rvprop=content&format=json +itempage: https://oldschool.runescape.wiki/api.php?action=parse&page=&prop=wikitext&format=json +``` + +## Verification pipeline (triple-source, then assert) + +1. Build `by_name` from mapping. Fetch `latest` once. +2. For each item fact: mapping → exchange module → item page. All must agree. +3. Assert `lowalch == highalch * 2 // 3` — catches copy/paste errors. +4. Record the **research date** in `verified:` and the endpoint used in `sources:`. + +## Known traps (learned the hard way — do not repeat) + +- **The `mapping` mirror is incomplete.** Observed 2026-08-27: 4,652 items, + dyes present but **no mantle/dye-variant entries at all**. An item missing + from mapping is NOT proof the item doesn't exist — cross-check the wiki + page before concluding anything. Never trust "not in mirror" as a fact. +- **`Module:Exchange` field is `limit`, not `buyLimit`.** Grepping the wrong + name yields false mismatches on every row. +- **Members flag vs intuition.** Items that "look F2P" can be members-only + (all bracelets, battlestaff, unstrung maple/yew/magic bows, unholy symbol, + orbs). The strung `Maple longbow` is F2P; `Maple longbow (u)` is members. + Always read the flag, never infer it. +- **Live prices move daily.** `latest` is a snapshot. Entries that embed + prices must carry `verified:` within the last 30 days to stay + `status: verified`; the validator warns (not fails) on staleness. +- **Alch economics are market-dependent.** Low alch is a *training* + activity at current prices (verified negative GP/cycle 2026-08-27). + Record the true economics; do not dress a loss as profit. + +## Research tooling + +- `osrs-wiki-item-data` skill (Vale) holds the working Python recipe, + the verified 20-item F2P low-alch catalog, and jar-verification notes. +- `ge-flip-scanner` skill: live margin/volume scanning (5m endpoint nulls + pitfall applies). +- OSRS wiki is reachable from this box via the Hermes web proxy + (`web_extract` / `web_search`), NOT direct DNS — direct + `services.runescape.wiki` resolves fail from WSL. `prices.runescape.wiki` + resolves fine directly. diff --git a/docs/schema.md b/docs/schema.md new file mode 100644 index 0000000..e3097ea --- /dev/null +++ b/docs/schema.md @@ -0,0 +1,224 @@ +# OSRS Codex — Entry Schema (v1) + +The codex is a **client-agnostic** knowledge base. Every bot-able OSRS +activity is described here once, in a strict YAML structure. AI agents do +**not** read the game — they read a codex entry and *translate* its +abstract actions into whatever client API they target (DreamBot, Microbot, +OSBot, RuneLite plugins, custom, ...). + +Two invariants make translation reliable: + +1. **One entry = one bot-able activity**, described end-to-end. +2. **Every action references the universal vocabulary** (`docs/action-grammar.md`). + No client-specific calls ever appear in an entry. + +--- + +## File layout + +``` +osrs-codex/ + docs/ + schema.md <- this file + action-grammar.md <- universal action vocabulary + data-sources.md <- where facts come from + verification rules + codex/ + economy/ <- money-making: flips, alching, drops, selling + training/ <- level training per skill (per band) + combat/ <- bossing, skilling-in-combat + combat-slayers/ <- slayer-specific + quests/ <- questable XP / requirement unlocks + transport/ <- movement, teleporting, banking logistics + meta/ <- account building, mule/logistics, world setup + schemas/ + entry.schema.json <- JSON-Schema for validation + tools/ + validate.py <- runs the schema over all codex/**.yaml + INDEX.md <- machine-generated catalog of every entry +``` + +An activity that spans domains (e.g. alchemy training *and* money) lives in +its **primary** domain; cross-refs go in `related`. + +--- + +## Entry structure (YAML) + +```yaml +id: alching-nature-runecoins # kebab-case, globally unique +title: Low-Level Alchemy (F2P) +domain: economy # one of the codex/ dirs +tags: [alching, money, f2p, p2p-safe] +status: verified # draft | verified | stale +version: 3 +updated: 2026-08-27 # last date facts were re-verified + +summary: > + One-sentence description of what the bot does and why it runs. + +# ---------------------------------------------------------------- accounts +accounts: + membership: f2p # f2p | members | hybrid (which items) + min_level: 1 # absolute floor to start + requires_quest: [] # quest ids (see codex/quests/) + requires_other: # free-form hard prerequisites + - "A bankable inventory + bank access" + +# ---------------------------------------------------------------- setup +best_practice: # REQUIRED — best-in-class execution + equipment: + - item: staff of fire + why: "unlimited fire runes; drops LA rune cost to 1 nature/cast" + source: "OSRS wiki — Staff of Fire; Low Level Alchemy (Cost)" + techniques: + - name: cast on noted (stacked) items + why: "one slot, minimal mouse travel" + source: "OSRS wiki — Low Level Alchemy (Usage)" + +# ---------------------------------------------------------------- economy +economy: + cost_per_cycle: 62 # GP consumed per meaningful action + income_per_cycle: 81 + net_per_cycle: 19 + gp_per_hour_low: 3000 + gp_per_hour_high: 6000 + xp_per_hour: 14000 + supplies: # everything the bot must carry / restock + - item: nature rune + qty_per_cycle: 1 + source: ge + ge_limit: 12500 # buy limit / 4h window (verified) + - item: coal + qty_per_cycle: 1 + source: ge + ge_limit: 8000 + profit_drivers: # what actually makes it profitable + - "Low alch value vs rune+material buy price" + +# ---------------------------------------------------------------- state +state_machine: + initial: idle + terminal: [complete, failed] + states: + idle: + goal: reach the banking/crafting location + next: [crafting, bank_supplies] + crafting: + goal: cast Low Level Alchemy on a sellable item + next: [bank_supplies, idle, complete, failed] + bank_supplies: + goal: bank products, withdraw fresh supplies + next: [crafting, idle] + complete: + goal: hit the run's completion condition + failed: + goal: a hard failure — stop, log, alert + transitions: + - from: crafting + when: "runes or materials < buffer" + to: bank_supplies + - from: idle + when: "target level or gp goal reached" + to: complete + +# ---------------------------------------------------------------- actions +actions: + - id: a1 + step: reach-bank + verb: move_to # <- universal verb (action-grammar.md) + target: bank + params: + location: "Bank of Gielinor" + method: [teleport, walk] # preferred, fallback + on_fail: retry # retry | skip | abort | alert + anti_ban: # OPTIONAL humanisation hints + mouse: "wander 20-80px around target" + delay: [400, 1600] + + - id: a2 + step: cast-alch + verb: use_item_on + target: "sellable item in inventory" + params: + item: "lowest-alch-value inventory item" + on: "itself" + on_fail: retry + anti_ban: + delay: [900, 2400] + + - id: a3 + step: bank-loop + verb: bank_inventory + target: bank + params: + deposit: [all products] + withdraw: "supplies up to buffer" + on_fail: alert + +# ---------------------------------------------------------------- loop / pacing +run_control: + iteration: "one full craft+bank cycle" + max_iterations: null # null = until complete condition + break: # humanised pause policy + every: [8, 14] # cycles + for: [60, 180] # seconds + stop_conditions: + - "target level reached" + - "gp under floor" + - "hard error 3x" + +# ---------------------------------------------------------------- safety +risk: + ban_level: low # low | medium | high + notes: + - "Rune purchase bursts are the main detection vector; pace GE buys." + +# ---------------------------------------------------------------- provenance +sources: + - "OSRS Wiki — Low level alchemy (price/limit verified 2026-08-27)" + - "prices.runescape.wiki mapping (item ids + GE limits)" +verified: 2026-08-27 + +related: + - mule-topup + - f2p-alch-catalog +``` + +Every key except `actions[].anti_ban`, `notes`, `params` details is +**required** (enforced by `schemas/entry.schema.json`). The validator is +strict — a draft that fails validation blocks the PR. + +--- + +## Best-in-class rule (required) + +Every entry MUST declare how a top operator actually runs the method, in +`best_practice`. This is where "staff of fire for alchemy", "noted items in +the spell slot", "marks of grace", "best prayer for the boss" etc. live. + +- `equipment[]` — item + `why` (mechanical reason) + `source`. +- `techniques[]` — named technique + `why` + `source`. +- Every line carries a `source`: an OSRS wiki page, **or** `researched + ` for a fact not yet documented on the wiki — a `researched` flag + is a TODO: a follow-up issue must land the proper citation. +- The translator MUST adopt the best-practice setup by default. Deviating + requires `improvised: true` in the translation header with a reason. + +## Translation contract (agent -> client) + +When an agent picks up an entry to build a script: + +1. Read `state_machine` → build the client's state machine 1:1 (same state + names, same transition conditions). +2. Walk `actions[]` in order; map each `verb` using the client's mapping + table in `docs/action-grammar.md` (DreamBot column, Microbot column, ...). +3. Apply `run_control` pacing and `break` policy verbatim. +4. Apply every `anti_ban` hint; the agent may **add** humanisation but must + never remove a codex-mandated one. +5. Emit a `translation` block in the generated script header: codex `id`, + `version`, client name, and any verb the agent had to improvise (flagged + `improvised: true`). + +If a verb has no mapping for the target client, the agent MUST mark it +`improvised: true` and leave a `TODO(client)` — never silently invent +behaviour. diff --git a/schemas/entry.schema.json b/schemas/entry.schema.json new file mode 100644 index 0000000..c50ce9b --- /dev/null +++ b/schemas/entry.schema.json @@ -0,0 +1,220 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OSRS Codex Entry", + "type": "object", + "additionalProperties": false, + "required": [ + "id", "title", "domain", "tags", "status", "version", "updated", + "summary", "accounts", "best_practice", "state_machine", "actions", + "run_control", "risk", "sources", "verified" + ], + "properties": { + "id": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", "maxLength": 64 }, + "title": { "type": "string", "minLength": 3 }, + "domain": { + "enum": ["economy", "training", "combat", "combat-slayers", "quests", "transport", "meta"] + }, + "tags": { "type": "array", "items": { "type": "string" }, "minItems": 1 }, + "status": { "enum": ["draft", "verified", "stale"] }, + "version": { "type": "integer", "minimum": 1 }, + "updated": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, + "summary": { "type": "string", "minLength": 10 }, + + "accounts": { + "type": "object", + "required": ["membership", "min_level"], + "additionalProperties": false, + "properties": { + "membership": { "enum": ["f2p", "members", "hybrid"] }, + "min_level": { "type": "integer", "minimum": 1, "maximum": 126 }, + "requires_quest": { "type": "array", "items": { "type": "string" } }, + "requires_other": { "type": "array", "items": { "type": "string" } } + } + }, + + "economy": { + "type": "object", + "additionalProperties": false, + "properties": { + "item": { "type": "string" }, + "item_id": { "type": "integer" }, + "cost_per_cycle": { "type": "number" }, + "income_per_cycle": { "type": "number" }, + "net_per_cycle": { "type": "number" }, + "gp_per_hour_low": { "type": "number" }, + "gp_per_hour_high": { "type": "number" }, + "xp_per_hour": { "type": "number" }, + "supplies": { + "type": "array", + "items": { + "type": "object", + "required": ["item", "qty_per_cycle", "source"], + "additionalProperties": false, + "properties": { + "item": { "type": "string" }, + "qty_per_cycle": { "type": "number", "exclusiveMinimum": 0 }, + "source": { "enum": ["ge", "p2p", "drops", "craft", "bank", "npc-shop"] }, + "ge_limit": { "type": "integer", "minimum": 1 } + } + } + }, + "profit_drivers": { "type": "array", "items": { "type": "string" } } + } + }, + + "best_practice": { + "type": "object", + "required": ["equipment"], + "additionalProperties": false, + "properties": { + "equipment": { + "type": "array", + "items": { + "type": "object", + "required": ["item", "why", "source"], + "additionalProperties": false, + "properties": { + "item": { "type": "string", "minLength": 2 }, + "why": { "type": "string", "minLength": 5 }, + "source": { "type": "string", "minLength": 5 } + } + } + }, + "techniques": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "why", "source"], + "additionalProperties": false, + "properties": { + "name": { "type": "string", "minLength": 2 }, + "why": { "type": "string", "minLength": 5 }, + "source": { "type": "string", "minLength": 5 } + } + } + } + } + }, + + "state_machine": { + "type": "object", + "required": ["initial", "terminal", "states"], + "additionalProperties": false, + "properties": { + "initial": { "type": "string" }, + "terminal": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "contains": { "const": "failed" } + }, + "states": { + "type": "object", + "minProperties": 2, + "additionalProperties": { + "type": "object", + "required": ["goal"], + "additionalProperties": false, + "properties": { + "goal": { "type": "string", "minLength": 3 }, + "next": { "type": "array", "items": { "type": "string" }, "minItems": 1 } + } + } + }, + "transitions": { + "type": "array", + "items": { + "type": "object", + "required": ["from", "when", "to"], + "additionalProperties": false, + "properties": { + "from": { "type": "string" }, + "when": { "type": "string", "minLength": 3 }, + "to": { "type": "string" } + } + } + } + } + }, + + "actions": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["id", "step", "verb", "on_fail"], + "additionalProperties": false, + "properties": { + "id": { "type": "string", "pattern": "^a[0-9]+$" }, + "step": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, + "verb": { + "enum": [ + "login", "select_world", "hop_world", "is_logged_in", + "move_to", "walk_to", "teleport", "enter_region", "open_door", + "use_item_on", "click_object", "right_click", "talk_to", + "click_button", "menu_option", + "bank_inventory", "deposit_all", "withdraw", "has_item", + "count_item", "move_to_inv", "sort_inventory", + "ge_open", "ge_buy", "ge_sell", "ge_offers_done", "ge_collect", + "ge_check_price", + "attack", "prayer", "buff", "eat", "retreat", "is_dead", "respawn", + "is_at", "item_count", "level_is", "xp_is", "hp_is", + "ge_clear", "bank_open", "quest_state", "npc_present", + "log", "alert", "sleep", "human_move", "take_break" + ] + }, + "target": { "type": "string" }, + "params": { "type": "object" }, + "on_fail": { "enum": ["retry", "skip", "abort", "alert"] }, + "anti_ban": { + "type": "object", + "additionalProperties": false, + "properties": { + "delay": { + "type": "array", + "items": { "type": "number", "minimum": 0 }, + "minItems": 2, + "maxItems": 2 + }, + "mouse": { "type": "string" }, + "jitter": { "type": "number", "minimum": 0 } + } + } + } + } + }, + + "run_control": { + "type": "object", + "required": ["iteration", "stop_conditions"], + "additionalProperties": false, + "properties": { + "iteration": { "type": "string" }, + "max_iterations": { "type": ["integer", "null"], "minimum": 1 }, + "break": { + "type": "object", + "additionalProperties": false, + "properties": { + "every": { "type": "array", "items": { "type": "number", "minimum": 1 }, "minItems": 2, "maxItems": 2 }, + "for": { "type": "array", "items": { "type": "number", "minimum": 1 }, "minItems": 2, "maxItems": 2 } + } + }, + "stop_conditions": { "type": "array", "items": { "type": "string" }, "minItems": 1 } + } + }, + + "risk": { + "type": "object", + "required": ["ban_level"], + "additionalProperties": false, + "properties": { + "ban_level": { "enum": ["low", "medium", "high"] }, + "notes": { "type": "array", "items": { "type": "string" } } + } + }, + + "sources": { "type": "array", "items": { "type": "string" }, "minItems": 1 }, + "verified": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, + "related": { "type": "array", "items": { "type": "string" } } + } +} diff --git a/tools/validate.py b/tools/validate.py new file mode 100644 index 0000000..7667c3d --- /dev/null +++ b/tools/validate.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +"""OSRS Codex validator — the gate for every codex/**.yaml entry. + +Runs: + 1. JSON-Schema validation (schemas/entry.schema.json) + 2. Cross-field consistency checks the schema can't express: + - state_machine `next`/transition targets must be declared states + - `initial` must be a declared state + - duplicate action ids / step names + - status:verified entries with `verified:` older than --stale-days + => WARNING (entry should be re-verified, not dropped) + - `best_practice` lines whose source starts with "researched" + => WARNING (citation TODO — file a follow-up issue) + 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] +""" +import datetime +import json +import re +import sys +from pathlib import Path + +import jsonschema +import yaml + +ROOT = Path(__file__).resolve().parent.parent +SCHEMA = json.loads((ROOT / "schemas" / "entry.schema.json").read_text()) +CODEX = ROOT / "codex" +STALE_DAYS = 30 + + +def load_entries(): + out = [] + for p in CODEX.rglob("*.yaml"): + if p.name.startswith("_"): # dataset files (e.g. _f2p-alch-catalog.yaml) + continue + out.append(p) + return sorted(out) + + +def main(): + global STALE_DAYS + if "--stale-days" in sys.argv: + STALE_DAYS = int(sys.argv[sys.argv.index("--stale-days") + 1]) + + errors, warnings = [], [] + entries = load_entries() + if not entries: + print("ERROR: no codex entries found under codex/") + return 1 + + ids = {} + today = datetime.date.today() + + for path in entries: + rel = str(path.relative_to(ROOT)) + try: + data = yaml.safe_load(path.read_text()) + except yaml.YAMLError as e: + errors.append(f"{rel}: YAML parse error: {e}") + continue + + # 1. schema + v = jsonschema.Draft7Validator(SCHEMA) + for err in sorted(v.iter_errors(data), key=lambda e: list(e.path)): + loc = "/".join(str(p) for p in err.path) or "(root)" + errors.append(f"{rel} [{loc}]: {err.message}") + if errors and errors[-1].startswith(rel): + # schema already broke; cross-checks on a broken doc are noise + continue + + eid = data.get("id", rel) + if eid in ids: + errors.append(f"{rel}: duplicate entry id '{eid}' (also in {ids[eid]})") + ids[eid] = rel + + # 2. state machine consistency + sm = data.get("state_machine", {}) + states = sm.get("states", {}) + declared = set(states) + terminals = set(sm.get("terminal", [])) + for sname, sdef in states.items(): + if sname not in terminals and not sdef.get("next"): + errors.append(f"{rel}: non-terminal state '{sname}' has no 'next'") + for nxt in sdef.get("next", []): + if nxt not in declared: + errors.append(f"{rel}: state '{sname}' next->'{nxt}' not declared") + if sm.get("initial") not in declared: + errors.append(f"{rel}: initial state '{sm.get('initial')}' not declared") + for t in sm.get("transitions", []): + if t["from"] not in declared: + errors.append(f"{rel}: transition from '{t['from']}' not declared") + if t["to"] not in declared: + errors.append(f"{rel}: transition to '{t['to']}' not declared") + for term in sm.get("terminal", []): + if term not in declared: + errors.append(f"{rel}: terminal '{term}' not declared") + + # action id/step uniqueness + a_ids = [a["id"] for a in data["actions"]] + a_steps = [a["step"] for a in data["actions"]] + if len(a_ids) != len(set(a_ids)): + errors.append(f"{rel}: duplicate action ids") + if len(a_steps) != len(set(a_steps)): + errors.append(f"{rel}: duplicate action step names") + + # 3. staleness / research TODOs + if data.get("status") == "verified": + try: + vdate = datetime.date.fromisoformat(data["verified"]) + age = (today - vdate).days + if age > STALE_DAYS: + warnings.append( + f"{rel}: status=verified but facts are {age}d old " + f"(> {STALE_DAYS}d) — re-verify or drop to status:stale") + except ValueError: + pass + for line in data.get("best_practice", {}).get("equipment", []) + \ + data.get("best_practice", {}).get("techniques", []): + if str(line.get("source", "")).startswith("researched"): + warnings.append( + f"{rel}: best_practice '{line.get('item') or line.get('name')}' " + f"carries a 'researched' source — citation TODO, file follow-up") + + # report + for w in warnings: + print(f"WARN {w}") + for e in errors: + print(f"ERROR {e}") + print(f"RESULT: {len(entries)} entries, {len(errors)} errors, {len(warnings)} warnings") + return 1 if errors else 0 + + +if __name__ == "__main__": + sys.exit(main())