The issue Lifecycle as data, checked for termination
Part of the epic "Epic: slice 0, foundations. The repo holds the line, its context is tested, and the platform facts are recorded".
Declares the issue Lifecycle of design §4.1 as data beside the pull request's, and proves that it terminates. The design merged in the pull request "Design the issue Lifecycle: triage, Readiness, the hand-over to a pull request, and its bounds", through the merge queue, after a third, narrow check that found no blocker.
What changes
lifecycle/issue.json: nine States (new,triage,ready,in_progress,in_review,held,not_served,done,declined), withdoneanddeclinedterminal, and 65 transitions. The 32 arrows drawn, two of them made by the Factory or a person alike; every arrow the prose under the diagram declares ("Any State butdonemay move todeclined…"); and eight the derivation table allows that neither the diagram nor the prose states, listed below.- Bounds:
max_attemptsonreadytoin_progressand back;max_unmerged_closesonin_progressorin_reviewback toreadyortriage;max_runs_per_itemontriagetoready. The Lifecycle schema acceptsmax_unmerged_closes, and its number, one ("starting at one"), is inconfig/defaults.jsonbeside the other limits. - The States only a person may leave are data:
person_only_exits, accepted by the schema. The pull request's Lifecycle listsheld; the issue's listsheldandnot_served, which for an issue is not terminal. The check refuses a transition out of one of them by the Factory or a Lens, and a Lifecycle that hasheldand does not list it. "Only a person, or the platform acting for one" is kept: the pull request's Lifecycle releases a Hold toverifyingby the platform. tools/check_lifecycle.py:run(root)checks every Lifecycle underlifecycle/, requires one for each Work item the schema names, and requires each bound to be a limit inconfig/defaults.json. It validates each against the Lifecycle schema first, and reports malformed data rather than raising.check(lc)checks one. Until nowmake verifyloaded the pull request's alone.- The counter-example: with
max_attemptstaken off, the Factory can cycle fromreadytoin_progressand back, and the check rejects it. The same holds formax_unmerged_closes. It is a test of the check, as the pull request's Lifecycle counter-examples are, because a Lifecycle with no bound is still valid against the schema.
Not here: the Kernel's derivation and Admission for an issue, and issues in the simulator. Those are the three stories that follow.
A known gap. Deleting max_runs_per_item from triage to ready passes every check today. Its cycle goes back through ready to triage, which a person makes, and the check counts only cycles made wholly by the Factory. That belongs to the story "Lifecycle check: bounds in the data for every cycle with no person in it" (number 38).
Open questions
- Eight transitions the derivation table allows that neither the diagram nor the prose states. All are declared here:
new,triage,ready,heldandnot_servedtodone, by a person: row 1 ranks first, so merging a linked pull request ends the issue from any State.readytoin_review, by a person who opens their own pull request that is not a draft (rows 6 and 8).triagetoheldby the Factory when the Run ceiling is reached (row 4, "The Kernel holds the issue"). The design drawstriagetoheldonly as a person's act.newtotriageby a person with write access who addsfactory:triage(I-41).
in_reviewis stale "as its pull request". The schema takes one number, soin_review: 30borrows the stale time ofverifying, the State a pull request enters when marked ready.- An outsider's edit that lapses acceptance is declared
human. So is the content change behindreadytotriage, which is made by a writer, since an outsider's edit lapses acceptance. The trial script usedplatformfor it; no check result depends on the choice. - Closing and merging are declared
human, as in the pull request's Lifecycle, though GitHub carries out a commit's closing keyword.
For the Kernel stories
in_progresstoreadyby the Factory is declared twice, with different bounds:max_attemptswhen an opening Run ends having opened nothing, andmax_unmerged_closeswhen a linked pull request closes unmerged. The Kernel must tell the two causes apart from the facts, and count each against its own bound.- Row 4 derives
heldfrom conditions that can clear with no person acting: the Run ceiling is counted over a window, soheldcan end when Runs age out of it. That contradictsheldinperson_only_exits. It needs a simulator test in the story for issues in the simulator (number 43).
Verification
make verify: 124 tests (5 expected to fail), 7 checks, all ok.python3 -m tools.mutants: 20 of 20 killed.- Each new test in
tests/test_tools.pyfailed before the change it tests: the counter-example formax_attempts, the planted transition fromnot_servedtoreadyby the Factory, and a Lifecycle that is a JSON list or has a transition missingby.
Closes #18
🤖 Generated with Claude Code