1@README.md
2
3- **A new Jev call is a new module implementing `Decision`, never an ad-hoc
4  request built somewhere else.** If a call site wants to ask Jev something
5  and there is no decision kind for it yet, the fix is a new
6  `src/<kind>.rs` (Facts/Answer/Memory/Key/Decision impl) plus one
7  `Engine<YourKind>`, not a request built inline against `jev-http`
8  directly.
9- **Off must stay upstream.** With no chooser installed the goal-choice
10  hook is NULL and the bot is byte-for-byte upstream's; a change here must
11  never install one implicitly. The apps default to off. A future decision
12  kind that drives some other part of the bot owes the same guarantee.
13- **Every failure is upstream's pick, never a stop.** `jev_http::Jev::ask`
14  refuses on the self-clearing guards (`Failure::Throttled`) and on what
15  only a human can clear (`Failure::Budget`: the vendor said out of credit,
16  or the ledger cannot be read; there is no self-imposed lifetime cap since
17  2026-09-22); `Engine::ask` always falls back to
18  `Decision::fallback` and the bot plays on. Do not retry around either, and
19  do not sleep for a throttle: `Engine` records `retry_in` and simply does
20  not ask again before it (`throttled_until`).
21- **`goal_choice`'s option order is the C shim's**: index 0 is upstream's
22  own pick, the rest cheapest first, and `IDS` is fixed at six because the
23  shim never offers more. The panel, the log, and `Chooser::overrides`
24  (goal `!= 0` means Jev's pick differed) rely on this.
25- Native only (this crate links the C bot and a socket, through `zbanks`
26  and `jev-http`); never add it to a wasm build. A `packages/` crate that
27  needs the network or a window is the exception here, same reasoning as
28  `zbanks`/`mcp`/`replay` - see `../CLAUDE.md`.
29- The key only ever arrives through `op-env-run`; see the repo CLAUDE.md.
30- **`Record<A>`'s JSON shape IS the log's schema** - there is no second,
31  hand-written mapping to keep in sync. Adding or renaming a field on a
32  decision's `Answer` or on `How` changes what a line looks like on disk;
33  `Engine::load_history` already treats a line it cannot parse, or whose
34  `kind` does not match, as belonging to some other kind or an incompatible
35  shape (skipped, not an error), so this is safe to evolve - but a field
36  rename silently stops matching every line written before it, which is a
37  real (if harmless) loss of that window's seeded history, not a compile
38  error to catch it.
39- **`kind`'s default (`default_kind` in `src/lib.rs`) is hardcoded to
40  `"goal_choice"`, not left generic.** Every line ever written before this
41  crate had more than one decision kind WAS a goal choice, so that is the
42  one sound default for an old line with no `kind` field. Do not change it
43  to something "more neutral" - there is no neutral answer for what an old
44  line meant, only the correct one.
45- **`Engine::history()`/`load_history()` are bounded by `HISTORY_CAP`; the
46  log they read from and write to is not.** Raising the cap only changes
47  how much a restart re-seeds into memory; it does not recover anything,
48  the full past is already on disk in the log either way.
49- **`goal_choice::Chooser::overrides` is not part of the shared `Totals`.**
50  Whether an `Answer` differs from "upstream's own pick" depends on that
51  decision kind's own convention (goal index 0 here); `Engine`/`Totals`
52  stay honestly generic and do not guess at it. A future decision kind with
53  its own notion of "differed from the default" keeps that count the same
54  way - beside its own `Chooser`-equivalent, not bolted onto `Totals`.
55- **`Decision::Memory` is deliberately not always `Decision::Answer`.**
56  `goal_choice` keeps a probability per underlying goal IDENTITY
57  (`kind|node|screen`), not per the sentence built for one particular call,
58  because the next call's offer grouping can name the same goals with
59  different wording once Link has moved. A decision kind whose reuse really
60  is "the exact same answer, unchanged" can set `Memory = Answer` and have
61  `reuse` ignore the sampling closure.
62- **`goal_choice::Facts` reaches SRAM/WRAM without touching `packages/zbanks`**
63  (`goal_choice::Snapshot`, read from `console.wram()`): the CALLER
64  (`apps/native`'s tick, `apps/zbanks`'s loop) calls `Chooser::set_snapshot`
65  once a frame, BEFORE `zbanks::Bot::tick`, since the C shim's goal-choice
66  hook can fire synchronously inside it. This is deliberate - the
67  `zbanks::GoalChooser` trait's signature is not this module's to change,
68  and a second, purely-additive channel avoids ever needing to. A caller
69  that forgets to call `set_snapshot` is not a compile error (the field
70  defaults to all-zero), so a NEW caller of `goal_choice` must be checked
71  against this, not assumed to inherit it.
72- **`Facts::sentences()`, not `words::offers()`'s own sentences, is what
73  goes to Jev and into `Answer`.** It appends vanilla dungeon knowledge
74  (`DUNGEON_LORE`) when an offer's sentence already names a place the bot
75  resolved - never for "somewhere never visited", which is honestly all
76  that is known about an unexplored door. Extend `DUNGEON_LORE`, not
77  `words.rs`, for a new named place: the wording change stays isolated
78  here (user, 2026-09-22).
79- The behaviour-preservation test in `goal_choice.rs`
80  (`the_request_sent_to_jev_is_unchanged_by_the_rewrite`) compares against a
81  request captured from the pre-rewrite goal-choice code (the crate
82  `packages/zbanks-jev`, which this crate replaced on 2026-09-22), byte
83  for byte. Any future edit to the wording or shape of the goal-choice request
84  should update that golden deliberately, in the same commit, with a note
85  saying why the wording changed - never adjust the test to match new
86  output without reading the diff.
87- **`MODEL` is pinned (`jev-1.13.0`), not an alias.** `FAVOURITE_THRESHOLD`
88  (0.70) was tuned against it; moving the pin is a change to re-measure, not
89  a version bump.