Linear: [AERIE-2312](https://linear.app/builder-team/issue/AERIE-2312)
Stacked on #1464 (AERIE-2297) → #1462 → #1460 → #1458 → #1457. Ships in tonight's release. The backfill runs as a post-release step (see below).
## Why
A read-only prod audit (169 sites) showed that a phase's retired planned end date (constructionScheduledEndDate / targetDate) is its construction finish date, which is the new M5 Completing Construction date. It is not the M10 Operating date: it matched M10 on only 5 of the 71 Phase 1 sites that have it. About 35 Phase 2s and 4 expansions carry real plans, and those plans feed the capacity projections. Rather than fall back to fields slated for removal, we copy the dates to M5 and read only M5.
## Changes
- Projected capacity reads M5 only. This covers capacity at a date, next planned, and the API capacity plan. The date is the M5 completed date, otherwise its due date, via phaseCapacityDates / applyPhaseCapacityDates. A phase without an M5 date has no projected date; there is no fallback. Current capacity is unchanged: M9 Ready to Open completed, plus phase status Completed.
- Migration migrations/backfillCompletingConstruction. Rules live in @bran/contracts/completing-construction-backfill:
- Due dates only. A planned date is never recorded as an actual completion date. Phases already past construction are skipped and keep an unset M5, which counts toward nothing, so no completion date is invented: Phase 1 with CO completed or N/A, and later phases with status Completed.
- N/A: a stored "N/A" becomes M5 Not applicable with a standard migration note. The canonical field decides; an N/A or non-date never falls through to the older targetDate alias.
- Phase 1: every site with a date or N/A that isn't past construction. M5 is Not started and due on that date.
- Phase 2: only when it carries data (a date, seats, or a non-default status). Empty default shells and cancelled or retired "no further expansion" phases are skipped.
- Additional expansions: all of them.
- Phase 2 and expansions get their full M4–M10 set. One with seats but no date still gets the set, with M5 unset, for review.
- It is idempotent and never overwrites a stored M5.
- Known gap: older sites have no trustworthy M5 completion date, and none is invented. For example, 4 active sites past CO but not yet Ready to Open show no "Buildout P1 Open Date" in the Ready-to-Open report until someone enters it.
- Retired Buildout fields are hidden everywhere they were still shown. The stored data is kept for a few weeks before a restore-or-purge decision; do not run purgeRetiredBuildoutFields.
- Buildout report: the occupancy columns are removed, and "Buildout P1 Open Date" now reads Phase 1's M5 date.
- FTO: the TCO Obtained/Expiration columns, sorts and CSV columns are removed.
- Phase 2 projected (FTO and portfolio) reads Phase 2's M5 date.
- Tooltips, docs and agent text: the capacity tooltip, OpenAPI, DSS contract, agent guidance and tool descriptions now say M5.
- Write errors: error messages no longer mention retired fields.
- Removed: the Buildout write check that rejected duplicate dates. It only compared the retired stored date.
- Phase 2 can be removed (found in the dev clickthrough). The Buildout card now has a Remove action for an existing Phase 2, like additional expansions.
- Removal is explicit: the patch form is removePhase2: true, and the full-write form is phase2: null.
- A payload that just omits Phase 2 still keeps it, so an older writer can't delete it by accident.
- Removal drops the section and its milestones, and clears the legacy sites.phase2 placeholder so Phase 2 isn't re-created on read.
- The public API and MCP don't expose removal; it's in-app only.
- Confirm dialog labels. Milestone changes now read "Phase 2 › M4 · Obtaining Permits › Due date" instead of raw keys, and long labels wrap instead of being cut off.
## Post-release steps (tonight, straight after deploy)
1. Dry run the preview:
npx convex run migrations/backfillCompletingConstruction:preview '{}' --prod
Expected counts from the offline audit:
| Count | Expected |
|---|---|
| phase1Due | 23 |
| phaseDue | 32 |
| phaseNotApplicable | 1 (300 Cambridge St, Phase 2) |
| phaseWithoutDate | 4 |
| skip:pastConstruction | 51 (48 Phase 1 + 3 later phases) |
| skip:noDate | 98 |
| skip:emptyDefaultPhase | 52 |
| skip:cancelledPhase | 9 |
2. Run the backfill:
npx convex run migrations:run '{"fn": "migrations/backfillCompletingConstruction:backfill"}' --prod
3. Re-run the preview and confirm it reports zero writes.
4. Spot-check the consumers:
- projected capacity for 5400 Beethoven St and 350 E South Water St (Chicago)
- one Phase 1 site that got a due date
- FTO "Phase 2 projected"
- the Ready-to-Open report
Treat the deploy and steps 1–4 as one operation. Until step 2 runs, sites whose only date is the retired planned end show no projected capacity date. That short gap is accepted.
## Verification
- pnpm typecheck passes; biome is clean.
- Contracts: 1196/1196 pass.
- Chat sweep of 482 test files: 8644 passed, 18 skipped.
- New tests:
- the planner: every case and skip reason, plus idempotency
- the migration: exact preview counts, stored results, and a second run is a no-op
- projected vs current capacity: M5 wins, and M10 and the retired date are ignored
- I also ran the planner offline against the prod export: all 169 sites plan cleanly with no errors.
🤖 Generated with [Claude Code](https://claude.com/claude-code)