Exploration: the issue's moves the world makes, and the world held to the exploration both ways (#228, #202)

Merged
DO
4d ago
+127-39

Closes #228. Closes #202. By the working rulings on both (comments of 2026-10-20).

What. The exploration of every change the issue Kernel can make (tests/test_transitions.py) gains the moves the world makes and it lacked: a person closes the issue, adds and removes factory:hands-off, converts the linked pull request back to a draft, merges it with an empty diff, opens a draft or a ready pull request of their own; the Factory's author App marks its draft ready; the queue merges the Factory's ready pull request while the issue is in review (the Maintainer's ruling on #227). It gains #202's base: a writer's issue one act short, carrying a factory:ready the readiness App added with no Run that judged it, from which the Controller's removal is the last act and new → held by the Factory follows at the act ceiling.

Held both ways. tests/test_issue_simulator.py drops UNEXPLORED: every change the world makes on its seeds is one the exploration finds (made ⊆ explored), and the nine the exploration finds that the world never makes are named as UNMADE with why; on the full 300 seeds the two sets are held equal, on fewer seeds the named rows must still be unmade. tests/test_transitions.py names, as ISSUE_UNREACHED, the seven rows lifecycle/issue.json declares that the exploration does not reach, with why each is kept, and holds the set exactly, as the pull request's UNREACHED is held. The exploration is computed once a process (issue_explored()), and the simulator's test shares it.

The exploration's time. Full depth 4 with the new moves: 22 seconds on this machine (the bare exploration; 32 with the old per-item wrapper, now gone), against the ruling's thirty. The split the ruling names (the Kernel's moves to depth 4, a person's one deep) runs in 8 to 18 seconds and misses two changes only four mixed steps reach (in_progress → triage by the Factory, ready → held by the Factory), so the full exploration is kept; a cheaper exploration that still finds those two is a story of its own if CI's runners make this one slow.

The world. One change to sim/issues.py: a person no longer merges a draft pull request, which GitHub refuses (a_draft_is_not_merged, listed in sim/grounding.py, asked by no trial). With it the world no longer makes in_progress → done by a person, which it had made only through that merge; the row stays declared and is named in ISSUE_UNREACHED as one a person's own ready pull request beside the Factory's draft would make, were the Kernel not to hold two links first.

#202. The base is explored and yields new → held by the Factory, (label_not_counted → act_ceiling). The row's bound stays null, as every → held by the Factory is declared: only a person leaves held, so no bound on the way in is what terminates (tools/check_lifecycle.py).

Record. No new decision. A row on decision 0028; sim/AGENTS.md's row on the issue world's changes names the exploration.

Reviews, one fix round on 31277ad: fresh-context Claude review and security pass, Muse (a change to sim/ tests). No AGY: nothing in kernel/. Taken: the reopen move was dead, since a closed issue is a dead end of the exploration and a writer's reopening is a new life, not a transition (Claude, Muse); the UNMADE check was one inclusion, now equality on the full seeds (Claude, Muse, security); three UNMADE reasons said what the seeds do not (Claude); the world's draft rule was unmarked and unlisted (security); seven declared rows nothing reaches were named by nothing (Claude, security); the exploration was computed twice and paid a key per item in the test's wrapper (Claude); queue_merges was defined a second time, which the repo check refuses in an export. Not taken: the split exploration, for the two changes it misses, said above.

🤖 Generated with Claude Code