--- name: blueprint-refactor-tranche description: "Take ONE target from the plan and land it without changing behaviour: guard it on the untouched code, move it verbatim, prove the equivalence by running both, mutation-sweep the tests, pass every gate, open a PR and merge it on green CI — or decline it with the cost written down." --- # One target, one change This step repeats. Each round does **one** target from `.blueprint/targets.json` — the first whose status is `todo` — and nothing else. The whole claim of the change is **"this behaves the same"**, and that is not provable by reading. What follows is how it is proved instead. Read `.blueprint/spec.md` (the plan the person approved), `.blueprint/gates.json` (the gates) and `.blueprint/answers.json` (`merge`: whether to merge on green CI). ## 0. The approved plan wins The person may have changed the plan by talking to it at the review gate, which rewrites `spec.md` but not `targets.json`. If the two disagree — a target added, dropped, reordered or reworded — bring `targets.json` in line with `spec.md` first. Keep finished targets as they are. ## A `ci` target Close the gaps in `.blueprint/ci.json` with the smallest change: prefer adding a step to the workflow that already installs dependencies over a new workflow, and use the install command from `gates.json`. Every workflow gets a top-level `permissions:` block (default `contents: read`; a job gets more only if it needs it) and `actions/checkout` gets `persist-credentials: false`. Then continue from step 8 — the pull request's own checks are the proof that the new CI runs and passes. ## 1. Start clean, and check nobody else is in the file - On the default branch, `git pull --ff-only`. Branch: `blueprint/`. - `gh pr list --state open` and `git log origin/ --oneline -20 -- `. A pull request merged an hour ago is in neither your tree nor the open list. If someone else is changing these lines, mark the target `skipped` with that as the note, and stop the round. - Confirm the target is still reachable: grep for its caller or renderer, not its import. ## 2. Find the guards before touching anything ```bash grep -rln "" test/ tests/ src/ --include='*.test.*' --include='*.spec.*' grep -rn "''\|\"\"" test/ tests/ src/ ``` Sort the hits: **behavioural** tests call the code and break loudly. **Source-text** guards (a test that reads a file and looks for a string, a census, an AST scan) **fail open**: when the text moves they go vacuously green, still running and checking nothing. Each of those has to be re-pointed by what it protected (step 5), and a guard should derive its population rather than carry a list written down. ## 3. Write the missing guard against the UNMODIFIED code If the behaviour you are about to move — above all an ordering or a shared answer that holds only because the code is adjacent — has no test, write one **now**, on the untouched code, where it passes. Then **break-verify it on the untouched code**: insert exactly the defect the move could introduce and watch it go red. A guard written after the move pins the new shape and proves nothing about the old one. Commit the guard on its own (`test:` prefix) before the move. For a `test`-kind target this is the whole change: tests for untested, high-value logic, each one mutation-verified (step 7). ## 4. Move the text verbatim - The moved body is the original text with mechanical renames only. No "while I'm here" tidying — a cleanup mixed into a move destroys the only cheap proof you have. - Pick the move from what the **call site** does on each path. Does this path already `await`? - An `async` helper suspends its caller even on a path that reaches no `await`: one more microtask before everything after it. Write the helper synchronous first; keep the `await` at the call site. - A write to shared state (a module singleton, `process.env`, a registry) and the call that reads it must stay in one continuation. If the region does I/O and then writes, split it: the async half returns a plan, a synchronous half applies it at the call site. - `let x;` filled by the block below it → `const x = f(…)`: the canonical, checkable move. - Prove only the control flow changed: a multiset of the file's code lines against the default branch, comments stripped, should show only the inversion. ```bash strip() { grep -vE '^\s*(//|\*|/\*)' "$1" | sed 's/[[:space:]]//g' | grep -v '^$' | sort; } diff <(git show origin/: | strip /dev/stdin) <(strip ) ``` ## 5. Re-point every source-text guard by what it protected Never by where it sat. For each moved anchor: what defect was this there to catch, and where can that defect now appear? Sometimes that is two places — inside the helper **and** around its call. Break-verify each re-pointing. ## 6. Prove the equivalence — run both, do not reason In order of preference: 1. **Replica differential.** Copy the OLD code verbatim into a throwaway harness and run it beside the new code over **generated** inputs — not inputs you chose; the ones you would think of are the ones you already believe are equal. Compare the whole result (`JSON.stringify` both sides), and treat a throw as an outcome (`ok:` vs `threw:`). Generate deliberately: missing keys, `null` elements, falsy-but-not-nullish (`0`, `''`, `false`), both alternatives of a `??`/`||` chain present at once, the same value at two positions, keys every object has (`constructor`, `__proto__`). 2. **Drive it** through the seam the codebase already has (an injected clock, client, or `realpath`). 3. Verbatim body plus the guards — and say out loud that this is all it rests on. **Mutation-verify the harness** before trusting its zero: change one edited line in the new code and the harness must report differences. A zero it cannot move off is not a measurement. Before deleting the harness, **harvest what outlives it**: the generator (which inputs matter) and the property (what must hold) become a permanent test. ## 7. Mutation sweep of the tests you wrote A test that cannot fail is documentation with a green tick. For each decision you touched: 1. Hash the file. Confirm the anchor matches **exactly once**. 2. Apply the mutation, re-read the file, confirm the old text is **gone** (it landed) and that it runs (a duplicate object key or a compile error is not a behaviour mutation). 3. Run the tests; read the **exit code** and the thrown assertion. 4. Restore; compare the hash back. Worth running: the helper suspends before its first statement; each guard dropped; each branch's answer swapped; a value handed on as a copy instead of the live object. **Record mutations that stay green** rather than silently dropping them — each is either a hole to close or a sentence saying why no input can tell the two apart. ## 8. Gates, by exit code Run the install and every gate from `.blueprint/gates.json`, and read `$?` — never the last line of output. The typecheck is its own gate; a test runner that only transpiles runs code with type errors happily. ## 9. Pull request, CI, merge - Commit with `refactor:` (or `test:` / `fix:` for what it is), adding files one by one — never `git add .`. - Push the branch. Open a PR whose body says: what the region was, which invariant was at risk, how behaviour was proved (and over what inputs), which mutations went red, and **what was not proved**. Say what moved and how, not by how much — no line counts. - **Cross-review with Codex, when it is available** — follow `cross-review.md` beside this file. Dispatch the first round as soon as the pull request is pushed; it runs alongside CI, not after it. - `gh pr checks --watch`. Read a red check and fix it; never retry blindly, never merge while pending. Merge only when CI is green **and** the Codex review ended clean (or could not run, and the pull request says why). - If `merge` is `CI が緑なら自動でマージする`: `gh pr merge --merge --delete-branch`. Otherwise leave it open. - Back to the default branch, `git pull --ff-only`, and confirm the gates are still green there. ## 10. Record the round In `.blueprint/targets.json`, set this target's `status` to `done` with `"pr": ""` and `"review": "codex: clean after rounds"` (or why there was no Codex review). In `.blueprint/spec.md` add one line under the target saying how it went. **Then stop — end your turn here**, even when there are targets left and even when an answer you just got reads like "carry on". The executor checks this round and starts the next one in a fresh session; a second target done in this session skips that check and the next round's fresh context. ## When to decline instead Declining with the cost written down is a real result and beats a move whose behaviour is only argued. Set `status` to `skipped` and write `note` — what it would buy, what the risk is, and what would have to be established first — then delete your branch (`git branch -D`, only the one you made) and return to the default branch. Decline when: - you cannot prove the move preserves behaviour; - the extracted piece is itself over the bound (the finding moved rather than cleared); - nothing reaches the code — say so and do not delete it on your own authority; - someone else is changing the same lines. Ask the person (and stop) only when a decision is theirs: a gate red for reasons outside this change, a fix that would change behaviour, or a target the plan did not foresee. Make it a pick, not an essay. Put the evidence in the question — the lines, the caller that reaches them, what you measured — and offer the options as `CHOICES`, each with what it costs and risks, and `RECOMMEND` the one you would take. The decisions this work stops for, and their usual options: | decision | options | |---|---| | a declared type is narrower or wider than what arrives | fix the type / narrow the code to it / leave it | | something looks like a bug | file an issue / leave it | | a fix would change behaviour | change it / keep today's behaviour | | a costed no | accept it / do it anyway | Done when the check passes: every finished target's PR is merged (or open, if the person merges), the clone is back on a clean, up-to-date default branch, every gate is green, and one more target is finished than before this round. ## Always - One target per round. Do not start the next one — the executor starts the next round. An answer from the person (say, that they merged the pull request) finishes THIS target; it is not a go-ahead for the next. - Never force-push, rebase or squash. Never push to the default branch. Never delete a branch you did not create. - Never silence a lint or type error to get green (`eslint-disable`, `@ts-ignore`, `as`): fix the cause or decline. - When you need a decision, ask it through the blueprint question tool and stop. Do not guess. - Say you are done by stopping; the executor runs the check. Do not claim success yourself.