The issue Lifecycle as data, checked for termination

Merged
DO
6d ago
+593-14

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), with done and declined terminal, 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 but done may move to declined…"); and eight the derivation table allows that neither the diagram nor the prose states, listed below.
  • Bounds: max_attempts on ready to in_progress and back; max_unmerged_closes on in_progress or in_review back to ready or triage; max_runs_per_item on triage to ready. The Lifecycle schema accepts max_unmerged_closes, and its number, one ("starting at one"), is in config/defaults.json beside the other limits.
  • The States only a person may leave are data: person_only_exits, accepted by the schema. The pull request's Lifecycle lists held; the issue's lists held and not_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 has held and does not list it. "Only a person, or the platform acting for one" is kept: the pull request's Lifecycle releases a Hold to verifying by the platform.
  • tools/check_lifecycle.py: run(root) checks every Lifecycle under lifecycle/, requires one for each Work item the schema names, and requires each bound to be a limit in config/defaults.json. It validates each against the Lifecycle schema first, and reports malformed data rather than raising. check(lc) checks one. Until now make verify loaded the pull request's alone.
  • The counter-example: with max_attempts taken off, the Factory can cycle from ready to in_progress and back, and the check rejects it. The same holds for max_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

  1. Eight transitions the derivation table allows that neither the diagram nor the prose states. All are declared here:
    • new, triage, ready, held and not_served to done, by a person: row 1 ranks first, so merging a linked pull request ends the issue from any State.
    • ready to in_review, by a person who opens their own pull request that is not a draft (rows 6 and 8).
    • triage to held by the Factory when the Run ceiling is reached (row 4, "The Kernel holds the issue"). The design draws triage to held only as a person's act.
    • new to triage by a person with write access who adds factory:triage (I-41).
  2. in_review is stale "as its pull request". The schema takes one number, so in_review: 30 borrows the stale time of verifying, the State a pull request enters when marked ready.
  3. An outsider's edit that lapses acceptance is declared human. So is the content change behind ready to triage, which is made by a writer, since an outsider's edit lapses acceptance. The trial script used platform for it; no check result depends on the choice.
  4. 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_progress to ready by the Factory is declared twice, with different bounds: max_attempts when an opening Run ends having opened nothing, and max_unmerged_closes when 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 held from conditions that can clear with no person acting: the Run ceiling is counted over a window, so held can end when Runs age out of it. That contradicts held in person_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.py failed before the change it tests: the counter-example for max_attempts, the planted transition from not_served to ready by the Factory, and a Lifecycle that is a JSON list or has a transition missing by.

Closes #18

🤖 Generated with Claude Code