docs(research): synthesis of game, state and primitives; next iteration

Join the three evidence sources that were kept apart, and extract the useful
result: each source independently decides what belongs in code and what belongs
to the model, and every serious bug we have found broke one of those rules.

  * Game mechanism  -> arithmetic and rules belong in CODE
  * Internal state  -> anything directly readable or writable belongs in CODE
  * TypeSafe        -> judgements go to the MODEL, in the primitive matching the
                       answer's shape

  * never blocked in boss fights        -> game mechanism (arithmetic left to the model)
  * card quality asked as a Noul        -> primitives (a spectrum forced into yes/no)
  * indices read from the wrong array   -> internal state

The counter-example is kept too: the Score we tried for fight-level planning was
unusable, so the rule is "match the primitive to the shape, and verify" rather
than "use Score more".

Also records the ordered plan (measurement first, then re-run, then primitives,
then deck composition), the continual-learning design, and why it is blocked:
with 0 wins in 37 runs the reward has no gradient, so training would fit the bug
rather than the game.
This commit is contained in:
0xrsydn 2026-09-22 06:06:21 +07:00
commit 65fe1171cd
2 changed files with 357 additions and 3 deletions

View file

@ -18,6 +18,9 @@ guess, it says so.
| 05 | [Failure modes](05-failure-modes.md) | Every infinite loop found live, with its fix |
| 06 | [Decision architecture](06-decision-architecture.md) | The three-layer design and why the split is where it is |
| 07 | [Run log](07-run-log.md) | Results per run, with seeds and outcomes |
| 08 | [What actually wins](08-what-actually-wins.md) | Why the web meta is unusable, and the measured cause of 9 run losses |
| 09 | [TypeSafe best practice](09-typesafe-best-practice.md) | Doc-by-doc gap analysis: what we match, what measurement killed, what is missing |
| 10 | [Synthesis and next iteration](10-synthesis-and-next-iteration.md) | Game + state + primitives joined; the three rules; the planned iteration and refactor |
Architecture summary lives in [`../DESIGN.md`](../DESIGN.md).
@ -28,6 +31,15 @@ Architecture summary lives in [`../DESIGN.md`](../DESIGN.md).
3. Record the **date** and the **game build** (`v0.107.1` today). Both move.
4. When a finding is later disproved, do not delete it. Mark it superseded
and say what replaced it. The wrong turn is often the useful part.
5. **Re-measure numbers before you copy them.** Test counts, latencies and run
totals in these docs have gone stale more than once. The suites print their
own totals; use those, not a figure quoted from an earlier session.
> Worked example: a hand-off summary recorded `29` and `118` assertions.
> Re-running the suites showed `50` and `131`, and the difference was two
> regression sections that already existed in the files. The stale numbers
> would have gone into this index unchallenged. Run the suite; do not trust
> the summary.
## Environment these notes were taken on
@ -57,11 +69,12 @@ Then restart the game. Mods load only at process start.
## Testing
Two suites, both offline. Run them before every session.
Three suites, all offline. Run them before every session.
```bash
python3 test_brain.py # 54 assertions — structural / programmatic
python3 test_facts.py # 29 assertions — arithmetic and parsing
python3 test_brain.py # 131 assertions — structural / programmatic
python3 test_facts.py # 50 assertions — arithmetic and parsing
python3 test_run.py # 21 assertions — the decision log and its joins
```
`test_brain.py` is the important one for catching usage bugs. It asserts that
@ -69,6 +82,12 @@ every `state_type` produces an action **legal for that state**, that every
action the decision layer can emit is declared somewhere, and that every
fallback respects its own inputs. It needs no model and no running game.
`test_run.py` drives `main()` end to end with a fake `sts2` and a stub client,
so it pins the decision log without a network: a model decision's row carries
that decision's answers, and a decision that asked nothing logs `jev: null`
rather than inheriting the previous step's. Removing the reset before each
`decide()` makes it fail.
It was added after a session in which four separate infinite loops and three
hardcoded fallbacks were found by hand. Most of them would have been caught
here.