Commit graph

95 commits

Author SHA1 Message Date
acamilo
2ef842ed90 session: one object helper, in the types facade where the rest of the shims live 2026-09-22 12:33:01 +00:00
acamilo
52ad46ecb0 refactor(session): build the session slice on the shared fly-session-types crate
CONTRACT-01 landed its crate, so the local stand-in module goes away and the domain
scalars, method payloads, their validation, the canonical digests and the trace format
all come from the contract. `src/types.rs` is a facade over that crate plus the few
things a coordinator needs that are not part of the type contract: a session-side
DomainError, the synthetic composition's schema and event-id derivations, and the
coordinator-local ControllerIntent, PortBinding and AgentOutcome that never cross the
bus.

Consequences worth naming:

- Payloads are read and written through DomainType::from_json / to_json instead of
  serde derives, so a misspelled required field fails where the contract says it
  should. serde, sha2 and ryu-js leave this crate's dependencies with them.
- The step-v1 section 8 trace is the contract's TransitionTrace, with behaviour and
  operational metadata already separated; the dispatch-order comparison now runs over
  the contract's own behaviour encoding.
- Two things the migration found. IN_PROGRESS is raised strictly before any mutation,
  so its certainty is "none", not "unknown"; the local module had it wrong.
  WorldObservation::validate_against checks the views a result carries but does not
  require every declared view to be present, so requiring them is made explicit in the
  coordinator's phase C check, where step-v1 puts it.
- The lost-Advance-result injection now waits for the worker to report the operation
  before abandoning the call, so the case it injects really is a loss after dispatch
  rather than a cancellation before it.

Gates: cargo test -p fly-session (61 tests, both transports), cargo clippy
--all-targets clean, and the runnable example produces the same behaviour trace over
both transports.
2026-09-22 12:30:41 +00:00
acamilo
825b584320 Merge feat/sf-contract-01: the fly-session-types crate, so the session slice builds on the shared contract types 2026-09-22 12:30:03 +00:00
acamilo
4f44894ede docs(design): a bus-v1 conformance report, and the amendments it asks for
bus-conformance.md audits the flybus crate against bus-v1 sections 2 to 11, one
row per normative sentence: requirement, status, code location, the test that
proves it. 195 rows, 177 conforming, 10 deviating with the sentence that allows
each, 1 must-fix (the teardown starvation, fixed in this branch) and 7 not
implemented with an owner named. It also maps every BUS-01/02/03 acceptance
bullet to its test, reconciles all ten of the crate's own differences from the
draft, records the frame measurement with the reasons it is not a capacity
claim, and lists the two contradictions inside the draft it will not guess at:
the per-client queued byte budget against the latest-mode guarantee, and
section 2's call budget against section 6's client-owned deadline. Neither
changed any code.

bus-v1 gains section 12: dated amendments for the three error codes the
implementation needs and the draft left unnamed (CONFLICT, NO_TOPIC,
ARTIFACT_MISMATCH), one line of reason each, and for the one operation it adds
(rpc.responder.release), which is how bounded call correlation ends when a
handler keeps reply authority after releasing the request delivery. Section 9's
error list is inclusive, and section 4 already provides for a digest change.

The crate README points at the report, carries the new measurement summary in
place of the old numbers, and describes the two new test files; the design index
and the flybus index point at the report too.
2026-09-22 12:07:11 +00:00
acamilo
cf757af510 test(flybus): assert the example's output, and split the frame measurement up
The guide's first deliverable is one small program with a counter RPC, a pub/sub
observer and a frame artifact held past its message object. examples/demo.rs was
already that; its body moves into run(), which returns the lines it prints, so
tests/example_demo.rs can assert all seven of them. The counter now uses the
spec's own example.counter / Counter.Increment / {"amount": 1} and pins the
registration it discovered, and the example prints the router's root count while
the frame is held and after the last handle goes, so the lifetime it
demonstrates is visible rather than implied. cargo run -p flybus --example demo
prints the same lines.

tests/perf.rs reports what BUS-03 and bus-v1 section 11.7 actually ask for.
Allocate, the producer's copy into staging, the router's seal copy, publish
admission, the RPC round trip and a consumer's readback are six separate
percentile lines instead of one. The router gets its own two-thread runtime
whose threads carry a distinct name, and per-thread CPU is sampled from /proc by
that name, so router CPU is separable from the clients' in the same process.
Store bytes, outstanding roots and queue lengths are reported live as well as
peak.
2026-09-22 12:01:43 +00:00
acamilo
148dbb2fae test(flybus): the BUS acceptance bullets that were unproven, and transport traces
One named test per implementation-guide bullet that the existing suites did not
already cover: a lost result that leaks no roots while the endpoint's own cache
still replays it, a retransmission of one domain body under a fresh call id, no
failover onto a replacement registration, a status RPC answered while another
handler is delayed, and a disconnect that reclaims logical ownership without
touching a file the consumer still has open. Each runs over both transports.

BUS-01 also asks for equivalent behaviour traces from the two transports, so
tests/common/mod.rs gains a Trace recorder whose record() panics on any
router-issued id: a trace can hold methods, payload fields, counts, sequences,
credits, cancel states and error codes, and nothing operational. One RPC
scenario and one pub/sub-with-artifacts scenario produce 29 events, identical in
memory and over a Unix socket. FLYBUS_TRACE=1 prints them.

The sixteen bullets the older suites already prove are cited in the conformance
report rather than duplicated here.
2026-09-22 12:01:34 +00:00
acamilo
7708bf12cd fix(flybus): stop connection teardown being starved by the frame it waits for
The write gate was one mutex the writer held across every synchronous transport
poll, and close_conn took that same mutex before reclaiming a connection's
owners. Under a transport that accepts a byte per poll the writer re-acquired it
hundreds of times while teardown queued behind it, so teardown could be starved
for a whole frame and the delivery completed just before its owner was
reclaimed. bus-v1 section 9 puts release and route-health control out of reach of
telemetry starvation; tests/sol_rereview_regressions.rs's poll-gate regression
failed 6 release runs out of 6 alone, and about one debug run in five.

The gate now separates "teardown has begun" from "a poll is in progress": a
mutex plus a condvar. begin_close sets closing once without waiting, then waits
for the poll already in progress and records whether it left a frame half
written. The writer brackets each poll with enter_poll (refused once closing)
and leave_poll(bytes) instead of holding a lock across it, so teardown's window
is one poll rather than a frame, and no byte follows it. close_conn no longer
holds the gate across reclamation. Every item is pub(crate): no public
signature changed.

The regression test no longer synchronises with a sleep. The shutdown thread
announces itself on a channel, and what follows is structural: teardown cannot
pass the gate until the held poll returns, which only release() allows. Its
delivery is 50 KB now, which a writer resuming a byte per poll cannot finish
inside the window, so the test observes the ordering instead of a coin toss and
asserts the two legal shapes of the stream: cut short with nothing appended, or
never begun with exactly the closing notices following.
2026-09-22 12:01:24 +00:00
acamilo
c567727351 feat(session-types): the TypeScript half of the contracts, over the same fixtures
packages/session-types mirrors the Rust crate: canonical JSON (RFC 8785), strict parsing,
canonical digests, readers with a validate step for every payload of ipc-v1, workers-v1,
state-media-v1 and publishing-v1, the trace comparator, seed derivation and the FLYSESS1
envelope. It loads the crate fixtures/ directory rather than keeping a copy.

43 tests with node test runner, as in packages/feed. Both languages assert the same recorded
canonical bytes and digests, so agreement is checked rather than assumed; the envelope test
goes further and requires the bytes this package writes to equal the fixture the Rust crate
wrote.

Also narrows the canonical number rule to a value-based one (finite, and within the exact
integer range when integral), because JSON.parse cannot tell 1e21 from the same digits
written out, and two implementations that disagree about one number agree about no digest.
2026-09-22 11:54:13 +00:00
acamilo
5defb6766d feat(session): fly-session, the SESSION-01 synthetic sequential transaction over flybus
The lockstep coordinator, its phase machine and a synthetic composition, as a new
workspace member. Every worker method is a Flybus RPC to an incarnation-pinned
service; the domain request ids and result caches of ipc-v1 section 5 sit in front of
every mutation.

- coordinator: the step-v1 section 3 transaction in order -- prepare every agent
  concurrently, run one executor per agent in sorted agent-id order, assemble all port
  controls in descriptor order, send exactly one Environment.Advance, evaluate the task
  once, commit every agent concurrently, and move the committed boundary only when all
  of them succeeded. Sequential, concurrent and reversed dispatch are selectable and
  must agree.
- phase: the section 2 machine as an explicit edge table, Paused and Failed included,
  with a committed-boundary predicate that gates pauses, captures and publication.
- clock: checked rational accumulation. A 60 Hz world with a 1 ms model tick runs
  16, 17, 17 ticks and comes back to a remainder of exactly zero.
- dedup: operation keys, the cached reply with its own artifact holds, CONFLICT,
  IN_PROGRESS, RESULT_EXPIRED, STALE_STEP, the lifecycle bound and Worker.Acknowledge.
- worker: the dispatch shell. Admission order and the result cache live here; the
  endpoint mutex is the worker's simulation lock, so one mutation runs at a time while
  Worker.Status answers from a separate cell.
- agent, environment, task: a fake model with an explicit seed and a mutation counter
  the tests read, a fixed readout stub, a counter arena that seals one immutable frame
  per boundary, a deterministic task and the identity executor.
- fly_session_types: the CONTRACT-01 domain types as a local stand-in, reconciled with
  the shared crate in a following commit.

Tests run twice, over the in-memory transport and over a Unix socket, through the same
router: the SESSION-01 acceptance bullets and every section 4 failure-injection row
that applies to this slice.
2026-09-22 11:46:57 +00:00
acamilo
1652afad11 test(session-types): the CONTRACT-01 fixtures, cross-checks and their Rust tests
fixtures/ is the shared contract corpus: accepted payloads with their canonical JSON and
digest, refused payloads with the rule each breaks, raw byte sequences that must not even
parse, recipes for the payloads too large to store, the U64 and double boundaries, rational
arithmetic vectors, the identity matrix, descriptor cross-checks, operation keys and bodies,
trace comparison variants, the schema set and its digest, seed vectors and one FLYSESS1
envelope. packages/session-types loads the same files.

Tests: 45 across nine files. Round trips preserve every field and reproduce the recorded
canonical bytes; duplicate keys, invalid UTF-8, NaN, trailing data, oversize typed values and
oversize envelopes fail; U64 and double boundaries reject on the right side; the four
identities never accept one another spellings; the accumulator produces 16, 17, 17 and a zero
remainder; the trace comparator ignores wall time, request ids, callIds and delivery ids and
notices every behaviour change; the contract digest survives reformatting and moves when a
field, bound, enum member or type changes.

Also: a crate README, and the update_fixtures example that regenerates the derived files.
2026-09-22 11:41:04 +00:00
acamilo
d2b916602e feat(session-types): the fly-session-types crate: domain scalars, closed enums, method payloads, canonical JSON and digests
The CONTRACT-01 type definitions, committed early so the session slice can build on
them while the fixtures and cross-language tests are still being written.

- scalar: Scope, RationalNs (reduced, positive denominator, 0/1 zero, checked
  arithmetic and the step-v1 accumulator), SchemaRef, TypedValue with its 32-KiB
  canonical-JSON cap, and the four identities that must not be confused (bus callId,
  domain requestId, artifact identity, delivery/hold owner token) as distinct types.
  Id, U64 and Digest call into flybus::wire instead of forking the encodings.
- canonical: RFC 8785 canonical JSON, SHA-256 digests, the operation key and the
  canonical body rules of ipc-v1 section 5, with bus identities refused in a body.
- rpc, workers, media, publishing: the payloads of ipc-v1, workers-v1, state-media-v1
  and publishing-v1, each with a validate step for the documented ranges, uniqueness,
  ordering and bounds.
- schema: the canonical schema set, whose digest is contractDigest, declared as data
  so source formatting cannot change it.
- trace: the step-v1 section 8 record, behaviour separated from operational metadata.
- seed, checkpoint: the seed derivation and FLYSESS1 envelope layout.
2026-09-22 11:27:06 +00:00
acamilo
e65e712cee Merge fix/loop-20260922T1012: readable YES/NO prompts deal their answers, the nurse enters the talked ledger, a prompt that reopens unchanged is excluded 2026-09-22 11:18:34 +00:00
acamilo
9280cfac2e macros: count the centre's presses, not the whole run's
The first run of the new ROM test failed on YES 134, and every one past
the sixth was in Pewter City or the gym the fly had walked to -- which is
the fly playing the game, not the ring. Row 41 is about the presses spent
in the room, so the harness records starts by name while the fly is still
on the map it resumed on: 6 macros there, YES 2.
2026-09-22 11:17:30 +00:00
acamilo
af7a009395 Merge feat/flybus: the flybus crate (Flybus v1 draft) replayed from the archive, with a design index
Some checks are pending
ci / node 22 (test + typecheck) (push) Waiting to run
ci / rust stable (cargo test --workspace --release) (push) Waiting to run
ci / infra/tests/lint.sh (push) Waiting to run
ci / playwright apps/stage (allowed to fail) (push) Waiting to run
2026-09-22 11:02:31 +00:00
acamilo
0bf4b965fb macros: tests for the nurse's box
Unit: HEAL is off a full party's pad, including the rung-10 party's own
numbers, which is the assertion that the loop was never the heal's; TALK
is off a rested nurse's pad and on a hurt one's, and an ordinary
villager is unaffected; her prompt deals one answer and a plain box still
deals three; a readable prompt that is not hers deals both answers and no
NEXT; an answer whose prompt reopens is excluded and one that settled the
box is not; a completed heal and a declined prompt both retire her.

ROM-gated from the live checkpoint (FLY_CENTER_CHECKPOINT): the fly
leaves map 0x3a on a bounded number of macros, YES starts stay under
five, NEXT is on no pad while a prompt is readable, and TALK is on no pad
at a rested nurse. The run also counts dialog and prompt frames, which is
what makes the forty-six-to-one ratio visible.
2026-09-22 10:39:15 +00:00
acamilo
70a2433e25 macros: the nurse is talked to once, and a reopened box excludes its answer
TALK's precondition reaches over a counter because the cartridge does;
its talked-ledger entry was read one tile ahead. So TALK was bound at the
counter and recorded nothing, 107 times, and the nurse was never retired.
The entry now comes from the same reading the precondition makes.

A completed HEAL writes her into the talked ledger: the macro has had the
conversation with its own presses, and the reached window would otherwise
expire in ten brain minutes and offer the ring again. A declined prompt
writes it too -- 12.4 inverted for one person, because the pad only ever
offers NO at her prompt when the party is already full.

And the general rule: a YES or NO whose prompt is up again on the same
map and tile within one hold did nothing, so that answer joins the
blocked ledger for its window.
2026-09-22 10:39:04 +00:00
acamilo
ce7644e2e8 macros: a choice's pad is its answers, and TALK is not a nurse's errand
On a box that is a readable YES/NO prompt, NEXT is YES under another
name -- an A press at a two-option menu confirms the option the cursor is
on -- so it is off that pad and the pad is the box's two answers. That is
12.10's pair rule in a dialog.

At the nurse's own prompt only the answer that changes something is
bound: YES with a hurt or statused party, NO with a full one, read from
the same byte HEAL's precondition has read since section 13.

And TALK is off the pad at a nurse the party has no use for. Her
conversation is a service whose need the cartridge publishes, and a ring
of text that ends where it began is section 12.2's trap. Nobody else is
narrowed.

An answer the reopened-prompt ledger excludes leaves the pad, and the
exclusion never empties one: a box nothing can answer cannot be left.
2026-09-22 10:39:04 +00:00
acamilo
86f0e7018d macros: read the nurse's YES/NO box, and survey her conversation
Row 41 is the Pokemon Center nurse: 2,142 YES starts on one tile of map
0x3a with a party at 70/70. The survey that names it is a new probe mode
(FLY_PROBE_CATCH=nurse) that leaves the box with B and then pulses A,
printing every state the conversation passes through with the two-option
menu's own geometry beside it. It is a ring of forty-six presses and the
box is a choice on exactly one of them.

So the reading: wFontLoaded plus the border DisplayTwoOptionMenu draws,
plus the cursor it parks inside it. Both halves are needed -- the cursor
bytes survive the box closing, so all forty-six frames carry the
geometry. It claims that box and not "a choice is open" in general.

TargetKey gains Answer { at, yes } for the reopened-prompt exclusion.
2026-09-22 10:38:52 +00:00
acamilo
dedcf3ad8e macros: say what the cursor rule found, not the first reading of it
The doc comment on Cursor::want credited THROW BALL's 63 blocked starts to the
bag drawing slowly, which is true of the frames and not the cause. What the rule
did was make the step report which list it had been left looking at, and the
answer was never the bag - it was the party list, because battle_entry's ITEM and
PKMN were the other way round. The rule stands on its own: a step that reads the
list it has already answered takes its length and its direction from entries it
is not walking.
2026-09-22 09:10:24 +00:00
acamilo
c06d3206e2 docs: section 12.11, and rows 44 to 47 of the trap audit
The rung-10 loop whole: which building the fly was in, why every candidate list
on that map emptied, the byte dump that named the battle menu's swapped entries,
the ROM-gated before and after, and the two arms of the trap hunt being identical
because a restore clears the ledgers the loop was built out of.

Section 13.1's tables follow: MENU off both overworld rows and off the narrow
counter row, the move list's conditional BACK, the start menu reachable only by
the raw START button, and two new rows in the pad-empty audit - the room whose
one door the ledger rests, and the map with no way out at all, which is the one
empty pad left and is asserted rather than covered.
2026-09-22 09:07:00 +00:00
acamilo
0163c66f6a tests: ROM-gated runs for the building pad and for the bag and party lists
A new test from the rung-10 Pewter checkpoint: the fly is on map 0x35, the
museum's upper floor, the objective is the gym where the badge is, and over
thirty-three brain minutes MENU is on no overworld pad, no overworld pad is
empty, MENU and BACK never alternate, and the fly leaves the building on frame
182. On v0.4.3 the same test fails at the first of those: MENU is on the pad of
maps 2, 52, 53, 54, 55, 57 and 58 with 147 starts.

The harness records where a macro reported blocked, which is what identified the
battle menu's swapped entries, and the forest test now asserts THROW BALL never
blocks: 15 starts and 0 blocked from the rung-9 checkpoint, against 6 of 6.

Its bound on a battle's macros goes from 400 to 700 against a re-measured worst
of 450, because a fly whose SWITCH and ITEM reach their own lists fights longer
than one that can only attack. Every battle still ends: 3 of 3.
2026-09-22 08:50:40 +00:00
acamilo
57d9bdff5a macros: Red's battle menu is two columns, so ITEM is under FIGHT and PKMN beside it
THROW BALL was 63 starts and 63 blocked on v0.4.3 and SWITCH 15 of them, and the
reason is neither macro's own script. The screen reads FIGHT PKMN over ITEM RUN
and the game's index does not: wCurrentMenuItem is the row inside the column the
cursor is in, and selection adds two for the right column, so the order is
FIGHT, ITEM, PKMN, RUN. battle_entry had PKMN 1 and ITEM 2 -- the row-major
reading of the picture -- so every macro that meant to open the bag opened the
party list and every macro that meant to open the party list opened the bag.

Surveyed on the cartridge from the rung-9 checkpoint: a THROW BALL aiming at 2
walked the cursor to wTopMenuItemX 15 / wCurrentMenuItem 0, pressed A, and the
party list opened -- wTopMenuItemY 1, wTopMenuItemX 0, wListMenuID $02 -- with
the game writing wCurrentMenuItem 2 on the frame after the press.

The fake had the same mistake in its own geometry, so it could not have caught
it: its two-by-two moved row-major. It is column-major now, like the cartridge.
2026-09-22 08:50:40 +00:00
acamilo
80325c8d57 macros: a cursor step waits for the list it was built for
THROW BALL on the cartridge was 63 starts and 63 `blocked`, mean sixty-nine
frames: the cursor to ITEM, the A that confirms it, the twenty settle frames,
and then a refusal on the very next frame. Red keeps one cursor for every menu
in the game, the bag takes longer than twenty frames to draw, and `listing` was
still answering for the battle *menu* -- four entries, max 3. A ball whose bag
index was above that read as off the end of the list and the step gave up at
once; one inside it was walked for by pressing UP and LEFT at the battle menu,
which is the blind pressing section 4 forbids.

So a Listing now says which list it is (ListKind) and a cursor step says which
list its target indexes into. A step whose list is not up waits exactly as it
waits for a list that reports no cursor at all, and takes its press order and
its budget from that list on the first frame it accepts input rather than from
whatever was up when the script was built. A confirming press that has already
begun still finishes: the press is what answers the list, so the cartridge is
already drawing the next one while it is issued.

The crossings are named: FIGHT and ITEM and PKMN and RUN over the battle menu,
a move slot over the move list, a bag index over the bag, a party slot over the
party list. A shop's own screens are one list and stay as they were.

The fake gained the one thing it could not express -- a list that takes frames
to draw -- and two of its fixtures had FIGHT and a potion opening nothing at
all, which is how they passed while the cartridge could not.
2026-09-22 08:06:42 +00:00
acamilo
029a3446fb macros: the move list deals BACK only where the moves can be read
The v0.4.3 residual: BACK was 263 of 797 macro starts and every one of them was
over an open move list. A move list whose battler the seam cannot place binds no
MOVE n at all -- move_slot_bound needs wBattleMon* -- so its pad was BACK alone,
and the only thing that button does is close the list MOVE 1 on the menu
underneath had just opened. That is section 12.10's pair with MOVE 1 standing
where NEXT used to.

So BACK is dealt on the move list only while battle.own reads, and MOVE 1 keeps
the backstop it has over the top-level menu: with nothing readable the pad is
MOVE 1 alone and its script confirms wherever the cursor stands, which is the
press that ends a turn. With the moves readable the pad is unchanged.
2026-09-22 08:06:19 +00:00
acamilo
3807fff422 macros: MENU is off the overworld pad, and the way out is the never-empty rule
Rung 10, thirty brain minutes inside the Pewter museum's upper floor (map 0x35):
macro starts MENU 82, BACK 82, GO FRONTIER 8, the event log alternating
`MENU start/done, BACK start/done`. MENU pressed START, the start menu opened,
and that scene's pad is CLOSE / CONFIRM / BACK -- so BACK pressed B and closed it
again. Two buttons that undo each other with nothing else changing, which is
section 12.10's rule one scene wider than a battle, and section 12.2's trap by
definition: a precondition satisfied wherever the fly stands and a macro that
completes without moving.

Nothing in the macro vocabulary uses the start menu for anything, so MENU comes
off scene_set's overworld row entirely rather than being narrowed. It stays a
type, a population, a tag and a script, so the roles, the channel order and
--print-compatibility are untouched, and the fly's raw START still opens the
start menu in macros mode.

MENU was also what made an empty overworld pad impossible. That guarantee moves
to the way out: `ways` gains one last resort for a *room*, the one GO ROUTE has
had outdoors since 13.1, so a map that offers nothing else at all still offers
its door or its staircase -- ignoring the blocked window, because a target the
ledger is resting is still the only place to go. The museum's staircase is a
passage and not an exit, which is why the old tier 3 could not answer for it.

A map with no way out at all is now a genuinely empty pad. No map in Red is
that, it is asserted as the named residual, and game.padEmptyMs reports it.
2026-09-22 08:06:06 +00:00
acamilo
4bd0a4b3fa Merge fix/loop-20260922T0459: NEXT never on a pad with an input-accepting cursor; MOVE 1 is the main menu backstop; the bag is the fly's turn 2026-09-22 06:18:34 +00:00
acamilo
520298e5c3 tests: the pad invariant, the bag's turn, and the rung-nine battle on the cartridge
Unit: no battle pad holds both NEXT and BACK, over every sub-state with and
without a potion and a ball, and NEXT is on none with a cursor accepting input
(the assertion the top-level menu failed before this branch). A battle frame
with a cursor accepting input is the fly's turn, against real WRAM, for all
four menus plus the two frames that correctly are not one. The bag deals
ITEM / THROW BALL / BACK, and BACK alone when the bag is empty.

ROM-gated, from the live rung-9 forest checkpoint: no pad with both buttons, no
NEXT while a menu accepts input, the longest NEXT/BACK alternation under four,
and every battle entered also left, each on a bounded number of macros. The
battle rules ask the scene the pad was dealt for rather than wIsInBattle, so
the $ff frame a lost battle passes through -- which reads Unknown, whose pad is
NEXT and BACK by contract -- is not accused of a battle rule.

The test fails on v0.4.2 from the same checkpoint with "NEXT was on the pad
while a battle menu was accepting input", NEXT dealt on battle/main, and a run
that never leaves map 51.
2026-09-22 06:14:59 +00:00
acamilo
05c2552d78 macros: NEXT off every own-turn pad, and the bag is the turn its cursor says it is
Live on rung 9 thirty-five minutes after v0.4.2: NEXT 1264 macro starts, BACK
1241, the log alternating NEXT start/done, BACK start/done every hold, on map
51. NEXT on the top-level battle menu is an A press on the cursor, and the
cursor sits on FIGHT, so it opened the move list; BACK on the move list closed
it again. Each button was legitimate where it stood, and the pair undid itself
with nothing else changing, so the turn never resolved.

NEXT is now on no pad with a cursor accepting input. It keeps the between-turns
row alone and the forced switch, which has no BACK beside it to undo it. MOVE 1
is the top-level menu's backstop instead: the question over that menu is 12.8's
"is there a move list to open", FIGHT always opens, and the script there is
confirm FIGHT and stop, which reads no move at all.

The bag is the fly's own turn, because a cursor accepting input is one, and its
pad is the bag's own three answers: ITEM, THROW BALL, BACK. Section 12.10.
2026-09-22 06:14:48 +00:00
claude
ba581e44d3 flysim: the trap hunt says which refusal a frame with no grid is 2026-09-22 04:50:24 +00:00
acamilo
9479681b9f Merge branch 'main' into feat/map-aware-walks 2026-09-22 04:21:54 +00:00
claude
c8de2e42e9 docs(gb): the module header names the corner the cartridge settled 2026-09-22 04:16:48 +00:00
claude
abd3e563a8 gb: refuse a map header that does not fit wOverworldMap
The buffer is ds 1300 and every real map plus its three-block border fits in it.
A header that says otherwise is one read mid-load, and decoding it would read
past the buffer into somebody else s WRAM, so it is a refusal rather than a
clamp.
2026-09-22 04:15:29 +00:00
claude
44a7011299 tests: the grid against the cartridge on two maps
ROM-gated and checkpoint-gated, skipped cleanly without either. The decode
against the window predicate on every tile the window can answer for; the decode
against a survey of real presses, where every refused press has to be a wall, a
directed wall or a sprite in the way, and every step the cartridge made has to be
one the grid would have planned; GO FRONTIER aiming at ground outside the window
and walking there; and every way out of the map reachable in one plan with no
guessed tile in it.

Pallet Town: 221 walkable, 207 reachable, 90 window tiles, 120 surveyed, 58
refused presses. Viridian Forest: 719 walkable, all reachable, 90 window tiles,
120 surveyed, 74 refused presses, a 215-frame frontier walk out of the window and
ways out 27 to 149 steps away.
2026-09-22 04:12:19 +00:00
claude
246e0c7d4c flysim: the map grid in the probes
scene_probe prints the grid s size, its walkable count, the count reachable from
where the fly stands and the count never stood on, draws the ground with the
reading it actually used, and names the refusal when there is no grid. trap_hunt
carries the same line into every trace line and into the summary, so a stalled
walk can be read at a glance: a fly with 719 walkable tiles and 4 reachable ones
is fenced in and no re-plan will help it.
2026-09-22 04:12:19 +00:00
claude
bcae388d99 gb: re-check a cached grid against the screen when it is served
A warp writes wCurMap before the map header and the block data: on Oak s lab
doormat wCurMap already reads PALLET_TOWN while the header still reads the lab s
ten-by-twelve. The decode agrees with the screen on such a frame -- both are the
old map -- so only the id is wrong, and a grid filed under it would stay wrong
for as long as it was cached. One byte answers it: does the cached grid still
agree with the screen about the tile the fly is standing on.
2026-09-22 04:10:28 +00:00
claude
1b06e6a6cb gb: the collision tile is the lower left of a map tile s quadrant
Measured on the cartridge, not derived: a map tile is 2x2 screen tiles and
CheckTilePassable matches one id, and the screen agrees with the lower-left tile
of the quadrant. Viridian Forest s (4, 32) reads 3, the second row of its
block, where the first row holds bash4. On open ground most quadrants hold one id
four times over, so the upper-left guess reads correctly on a town and falls
apart in a forest -- which is why the decode is cross-checked against the screen
before it is trusted.
2026-09-22 04:10:28 +00:00
acamilo
093db147eb feat(flybus): add standalone IPC bus and conformance tests
(cherry picked from commit 95ac8d1bdbc866a7c49fad3ddba468d945267650)
2026-09-22 03:46:27 +00:00
claude
f7fa39afa7 macros: plan every walk over the map grid
route and frontier ask for the grid once per plan and fall back to the window
when there is none. With it: one plan crosses a whole map, GO WARP / GO OUT /
GO ROUTE route to their warp or connection tile rather than to the nearest tile
of an edge that reads Unknown, a tile-pair wall is planned around instead of
being learned by walking into it, and the frontier is the nearest unstood
walkable tile anywhere on the map instead of the nearest one on screen.
2026-09-22 03:32:35 +00:00
claude
68228ef2a1 gb: decode the whole current map into a walkability grid
The walkable predicate answered for the ten-by-nine screen window and Unknown
everywhere else. MapGrid is the same rule over every tile of the loaded map:
block ids out of wOverworldMap, a blocks-to-tiles read of the tileset header s
blockset through the new bank-aware ROM read, the tileset s collision list as
before, and the TilePairCollisionsLand values as directed walls both ways.

The reader checks itself before it answers: the decode is compared against the
window predicate over the player s own neighbourhood, and a frame where the
window can answer for none of it -- a battle, a text box, a frame mid-warp -- is
refused, because wOverworldMap shares its bytes with the picture buffer. Every
refusal is named (GridRefusal) and leaves the window predicate in charge.

Cached per map id and size, so a map is decoded once on arrival rather than once
per question, and owned beside the session ledgers: never checkpointed.
2026-09-22 03:32:35 +00:00
claude
d75b7320ea seam: a bank-aware ROM read beside the bus read
MemoryReader::read8 reads the CPU bus, where banks 1 and up are whichever bank
the cartridge last switched to, so a table in another bank could only be read by
writing the mapper register -- and the joypad is the only write this workspace
makes into a running game. read_rom takes a bank and a CPU address and reads the
cartridge image the process already holds instead. Defaulted to None, so every
reader without a cartridge behind it narrows rather than guesses.
2026-09-22 03:32:19 +00:00
acamilo
7d5c788876 tests: the rung-9 forest checkpoint, on the cartridge
A ROM-gated run from the checkpoint the stream stalled on, gated on its
own variable because the two checkpoint tests beside it assert the map
their envelope is on and one envelope cannot be both.

What it holds: the objective is Pewter City, the road there is on the pad,
BACK is never on a battle pad with no list open, no ball is thrown at a
species the party holds, the fly gets turns, a move or a ball is chosen,
and a battle ends. Which move and which way it walks are the fly's, so
they are reported rather than asserted -- including the forest's north
gate, which this run does not reach.
2026-09-22 03:28:28 +00:00
acamilo
ce02a89922 macros: BACK belongs to a list, and no ball at a species the party holds
Two traps from the rung-9 forest checkpoint, both section 12.2's rule: a
macro that completes without moving because its precondition is already
satisfied where the fly stands.

BACK was dealt by the between-turns row, which section 13.1 gave NEXT and
BACK for the sake of the battle bag -- the bag reads as nobody's turn, so
it lands there -- and which is also every frame of battle text. With
nothing open there is nothing to back out of, so the press completes in
fifteen frames on the tile it started on: 163 of 959 macro starts in
twenty brain minutes from the checkpoint, plus twelve more on a move list
whose cursor the seam cannot place. The row is now the sub-state's: BACK
with the bag open, NEXT alone otherwise. The move list and the party list
keep it, and the top-level menu never had it.

THROW BALL gains a fourth fact: the species on the other side is not one
the party already holds. The party is the caught set, because it is the
cartridge's own lifetime record in the same internal numbering the enemy
is read in; wPokedexOwned is by Pokedex number and the table that converts
one to the other is in a ROM bank this crate cannot read. An enemy the
seam cannot place leaves the button where it was.

RUN is unchanged. Its blocked outcomes are the script and not the
cartridge: a cursor macro started as the menu closes waits out CURSOR_WAIT
pressing nothing (mean 184 frames over ten starts), which is audit row 19.

The decoder, the reward catalog, the adapter version and the compatibility
string are untouched: --print-compatibility is byte-identical at 648 bytes.
2026-09-22 03:28:20 +00:00
claude
320b05ecee symbols: the four WRAM names the whole-map grid reads
resolve_wram.py is a second reading of the disassembly beside gen_symbols.py:
it walks ram/wram.asm with a cursor that is only ever live while anchored on an
address symbols.rs already pins, re-derives 40 of the 63 it carries with no
disagreement, and emits an address only when a pinned one after it agrees too.
wOverworldMap, wCurMapTileset, wTilesetBank and wTilesetBlocksPtr come out of
that pass; no address is hand-written and nothing else in the table moves.
2026-09-22 03:17:20 +00:00
acamilo
097a11196b trap hunt: report the battle sub-state, its pad and the starts on it
A scene histogram says "battle" and a battle deals five different pads
(docs/design/macros.md 12.6, 13.1), so a loop inside one of them is
invisible in the tables the hunt already prints. Three more: frames per
sub-state, every macro channel ever bound in each one, and the macro
starts by the sub-state they started on.
2026-09-22 03:13:27 +00:00
acamilo
660c3cf00d flybrain v0.4.0: public tree (history retained privately)
Some checks failed
ci / node 22 (test + typecheck) (push) Has been cancelled
ci / rust stable (cargo test --workspace --release) (push) Has been cancelled
ci / infra/tests/lint.sh (push) Has been cancelled
ci / playwright apps/stage (allowed to fail) (push) Has been cancelled
2026-09-21 15:09:46 +00:00