Codex skeleton: schema contract, action grammar, data-sources rules, JSON schema, validator, golden example (low-alch-f2p, wiki+price verified), README

This commit is contained in:
Vale 2026-08-27 02:59:55 -05:00
parent f419bde235
commit 986fc1a6a8
7 changed files with 1008 additions and 2 deletions

View File

@ -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.
**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/<domain>/<entry-id>.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 <date>`
(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.

View File

@ -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

140
docs/action-grammar.md Normal file
View File

@ -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.

64
docs/data-sources.md Normal file
View File

@ -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/<Item>` 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/<Topic>) |
| **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 <date>` (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/<Name>&prop=revisions&rvprop=content&format=json
itempage: https://oldschool.runescape.wiki/api.php?action=parse&page=<Name>&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.

224
docs/schema.md Normal file
View File

@ -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
<date>` 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.

220
schemas/entry.schema.json Normal file
View File

@ -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" } }
}
}

138
tools/validate.py Normal file
View File

@ -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())