v1.9.0: phase 1 becomes a gate instead of a formality
Released 2026-07-23. Two dogfood runs broke the migration pipeline the same day, in opposite directions, and turned out to be the two edges of one missing classifier.
What we were trying to fix
The assessment phase always said go. Whatever it found, /migrate continued into phase 2 and beyond, because there was no field for it to decide anything with. Two real targets, run on the same day, showed what that costs at both ends of the range.
Atypical-Consulting/StaticWGen was already modern: net10.0 across every project, the SDK pinned in global.json, packages held current by Renovate, no obsolete-API cluster anywhere. The pipeline retargeted nothing, at length, and would have gone on to a deployment phase that puts a Blazor app on Pages, for a command-line tool with no web target at all.
phmatray/DotnetChain failed in the mirror image. Its projects were on net9.0 while Renovate had pushed EF Core 10 and ASP.NET 10 packages into them, which are net10 only. The result is NU1202 and a restore that cannot succeed before migration. Phase 2’s rule was that a green baseline must be recorded before anything is changed, and that rule, which sounds unarguable, blocked precisely the repositories the tool exists to rescue.
Both had been sitting in docs/backlog.md as items with unfired triggers. On 2026-07-23 both triggers fired.
What we decided
Ship them as one change, because they are not two tickets. They are the two non-default outcomes of a single classifier at the same step, with inverted symptoms.
migration/assessment.md now opens with a verdict field, computed at the end of phase 1, holding ALREADY_MODERN, RED_BY_TFM_LAG or NORMAL. /migrate branches on it at exactly one place.
ALREADY_MODERNstops the run after phase 1 and routes to/migrate-verify. Not to “done”: modern is not the same as clean, and the proof is that StaticWGen’s one successful restore raisedNU1903, a high-severity transitive advisory. A target with nothing to migrate can still have something to fix, and the article that says otherwise would be wrong about the case that produced the verdict.RED_BY_TFM_LAGinverts the baseline rule for one specific signature: the retarget is the prerequisite of the baseline rather than the other way around. Phase 2 records the baseline as deferred, and phase 3 captures the first green after the retarget as the recorded baseline. On DotnetChain that produced 88 passing tests where a restore had previously been impossible.NORMALbehaves exactly as before, which is the point of naming it.
No new scope command was introduced. The plumbing this needed already existed, since /migrate-assess was already phase 1 alone and /migrate-verify already phase 6 alone, so what changed was the routing, not the surface. The executive audit gained a third profile for the “healthy, nothing to migrate” case, so the read-only report can reach the same conclusion the pipeline does.
Both dogfood cases were then pinned as documented verdict fixtures in the phase-1 reference: the input signature and the expected verdict, written down so that re-deriving a different verdict for either signature is a regression rather than a judgement call. A new classifier with no regression lock forgets the cases that produced it within a month.
What got cut
The five other backlog items were left unbuilt on purpose, triggers intact. The review that prioritised this work said so as an explicit finding rather than as an omission: do nothing on those five this cycle, and revisit each only when its own named trigger actually fires. Two items shipped because reality had just demonstrated both of them; the rest had not earned it, and building them while the file was open would have been the cheapest possible mistake.
The two findings were also refused as two tickets. Delivering them separately would have produced two branches in /migrate, in two places, disagreeing about which phase owns the decision. One field in one artifact, read at one place, was the smaller change and the more durable one.
What bit us
Making DotnetChain’s long-red build compile exposed 536 compiler errors that had nothing to do with the framework: a half-finished domain refactor that had been invisible for as long as the restore had been broken. This is the failure mode nobody plans for, because it looks like the migration broke the repository when in fact the migration is the first thing in months to compile it.
The rule that came out of it is the one the pipeline still follows: repair everything verifiable, and name the edges without inventing the missing behaviour. A migration tool that guesses at what a half-written domain method was supposed to do produces a green build and a wrong program, which is strictly worse than the red build it started from.
The other lesson is subtler. Phase 2’s green-baseline rule was not a sloppy rule. It was a good rule, correct for most targets, and it was still the bug for one class of repository, which nobody noticed until a target arrived that it locked out entirely. A gate that only ever returns one answer is not a gate, and this release is where phase 1 stopped being one.